Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions src/triggers/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ function parseOptionalInt(raw: unknown): number | undefined {
return Number.isFinite(n) ? n : undefined;
}

function parsePositiveLimit(raw: unknown): number | undefined {
const n = parseOptionalInt(raw);
return n !== undefined && n > 0 ? n : undefined;
}

function checkAuth(
req: ApiRequest,
secret: string | undefined,
Expand Down Expand Up @@ -502,7 +507,9 @@ export function registerApiTriggers(
sessions.sort((a, b) =>
(b.startedAt || "").localeCompare(a.startedAt || ""),
);
return { status_code: 200, body: { success: true, sessions } };
const limit = parsePositiveLimit(req.query_params?.["limit"]);
const limited = limit !== undefined ? sessions.slice(0, limit) : sessions;
return { status_code: 200, body: { success: true, sessions: limited } };
},
);
sdk.registerTrigger({
Expand Down Expand Up @@ -823,12 +830,14 @@ export function registerApiTriggers(
const filtered = filterAgentId
? sessions.filter((s) => s.agentId === filterAgentId)
: sessions;
const limit = parsePositiveLimit(req.query_params?.["limit"]);
const sliced = limit !== undefined ? filtered.slice(0, limit) : filtered;
const summaries = await Promise.all(
filtered.map((s) =>
sliced.map((s) =>
kv.get<SessionSummary>(KV.summaries, s.id).catch(() => null),
),
);
const withSummary = filtered.map((s, i) =>
const withSummary = sliced.map((s, i) =>
summaries[i] ? { ...s, summary: summaries[i] } : s,
);
return { status_code: 200, body: { sessions: withSummary } };
Expand Down
56 changes: 56 additions & 0 deletions test/sessions-limit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";

// GET /agentmemory/sessions and GET /agentmemory/replay/sessions must
// accept a `limit` query param so the viewer and CLI can page through
// large session tables without forcing the engine to ship every
// record on every request (#1022).
//
// Source-level assertions: the HTTP endpoint is defined inside a
// single big registerFunction call and spinning up the iii engine
// just to test a query param is out of scope for this fix.
describe("sessions endpoint limit query param (#1022)", () => {
const api = readFileSync("src/triggers/api.ts", "utf-8");

it("api::sessions uses parsePositiveLimit for the limit query param", () => {
expect(api).toMatch(
/sdk\.registerFunction\(\s*"api::sessions"[\s\S]*?parsePositiveLimit\(req\.query_params\?\.\["limit"\]\)/,
);
});

it("api::sessions slices filtered before summary lookups", () => {
expect(api).toMatch(
/sdk\.registerFunction\(\s*"api::sessions"[\s\S]*?sliced\.map\([\s\S]*?kv\.get/,
);
});

it("api::sessions falls back to full list when limit is missing or invalid", () => {
expect(api).toMatch(
/sdk\.registerFunction\(\s*"api::sessions"[\s\S]*?limit !== undefined \? filtered\.slice/,
);
});

it("api::replay::sessions also honours limit (companion fix)", () => {
expect(api).toMatch(
/sdk\.registerFunction\(\s*"api::replay::sessions"[\s\S]*?parsePositiveLimit\(req\.query_params\?\.\["limit"\]\)/,
);
expect(api).toMatch(
/sdk\.registerFunction\(\s*"api::replay::sessions"[\s\S]*?sessions\.slice\(0,\s*limit\)/,
);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it("api::replay::sessions falls back to full list when limit is missing or invalid", () => {
expect(api).toMatch(
/sdk\.registerFunction\(\s*"api::replay::sessions"[\s\S]*?limit !== undefined \? sessions\.slice/,
);
});

it("parsePositiveLimit helper is defined and delegates to parseOptionalInt", () => {
expect(api).toMatch(
/function parsePositiveLimit\(raw: unknown\): number \| undefined/,
);
expect(api).toMatch(
/parsePositiveLimit[\s\S]*?parseOptionalInt\(raw\)/,
);
});
});