fix(agents): page sessions after filtering (#122725)

This commit is contained in:
Peter Steinberger
2026-08-12 10:14:23 -07:00
committed by GitHub
parent 5af8935b53
commit b50a7fc4cd
3 changed files with 188 additions and 79 deletions
+2 -1
View File
@@ -524,7 +524,8 @@ describe("sessions tools", () => {
includeGlobal: true,
includeUnknown: true,
label: "mailbox",
limit: undefined,
limit: 200,
offset: 0,
search: "review",
spawnedBy: undefined,
},
@@ -66,6 +66,28 @@ function getSessionsListDetails(result: { details?: unknown }): SessionsListDeta
return result.details as SessionsListDetails;
}
function sessionRow(key: string, classification = "dashboard", agentId = "main") {
return { key, agentId, kind: "direct", classification };
}
function mockSessionPages(pages: Array<Array<Record<string, unknown>>>) {
let pageIndex = 0;
let nextOffset = 0;
mocks.gatewayCall.mockImplementation(async (opts: unknown) => {
const request = opts as { params?: { limit?: number; offset?: number } };
expect(request.params).toEqual(expect.objectContaining({ limit: 200, offset: nextOffset }));
const sessions = pages[pageIndex] ?? [];
pageIndex += 1;
nextOffset += sessions.length;
return {
path: "/tmp/sessions.json",
sessions,
hasMore: pageIndex < pages.length,
nextOffset: pageIndex < pages.length ? nextOffset : null,
};
});
}
describe("sessions-list-tool", () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -155,6 +177,68 @@ describe("sessions-list-tool", () => {
});
});
it.each([
{
name: "hidden and global rows",
params: { limit: 1 },
pages: [
[
{ key: "global", kind: "global", classification: "global", agentId: "main" },
sessionRow("agent:other:dashboard:hidden", "dashboard", "other"),
],
[sessionRow("agent:main:main", "main")],
],
},
{
name: "non-matching kinds",
params: { kinds: ["main"], limit: 1 },
pages: [[sessionRow("agent:main:dashboard:other")], [sessionRow("agent:main:main", "main")]],
},
])("fills the requested output limit past $name", async ({ params, pages }) => {
mockSessionPages(pages);
const result = await createSessionsListTool({ config: VALID_CONFIG }).execute(
"paged-list",
params,
);
expect(getSessionsListDetails(result).sessions?.map((session) => session.key)).toEqual([
"agent:main:main",
]);
expect(mocks.gatewayCall).toHaveBeenCalledTimes(2);
});
it("fails visibly when Gateway pagination stalls", async () => {
mocks.gatewayCall.mockResolvedValue({
path: "/tmp/sessions.json",
sessions: [{ key: "global", kind: "global", classification: "global" }],
hasMore: true,
nextOffset: 0,
});
await expect(
createSessionsListTool({ config: VALID_CONFIG }).execute("stalled-list", { limit: 1 }),
).rejects.toThrow("sessions.list returned invalid pagination");
});
it("deduplicates rows when a changing Gateway page overlaps the prior page", async () => {
const first = sessionRow("agent:main:dashboard:first");
const overlap = sessionRow("agent:main:dashboard:overlap");
const finalRow = { ...first, key: "agent:main:dashboard:final" };
mockSessionPages([
[first, overlap],
[overlap, finalRow],
]);
const result = await createSessionsListTool({ config: VALID_CONFIG }).execute(
"overlapping-list",
{ limit: 3 },
);
const keys = getSessionsListDetails(result).sessions?.map((session) => session.key) ?? [];
expect(keys).toEqual([first.key, overlap.key, finalRow.key]);
});
it("adds nonzero state versions with one batch lookup", async () => {
mocks.gatewayCall.mockResolvedValue({
path: "/tmp/sessions.json",
+102 -78
View File
@@ -190,50 +190,7 @@ export function createSessionsListTool(opts?: {
const gatewayCall = opts?.callGateway ?? callAgentToolGatewayRequest;
const a2aPolicy = createAgentToAgentPolicy(cfg);
const hydrateTranscriptFieldsAfterFiltering = includeDerivedTitles || includeLastMessage;
const list = await gatewayCall<{ sessions: Array<GatewaySessionListRow>; path: string }>({
method: "sessions.list",
params: {
limit,
activeMinutes,
label,
agentId,
search,
archived,
includeDerivedTitles: false,
includeLastMessage: false,
includeGlobal: !restrictToSpawned,
includeUnknown: !restrictToSpawned,
spawnedBy: restrictToSpawned ? effectiveRequesterKey : undefined,
},
});
// Cross-session tool output is copied into durable transcripts, so exposing
// incognito rows here would defeat their process-only lifetime.
const sessions = (Array.isArray(list?.sessions) ? list.sessions : []).filter(
(entry) => !entry || typeof entry !== "object" || !isIncognitoSessionKey(entry.key),
);
const defaultAgentId = resolveDefaultAgentId(cfg);
const stateVersions = getSessionStateVersions(
sessions.flatMap((entry) => {
if (!entry || typeof entry !== "object" || typeof entry.key !== "string") {
return [];
}
let stateAgentId =
typeof entry.agentId === "string" && entry.agentId ? entry.agentId : undefined;
if (!stateAgentId) {
try {
stateAgentId = resolveAgentIdFromSessionKey(entry.key, defaultAgentId);
} catch {
// Malformed rows remain subject to the fail-closed visibility checker below,
// but cannot participate in agent state-version lookup.
return [];
}
}
return [{ sessionKey: entry.key, agentId: stateAgentId }];
}),
);
const storePath = typeof list?.path === "string" ? list.path : undefined;
const visibilityGuard = createSessionVisibilityRowChecker({
action: "list",
defaultAgentId,
@@ -241,6 +198,107 @@ export function createSessionsListTool(opts?: {
visibility,
a2aPolicy,
});
const sessions: GatewaySessionListRow[] = [];
const seenKeys = new Set<string>();
const outputLimit = limit ?? 100;
let offset = 0;
let storePath: string | undefined;
for (let pageIndex = 0; sessions.length < outputLimit; pageIndex += 1) {
const page = await gatewayCall<{
sessions?: GatewaySessionListRow[];
path?: string;
hasMore?: boolean;
nextOffset?: number | null;
}>({
method: "sessions.list",
params: {
limit: 200,
offset,
activeMinutes,
label,
agentId,
search,
archived,
includeDerivedTitles: false,
includeLastMessage: false,
includeGlobal: !restrictToSpawned,
includeUnknown: !restrictToSpawned,
spawnedBy: restrictToSpawned ? effectiveRequesterKey : undefined,
},
});
storePath ??= typeof page?.path === "string" ? page.path : undefined;
const pageSessions = Array.isArray(page?.sessions) ? page.sessions : [];
for (const entry of pageSessions) {
const key =
entry && typeof entry === "object" && typeof entry.key === "string" ? entry.key : "";
if (!key || seenKeys.has(key)) {
continue;
}
seenKeys.add(key);
// Cross-session tool output is copied into durable transcripts, so exposing
// incognito rows here would defeat their process-only lifetime.
if (isIncognitoSessionKey(key)) {
continue;
}
const access = visibilityGuard.check({
key,
agentId: typeof entry.agentId === "string" ? entry.agentId : undefined,
ownerSessionKey:
typeof (entry as { ownerSessionKey?: unknown }).ownerSessionKey === "string"
? (entry as { ownerSessionKey?: string }).ownerSessionKey
: undefined,
spawnedBy: typeof entry.spawnedBy === "string" ? entry.spawnedBy : undefined,
parentSessionKey:
typeof entry.parentSessionKey === "string" ? entry.parentSessionKey : undefined,
});
const kind = classifySessionListKind(entry);
if (
access.allowed &&
key !== "unknown" &&
(key !== "global" || alias === "global") &&
(!allowedKinds || allowedKinds.has(kind))
) {
sessions.push(entry);
if (sessions.length === outputLimit) {
break;
}
}
}
if (sessions.length === outputLimit || page?.hasMore !== true) {
break;
}
const nextOffset = page.nextOffset;
if (
typeof nextOffset !== "number" ||
!Number.isSafeInteger(nextOffset) ||
nextOffset !== offset + pageSessions.length
) {
throw new Error(
`sessions.list returned invalid pagination metadata (offset=${offset}, nextOffset=${String(nextOffset)})`,
);
}
// Bound unstable Gateway snapshots by both request count and scanned rows.
if (pageIndex >= 49 || nextOffset > 10_000) {
throw new Error("sessions.list exceeded the 50-page/10,000-row pagination scan limit");
}
offset = nextOffset;
}
const stateVersions = getSessionStateVersions(
sessions.flatMap((entry) => {
const key = entry.key;
let stateAgentId =
typeof entry.agentId === "string" && entry.agentId ? entry.agentId : undefined;
if (!stateAgentId) {
try {
stateAgentId = resolveAgentIdFromSessionKey(key, defaultAgentId);
} catch {
return [];
}
}
return [{ sessionKey: key, agentId: stateAgentId }];
}),
);
const rows: SessionListRow[] = [];
const historyTargets: Array<{ row: SessionListRow; resolvedKey: string }> = [];
const titleTargets: Array<{
@@ -253,42 +311,8 @@ export function createSessionsListTool(opts?: {
}> = [];
for (const entry of sessions) {
if (!entry || typeof entry !== "object") {
continue;
}
const key = typeof entry.key === "string" ? entry.key : "";
if (!key) {
continue;
}
const access = visibilityGuard.check({
key,
agentId: typeof entry.agentId === "string" ? entry.agentId : undefined,
ownerSessionKey:
typeof (entry as { ownerSessionKey?: unknown }).ownerSessionKey === "string"
? (entry as { ownerSessionKey?: string }).ownerSessionKey
: undefined,
spawnedBy: typeof entry.spawnedBy === "string" ? entry.spawnedBy : undefined,
parentSessionKey:
typeof entry.parentSessionKey === "string" ? entry.parentSessionKey : undefined,
});
if (!access.allowed) {
continue;
}
// Gateway listings include pseudo/global rows for UI callers. The tool only exposes real
// sessions and the explicit global session when the requester is already global.
if (key === "unknown") {
continue;
}
if (key === "global" && alias !== "global") {
continue;
}
const key = entry.key;
const kind = classifySessionListKind(entry);
if (allowedKinds && !allowedKinds.has(kind)) {
continue;
}
const displayKey = resolveDisplaySessionKey({
key,
alias,