earendil-works/pi · error · SessionError
invalid_query
invalid_query
Error message
limit must be a positive integer
What it means
All Session query APIs — findEntries, findEntriesOnBranch, findRecords, getLog, findOpenOperations — validate their limit option up front. limit must be an integer strictly greater than zero; undefined means unlimited. This is the facade copy in session.ts; the identical storage-layer copy lives at state.ts:32, so backends built on SessionState get the same guard.
Source
Thrown at packages/agent/src/harness/session/session.ts:32
ProvisionedEntry,
RecordBase,
RecordQuery,
SessionMetadata,
SessionStats,
SessionStorage,
SessionTree,
} from "./types.ts";
import { SessionError } from "./types.ts";
type JsonValidationFrame = { value: unknown } | { exit: object };
function invalidPayload(reason: string): never {
throw new SessionError("invalid_payload", `Durable payload ${reason}`);
}
function assertValidLimit(limit: number | undefined): void {
if (limit !== undefined && (!Number.isInteger(limit) || limit <= 0)) {
throw new SessionError("invalid_query", "limit must be a positive integer");
}
}
function assertValidCursor(afterSeq: number | undefined): void {
if (afterSeq !== undefined && (!Number.isInteger(afterSeq) || afterSeq < 0)) {
throw new SessionError("invalid_query", "cursor sequence must be a non-negative integer");
}
}
export function assertJsonSerializable(value: unknown): void {
const active = new WeakSet<object>();
const stack: JsonValidationFrame[] = [{ value }];
while (stack.length > 0) {
const frame = stack.pop()!;
if ("exit" in frame) {
active.delete(frame.exit);
continue;
}View on GitHub (pinned to 4af9d21d3b)
Solutions
- Omit limit (or pass undefined) when you want all results — 0 is not 'unlimited'
- Sanitize before querying: const safe = Number.isInteger(limit) && limit > 0 ? limit : undefined
- Parse user-supplied limits with Number.parseInt and range-check them before they reach a query
- Use findEntry/findEntryOnBranch for single results instead of limit arithmetic
Example fix
// before
const entries = await session.findEntries({ limit: 0 });
// after
const limit = Number.isInteger(n) && n > 0 ? n : undefined;
const entries = await session.findEntries({ limit }); Defensive patterns
Strategy: validation
Validate before calling
const normalizeLimit = (limit: number | undefined): number | undefined =>
limit !== undefined && Number.isInteger(limit) && limit > 0 ? limit : undefined;
await session.findEntries({ limit: normalizeLimit(userLimit) }); Type guard
const isValidLimit = (limit: unknown): limit is number => typeof limit === 'number' && Number.isInteger(limit) && limit > 0;
Try / catch
try {
await session.getLog({ limit: parsed });
} catch (error) {
if (error instanceof SessionError && error.code === 'invalid_query' && error.message.startsWith('limit')) {
await session.getLog({}); // safe fallback: unlimited
} else {
throw error;
}
} Prevention
- Build all query objects through one pagination helper that validates limit
- Use undefined, never 0, to mean 'no limit'
- Validate config- and CLI-derived limits at startup, not at query time
When it happens
Trigger: findEntries({ limit: 0 }) where 0 was meant as 'no limit' (undefined is the way to express that); limit: -1; fractional limits such as 10.5; NaN produced by Number('') or unvalidated user input; computed limits like arr.length - 1 on an empty array.
Common situations: Pagination code that encodes 'return everything' as 0; page sizes parsed from CLI args, env vars, or config strings without range checks; float-producing page-size math; a typoed default limit in a config file.
Related errors
AI-assisted analysis of earendil-works/pi@4af9d21d3b (2026-08-24).
Data as JSON: /api/errors/6524317e8115a3fc.
Report an issue: GitHub.