Gateway: harden manual compaction checkpoints

This commit is contained in:
scoootscooob
2026-04-06 16:39:43 -07:00
parent 66a281c4f9
commit 4af9bf751f
5 changed files with 422 additions and 31 deletions

View File

@@ -1,5 +1,10 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { deleteSessionsAndRefresh, subscribeSessions, type SessionsState } from "./sessions.ts";
import {
deleteSessionsAndRefresh,
loadSessions,
subscribeSessions,
type SessionsState,
} from "./sessions.ts";
type RequestFn = (method: string, params?: unknown) => Promise<unknown>;
@@ -125,3 +130,91 @@ describe("deleteSessionsAndRefresh", () => {
expect(request).not.toHaveBeenCalled();
});
});
describe("loadSessions", () => {
it("refreshes expanded checkpoint cards when the row summary changes", async () => {
const request = vi.fn(async (method: string) => {
if (method === "sessions.list") {
return {
ts: 1,
path: "(multiple)",
count: 1,
defaults: {},
sessions: [
{
key: "agent:main:main",
kind: "direct",
updatedAt: 1,
compactionCheckpointCount: 1,
latestCompactionCheckpoint: {
checkpointId: "checkpoint-new",
createdAt: 20,
},
},
],
};
}
if (method === "sessions.compaction.list") {
return {
ok: true,
key: "agent:main:main",
checkpoints: [
{
checkpointId: "checkpoint-new",
sessionKey: "agent:main:main",
sessionId: "session-1",
createdAt: 20,
reason: "manual",
},
],
};
}
throw new Error(`unexpected method: ${method}`);
});
const state = createState(request, {
sessionsExpandedCheckpointKey: "agent:main:main",
sessionsResult: {
ts: 0,
path: "(multiple)",
count: 1,
defaults: {},
sessions: [
{
key: "agent:main:main",
kind: "direct",
updatedAt: 0,
compactionCheckpointCount: 3,
latestCompactionCheckpoint: {
checkpointId: "checkpoint-old",
createdAt: 10,
},
},
],
} as never,
sessionsCheckpointItemsByKey: {
"agent:main:main": [
{
checkpointId: "checkpoint-old",
sessionKey: "agent:main:main",
sessionId: "session-old",
createdAt: 10,
reason: "manual",
},
] as never,
},
});
await loadSessions(state);
expect(request).toHaveBeenNthCalledWith(1, "sessions.list", {
includeGlobal: true,
includeUnknown: true,
});
expect(request).toHaveBeenNthCalledWith(2, "sessions.compaction.list", {
key: "agent:main:main",
});
expect(
state.sessionsCheckpointItemsByKey["agent:main:main"]?.map((item) => item.checkpointId),
).toEqual(["checkpoint-new"]);
});
});

View File

