fix(canvas): repair an expanded card returning from another page
The canvas is a full-page route, so opening Tasks or Conversations unmounts every card on it while the workspace's own tabs are merely hidden behind the overlay. An expanded card is therefore the one live-conversation surface that routinely disappears while its conversation keeps working, and the runtime session it left behind stopped keeping up with it. `liveMessage` is written by a sink the card registers, so it froze at whatever had streamed in by the time the route changed; the global `turn_complete` handler that promotes turns for any conversation with no tab open then drained that frozen fragment into `localTurns`, where the timeline let it mask the complete persisted reply. A turn that ended with no completion this client saw instead left `awaiting_persist` pinned, and the optimistic user turn with it, on top of the persisted copy of the same message. Neither healed, because `useConversationDetail` never re-fetches a detail it already holds: collapsing and re-expanding the card, or coming back the next day, showed the same truncated transcript until the app restarted. A card re-entering a session an earlier surface loaded now unpins `awaiting_persist` and refetches, so the database has the last word. The unpin is gated on this card's own connection not being the one prompting, and happens before the live-message sink registers, whose setup replays the connection's retained message — pinned, `SET_LIVE_MESSAGE` would write the stale fragment straight back in. A reply that is genuinely still streaming survives untouched either way: the backend stamps `in_flight_user_turn_id` on a mid-turn detail and the reducer keeps every live buffer for those. Cards whose agent is already running now come back live rather than dormant. Dormancy is there so a board with six remembered expansions doesn't start six agent CLIs, not to refuse one that is already there — and left asleep, such a card showed a connection nobody owned, which `registerLiveSurfaceKeys` therefore never claimed, so the idle sweep reclaimed it a minute after the turn settled. Adopting one starts nothing: `connect()` returns on its fast path for an entry with the same agent and working directory. The connection key a card inherits from the draft that created it is remembered too, stored with the conversation and the `created_at` it was written for. Held only in memory it died with the route switch, and the card then looked under `canvas-node-<id>` while the agent its first message started was still running under the draft's key. Naming the card rather than its id is what keeps an entry from another database — this storage is deliberately not backend-scoped — from resolving onto an unrelated card, and it replaces a neighbouring comment's claim that SQLite reuses these rowids, which `AUTOINCREMENT` rules out.
X
xintaofei committed
c726d96e883c050dd08335cb150a9fb5eff3374c
Parent: 17e60fc