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>
This commit is contained in:
ClawSweeper
2026-08-20 15:17:12 +00:00
parent e68b5c778e
commit e810dd38bf
2 changed files with 84 additions and 61 deletions
+12 -61
View File
@@ -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<GatewaySessionRow, "archivedAt" | "archivedBy" | "sessionId">;
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<string, ConfirmedArchiveState>();
const confirmedOwners = new Map<string, ConfirmedOwnerState>();
const ownerAssignments = createSessionOwnerAssignmentOverlay();
const preparedWorkSessionKeys = new Set<string>();
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();
},
};
@@ -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<string, ConfirmedOwnerClaim>();
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);
}
}
},
};
}