SIGN IN SIGN UP

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