Repository navigation
feat(agent-chat): expose earlier prompt index for thread rail - #26033
teamleaderleo wants to merge 15 commits into
Conversation
Leo 2026-10-25: "if you haven't loaded in chats ... the session rails ... should be able to still, like, gradually load that stuff in". A long chat opens on its tail; the pane now reads the chat's older turn-start records (user_message, queue_order, turn_result, queue_removed, prompt_refused) from acpmux, one page of 400 per idle callback, and keeps the prompts that open a turn by the transcript's rule (TurnStarts: steers and prompts sent now that continue the turn they stopped open none). Pages append their own rows; the rail reads them from their own store (client.earlier), told once they grow by half. Rows are built as the transcript builds them, again when a sender is listed late, and keyed by promptId (turnKey, #25161). Stops when acpmux has nothing older, after 200 pages, on a failed read, on a chat switch or close. The rail draws them in the PR stacked on this one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16f7b-11f6-73e0-8276-7c0e3bb3ead4&host=huge-blue
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
Passes: CI passes on CI passes on Written by web and size checks: green Gallery before/after for
States
Pick other states with a |
|
Found 1 test failure on Blacksmith runners: Failure
|
|
Bead |
|
|
|
Review (codex gpt-6-astra xhigh, next-review): SHIP at 181d979 No blocking findings. |
|
Orphaned: no activity for 14 h, and no lane holds a lease on it. Adopted by the orphan sweep: merged |
…arlier-prompts-index # Conflicts: # webviews/src/agent-session/acpmux/direct.ts
…scribes The index had no consumer yet but paged up to 200 pages of turn-start records on every attach with older history. It now records where to start and reads only when `earlier.subscribe` has a listener, so a pane with no rail mounted pays no paging and holds no index. Restores the retry- continuation comment lost when the prompt reading moved to shownPrompt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d750cf10922c785efa64
|
Review (codex gpt-6-astra high, router-review): BLOCK at 2f0cb9a BLOCK Visual verdict: BLOCK — this
|
… index Review (gpt-6-astra): an in-flight read kept paging after the rail went away. The last unsubscribe now stops reading and drops the index; a later subscriber reads again from where the transcript starts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d750cf10922c785efa64
|
Review (codex gpt-6-astra high, router-review): BLOCK at 4bb0f05 BLOCK Visual verdict: No visual change; no after screenshot applies.
Focused tests could not run because |
…mpt or nothing Review (gpt-6-astra): a queue_order at the page boundary stored only UNKNOWN, so its later removal or refusal, or a prompt with another id, could not settle the next prompt's turn when paging stopped. With no turn running nothing is interrupted, so the field is that prompt or nothing; track it as such. Worst-case transitions are 6N (still linear). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d750cf10922c785efa64
|
Review (codex gpt-6-astra high, router-review): BLOCK at 42a5a1e BLOCK Visual verdict: Incomplete—no after screenshots were supplied, and the PR body contains no confirmation that the landing lane viewed one.
Validation: standalone index reproductions ran through Babel/Node. The requested Bun suites could not run because Bun and project dependencies are absent. |
…last answer Review (gpt-6-astra): a turn the agent carries on opens with turn_started and no prompt, and a prompt sent now into it goes on with it in the transcript; the index did not read turn_started and showed it as a new turn. It now does, as the transcript's turnOpen does. stop() also drops the memoized answer, so a stopped index keeps no prompt text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d750cf10922c785efa64
|
Review (codex gpt-6-astra high, router-review): BLOCK at bb733c6 BLOCK Visual verdict: No after screenshots were supplied; the PR has no UI consumer yet.
Focused tests could not be run because |
…very page split Review (gpt-6-astra) asked for this regression: a queue_order(next) seen with an unknown boundary, then resolved to idle by an older page. Each page split re-evaluates the newer page from the resolved input, so the prompt opens its own turn; the test covers every split. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d750cf10922c785efa64
|
Review (codex gpt-6-astra high, router-review): BLOCK at 76b7865 BLOCK Visual verdict: BLOCK — no after screenshots or landing-lane confirmation were supplied, so the required visual comparison cannot be completed.
Verification: reproduced finding 1 using the actual client and index through a Babel/Node harness with UI dependencies stubbed. The requested Bun tests could not run because Bun is unavailable. |
|
Blocker (not landing yet): the index re-implements the transcript's turn-start reducer and keeps diverging from it. What changed at
Fleet: The 4th review still BLOCKs at What landing needs, here or in PR B (#25160):
The HOLD line stays. |
|
Superseded by #29632 (merged): the prompt rail now gets every earlier prompt from the acpmux prompt index on first frame, so this client-side index (whose open blocker was its divergence from the transcript reducer) is no longer needed. Closing. |
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
HOLD: base red
web / react-apps-checkhides webview test signal on feat-cmux-next (owner nx-ci); this PR touches webviews/ and lands once that check is green again.Summary
PR A for the split rebuild of #25160. This lands the prompt-level history index and paging contract only; it has no rail consumer yet, so it changes nothing visible until the stacked rail PR lands.
This reads the selected chat's
_acpmux/eventsrecords that can start prompts (user_message, withturn_started, queue and turn records used to match the transcript's turn-start semantics: a turn the agent carries on opens withturn_startedalone, and a prompt sent now into it goes on with it), paging backward 400 records at a time and pacing each page from idle work. The index has its own cursor and store. Index-only pages never advance the loaded transcript'sfirstSeq/historyFrom, fold transcript rows, or remount them.The public contract is:
Reading starts only once something subscribes to
client.earlier(the rail, in #25160). Attach records where the index would start (below the attach page's oldest record); a pane with no rail listening pays no paging and holds no index (the last unsubscribe stops reading and drops it), so landing this PR alone changes neither the loaded transcript's behavior nor its cost. Test:a pane with no rail listening reads no earlier prompts.loadThroughdelegates to the existingloadOlderpath, retaining its in-flight, selection-generation, history-epoch, and exhaustion guards. It does not writescrollTop;preserveScrollAnchorTopremains the only scroll correction. Stale index responses become inert after chat selection changes or disposal.Coordination
nx-chat acknowledged
ACK-nx-history-contract-1027: this PR owns the prompt index, while nx-chat owns logical/physical history coordinates and the existing row-id prepend anchor. TheirhistoryCoordinateswork consumes event sequence and page geometry separately.Tests
thread-rail-earlier-prompts.test.ts: 4,000-prompt paging, stable rows, prompt extraction, failed-read stop, stale in-flight disposal/chat switch, index-only cursor/row identity invariants,loadThroughmaterialization/no-scroll invariant, continuation acrossmaxBytespage boundaries, and final pending-batch publication after a failed read.agent-pane-history-paging.test.tsexisting browser-lane history paging coverage is retained.bun test test/thread-rail-earlier-prompts.test.ts test/agent-pane-history-paging.test.ts— 19 pass, 1 browser-lane skip outside CI/Freestyle.vp fmtandgit diff --checkpass.Failed-result delta
A failed
turn_resultnow clearscontinued, including UNKNOWN boundary state. The next prompt is therefore a definite new turn even if older paging fails or reaches the 200-page cap. Both stop-path regressions fail on1c1fd5d4and pass after the fix; each asserts the prompt is visible and published exactly once. All 19 index tests, including the 235 generated splits and cap-cost tests, pass.Boundary state, publication, and cost
The retained window starts with unknown
open,interrupting, andcontinuedfields until the server reports exhaustion (or seq 1 is reached). Reaching the 200-page cap, a no-progress page, or a failed read does not invent a closed boundary. A prompt whose classification still depends on older records stays out of the visible index until resolved."This prompt or nothing" (
{ maybe }) only lives while the boundary is unknown: when an older page resolves it (say, to idle), the newer page is evaluated again from the resolved input, and themaybeis replaced by what that input says.a prompt sent now while idle stays on the rail once older pages say no turn ranchecks this at every page split aroundqueue_order/user_message(acpmux promotes a queued prompt with no turn running, queue_edit.rs).Each record caches its incoming state. Older context can only refine one of the three fields from unknown to known; replay starts at the changed boundary and stops on matching input/output state. Once outgoing state is fixed, transition records behind it are dropped. Unresolved records retain only seq/kind/direction and the four transition fields; queue payloads and unrelated metadata are discarded. Prompt records still needed for display or late-sender refresh are retained separately.
Publication tracks the last published version, independently of the row count. Equal-count dirtiness flushes at the end of a page; ordinary growth remains batched. Failure and other terminal stops flush any unpublished version, while chat switch/disposal invalidates responses and clears state without publishing stale rows. There is at most one publication per page, and terminal flushes do not duplicate an already published version.
Both new regressions fail on
8885fc97: the cap no longer shows an unresolved continuation, and the equal-count replacement becomes observable before the next page/read failure. The original boundary regressions and all 235 generated page-size/byte-budget comparisons still pass.Cost at the 200 × 400-record cap
The worst-case transition bound is 6N = 480,000, down from about 8 million: one initial evaluation and at most five incoming-field refinements per record (
openonce; each prompt field from unknown to "this prompt or nothing" to known). A prompt sent now at the unread boundary is that prompt or nothing (no turn running interrupts nothing), so its later removal or refusal, or a prompt with another id, settles the next prompt without older history. No transition work runs on streamed tokens.cap cost stays linear and releases payloads after state becomes knownmeasures 80,000 rowless records carrying 19,350,000 serialized input bytes:5,749,799 bytes is the worst measured retained transition payload in this harness, a 70.3% reduction from its raw input. It is serialized payload, not a heap measurement: JS objects/state snapshots and displayed/refreshable prompt content are additional. There is no universal 5.75 MB total-memory guarantee: arbitrary prompt text and prompt-ID lengths remain payload dependent. The prior ~19 MB raw paging payload is no longer retained wholesale merely to replay transitions.
Timers and stop conditions
earlier.subscribehas a listener.loadThroughstops when the requested row is materialized, history is exhausted, the cursor cannot advance, or the session becomes stale.Screenshots
None apply: this PR draws nothing, and there is no after image to view. No component reads
client.earlieror callsloadThrough, and the index reads nothing until one subscribes, so the pane renders byte-for-byte as on base. The rail's before/after belongs to #25160, its consumer.Changelog
No visible change in this PR; the consumer is in stacked PR #25160.
Agent session: agent session