Chat paging with Codex backtracks - #5
Conversation
devswha
left a comment
There was a problem hiding this comment.
Thanks. The paging design looks sound: cursors are checked against the file's inode, and thread ids and paths are validated. A few things before merge:
- Codex history can be wrong after more than one backtrack.
historyChain(server/codex.ts) chooses the earlier rollout by name order and size alone. After a second backtrack to a turn before the first one, it can pick the intermediate rollout, so the chat shows the wrong earlier history and nothing errors. The result, including an empty chain from a failed lookup, is cached for the life of the process. Please resolve the chain from the rollout's own base reference and add a test with two or more backtracks. - Held-start polling has no size limit. Once an older page is loaded, every 2s poll re-reads and re-parses everything from the held start, up to 64MB. When the growth passes
MAX_PAGE_BYTES, the server returns 409 even though nothing changed, and a reader who has scrolled up loses their place. Separately, a large file with no turn markers makespageBeforeread 112MB on every poll. - Please split the PR. The Windows font, per-window reload, resizable composer and Claude effort changes are small and independent. Merged on their own, they would leave paging to be reviewed by itself.
Minor: loadingOlder isn't reset when the pane changes, and the composer grip's aria-valuenow is wrong while the height is automatic. Also, #8 is now merged and touches the same .chat-view rule, so this branch needs a rebase.
…ktracks - Transcripts are read a page at a time instead of whole on every append: a 400MB Codex rollout took 1.1s and 1.7GB per re-read and sent 3.4MB every poll. A page is at most 16MB and 50 prompts, and always starts where a turn starts, so pages never split a turn. - Scrolling near the top loads the page before; the reader's place is kept, and once older pages show, the newest page is polled from the start it had then, so the two always meet. The top shows where the conversation begins. A cursor from a replaced transcript answers 409 and the chat starts over from the newest page. - Codex 0.156 writes paginated rollouts: a backtrack or fork starts a new file whose session_meta.history_base names the earlier history by thread and byte offset. The chat followed only the new, nearly empty file; it now reads the whole chain, without the turns a backtrack discarded. - Claude Code's <pasted_content> wrapper is unwrapped: the chat, like the TUI, shows only the pasted text. - An opened conversation starts at its end instead of smooth-scrolling there from the top, and stays at its end when the view shrinks. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…und every poll to a page Review follow-ups: - A thread gets one rollout per backtrack, and a later backtrack can cut into any of them. The chain took the latest earlier rollout by name and size, so after a second backtrack to before the first one it continued the wrong file. A cut names (thread, ordinal, byte); every record is one line and a rollout's ordinals start at its own first ordinal, so the rollout that continues is the one with exactly `ordinal - first` lines before the byte, ending a line there (checked on a three-rollout chain against Codex's thread_history_1.sqlite). A chain that stops at a cut no file holds shows less history, is not cached, and is looked up again next time. - The newest page is read in one window even when no turn starts in it (it then starts mid-turn); only an older page, read once, reaches further back, a new chunk at a time. - A held start no longer makes every poll read everything after it, and no longer answers 409 when the conversation outgrows a page: `from` answers the newest page once it has moved past the held start, and the chat fetches the turns in between with `before` + `since`, a page at a time, then holds the new start. - The earlier-messages row keeps one height while loading, a pane switch resets the loading state and ignores a load still in flight, and the view-shrink pinning moved to the resizable composer, which needs it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
31b0c6f to
8d5c8e0
Compare
|
Thanks for the review. I've addressed all of it:
Minor: a pane switch now resets |
|
Merged. Both earlier concerns are fixed: the chain is now verified against One follow-up for a separate PR: cursors aren't tied to the earlier-rollout chain. The cursor id covers only the live file ( Minor: an unresolvable chain isn't cached, so every file change walks |
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>
Summary
Chat paging only. The four small changes that were bundled here are now separate PRs: #9 (Windows terminal font), #10 (per-window reload), #11 (resizable composer) and #12 (Claude reasoning effort).
before+since, one page at a time, and then holds the new start.history_basenames what it continues as a (thread, ordinal, byte) cut. The rollout that continues is the one with exactlyordinal - first ordinallines before the byte. That holds however many backtracks there were, and it was checked on a real three-rollout chain against Codex's ownthread_history_1.sqlite. Turns discarded by a backtrack don't appear. A cut that no file holds shows less history rather than the wrong history.<pasted_content>wrapper is removed, so the chat, like the TUI, shows only the pasted text.API
GET /api/pane/conversationtakesbefore,sinceandfrom, and returns acursor(seeConversationResponse). A cursor from another file gets409 history_changed. Bridges without paging send nocursor, so the chat shows their newest page only.Testing
bun run typecheckpasses andbun testpasses 265/265.🤖 Generated with Claude Code