Add "long answer opening with a conversational word is still recovered"
(regression guard); retarget the skipped-preamble case to the borderline band;
drop the external "#N" annotations in favor of self-explanatory comments.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- isSubstantiveAnswer: restore the original "≥200 chars ⇒ real answer"
unconditional accept (the preamble filter now applies only in the
borderline 80–200 band). Applying preambleRe regardless of length regressed
the recovery path: a long front-loaded answer opening with a conversational
word ("Sure,", "Let me explain:", "First,") was wrongly discarded
(flagged by glm-5.2 + claude-code/opus). The NVD-style planning-narration
case is already handled by the citation-dominance check, not this filter.
- citations dedup: compare with <url> angle-bracket wrappers stripped, so a
front-loaded turn that listed the same sources unwrapped still suppresses the
duplicate block.
- doc: spell out the citationLabelRe leading class members (+ list markers);
drop the stale "gated by preamble" note on recoverMinChars.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
finalOutput already recovered a front-loaded answer when the terminal
(no-tool-call) turn was empty or a short back-reference. But some models
(glm-5.2 via a "cite" tool) front-load the prose answer into an earlier
tool-call turn and close with a terminal turn that is ONLY their sources
("Sources: [pcprice.watch](url) (...), [ebay](url) (...)"). That list is
neither empty nor a back-reference, so it was returned verbatim and the real
answer was discarded (mort issue #1418: the user got a bare sources list; the
good answer was only visible in the trace).
Add isCitationsOnly: a terminal that OPENS with a sources/citations heading,
carries a link, and is dominated by that citation structure (once heading+links
are stripped, little prose remains). Unlike a back-reference the citations are
real content, so finalOutput recovers the prior substantive answer and APPENDS
the citations below it (deduped). The dominance check keeps a real prose answer
that merely opens with "Source: ... http://..." from being misclassified; the ^
anchor keeps an answer that ends with a Sources section from matching; the
citations branch is decoupled from the terminal-length ratio so a CONCISE
front-loaded answer is still recovered; and preambleRe now applies regardless of
length so planning narration is never recovered. Healthy terminal answers are
unchanged; zero extra model calls.
Tests: TestIsCitationsOnly (19 cases incl. bold/ATX/backref/prose negatives),
new TestFinalOutput cases (concise+long-citations, source-led-not-hijacked,
long-preamble-skipped, dedup), and end-to-end
TestRun_RecoversFrontLoadedAnswerWithCitations. Full agent suite green (-race).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The agent loop took the final answer only from the terminal (no-tool-call)
turn. Models that "front-load" their answer into an earlier turn that also
calls a tool — then close with a trivial pointer like "(Already answered
above.)" — had their real answer discarded and the pointer delivered. This
recurs across several open-weight models (glm-5.2, etc.); well-behaved models
(Claude/GPT) defer their answer to the terminal turn and are unaffected.
finalOutput() now falls back to the last substantive assistant content in the
transcript when the terminal text is weak (empty, or a short back-reference).
The predicate is narrow and back-reference-gated so short-but-correct answers
("42", "It's down, restarting now.") are never overridden; recovery only picks
a prior turn that reads like a real answer, not a preamble. Zero extra model
calls. Terminal-answer behavior for normal runs is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>