Skip to content

fix(chat): tie conversation cursors to the Codex rollout chain - #18

Merged
devswha merged 1 commit into
devswha:mainfrom
Yoonwoo-Ha:fix/cursor-chain-fingerprint
Sep 24, 2026
Merged

devswha merged 1 commit into
devswha:mainfrom
Yoonwoo-Ha:fix/cursor-chain-fingerprint

Conversation

@Yoonwoo-Ha

Copy link
Copy Markdown
Contributor

Follow-up to #5, as requested in its merge review.

Cursors are tied to the earlier-rollout chain

A cursor's id named only the live file (dev-ino), while its offset counts from the start of the whole Codex chain. If the chain changed between requests (an earlier rollout found later, or a cached chain evicted after its parent was archived), an old cursor pointed at other turns. before returned a page from elsewhere, and the gap fill showed turns twice and out of order. It never returned a 409.

The id now carries a fingerprint of the earlier rollouts: a hash of each one's path, the bytes of it the chain uses, and its file identity (dev:ino). A cursor from a different chain fails parseCursor, answers 409 history_changed, and the chat reloads.

  • The stream is identified before the page cache is consulted, so a cached older page is keyed by the chain as well.
  • A single-file conversation keeps its old id format (no suffix), so existing cursors stay valid across the upgrade.

Unresolved chains are remembered briefly

A chain that stops at a cut that no file holds used to walk sessions/ again on every append. It is now cached for 30s (complete chains stay cached as before), so a rollout that shows up later is still picked up.

Tests

  • conversation.test.ts: a cursor issued before the earlier rollout appears answers HistoryChanged afterwards, for both before and from, and a fresh read then pages back to the earlier rollout's first prompt. Without the fingerprint the test fails: the old cursor returns a page from the wrong place.
  • codex.test.ts: the "shows less history" case clears the chain cache (forgetHistoryChains) before it expects the late rollout to resolve.
  • bun test: 269 pass, 0 fail. Typecheck clean.

🤖 Generated with Claude Code

Follow-up to devswha#5's review. A cursor's id named only the live file, while its
offset counts from the start of the whole Codex chain. When the chain
changed between requests (an earlier rollout found later, or a parent
archived after its chain was evicted), an old cursor pointed at other turns:
`before` answered a page from elsewhere and the gap fill showed turns twice
and out of order, never a 409. The id now also carries a fingerprint of the
earlier rollouts (path, bytes used, file identity), so a cursor from another
chain answers 409 and the chat reloads; older pages are cached under the same
id, and the stream is identified before the cache is consulted.

A chain that stops at a cut no file holds is now remembered for 30s rather
than walked again on every append.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@devswha

devswha commented Sep 24, 2026

Copy link
Copy Markdown
Owner

Merged. Thanks. The chain fingerprint covers what can change, and the new test fails without it.

One follow-up for a separate PR: a cached chain doesn't recover when its parent is archived. historyChain (server/codex.ts) keeps a complete chain until 64 others push it out. If the parent rollout is archived during that time, every read that reaches the parent fails with transcript_missing, and the route answers scrollback. Right after a backtrack the live file is small, so even the newest page reaches the parent, and the whole chat falls back to terminal output until the entry is pushed out or the server restarts. Suggested fix: forget the cached chain when an earlier segment fails to open. The chain is then looked up again, comes back shorter because archived_sessions/ isn't searched, and gives one 409, which is the right outcome.

devswha pushed a commit that referenced this pull request Sep 24, 2026
Follow-up to #18's review. A complete chain stays cached until 64 others
push it out. If its parent rollout was archived meanwhile, every read that
reached the parent failed with transcript_missing and the chat fell back to
terminal output. Right after a backtrack the live file is small, so even the
newest page reached the parent.

When a remembered chain names a file that no longer exists, the chain is
forgotten and looked up again. It comes back shorter (archived_sessions/ is
not searched) under another stream id, so a cursor into the old chain
answers 409 once and the chat reloads.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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