Skip to content

fix(history): treat mid-turn absorption as delivery, not withdrawal - #14

Open
Antisophy wants to merge 1 commit into
CyberShadow:masterfrom
Antisophy:fix/midturn-absorption-delivery
Open

fix(history): treat mid-turn absorption as delivery, not withdrawal#14
Antisophy wants to merge 1 commit into
CyberShadow:masterfrom
Antisophy:fix/midturn-absorption-delivery

Conversation

@Antisophy

@Antisophy Antisophy commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Recent Claude CLIs absorb queued messages into the running turn at a tool boundary instead of holding them for the next one. The queue entry is removed after delivery, and the delivery itself is recorded as a queued_command attachment rather than as a user line: the CLI's own absorption path removes the entry only once it has been folded into the running request, and marks each folded command's lifecycle as started. The queue-primary pipeline read every remove as "removed without being consumed", so absorbed messages were badged "not delivered" while the model demonstrably received them, and the delivery record was invisible because attachment lines were never translated.

This PR translates queued_command attachments into user messages and pairs each one strictly with a remove. The pair's records land in either order, so an attachment against a still-queued entry is held until the entry's fate is known: a remove completes the absorption and releases it; a dequeue discards it, since in that flow the echo carries the message and recent CLIs write the attachment as presentation only; one matching nothing renders nothing. An entry removed with no delivery record keeps the not-delivered outcome, so a genuine queue clear still reports honestly.

The delivery record becomes the visible message, so it anchors checkpoints like any user line, and undo truncation takes its queue records with it, matched by the text they carry because the enqueue is not adjacent to the delivery (the turn's own output sits between them). An absorbed delivery carries no file checkpoint, as there is no user record for the CLI to checkpoint at, so its boundary offers conversation-only undo instead of a file revert the CLI would reject with "not a user message in this session".

Covered by a replay unit test exercising both pairing orders, the dequeue-flow attachment, and an undelivered queue clear, plus reducer tests for the provisional handover. Full check set run; a handful of contention flakes passed on serial retry.

(Rebased onto 50cd0f9; the previously stacked undo fix is superseded by it and dropped.)

Update: real-world replay surfaced two hardening requirements now folded in. Queue records are matched resiliently rather than by content text alone (the CLI omits content for array-valued messages and texts can repeat): exact match, then empty-to-empty, and a remove finally settles the oldest unsettled entry. Echo pairing likewise prefers the awaiting entry whose text the echo carries, and a joined echo (several entries drained under one dequeue record, an older CLI behavior still present in long-lived sessions) settles every queued entry whose text appears in it. Without these, one stranded entry shifts every later pairing behind by one, and each turn opener renders twice after reload. The replay unit test covers the array-message flow and the pairing after it.

@Antisophy
Antisophy force-pushed the fix/midturn-absorption-delivery branch from d6153fc to 880e2c3 Compare August 16, 2026 17:31
Recent Claude CLIs absorb queued messages into the running turn at a
tool boundary instead of holding them for the next one: the entry is
removed from the queue *after* delivery, and the delivery is recorded
as a queued_command attachment rather than as a user line. Every remove
was reported as consumed_as "removed", so messages the model
demonstrably received were badged "not delivered", and the delivery
itself was invisible because attachment records were never translated.

Translate queued_command attachments into user messages and pair each
one strictly with a remove. The pair's records land in either order, so
an attachment against a still-queued entry is held until the entry's
fate is known: a remove completes the absorption and releases it, a
dequeue discards it (the echo carries the message in that flow, where
recent CLIs also write the attachment as presentation), and one
matching nothing renders nothing. An entry removed with no delivery
record keeps the not-delivered outcome, so a genuine queue clear still
reports honestly.

Queue records are matched resiliently, because content is an unreliable
key: the CLI omits it entirely for array-valued messages and texts can
repeat. Matching prefers an exact text match, then empty-to-empty (an
empty search names an array message, whose entry is stored empty too),
and a remove finally settles the oldest unsettled entry, since the
queue really did remove one. Echo pairing follows the same principle:
dequeue records do not map enqueues one to one (the CLI can drain
several entries under one dequeue, joining their texts into one echo),
so the echo prefers the awaiting entry whose text it carries and a
joined echo settles every queued entry whose text appears in it. Any
entry left permanently unsettled would shift every later pairing behind
by one, confirming each echo against the previous message's provisional
and rendering every turn opener twice after reload.

The delivery record is now the visible message, so it anchors
checkpoints like any user line, and undo truncation takes its queue
records with it, matched by the text they carry since the enqueue is
not adjacent to the delivery. An absorbed delivery carries no file
checkpoint (there is no user record for the CLI to checkpoint at), so
its boundary offers conversation-only undo instead of a file revert
the CLI would reject.
@Antisophy
Antisophy force-pushed the fix/midturn-absorption-delivery branch from 880e2c3 to 8aaef2c Compare August 16, 2026 19:14
@CyberShadow

CyberShadow commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Sorry, I can't make any sense of this change.

  • "Recent Claude CLIs absorb queued messages into the running turn at a tool boundary instead of holding them for the next one." - I don't think the previous behavior described here has ever been true for Claude Code (at least for as long as CyDo existed). Codex, however, indeed buffered messages until the end of the turn rather than tool call, with steering being relatively recent.
  • The description of the problem describes an issue with Claude Code, but the bulk of the changes is in the generic machinery.
  • This seems to introduce a new translation layer (i.e. a band-aid) rather than building upon the existing four-tier confirmation level system we introduced recently.
  • The added test is a pure frontend test, but there are no frontend changes, so it doesn't seem to be asserting anything about this change.

@Antisophy

Copy link
Copy Markdown
Contributor Author

Without this change, user messages queued in the middle of the agent's turn are falsely marked "not delivered" after being later delivered. With it, they are shown as delivered (white text) after the agent's next tool use or response.

@CyberShadow

Copy link
Copy Markdown
Owner

I understand, and I don't deny that there is an improvement to be made here, but the technical claims of this PR do not add up. It cannot be merged as is, as that would imply permanently codifying untruths.

@Antisophy

Antisophy commented Aug 29, 2026

Copy link
Copy Markdown
Contributor Author

Untruths are codified before this change, such as messages being marked "not delivered" which actually were delivered. My opinion for the path forward is to first resolve the user-facing issue, then refactor as desired.

Edit: To clarify, I'm not saying you should blindly merge this. I'm just stating my opinion as a user. I intend to look into this further soon.

@CyberShadow

Copy link
Copy Markdown
Owner

Untruths are codified before this change, such as messages being marked "not delivered" which actually were delivered.

No, that would be a bug, not an untruth.

Do you see any code, documentation, or comments which makes false statements about this or other systems' behavior?

This PR, as is, adds a bunch of them - and these are much worse than a bug, because they compromise the project's future. All future work builds on the assumption that stated claims are true.

My opinion for the path forward is to first resolve the user-facing issue, then refactor as desired.

Sorry, we really can't afford to go into technical debt.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants