The salvaged guard migrated the `__new__` bucket when the composer's runtime
session id went from empty to set in the same render as the scope change.
That misses the reporter's path and over-reaches on another:
- Cold-start resume-last-session (use-desktop-integrations) navigates to the
remembered route as soon as the session list loads; the route flips the
composer scope immediately while `session.resume` publishes the runtime id
later, so the guard saw an empty id and never migrated — the typed text
vanished exactly as reported.
- Opening an existing session from a fresh chat also goes empty → set, so the
guard would carry the new-chat draft into whatever session the user clicked.
Drafts are per scope by design (the id-rotation migration in chat/index.tsx
is deliberately same-conversation only); a sidebar click must leave the
new-chat draft in its bucket.
The composer swap cannot tell the two apart from its props, so the site that
assigns the id says so: `announceNewSessionDraftKey(stored)` at the two seams
where a fresh chat becomes a stored session (first-send `session.create` in
createBackendSessionForSend, cold-start restore of the remembered session /
route), consumed once by the swap effect via `adoptNewSessionDraft(scope)`,
which moves the bucket after the outgoing cleanup stashed the live editor text
under it and before the incoming scope is restored — so the text follows the
chat with no duplicate left in `__new__`. The existing helper still refuses to
overwrite a non-empty destination.
The `'active'` sentinel in use-composer-draft.ts is a focus-routing target
(`markActiveComposer`), never a draft key, so it needs no migration.
Tests: the hook test now flips the scope with the runtime id still unknown
(red on origin/main and on the salvaged guard alone) plus a control that a
plain new-chat → existing-session switch leaves the bucket untouched (red on
the salvaged guard alone). The store-level duplicate of the migration test is
dropped; the non-empty-destination invariant stays.