Four findings, all real.
**A deictic closer now needs a compression marker too** (opus,
correctness). The pointsAbove branch of isSummaryCloser required only a
back-reference, so a terminal like "Given the analysis above, I
recommend option B because X" — a pointer followed by a CONCLUSION the
earlier turn never contained — would be discarded in favour of that
turn, throwing away the answer. The ack shape does not have this problem
because "Citations are logged." carries nothing; a bare pointer does not
carry that guarantee.
Both halves are now required: the pointer says the full answer is
elsewhere, and compressionMarkerRe ("Short version:", "TL;DR", "In
short") is the model saying what sits beside it is a condensation rather
than new reasoning. #1611's closer has both. A marker without a pointer
stays out of scope for the reason already documented — a user who asked
for brevity gets exactly that shape. Unmatched closers keep today's
behaviour, so the narrowing fails closed.
**CRLF** (opus, error-handling): "above\r\n" was not clause-final, so a
CRLF transcript quietly lost every line-final deictic. CR joins LF in the
terminator set.
**A comment wrapped mid-phrase** so that "// -style" read as a list
marker (sonnet) — reflowed as part of rewriting that doc block.
**Process provenance in a test comment** (sonnet): "(gadfly, 3 models)"
is an execution log, not an invariant. The lesson survives, the
attribution does not.
Break-check is ten mutations now — dropping CR and dropping the
compression requirement are each killed by their own named cases — with
the control surviving.
Round 2 (glm): the isSummaryCloser comment still explained itself in
terms of "the whole isBackRef class" — a symbol deleted in f97c2b7. It
named the right distinction with the wrong noun. Reworded to name
backRefRe directly; no isBackRef occurrences remain in the package.
Sonnet's three findings, all on the tests rather than the rule:
- The "at the bound" case sat at index 115 (and 101 after the first
round), never at 120 itself, so <= vs < was never exercised. Both sides
of the boundary are now pinned — index 120 accepted, 121 rejected — and
an off-by-one mutation is killed by the new case.
- A SHORT (<=120-byte) deictic closer takes the modeBackRef bar, not the
summary closer's mandatory dwarf ratio, even though it can carry a
scrap of answer content. That asymmetry is deliberate and pre-existing
(a "see above" closer has always had it): inside 120 bytes there is no
room for both a pointer and a real answer. Pinned with a case sized so
the two bars actually DISAGREE — 3x98 = 294 > the 275-byte prior turn,
so the summary bar would reject what the back-ref bar accepts. The
first draft of this fixture was 83 bytes and passed under either bar,
proving nothing.
- The isBackRef doc finding was sonnet's fourth independent report of it;
already fixed in the previous commit.
Break-check now runs eight mutations, including the off-by-one and a
swap of the classifier ordering; each is killed by a named test and the
control survives.
Three of four reviewers independently found the same defect: `\b` holds
between "above" and "-", so the literal hyphen in aboveRefRe's terminator
class made every hyphenated compound clause-final. "above-average",
"above-board", "above-ground" all read as deictic pointers, putting a
legitimate short closer at risk of being discarded. My own test used the
SPACE-separated "above average" — it tested the neighbour, not the named
path, which is exactly why the class survived a round of review. Hyphen
dropped from the class; the four compounds are now table cases.
Two more findings, both real:
- backRefHeadChars and weakFinalMaxChars were two unlinked 120 literals
that the comment called "the same guard". Now defined by reference,
with a test pinning the identity.
- isBackRef claimed to be the shared extension point for both shapes but
had a single caller — isSummaryCloser deliberately uses pointsAbove
directly, because backRefRe's fixed phrases match ANYWHERE and are only
safe under the 120-byte weak cap. Folding the two together for
tidiness would widen the gate, not deduplicate it. Helper deleted, the
doc moved onto pointsAbove where the real sharing is, and it now says
why backRefRe is not shared.
Also shared the front-loaded-analysis fixture between the table and the
end-to-end test (kimi), and de-hollowed the offset-bound cases: they were
sized as backRefHeadChars±n, so they moved with the constant they were
meant to pin — a break-check that widened the bound to 100000 sailed
through. Literal lengths now, plus an explicit identity assertion.
Break-check: six mutations, each killed by a named test; control survives.
mort issue #1611: run 8eea3e82 front-loaded a 2,245-char analysis into
its cite-call turn and closed with 220 bytes — "Done — that's the full
chain above. Short version: …". The user got the 220 bytes and a
pointer at a chain that was never posted.
finalOutput already had three shapes for this pathology, and the closer
matched none of them: too long for the weak-final cap (220 > 120), no
citations heading, and no "Citations are logged." ack to open the
summary-closer class. So it was delivered verbatim.
The three shapes were each a separate vocabulary of ack phrases, which
is why a fourth phrasing walked straight through. Two of them are really
one signal — the terminal DEFERS, telling us the answer is somewhere the
user cannot see — differing only in whether the terminal also carries
content of its own. That signal is now isBackRef, shared by both, so a
new phrasing is added once and covered in the bare and the
"+ compression" variant at the same time.
Its open-ended half is aboveRefRe: a DEICTIC "above", separated from the
preposition by what follows the word. The deictic use ends its clause
("that's the full chain above.", "as shown above,"); the preposition
always continues into a noun phrase ("above 100°C", "above the fold",
"above all, …"). pointsAbove additionally requires the reference in the
terminal's first 120 bytes — with almost no text before it in THIS
message, it cannot be pointing at the message's own content.
isSummaryCloser now opens on either the citations ack or pointsAbove.
Recovery is unchanged: the mandatory dwarf ratio and the user-message
scan boundary still gate it, so a closer only loses to a prior turn in
the same user turn that is clearly the fuller original.
Break-checked: reverting either classifier, dropping the clause-final
rule, or dropping the offset bound each kills a named test; the
unmutated control survives.
All tidiness, no behavior change: the leading-marker class is one shared
constant for citationLabelRe and summaryCloserRe (hand-copying it is how
'+' went missing the first time); the deliberate 'all' duplication across
summaryCopulas/summaryArticle is now stated at both sites; the weak-final
switch case assigns modeBackRef explicitly; test comments state the
constraint they guard instead of which reviewer asked for them.
Co-Authored-By: Claude Fable 5 <[email protected]>
Two behavioral fixes from the review:
- modeSummary's backward scan now stops at the most recent user message.
With the dwarf ratio rejecting the current turn's 1x-3x answer, the old
unbounded scan could walk into WithHistory content and resurrect a stale
answer to a DIFFERENT question — strictly worse than keeping the closer
(opus, correctness). Other modes keep their historical unbounded scan.
- A terminal matching BOTH the ack shape and a back-reference is now
classified back-ref: it carries no answer content, so the looser bar is
the right one (opus, error-handling).
Plus the nits: summaryCloserRe assembled from named fragments, the leading
marker class gains '+' (parity with citationLabelRe), verb-first form takes
'all the', dwarf ratio hoisted into one named local, and the 151-vs-153
char/byte comment inaccuracy corrected.
Co-Authored-By: Claude Fable 5 <[email protected]>
A third degenerate terminal shape from the glm-5.2 cite pattern: the model
front-loads its full answer into the cite-call turn, then closes with a
bookkeeping ack plus a one-line compression ("Citations are logged. Short
version: ..."). mort run b3cb9ee9 delivered 151 chars of a 2,089-char
answer this way — the closer was neither a back-reference (over the 120
cap, no back-ref phrase) nor a citations addendum (no label-colon, no
links), so finalOutput let it stand.
isSummaryCloser keys on the ack sentence alone (the verb must end the
sentence, so prose about citations never matches; a compression marker
without the ack is deliberately out of scope), and the new modeSummary
recovery bar makes the 3x dwarf ratio mandatory at every length: unlike a
back-reference this closer carries real answer content, so it is only
displaced by the clearly-fuller original it compressed.
The citations/back-ref bool becomes a three-way recoveryMode; existing
behavior for both old modes is unchanged.
Co-Authored-By: Claude Fable 5 <[email protected]>
Merge the parallel callCounts/lastResults maps into one repeatState struct
map (removes the "two maps in sync" smell + double lookup), drop the dead
i<len(results) bounds branch that contradicted the documented index invariant,
and note in-code that exact-string result equality is a deliberate err-toward-
not-tripping choice (a hung job whose poll reports a ticking field is left to
MaxRuntime / the job ceiling rather than risking a false kill of real progress).
No behavior change; guard tests still green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01HgEuVfZJN9mhRhzEsMEVog
The maxSameCallRepeats guard counted identical (name+arguments) tool calls
across a run and tripped ErrToolLoop past the ceiling — regardless of whether
each call made progress. This killed legitimate polling of long-running
background jobs: code_exec_poll must be called with identical args (same
job_id), so a render/encode that needs more than N polls was guillotined
mid-flight even as each poll returned an advancing result (elapsed/status
moving forward).
Only count an identical call toward the trip when its RESULT is unchanged
from the previous identical call. A call whose result keeps changing is
progress and resets its count; a genuinely stuck call returning the same
output still trips. This can never trip more than before, only less, and
covers every idempotent poller with no per-tool configuration — matching the
progress-over-usage thesis behind the stall-detection work.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01HgEuVfZJN9mhRhzEsMEVog
finalOutput now recovers the front-loaded answer when the terminal turn is a
sources/citations-only addendum ("Sources: [x](url), ..."), not just when it is
empty or a back-reference. It recovers the prior substantive answer and appends
the (real) citations below it. Guards: citation-DOMINANCE (a prose answer that
merely opens with "Source: ... http://..." is left as the answer), ^-anchored
heading, citations recovery decoupled from the terminal-length ratio (concise
answers recover too), preamble filter applied only in the borderline band
(long answers opening with "Sure,"/"Let me" are not vetoed), and a dedup that
ignores <url> angle-bracket wrappers. Healthy terminal answers unchanged; zero
extra model calls.
Fixes mort #1418. Gadfly-reviewed (6 reviewers) + adversarially pre-verified;
all findings graded, real ones addressed.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
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) <[email protected]>