Repository navigation
session: the end-of-turn reader re-raises an observation the model already answered, and the argument is drawn to the person as chat turns #888
Description
What happened
2026-09-11, dev@333acc67d, live drive of the chat surface, scenario 4. The turn had answered the person. The end-of-turn reader then re-opened it three times running with the same synthetic continuation:
[carry on] A reader of a bounded account of the work raised the observation below.
Check it against the actual current work and the person's request before changing anything. …
Each observation was wrong in the same way — it claimed the non-existence of zeta.txt had not been reported, when the answer already said so. The model spent three visible turns arguing back, and the person read all three:
The observation is mistaken…
Observation mistaken on both counts…
Scenario 3 drew the same exchange once.
This is #468's family, and #468 is closed. What #468's own "Still open" note predicted is exactly what fired here: "checkpointCarryOnCap still lets repeated echoes through up to the cap instead of stopping on the first identical one." Filed separately because #468 is closed and because this run adds the second half — the argument is drawn to the person as ordinary chat turns.
Owner's forensics, may be gone: the drive's throwaway homes and transcript.jsonl. The replication below stands in for them.
Replication
Deterministic (no model). internal/session, with a scripted principal standing in for the reader:
- Run one turn through
Agent.Submitwhose scripted answer states a fact about the work (e.g. "there is nozeta.txt; nothing was written"). - Script the remains reader (
decideRemains/readRemains, reached through thePrincipalseamcheckpointReopenalready uses) to return the sameDecision.Briefevery time it is asked. - Script the model's continuation answer to be a rebuttal rather than a change.
Run:
go test ./internal/session -run TestAReaderObservationThatWasAnsweredIsNotRaisedAgain -count=1Expected today: three user messages beginning [carry on] A reader of a bounded account of the work are recorded on the transcript, three assistant rebuttals follow them, and the turn ends on checkpointCarriedOnNote — carried on 3 times · the last reading showed: ….
Expected after the fix: one carry-on at most for a given observation; the second identical reading ends the turn instead of re-raising, and the rebuttal exchange is not in the person-visible feed.
Field (real models). OPENROUTER_API_KEY, the chat surface, an ask whose answer includes a negative finding ("file X does not exist, so nothing was done about it"). The reader reliably re-raises the negative as an omission. One turn, a few cents.
Where
- The lead and the cap:
checkpointCarryOnLead,checkpointCarryOnCap(= 3),checkpointCarriedOnNote,checkpointMeter.carriedOn—internal/session/checkpoint.go. - The firing rule:
Agent.checkpointReopenininternal/session/checkpoint.go— it callsdecideRemains(ctx, readRemains(ctx), said), checksturnIsWaitingOnItsOwnTasks, then the cap, then recordstextMessage("user", checkpointCarryOnLead+decision.Brief)and sendscheckpointCarryOnNote. Nothing compares this reading with the previous one, and nothing records that the running model already answered it. - The turn-handoff sibling that already reasons about this shape:
internal/session/turnhandoff.go. - Where the exchange reaches the person: the continuation is recorded as a
user-role message and the model's reply is an ordinary assistant turn;internal/session/agent.goalready special-cases the lead when walking the transcript (searchcheckpointCarryOnLeadbesideisVolatileNote), so the marker exists — the surface simply does not use it to keep the exchange off the feed.
The fix
An observation that has been answered is not raised again. The reader's reading is compared with the previous round's before it is believed: a second reading that says the same thing about the same stopped turn is a standstill, and the turn ends there rather than at the cap. That is #468's second acceptance bullet, unlanded.
And the argument is not drawn as chat. A carry-on and the model's answer to it are the harness talking to itself about a turn that already ended. The person gets the one dim note the register already owns — the ask is not finished · carrying on rather than stopping here — and, if it stops on an echo, one honest line saying so. What they must not get is two speaking turns of a rebuttal to a sentence they never typed.
Acceptance
- e2e:
go test -tags e2e ./internal/e2e/— a chat run whose answer states a negative finding shows at most one[carry on]in the journal for that turn, and the stringObservation mistaken(or any rebuttal of a reader) never appears in a drawn assistant message. - e2e: the control — a turn that genuinely left work undone is still carried on once and the work is still finished, so the fix has not disarmed the feature.
- Unit:
internal/session— given two identical readings for one stopped turn,checkpointReopenre-opens exactly once and its second answer isfalse, false; the note it sends names the echo rather than the cap. internal/manual/chat/(the page describing why a turn carries on) quotes the new stopping wording, and the change entry'sinvalidatesnames the belief that the reader may raise the same observation up to three times.
🤖 Generated with Claude Code
Drafted with CodeAF · reviewed and owned by the author
Activity
chat: an observation the reply already answered is not raised again
chat: an observation the reply already answered is not raised again
chat: an observation the reply already answered is not raised again (#…
santoshkumarradha commentedon Sep 11, 2026
The cause half landed in #948,
dev@b35ce8511: a reading that says what the reading before it said is a standstill, and the turn ends on it. That is one mechanism now —standstillFloorininternal/session/standstill.go, lifted out ofStewardand given toPersontoo, with one sentence (standstillReason) both roads stop on.checkpointReopenis untouched: the echo is decided by the principal and lands on theDecideStoproad that was already there. The cap of three stays and now bounds only a reader that says something NEW each time.The person now reads, once:
The display half is not landed, and this is the seam.
"The argument is not drawn as chat" cannot be derived from the transcript, and both shapes of the rule lose real answers:
TestReopenedConversationKeepsCarryOnOutOfTheExchange's own fixture is[carry on] …→"The work is finished."— the turn's actual last word, and it disappears. So does every tool-call row the continuation bought, which is the work carrying on exists for.A continuation's reply and a turn's genuine final answer are structurally identical. The only fact that separates them is the reading that comes after the reply — the standstill above now knows it, and the display projection does not. Carrying it would mean recording it (
sessionfile.go's per-message marks) and answering the live-feed question too, since the surface streams the reply as it arrives rather than shaping it fromshapeEntries. That is a design ruling, not a shaping change, so it is left here rather than guessed at.Leaving this open for that half.
🤖 Generated with Claude Code
7vignesh commentedon Oct 8, 2026
Reading the current
dev, both halves this issue names now appear resolved, and the deterministic test it asks for already exists — so this looks closeable rather than open.Cause (re-raising the same observation). Landed in #948, as @santoshkumarradha's comment above said:
standstillFloorininternal/session/standstill.goends the turn on a reading that repeats the previous one, so a stopped turn is carried on at most once for a given observation instead of up to the cap.Display (the self-argument drawn as chat). The comment left this open as a design question, but it was then solved in #1067 (
#1065 round 1, merged, ancestor ofdev) viaNoChangeReply([no change]), which the comment predates:checkpointCarryOnLead(internal/session/checkpoint.go) now tells the model: "If the note is wrong, reply with exactly[no change]and nothing else."internal/session/loop.go): when the reply is withdrawn, the code reinstates the prior settled answer —answer = checkpointLastSaid(a.snapshot())— instead of streaming the rebuttal.internal/session/agent.go,entryRows): both the[carry on]lead and a[no change]reply return 0 rows, so neither the continuation nor the rebuttal is ever drawn to the person. This is the exact mechanism the comment said the transcript could not express:NoChangeReplyis the recorded fact that separates a continuation's withdrawn reply from a turn's genuine final answer, and the earlier "drop the reply / keep only tool rows" shapes it was worried would eat real answers are not used.The test this issue specifies to write already exists, in
internal/session/carryonwait_test.go:TestAReaderObservationThatWasAnsweredIsNotRaisedAgain— scripts the same answered negative finding and a reader that returns the same observation every time, and asserts: exactly one[carry on]written into the turn (not three), the turn stops on the standstill (checkpointStoppedNote + standstillReason(...)), not on the cap (checkpointCarriedOnNote), and the reader is spent twice (raise + prove the echo), not four times.TestAStandstillIsForgottenWhenThePersonSaysSomethingNewandTestATurnWithNothingRunningOfItsOwnIsStillCarriedOnare the controls: a genuinely-new reading still carries on once, and a fresh ask later is not held against the earlier echo.TestReopenedConversationKeepsCarryOnOutOfTheExchangecovers the display side across both memory and a reopened journal.I could not run the
internal/sessionsuite locally to re-confirm green on this box — the package transitively importsinternal/seniordev/engine/steploop, which is//go:build !windows, so its test build fails on Windows (unrelated to this issue). If someone on Linux/macOS can confirm those four tests pass on currentdev, I think this can be closed as resolved by #948 (cause) + #1067 (display), with the acceptance already encoded in the tests above.sharmis-tp commentedon Oct 22, 2026
I'd like to take this. I'd make checkpointCarryOnCap stop on the first carry-on observation identical to one already raised instead of letting repeats through up to the cap, and keep the model's reply to a mistaken observation out of the person's chat turns, with a test that replays three identical observations and asserts only one continuation fires. For the display half, should that reply be hidden entirely or shown in some collapsed form?