diff --git a/apps/desktop/src/api/sessions.ts b/apps/desktop/src/api/sessions.ts index c89ce10756..3e51132d06 100644 --- a/apps/desktop/src/api/sessions.ts +++ b/apps/desktop/src/api/sessions.ts @@ -1,4 +1,5 @@ import { isMissingRestEndpoint } from '@/lib/gateway-rpc' +import { stampRowsWithOwningConnection } from '@/lib/session-owner-stamp' import { recordTranscriptTail } from '@/store/transcript-tail' import type { PaginatedSessions, @@ -37,16 +38,11 @@ function sessionScopeQuery(scope?: ProfileScope): string { * correctly know nothing about this Desktop-local registry id. Preserve an * explicit owner from a multi-source response; otherwise stamp the active * non-local source so a later resume cannot fall back to a same-named local - * profile. + * profile. Delegates to the canonical row-stamp helper so this stays the ONE + * write shape for connection_id on backend-returned rows. */ function stampActiveConnectionOwner(sessions: SessionInfo[]): SessionInfo[] { - const connectionId = getApiRequestConnection()?.trim() - - if (!connectionId || connectionId === 'local') { - return sessions - } - - return sessions.map(session => (session.connection_id ? session : { ...session, connection_id: connectionId })) + return stampRowsWithOwningConnection(sessions, getApiRequestConnection()) } /** diff --git a/apps/desktop/src/lib/session-owner-stamp.ts b/apps/desktop/src/lib/session-owner-stamp.ts new file mode 100644 index 0000000000..09a6b8a58d --- /dev/null +++ b/apps/desktop/src/lib/session-owner-stamp.ts @@ -0,0 +1,28 @@ +import type { SessionInfo } from '@/types/hermes' + +/** + * THE canonical write path for tagging backend-returned session rows with the + * registry connection that owns them (the read counterpart is + * `sessionOwnerRouteFromRow` in store/session-request-router). + * + * Every other `connection_id` writer works from an EXACT captured owner route + * (the optimistic row in upsertOptimisticSession, the cache patch in + * use-session-actions/utils, the mergeSessionPage carry) — those are + * authoritative and this helper must never clobber them, so a row that + * already names an owner is returned untouched. Only untagged rows served by + * an active NON-local source get stamped: the gateway's HTTP APIs correctly + * know nothing about Desktop-local registry ids, and an untagged remote row + * would let a later resume fall back to a same-named local profile + * ("session not found" on turn two). `local` is never stamped — a bare local + * row already routes correctly and a `local` tag would only pin it against + * the fail-closed owner resolution for no benefit. + */ +export function stampRowsWithOwningConnection(sessions: SessionInfo[], connectionId: null | string | undefined): SessionInfo[] { + const owner = String(connectionId ?? '').trim() + + if (!owner || owner === 'local') { + return sessions + } + + return sessions.map(session => (session.connection_id?.trim() ? session : { ...session, connection_id: owner })) +}