From e810dd38bf21abb5c083eda0604cccddf97d4465 Mon Sep 17 00:00:00 2001 From: ClawSweeper Date: Thu, 20 Aug 2026 15:17:12 +0000 Subject: [PATCH] refactor(ui): isolate confirmed owner overlay Keep confirmed owner reconciliation in one owner-boundary module so stale list responses cannot bypass it and the mutation coordinator remains within repository size limits. Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com> --- ui/src/lib/sessions/session-mutations.ts | 73 +++---------------- .../session-owner-assignment-overlay.ts | 72 ++++++++++++++++++ 2 files changed, 84 insertions(+), 61 deletions(-) create mode 100644 ui/src/lib/sessions/session-owner-assignment-overlay.ts diff --git a/ui/src/lib/sessions/session-mutations.ts b/ui/src/lib/sessions/session-mutations.ts index a63856d3a530..8f127feae439 100644 --- a/ui/src/lib/sessions/session-mutations.ts +++ b/ui/src/lib/sessions/session-mutations.ts @@ -29,6 +29,7 @@ import type { SessionResetResult, SessionState, } from "./session-capability.ts"; +import { createSessionOwnerAssignmentOverlay } from "./session-owner-assignment-overlay.ts"; import { confirmsSessionDeletion, requestSessionDelete, @@ -40,11 +41,6 @@ import { type SessionPinFields = { pinned: boolean; pinnedAt: number | undefined }; type ConfirmedArchiveState = Pick; -type ConfirmedOwnerState = { - token: symbol; - owner: SessionOwner; - sessionId?: string; -}; type SessionMutationsHost = { connection: SessionConnectionOwner; @@ -67,16 +63,9 @@ export function createSessionMutations(host: SessionMutationsHost) { { token: symbol; previous: SessionPinFields; next: SessionPinFields } >(); const confirmedArchives = new Map(); - const confirmedOwners = new Map(); + const ownerAssignments = createSessionOwnerAssignmentOverlay(); const preparedWorkSessionKeys = new Set(); - const ownersMatch = (left: SessionOwner | undefined, right: SessionOwner | undefined) => - left?.actor.type === right?.actor.type && - left?.actor.id === right?.actor.id && - left?.assignedBy?.type === right?.assignedBy?.type && - left?.assignedBy?.id === right?.assignedBy?.id && - left?.assignedAt === right?.assignedAt; - const setModelOverride = (key: string, value: string | null | undefined) => { const normalizedKey = key.trim(); if (!normalizedKey) { @@ -478,7 +467,7 @@ export function createSessionMutations(host: SessionMutationsHost) { const retireBeforeRevision = Date.now(); host.retirePullRequestSummary(key); confirmedArchives.delete(key.trim()); - confirmedOwners.delete(key.trim()); + ownerAssignments.retire(key.trim()); preparedWorkSessionKeys.delete(key.trim()); host.publish({ ...host.readState(), @@ -564,7 +553,7 @@ export function createSessionMutations(host: SessionMutationsHost) { for (const key of deleted) { host.retirePullRequestSummary(key); confirmedArchives.delete(key.trim()); - confirmedOwners.delete(key.trim()); + ownerAssignments.retire(key.trim()); preparedWorkSessionKeys.delete(key.trim()); } host.publish({ @@ -623,23 +612,18 @@ export function createSessionMutations(host: SessionMutationsHost) { return null; } const normalizedKey = result.key.trim(); - const token = Symbol("session-owner-assignment"); const publishedRow = host.publishedRow(normalizedKey); - confirmedOwners.set(normalizedKey, { - token, - owner: result.owner, - ...(publishedRow?.sessionId ? { sessionId: publishedRow.sessionId } : {}), - }); + const claim = ownerAssignments.confirm(normalizedKey, result.owner, publishedRow?.sessionId); patchRowLocal(result.key, { owner: result.owner }); host.redecorateLists(); void host.refreshReplacement(options.agentId).then(() => { - if (confirmedOwners.get(normalizedKey)?.token !== token) { + if (!ownerAssignments.isCurrent(normalizedKey, claim)) { return; } // A replacement that still disagrees is authoritative (for example, // another window reassigned the session). Drop the overlay, then load // once more without an older request ahead of it. - confirmedOwners.delete(normalizedKey); + ownerAssignments.retire(normalizedKey); void host.refreshReplacement(options.agentId); }); return result.owner; @@ -714,42 +698,9 @@ export function createSessionMutations(host: SessionMutationsHost) { }); return changed ? { ...result, sessions } : result; }, - applyConfirmedOwners(result: SessionsListResult | null): SessionsListResult | null { - if (!result || confirmedOwners.size === 0) { - return result; - } - let changed = result.owners !== undefined; - const sessions = result.sessions.map((row) => { - const confirmed = confirmedOwners.get(row.key); - if (!confirmed) { - return row; - } - if (confirmed.sessionId && row.sessionId && confirmed.sessionId !== row.sessionId) { - confirmedOwners.delete(row.key); - return row; - } - if (ownersMatch(row.owner, confirmed.owner)) { - return row; - } - changed = true; - return { ...row, owner: confirmed.owner }; - }); - return changed ? { ...result, sessions, owners: undefined } : result; - }, - observeCanonicalOwners(result: SessionsListResult | null): void { - for (const row of result?.sessions ?? []) { - const confirmed = confirmedOwners.get(row.key); - if (!confirmed) { - continue; - } - if ( - (confirmed.sessionId && row.sessionId && confirmed.sessionId !== row.sessionId) || - ownersMatch(row.owner, confirmed.owner) - ) { - confirmedOwners.delete(row.key); - } - } - }, + applyConfirmedOwners: (result: SessionsListResult | null) => ownerAssignments.decorate(result), + observeCanonicalOwners: (result: SessionsListResult | null) => + ownerAssignments.observeCanonical(result), observeArchiveState(key: string, archived: boolean | null, row?: GatewaySessionRow): void { const normalizedKey = key.trim(); if (!normalizedKey || archived === null) { @@ -796,7 +747,7 @@ export function createSessionMutations(host: SessionMutationsHost) { // replacement, so it is the one that needs an explicit rollback below. pendingPinPatches.clear(); confirmedArchives.clear(); - confirmedOwners.clear(); + ownerAssignments.clear(); preparedWorkSessionKeys.clear(); const state = host.readState(); if (Object.keys(state.modelOverrides).length > 0) { @@ -807,7 +758,7 @@ export function createSessionMutations(host: SessionMutationsHost) { pendingModelPatches.clear(); pendingPinPatches.clear(); confirmedArchives.clear(); - confirmedOwners.clear(); + ownerAssignments.clear(); preparedWorkSessionKeys.clear(); }, }; diff --git a/ui/src/lib/sessions/session-owner-assignment-overlay.ts b/ui/src/lib/sessions/session-owner-assignment-overlay.ts new file mode 100644 index 000000000000..796cbead1025 --- /dev/null +++ b/ui/src/lib/sessions/session-owner-assignment-overlay.ts @@ -0,0 +1,72 @@ +import type { SessionOwner } from "../../../../packages/gateway-protocol/src/index.js"; +import type { SessionsListResult } from "../../api/types.ts"; + +type ConfirmedOwnerClaim = { + owner: SessionOwner; + sessionId?: string; +}; + +function ownersMatch(left: SessionOwner | undefined, right: SessionOwner): boolean { + return ( + left?.actor.type === right.actor.type && + left.actor.id === right.actor.id && + left.assignedBy?.type === right.assignedBy?.type && + left.assignedBy?.id === right.assignedBy?.id && + left.assignedAt === right.assignedAt + ); +} + +export function createSessionOwnerAssignmentOverlay() { + const claims = new Map(); + + return { + confirm(key: string, owner: SessionOwner, sessionId?: string): ConfirmedOwnerClaim { + const claim = { owner, ...(sessionId ? { sessionId } : {}) }; + claims.set(key, claim); + return claim; + }, + isCurrent(key: string, claim: ConfirmedOwnerClaim): boolean { + return claims.get(key) === claim; + }, + retire(key: string): void { + claims.delete(key); + }, + clear(): void { + claims.clear(); + }, + decorate(result: SessionsListResult | null): SessionsListResult | null { + if (!result || claims.size === 0) { + return result; + } + let changed = result.owners !== undefined; + const sessions = result.sessions.map((row) => { + const claim = claims.get(row.key); + if (!claim) { + return row; + } + if (claim.sessionId && row.sessionId && claim.sessionId !== row.sessionId) { + claims.delete(row.key); + return row; + } + if (ownersMatch(row.owner, claim.owner)) { + return row; + } + changed = true; + return { ...row, owner: claim.owner }; + }); + return changed ? { ...result, sessions, owners: undefined } : result; + }, + observeCanonical(result: SessionsListResult | null): void { + for (const row of result?.sessions ?? []) { + const claim = claims.get(row.key); + if ( + claim && + ((claim.sessionId && row.sessionId && claim.sessionId !== row.sessionId) || + ownersMatch(row.owner, claim.owner)) + ) { + claims.delete(row.key); + } + } + }, + }; +}