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

  1. Validate before calling storage: Number.isInteger(limit) && limit > 0, otherwise omit the field
  2. If you wrap SessionStorage in a custom Session, mirror session.ts and validate limit in your facade first
  3. 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

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.