07b87f1470
selectProfile(name) / newSessionInProfile(name) keep only $newChatProfile and clear $newChatRoute. #94147 taught the send path to capture the (registry source, profile) pair as the draft's exact owner, but three gaps still let a session created on the composite gateway conn:local::omar degrade to the bare string "omar" — and requestGatewayForProfile("omar") is a DIFFERENT socket than the one that minted the runtime, so the next session-scoped RPC 4001'd "session not found" while the runtime was ws-orphan-reaped: 1. Ownership persistence. The exact owner lived only in the bounded, in-memory owner-hint map. The primary aggregate serves a `local` registry source's rows WITHOUT connection_id (the unified-list splice tags non-local sources only), and mergeSessionPage replaced the optimistic row with that untagged row on the first sidebar refresh. After a hint eviction or a relaunch nothing exact was left. - One canonical SessionOwnerRoute type (store/session-request-router); AgentProfileRoute / SessionProfileRoute / SessionRpcOwnerRoute alias it. - Every owner ladder gains the connection-tagged ROW rung (knownSessionOwner / sessionOwnerRouteFromRow): the session-RPC dispatcher, knownOwnerForSession, the tile delegate, foregroundSessionScopes and the async probe (resolveSessionOwner) all yield the exact route when the row carries its connection. - mergeSessionPage carries connection_id onto a row that comes back untagged for the same profile (merge, don't clobber). - Owner hints are persisted (bounded LRU, hermes.desktop.sessionOwnerHints.v1) and rehydrated in LRU order; a removed registry connection drops its hints (use-gateway-boot onChanged) so fail-closed can't pin sessions to a dead source. 2. Fail closed. A request carrying session_id whose owner no rung could name silently fell to the ambient presentation gateway, turning missing metadata into a misleading backend "session not found". createSessionRpcDispatcher and requestForOwnedSession now reject with an explicit SessionOwnerResolutionError (store/session-owner-resolution). The ONE case where ambient is the owner by construction stays ambient: no registry source live AND at most one profile (legacy single-backend Desktop, whose older backends omit `profile` on rows). Main-pane runtime ids (native approval.respond, queued sends) now translate to their stored id through the per-runtime state mirror (storedSessionIdForRuntimeId), so they resolve an owner instead of tripping the gate. 3. Lifecycle. Between session.create returning and the foreground publication ($selectedStoredSessionId via navigate → route effect, or $sessionTiles), the owner entry had no active request and was not yet foreground-pinned: a prune recompute or a refcount-0 lease release could close the socket holding the just-minted runtime before the first prompt.submit. Both create paths now hold retainGatewayForAgent across the create RPC and hand off to holdSessionOwnerUntilForeground (session-states), which names the owner in foregroundSessionScopes — every registry dispose path honors it — until the session becomes selected/tiled, the caller releases it (failed create, mid-create drift close), or a 60s TTL expires. Nothing latches. Regressions: - profile-rail-fresh-chat-owner.test.tsx: new case evicts the hint AND merges an untagged refresh row after turn one; turn two still rides the same conn:local::omar socket, no probe, no session.close, no v1 socket; the existing case also asserts the owner is foreground-pinned from create. - session-rpc-dispatcher.test.ts (new): fail-closed error + ambient never called; legacy single-backend stays ambient; tagged-row rung routes by runtime id; hint outranks untagged row; probe result routes exactly. - session.test.ts: persisted hints survive a simulated relaunch in LRU order, malformed storage is ignored, per-connection forget; knownSessionOwner; mergeSessionPage carries / drops / preserves identity on connection_id. - session-states-foreground-scopes.test.ts: tagged-row scope for the selected thread; hold pins from create, retires on selection / tile mount, explicit release, TTL expiry. - session-states-runtime-map.test.ts: state-mirror rung; knownOwnerForSession through mirror + hint / tagged row; requestForOwnedSession fail-closed vs legacy ambient. - wiring-routing.test.ts: connection-tagged row rung; hint still outranks it. Stacked on #94145, #94147 and #94178 (merged as the integration base); review from this commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>