Summary
A Tuval window restored from its checkpoint keeps offering the previous process's model and thinking
rows when the rebuilt layer's reconnect is refused. Nothing says that catalog is dead, so the picker
presents rows no session can take until the operator touches a control, which is the first thing that
makes the layer publish an empty offer.
What I was doing
Building #8542 on apps/tuval/src/claude/agent/ClaudeAiAgent.ts — ending both catalogs at teardown
and announcing the clear so a subscriber stops painting the dead session's rows (PR
#8633).
What I observed
The announcement that PR adds fires only when a session was actually torn down in-process: start
emits it behind the new queue's starting and guards it on previous !== null. On a rebuilt layer —
the desk restarting, the process restored from disk — there is no previous session and the layer holds
no selection, so announcing {current: null, available: []} there would erase the selection the
restored window is showing. So nothing is announced.
Meanwhile models is a checkpointed field (ModelState in apps/tuval/src/ai-agent/core/state.ts),
so the restored AiAgentSessionState.models.available still carries the rows the previous process
read off its session, and the phase reaches gone with those rows intact. The composer's
offerResolved reads the offer as resolved at gone, so the picker paints them as a live offer.
The first setModel or setThinkingLevel corrects it — the layer publishes available: [] against
its own empty catalog — so the window is wrong only until an operator touches a picker.
Why it matters
An operator can open the model menu on a restored, disconnected window and pick a row from a session
that is gone. The pick is not judged against those rows (the layer holds none, so it is held
unvalidated and re-validated at the next open per #7981), so this is display truthfulness rather than
a correctness break — the same class of cost #8542 was filed for, on the one lifetime that issue's
criteria did not cover.
Pointers
Suggested next step (non-binding)
A guess, either shape: the restore hands the layer the checkpointed selection so it can announce an
empty offer without losing the current, or the core marks a restored catalog as unresolved until a
layer speaks for this session.
Filed by an agent · session 425cb3d4-b7c2-4bc3-b562-0776c88e7787 · branch build/8542-claude-catalog-lifetime-a031e161 · 2026-09-08T21:11:35Z
Summary
A Tuval window restored from its checkpoint keeps offering the previous process's model and thinking
rows when the rebuilt layer's reconnect is refused. Nothing says that catalog is dead, so the picker
presents rows no session can take until the operator touches a control, which is the first thing that
makes the layer publish an empty offer.
What I was doing
Building #8542 on
apps/tuval/src/claude/agent/ClaudeAiAgent.ts— ending both catalogs at teardownand announcing the clear so a subscriber stops painting the dead session's rows (PR
#8633).
What I observed
The announcement that PR adds fires only when a session was actually torn down in-process:
startemits it behind the new queue's
startingand guards it onprevious !== null. On a rebuilt layer —the desk restarting, the process restored from disk — there is no previous session and the layer holds
no selection, so announcing
{current: null, available: []}there would erase the selection therestored window is showing. So nothing is announced.
Meanwhile
modelsis a checkpointed field (ModelStateinapps/tuval/src/ai-agent/core/state.ts),so the restored
AiAgentSessionState.models.availablestill carries the rows the previous processread off its session, and the phase reaches
gonewith those rows intact. The composer'sofferResolvedreads the offer as resolved atgone, so the picker paints them as a live offer.The first
setModelorsetThinkingLevelcorrects it — the layer publishesavailable: []againstits own empty catalog — so the window is wrong only until an operator touches a picker.
Why it matters
An operator can open the model menu on a restored, disconnected window and pick a row from a session
that is gone. The pick is not judged against those rows (the layer holds none, so it is held
unvalidated and re-validated at the next open per #7981), so this is display truthfulness rather than
a correctness break — the same class of cost #8542 was filed for, on the one lifetime that issue's
criteria did not cover.
Pointers
apps/tuval/src/claude/agent/ClaudeAiAgent.ts—start, the teardown-clear emit behindstartingand its
previous !== nullguardapps/tuval/src/ai-agent/core/state.ts—ModelState/ThinkingState, the checkpointed slicesapps/tuval/src/shell/chat/composer-bridge.ts—offerResolved, which readsgoneas resolved("Known defect left unfixed"); The chat composer's model picker and agent settings are inert in Tuval; the founder wants model switching in the input #7981 for the held-pick ruling; The Tuval composer's thinking-level control reads loading for the whole session #8425 for the resolved-empty offer
Suggested next step (non-binding)
A guess, either shape: the restore hands the layer the checkpointed selection so it can announce an
empty offer without losing the current, or the core marks a restored catalog as unresolved until a
layer speaks for this session.
Filed by an agent · session
425cb3d4-b7c2-4bc3-b562-0776c88e7787· branchbuild/8542-claude-catalog-lifetime-a031e161· 2026-09-08T21:11:35Z