earendil-works/pi · error · SessionError
invalid_query
invalid_query
Error message
limit must be a positive integer
What it means
Identical rule to the facade check at session.ts:32, enforced inside SessionState — the state machine shared by InMemorySessionStorage and custom backends. It fires when storage-level query methods (findEntries, findEntriesOnBranch, findRecords, findOpenOperations, getLog) receive a limit that is not an integer greater than zero; undefined means unlimited. Seeing this site rather than the facade one means the query reached storage directly — a custom Session implementation or direct storage use skipped the facade validation.
Source
Thrown at packages/agent/src/harness/session/state.ts:32
type SessionStats,
} from "./types.ts";
export type SessionMutation =
| { kind: "entry"; lane?: string; entry: Entry }
| { kind: "record"; record: LaneRecord }
| { kind: "lane"; seq: number; lane: string; leafId: string | null }
| { kind: "fact"; seq: number; fact: "name"; name: string | undefined }
| { kind: "fact"; seq: number; fact: "label"; targetId: string; label: string | undefined };
type InvalidMutation = (message: string) => never;
function invalidMutation(message: string): never {
throw new SessionError("invalid_entry", `Invalid session mutation: ${message}`);
}
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");
}
}
function* ordered<T>(items: readonly T[], order: EntryOrder | undefined): IterableIterator<T> {
if (order === "oldestFirst") {
yield* items;
return;
}
for (let index = items.length - 1; index >= 0; index--) yield items[index]!;
}
export class SessionState {View on GitHub (pinned to 4af9d21d3b)
Solutions
- Validate before calling storage: Number.isInteger(limit) && limit > 0, otherwise omit the field
- If you wrap SessionStorage in a custom Session, mirror session.ts and validate limit in your facade first
- Route user-supplied pagination through one sanitizer that normalizes bad limits to undefined
Example fix
// before
await storage.findOpenOperations('main', { limit: 0 });
// after
await storage.findOpenOperations('main', {}); Defensive patterns
Strategy: validation
Validate before calling
const normalizeLimit = (limit: number | undefined): number | undefined =>
limit !== undefined && Number.isInteger(limit) && limit > 0 ? limit : undefined;
await storage.findOpenOperations('main', { limit: normalizeLimit(options.limit) }); Type guard
const isValidLimit = (limit: unknown): limit is number => typeof limit === 'number' && Number.isInteger(limit) && limit > 0;
Try / catch
try {
await storage.getLog(options);
} catch (error) {
if (error instanceof SessionError && error.code === 'invalid_query' && error.message.startsWith('limit')) {
await storage.getLog({ ...options, limit: undefined });
} else {
throw error;
}
} Prevention
- If you implement a custom Session facade over SessionStorage, validate limit before delegating (mirror session.ts)
- Route user-supplied pagination through one sanitizer that normalizes bad limits to undefined
- Never forward raw request query params straight into storage methods
When it happens
Trigger: Calling inMemoryStorage.findEntries({ limit: 0 }) or findOpenOperations('main', { limit: 0 }) directly on the storage object; a custom SessionStorage subclass forwarding unvalidated user query objects into SessionState; passing a computed negative or fractional limit to any storage query method.
Common situations: Building a custom repo or Session facade over SessionStorage and forwarding raw request params; test harnesses exercising storage directly with hand-built query objects.
Related errors
AI-assisted analysis of earendil-works/pi@4af9d21d3b (2026-08-24).
Data as JSON: /api/errors/ee0c8aa92ca4d722.
Report an issue: GitHub.