Repository navigation
Description
Title: tui: new background steps refill transcripts of already-evicted session families
Summary
After the real recent-three retention owner evicts a session family's heavy transcript cache, later background session.step.started and text events recreate and retain full assistant messages for that excluded family.
In a deterministic reproduction, an already-evicted running session accumulates 0 → 1 → 2 complete assistant messages without being revisited or explicitly resynchronized. A later metadata update recomputes retention, but the messages remain and its eviction count stays 1 → 1.
The concern is the mismatch between the existing family-retention policy and subsequent heavy-data admission. This is not an OOM, heap-size, throughput, or latency claim.
Environment
- Windows; Bun 1.4.2.
- Dynamically reproduced against a custom checkout at
72d0dce8ea437ea061e0680110fa19a81aa118fd. - Compared official references:
63c1a27af5a391d9caa55b82694f496911e1859band latest-checked60a5a6ca7ce176b04cd2321ffd1819b1ea8e6006. - The retention owner/caller, message mutation helpers, and step-start handler are identical across those revisions. The official revisions were source-compared, not dynamically executed.
- The fixture uses real production
createDataandcreateSessionRetention, injected in-memory HTTP responses, Solid signals for navigation, and synthetic events. No TUI renderer, real service, database, model, or user session is accessed.
Reproduction
Use the fixture approach from packages/tui/test/context/session-retention.test.ts, mounting the production retention owner over production client data inside a Solid root.
- Configure
currentas a writable Solid signal,keep: () => [], andlimit: 3. This corresponds to the existing tabs-disabled/no-kept-tabs path; do not change the policy to a smaller artificial budget. - Wrap
session.evictonly to count calls, forwarding every call to the realdata.session.evictimplementation. - For four fixture root sessions A, B, C, and D, sequentially:
- Register valid metadata through
data.session.remember. - Set
currentto that session. - Await
data.session.message.sync, with the injected fetch returning one synthetic history message and an empty cursor.
- Register valid metadata through
- After initially viewing A, deliver a valid synthetic
session.execution.startedevent for A. Keep it running while navigating to B, C, and D. - After viewing D, verify that A's transcript is empty and B/C/D each retain their one history row. A remains a known running session, and A's eviction count is one.
- Send a
session.text.deltafor an assistant message missing from A's evicted transcript. Confirm that it does not restore a message. - Send a subsequent complete step for background A through the normal event listener:
session.step.startedwith a new assistant-message ID, fixture agent/model, and start time;session.text.started,session.text.delta, andsession.text.endedfor ordinal zero;session.step.endedwith ordinary finish/cost/token data.
- Observe one full assistant message, including its final text, in A's transcript. Repeat the sequence with a second assistant-message ID and observe two messages.
- Update A's metadata using
data.session.rememberwith a newertime.updated, leaving its root, navigation state, and keep list unchanged. Both messages remain; A's eviction count remains one.
All HTTP responses are fixture-only. The only transcript requests are the four initial reads for A/B/C/D; no explicit read or navigation for A occurs after eviction. Events for A are delivered in order.
Expected Behavior
The existing retention owner should have a coherent contract with subsequent heavy-data admission: a family excluded from the retained set should not silently rebuild and retain a full transcript solely because background steps continue.
This expectation is grounded in the current implementation and tests, rather than an assumption that all inactive activity must stop:
packages/tui/src/context/session-tabs.tsx:139–145explicitly retains open-tab families and three other recently viewed families.packages/tui/src/context/session-retention.ts:26–39computes retained/excluded families and calls the heavy-cache eviction operation for excluded members.- Existing retention tests cover eviction of unviewed families, newly discovered descendants, and hydration when an evicted session is revisited.
- The message mutation code explicitly describes a missing streaming target as unloaded or evicted and drops edits to that target.
If retaining new full transcripts for every excluded running family is intentional, that exception needs an explicit policy and bounds; the observed behavior currently arises from step-start bypassing the one-time eviction decision.
Actual Behavior
| Observation | Result |
|---|---|
| A after recent-three eviction | 0 transcript messages |
| A after a delta targeting a missing assistant | 0 transcript messages |
| A after first later complete step | 1 complete assistant message with text |
| A after second later complete step | 2 complete assistant messages with text |
| A after metadata-triggered retention recomputation | Both messages still retained |
| A eviction count before/after those events | 1 / 1 |
The assertion requiring the excluded transcript to remain empty fails. This proves retention bypass and incremental message/text accumulation, not a measured memory-exhaustion threshold.
Additional Context
Source locations are repository-relative:
packages/tui/src/context/session-retention.ts:30–39: eviction is deduplicated using the previously excluded member/root map. An unchanged excluded member is not evicted again.packages/client/src/solid/data.ts:453–469in the tested checkout, corresponding to:449–465in both official references: message updates lazily recreate a missing per-session message array and index.packages/client/src/solid/data.ts:886–920in the tested checkout, corresponding to:881–915in both official references: a new step appends an assistant row; following text events populate it normally.
Controls in the same fixture:
- Initial recent-three eviction works: A is empty while B/C/D retain their history.
- An equivalent complete streamed step for current D retains the correct assistant text. Dropping all streaming events would therefore fail the control.
- A delta for an absent assistant does not resurrect that assistant before a new step starts.
- The final companion run recorded 3 passing controls and 3 failing product assertions, with this retention invariant accounting for one failure. Exit status was 1, not a passing product gate. All fixtures disposed with zero pending responses/listeners and zero captured request errors; no fallback fetch occurred.
Related context: #36185 concerns active Location scoping. This finding is about the interaction between session-family transcript retention and later live events; that related issue does not establish that this event-publication path is covered or fixed.
Suggested solution — not implemented
Maintainer decision requested: coordinate heavy-data admission with the existing retention owner so the selected policy remains true after eviction, not just at the moment of cleanup. This should be a focused change at the existing ownership boundaries, not a new generic GC framework.
Do not indiscriminately drop session metadata, activity/attention state, permission/form requests, or notification inputs: those have different consumers and lifetimes. Preserve current/active-view and open-tab protections, and explicitly decide how excluded but still-running families should be handled rather than silently changing that policy. Preserve optimistic submissions and the ability to rehydrate a transcript when a session is revisited.
Keep the current-session streaming control and the excluded-family background-step sequence as distinguishing acceptance cases. No implementation or production behavior change is included; the appropriate policy and mechanism remain maintainer decisions.
Activity
opencode-agent commentedon Oct 18, 2026
Thanks for the detailed report. I reproduced this on the latest
dev(60a5a6ca7) with a small test that uses the realcreateDataandcreateSessionRetention, fake HTTP responses and made-up events.Steps:
session.step.started, text started/delta/ended,session.step.ended).time.updated.Result:
The script below reproduces it. Run it from
packages/tuiwithbun test. Its last assertion, that A's transcript stays empty, fails with 2 messages.Reproduction script (repro-56737.test.ts)
Run it:
bun repro-56737.test.ts