{"record":{"id":"ee0c8aa92ca4d722","repo":"earendil-works/pi","slug":"invalid-query-ee0c8a","errorCode":"invalid_query","errorMessage":"limit must be a positive integer","messagePattern":"limit must be a positive integer","errorType":"validation","errorClass":"SessionError","httpStatus":null,"severity":"error","filePath":"packages/agent/src/harness/session/state.ts","lineNumber":32,"sourceCode":"\ttype SessionStats,\n} from \"./types.ts\";\n\nexport type SessionMutation =\n\t| { kind: \"entry\"; lane?: string; entry: Entry }\n\t| { kind: \"record\"; record: LaneRecord }\n\t| { kind: \"lane\"; seq: number; lane: string; leafId: string | null }\n\t| { kind: \"fact\"; seq: number; fact: \"name\"; name: string | undefined }\n\t| { kind: \"fact\"; seq: number; fact: \"label\"; targetId: string; label: string | undefined };\n\ntype InvalidMutation = (message: string) => never;\n\nfunction invalidMutation(message: string): never {\n\tthrow new SessionError(\"invalid_entry\", `Invalid session mutation: ${message}`);\n}\n\nfunction assertValidLimit(limit: number | undefined): void {\n\tif (limit !== undefined && (!Number.isInteger(limit) || limit <= 0)) {\n\t\tthrow new SessionError(\"invalid_query\", \"limit must be a positive integer\");\n\t}\n}\n\nfunction assertValidCursor(afterSeq: number | undefined): void {\n\tif (afterSeq !== undefined && (!Number.isInteger(afterSeq) || afterSeq < 0)) {\n\t\tthrow new SessionError(\"invalid_query\", \"cursor sequence must be a non-negative integer\");\n\t}\n}\n\nfunction* ordered<T>(items: readonly T[], order: EntryOrder | undefined): IterableIterator<T> {\n\tif (order === \"oldestFirst\") {\n\t\tyield* items;\n\t\treturn;\n\t}\n\tfor (let index = items.length - 1; index >= 0; index--) yield items[index]!;\n}\n\nexport class SessionState {","sourceCodeStart":14,"sourceCodeEnd":50,"githubUrl":"https://github.com/earendil-works/pi/blob/4af9d21d3b4d664e4a29fcabfec85171077248e3/packages/agent/src/harness/session/state.ts#L14-L50","documentation":"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.","triggerScenarios":"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.","commonSituations":"Building a custom repo or Session facade over SessionStorage and forwarding raw request params; test harnesses exercising storage directly with hand-built query objects.","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"],"exampleFix":"// before\nawait storage.findOpenOperations('main', { limit: 0 });\n// after\nawait storage.findOpenOperations('main', {});","handlingStrategy":"validation","validationCode":"const normalizeLimit = (limit: number | undefined): number | undefined =>\n  limit !== undefined && Number.isInteger(limit) && limit > 0 ? limit : undefined;\n\nawait storage.findOpenOperations('main', { limit: normalizeLimit(options.limit) });","typeGuard":"const isValidLimit = (limit: unknown): limit is number =>\n  typeof limit === 'number' && Number.isInteger(limit) && limit > 0;","tryCatchPattern":"try {\n  await storage.getLog(options);\n} catch (error) {\n  if (error instanceof SessionError && error.code === 'invalid_query' && error.message.startsWith('limit')) {\n    await storage.getLog({ ...options, limit: undefined });\n  } else {\n    throw error;\n  }\n}","preventionTips":["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"],"tags":["query","pagination","validation","storage","session"],"backgroundTag":"invalid-pagination-limit","analyzedSha":"4af9d21d3b4d664e4a29fcabfec85171077248e3","analyzedAt":"2026-08-24T13:07:14.692Z","schemaVersion":2},"datasetVersion":"2026-08-24T17:17:21.512Z"}