fix(tui): preserve pending session work#36433
Open
kitlangton wants to merge 7 commits into
Open
Conversation
c52ef08 to
d44a729
Compare
d44a729 to
0b1662f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Opening or reconnecting to a V2 Session no longer drops an admitted prompt when projected-history hydration races with promotion.
Before this change, pending content was inserted into the projected-message array while a separate string array indicated which messages were still pending. Hydration had to reconcile those parallel representations, and two concurrent API reads could observe opposite sides of promotion.
After this change:
UI States
1. Admitted
The prompt is visible at the pending boundary and retains queued styling.
2. Promoted
The same row loses queued styling but remains visible while projected history catches up.
3. Projected
The server-projected message replaces the overlay representation by ID without changing the outer row owner.
How
Timeline Model
packages/tui/src/context/session-timeline.tsdefines an exhaustive union containing only facts actually known in each state:A promotion received before admission creates a promoted placeholder containing only its ID and promotion sequence. A later admission may provide content, but cannot downgrade it to pending.
Projected IDs always take precedence over overlay entries with the same ID.
stateDiagram-v2 [*] --> PendingInput: input admitted [*] --> QueuedCompaction: compaction admitted PendingInput --> Promoted: input promoted PendingInput --> Projected: projected snapshot wins Promoted --> Projected: projected message observed QueuedCompaction --> RunningCompaction: compaction started QueuedCompaction --> FailedCompaction: compaction failed RunningCompaction --> CompletedCompaction: compaction ended RunningCompaction --> FailedCompaction: compaction failed Projected --> [*] CompletedCompaction --> [*] FailedCompaction --> [*]Hydration Ordering
packages/tui/src/context/data.tsxreads pending work before projected history.Promotion atomically deletes the pending record and inserts its projected message. Let
Pbe the pending read,Mthe projected-message read, andXpromotion. BecauseP < M:X < P: projected history contains the input.P < X < M: both may contain it; projected history wins by ID.M < X: pending contains it; the live promotion operation retains it in the overlay.Concurrent reads permit the unsafe order
M < X < P, where both responses omit the input. Sequential reads eliminate that ordering.sequenceDiagram participant Events as Live event stream participant TUI as TUI timeline participant Pending as Pending API participant Messages as Message API TUI->>Pending: list pending work Events-->>TUI: journal topology operations Pending-->>TUI: input + synthetic + compaction TUI->>Messages: list projected history Events-->>TUI: journal topology operations Messages-->>TUI: projected messages TUI->>TUI: verify refresh token TUI->>TUI: fold journal onto snapshots TUI->>TUI: projected IDs replace overlay entries TUI->>TUI: reconcile rows by stable IDLive Operations
Only topology changes are journaled while hydration is in flight:
Text, reasoning, tool, shell, and compaction deltas are deliberately not replayed. They are additive, and projected responses expose no event watermark proving whether a delta is already included.
Refresh Ownership
Each Session has at most one active refresh record:
Same-Session callers join the promise. Reconnect invalidates active records and starts replacements for loaded, pending-only, or refreshing Sessions. Invalidated callers join the replacement when one exists. Deletion and committed revert invalidate the Session record, preventing stale installation. Failed reads install nothing and preserve visible state.
Solid Identity
packages/tui/src/routes/session/rows.tsgives every row a stable semantic ID and installs reductions with:The same durable input ID identifies pending, promoted, and projected prompt rows. Queued and running compaction also share one ID. Tests verify that Solid retains the same row object across queued-to-running compaction and while exploration groups gain parts.
Solid and Performance
This is intentionally a correctness-first trade, not a claim that every operation is faster. Pending and projected snapshots are read sequentially because parallel reads admit the missing-input race described above. That adds the pending-endpoint RTT to hydration, but the projected history request remains capped at 200 messages and same-Session refresh callers now share one in-flight promise instead of issuing duplicate reads.
The Solid update path is efficient for the work it performs:
timeline.list(). Allocating that derived array does not itself schedule another update. See Solid's store tracking and fine-grained reactivity documentation.batch, so consumers observe one coherent transition rather than reducing once for each store update.reconcile. Solid reuses matching store objects, moves retained entries, and patches only changed fields. The implementation matches the installed Solid 1.9.10 behavior:reconcilesource.<For>, whose underlyingmapArraycaches children by item identity. Stable row proxies therefore preserve existingSessionRowViewowners when a prompt or compaction moves between lifecycle positions; retained owners are moved rather than disposed and recreated. See the taggedmapArraysource.While pending work exists, timeline derivation is linear in the visible messages plus pending work, and topology reconciliation also sorts the pending overlay. With projected history capped at 200 and pending work normally small, this is bounded CPU/allocation work. It is deliberately exchanged for race-free hydration; keyed reconciliation prevents that computation from becoming equivalent view churn.
flowchart LR Delta[Streaming delta] --> Targeted[Targeted message-store update] Targeted --> Fine[Fine-grained dependent fields] Topology[Topology change] --> Derive[Derive bounded timeline and rows] Derive --> Reconcile[Reconcile by stable row ID] Reconcile --> Retain[Retain or move existing row owners] Reconcile --> Membership[Create or dispose changed membership only]Scope
The guarantee is scoped to pending-work visibility under the existing atomic-promotion, ordered-live-connection, and reconnect contracts. Recovering arbitrary silently dropped streaming events would require server replay or a snapshot cursor.
Testing
bun run test test/context/session-timeline.test.ts test/cli/tui/data.test.tsx test/cli/tui/session-rows.test.ts(50 passed)bun typecheckbun run test(247 passed, 1 skipped)Coverage includes pending-only hydration, promotion across every snapshot boundary, late admission, promotion-before-admission, stale revert data, refresh joining and replacement, overlay-only reconnect, queued compaction restoration, queued-to-running compaction row ownership, delivery ordering, and keyed exploration-row ownership.
The full TUI suite still prints the existing missing
/tmp/opencode/state/kv.jsonwarnings; all tests pass.packages/tui/TIMELINE_PLAN.mdcontains the compact final contract and proof.Supersedes #36158.
Closes #35988