Skip to content

A restored window paints its checkpointed model catalog as live after a refused reconnect #8634

Description

@usirin

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions