fix(agent): gadfly round 2 — a pointer is not a compression
CI / Tidy (pull_request) Successful in 9m22s
CI / Build & Test (pull_request) Successful in 9m47s

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.
This commit is contained in:
2026-08-22 00:03:15 -04:00
parent 7dccb233ad
commit 3f1c016e74
2 changed files with 68 additions and 9 deletions
+37 -7
View File
@@ -136,8 +136,9 @@ var backRefRe = regexp.MustCompile(`(?i)(already answered|see above|as (i )?(sai
// hyphenated compound — "above-average", "above-board", "above-ground" —
// read as a clause-final deictic and put a legitimate short answer at risk of
// being discarded. The em dash stays: a model writes "…above — see the
// links", never "above-" as a separator.
var aboveRefRe = regexp.MustCompile(`(?i)\babove\b[ \t]*([.,;:!?)\]"'’”—]|\n|$)`)
// links", never "above-" as a separator. CR is listed alongside LF so a
// CRLF transcript does not quietly lose every line-final "above".
var aboveRefRe = regexp.MustCompile(`(?i)\babove\b[ \t]*([.,;:!?)\]"'’”—]|\r|\n|$)`)
// pointsAbove reports whether a deictic "above" appears in the terminal's
// OPENING. The offset bound is what makes a bare "above" safe to key on: with
@@ -157,6 +158,23 @@ func pointsAbove(t string) bool {
return loc != nil && loc[0] <= backRefHeadChars
}
// compressionMarkerRe matches a model announcing that what follows is the
// short form of something longer ("Short version: …", "TL;DR: …", "In short,
// …"). It is the second half of the deictic summary-closer test: the pointer
// says the full answer is elsewhere, and this says the text beside it is a
// condensation rather than new reasoning.
//
// Both halves are required, because a deictic pointer alone does not mean the
// terminal is disposable. "Given the analysis above, I recommend option B
// because X" opens with a pointer and then states a CONCLUSION the earlier
// turn never contained — discarding it in favour of that turn would throw away
// the answer. A compression marker is the model telling us the opposite.
//
// A marker WITHOUT a pointer stays out of scope, as summaryCloserRe's own
// comment explains: a user who asked for brevity is answered with exactly that
// shape.
var compressionMarkerRe = regexp.MustCompile(`(?i)\b(short version|shorter version|short answer|tl;?dr|in short|in brief|in summary|in sum|bottom line|net[- ]net|the gist)\b`)
// summaryCloserRe matches a terminal turn that OPENS with a bookkeeping
// acknowledgment of the citation round — "Citations are logged.", "Sources
// cited.", "Logged the citations." — the shape a model produces when it
@@ -296,10 +314,21 @@ func isCitationsOnly(s string) bool {
// isSummaryCloser reports whether a terminal turn defers to an earlier answer
// and is short enough that whatever follows the deferral can only be a
// compression of it. Two openers qualify: a complete "citations are logged"
// -style ack sentence (summaryCloserRe), and a deictic back-reference in the
// terminal's opening (pointsAbove — mort issue #1611's "Done — that's the full
// chain above. Short version: …", which the ack shape alone did not cover).
// compression of it. Two openers qualify:
//
// - a complete bookkeeping ack sentence — "Citations are logged." — which is
// summaryCloserRe, and carries no answer content of its own;
// - a deictic back-reference in the opening PLUS a compression marker —
// mort issue #1611's "Done — that's the full chain above. Short version:
// …" — which the ack shape alone did not cover.
//
// The second opener needs both halves. A pointer on its own does not make a
// terminal disposable: "Given the analysis above, I recommend option B because
// X" points backwards and then states a conclusion the earlier turn never
// contained, and replacing it with that turn would discard the answer. The
// compression marker is the model saying the opposite — that what follows is
// the short form of something it already wrote.
//
// Whether the fuller answer actually exists is modeSummary's job — the dwarf
// ratio in isSubstantiveAnswer keeps a matching closer in place when nothing
// earlier clearly outweighs it.
@@ -313,7 +342,8 @@ func isSummaryCloser(s string) bool {
if t == "" || len(t) > summaryCloserMaxChars {
return false
}
return summaryCloserRe.MatchString(t) || pointsAbove(t)
return summaryCloserRe.MatchString(t) ||
(pointsAbove(t) && compressionMarkerRe.MatchString(t))
}
// lastSubstantiveAssistantText scans msgs newest→oldest (skipping the terminal