@@ -29,6 +29,67 @@ export type SessionsState = {
sessionsCheckpointErrorByKey: Record<string, string>;
};
function checkpointSignature(
row:
| {
key: string;
compactionCheckpointCount?: number;
latestCompactionCheckpoint?: { checkpointId?: string; createdAt?: number } | null;
}
| undefined,
): string {
return JSON.stringify({
key: row?.key ?? "",
count: row?.compactionCheckpointCount ?? 0,
latestCheckpointId: row?.latestCompactionCheckpoint?.checkpointId ?? "",
latestCreatedAt: row?.latestCompactionCheckpoint?.createdAt ?? 0,
});
}
function invalidateCheckpointCacheForKey(state: SessionsState, key: string) {
if (
!(key in state.sessionsCheckpointItemsByKey) &&
!(key in state.sessionsCheckpointErrorByKey)
) {
return;
}
const nextItems = { ...state.sessionsCheckpointItemsByKey };
const nextErrors = { ...state.sessionsCheckpointErrorByKey };
delete nextItems[key];
delete nextErrors[key];
state.sessionsCheckpointItemsByKey = nextItems;
state.sessionsCheckpointErrorByKey = nextErrors;
}
async function fetchSessionCompactionCheckpoints(state: SessionsState, key: string) {
state.sessionsCheckpointLoadingKey = key;
state.sessionsCheckpointErrorByKey = {
...state.sessionsCheckpointErrorByKey,
[key]: "",
};
try {
const result = await state.client?.request<SessionsCompactionListResult>(
"sessions.compaction.list",
{ key },
);
if (result) {
state.sessionsCheckpointItemsByKey = {
...state.sessionsCheckpointItemsByKey,
[key]: result.checkpoints ?? [],
};
}
} catch (err) {
state.sessionsCheckpointErrorByKey = {
...state.sessionsCheckpointErrorByKey,
[key]: String(err),
};
} finally {
if (state.sessionsCheckpointLoadingKey === key) {
state.sessionsCheckpointLoadingKey = null;
}
}
}
export async function subscribeSessions(state: SessionsState) {
if (!state.client || !state.connected) {
return;
@@ -58,6 +119,9 @@ export async function loadSessions(
state.sessionsLoading = true;
state.sessionsError = null;
try {
const previousRows = new Map(
(state.sessionsResult?.sessions ?? []).map((row) => [row.key, row] as const),
);
const includeGlobal = overrides?.includeGlobal ?? state.sessionsIncludeGlobal;
const includeUnknown = overrides?.includeUnknown ?? state.sessionsIncludeUnknown;
const activeMinutes = overrides?.activeMinutes ?? toNumber(state.sessionsFilterActive, 0);
@@ -75,6 +139,30 @@ export async function loadSessions(
const res = await state.client.request<SessionsListResult | undefined>("sessions.list", params);
if (res) {
state.sessionsResult = res;
const nextKeys = new Set(res.sessions.map((row) => row.key));
for (const key of Object.keys(state.sessionsCheckpointItemsByKey)) {
if (!nextKeys.has(key)) {
invalidateCheckpointCacheForKey(state, key);
}
}
let expandedNeedsRefetch = false;
for (const row of res.sessions) {
const previous = previousRows.get(row.key);
if (checkpointSignature(previous) !== checkpointSignature(row)) {
invalidateCheckpointCacheForKey(state, row.key);
if (state.sessionsExpandedCheckpointKey === row.key) {
expandedNeedsRefetch = true;
}
}
}
const expandedKey = state.sessionsExpandedCheckpointKey;
if (
expandedKey &&
nextKeys.has(expandedKey) &&
(expandedNeedsRefetch || !state.sessionsCheckpointItemsByKey[expandedKey])
) {
await fetchSessionCompactionCheckpoints(state, expandedKey);
}
}
} catch (err) {
if (isMissingOperatorReadScopeError(err)) {
@@ -181,32 +269,7 @@ export async function toggleSessionCompactionCheckpoints(state: SessionsState, k
if (state.sessionsCheckpointItemsByKey[trimmedKey]) {
return;
}
state.sessionsCheckpointLoadingKey = trimmedKey;
state.sessionsCheckpointErrorByKey = {
...state.sessionsCheckpointErrorByKey,
[trimmedKey]: "",
};
try {
const result = await state.client?.request<SessionsCompactionListResult>(
"sessions.compaction.list",
{ key: trimmedKey },
);
if (result) {
state.sessionsCheckpointItemsByKey = {
...state.sessionsCheckpointItemsByKey,
[trimmedKey]: result.checkpoints ?? [],
};
}
} catch (err) {
state.sessionsCheckpointErrorByKey = {
...state.sessionsCheckpointErrorByKey,
[trimmedKey]: String(err),
};
} finally {
if (state.sessionsCheckpointLoadingKey === trimmedKey) {
state.sessionsCheckpointLoadingKey = null;
}
}
await fetchSessionCompactionCheckpoints(state, trimmedKey);
}
export async function branchSessionFromCheckpoint(