Three handlers write the same React Query key with incompatible semantics, and one of them replaces the whole object.
Verified state
Checked 2026-08-12 against origin/main @ 86eeb2a1.
| site |
operation |
effect on a field the WS frame omits |
app/_layout.tsx:164-166 |
{ ...prev, ...msg.session } — merge |
sticky: keeps the old value |
components/conversation/LiveConversationView.tsx:143 |
qc.setQueryData(key, msg.session) — whole-object replace |
destroyed |
lib/eagerCacheSync.ts:25-31 |
{ ...s, ...session } — merge |
sticky |
effort, permissionMode and model are populated only on the REST path — parseStatusLine is called once, from handleGetSession (tb-streamer/src/server.ts:4522-4537), and never from the broadcast path, because managedToResponse is synchronous and holds no PTY handle.
So those fields are absent from every session_update frame, and the replace at LiveConversationView.tsx:143 wipes them from the cache for as long as the conversation screen is mounted.
Both handlers are subscribed simultaneously on the session detail screen, so whichever fires last wins.
Severity is bounded, but confirm before closing
The alarming version of this — the replace flipping deriveSessionPresentation out of its external_live or stale branch and changing what the badge says — does not apply.
Checked on the streamer side: all six session_update emit sites broadcast sessionStore.get(), whose managedToResponse carries lifecycle and ownership unconditionally (tb-streamer/src/session-store.ts:250, :272).
processLiveness has exactly one assignment site in the entire streamer (:325, hard-coded to "alive"), so the stale branch cannot be entered from a server signal at all.
That leaves this as lost metadata rather than a wrong label.
Why fix it anyway
A whole-object replace against a frame that is a strict subset of the REST shape is fragile regardless of today's field list.
It also interacts with planned work: merging is strictly better for the fields that exist now, and is simultaneously what would make a future liveness-sensitive field latch — which is why the agent sub-status design (threadbase-streamer#538) puts an explicit-null clearing contract on the server rather than relying on absence.
Land the merge with a comment saying so, rather than rediscovering the interaction later.
Found during review of the sub-status plan (threadbase-streamer#537); deliberately kept out of that feature's scope because it is independently correct.
Three handlers write the same React Query key with incompatible semantics, and one of them replaces the whole object.
Verified state
Checked 2026-08-12 against
origin/main@86eeb2a1.app/_layout.tsx:164-166{ ...prev, ...msg.session }— mergecomponents/conversation/LiveConversationView.tsx:143qc.setQueryData(key, msg.session)— whole-object replacelib/eagerCacheSync.ts:25-31{ ...s, ...session }— mergeeffort,permissionModeandmodelare populated only on the REST path —parseStatusLineis called once, fromhandleGetSession(tb-streamer/src/server.ts:4522-4537), and never from the broadcast path, becausemanagedToResponseis synchronous and holds no PTY handle.So those fields are absent from every
session_updateframe, and the replace atLiveConversationView.tsx:143wipes them from the cache for as long as the conversation screen is mounted.Both handlers are subscribed simultaneously on the session detail screen, so whichever fires last wins.
Severity is bounded, but confirm before closing
The alarming version of this — the replace flipping
deriveSessionPresentationout of itsexternal_liveorstalebranch and changing what the badge says — does not apply.Checked on the streamer side: all six
session_updateemit sites broadcastsessionStore.get(), whosemanagedToResponsecarrieslifecycleandownershipunconditionally (tb-streamer/src/session-store.ts:250,:272).processLivenesshas exactly one assignment site in the entire streamer (:325, hard-coded to"alive"), so thestalebranch cannot be entered from a server signal at all.That leaves this as lost metadata rather than a wrong label.
Why fix it anyway
A whole-object replace against a frame that is a strict subset of the REST shape is fragile regardless of today's field list.
It also interacts with planned work: merging is strictly better for the fields that exist now, and is simultaneously what would make a future liveness-sensitive field latch — which is why the agent sub-status design (threadbase-streamer#538) puts an explicit-null clearing contract on the server rather than relying on absence.
Land the merge with a comment saying so, rather than rediscovering the interaction later.
Found during review of the sub-status plan (threadbase-streamer#537); deliberately kept out of that feature's scope because it is independently correct.