{"record":{"id":"fe225cfa032b6245","repo":"earendil-works/pi","slug":"invalid-payload-fe225c","errorCode":"invalid_payload","errorMessage":"Durable payload ${reason}","messagePattern":"Durable payload (.+?)","errorType":"exception","errorClass":"SessionError","httpStatus":null,"severity":"error","filePath":"packages/agent/src/harness/session/session.ts","lineNumber":27,"sourceCode":"\tLaneRecord,\n\tLogItem,\n\tLogOptions,\n\tNewRecord,\n\tOperationStartedRecord,\n\tProvisionedEntry,\n\tRecordBase,\n\tRecordQuery,\n\tSessionMetadata,\n\tSessionStats,\n\tSessionStorage,\n\tSessionTree,\n} from \"./types.ts\";\nimport { SessionError } from \"./types.ts\";\n\ntype JsonValidationFrame = { value: unknown } | { exit: object };\n\nfunction invalidPayload(reason: string): never {\n\tthrow new SessionError(\"invalid_payload\", `Durable payload ${reason}`);\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\nexport function assertJsonSerializable(value: unknown): void {\n\tconst active = new WeakSet<object>();\n\tconst stack: JsonValidationFrame[] = [{ value }];\n\twhile (stack.length > 0) {","sourceCodeStart":9,"sourceCodeEnd":45,"githubUrl":"https://github.com/earendil-works/pi/blob/4af9d21d3b4d664e4a29fcabfec85171077248e3/packages/agent/src/harness/session/session.ts#L9-L45","documentation":"Every entry and record committed through Session (appendMessage, appendCustomEntry, appendEntry, appendRecord) must be strictly JSON-serializable because the session log is durable and replayable. commitEntry and commitRecord run assertJsonSerializable over the whole payload first, and this error carries the reason for the first violation found: circular references, NaN/Infinity, class instances such as Date or Map (non-plain objects), functions/symbols/bigint, sparse arrays, arrays with extra or non-index properties, and getter/setter accessors.","triggerScenarios":"appendCustomEntry('snap', { at: new Date() }) since Date is not a plain object; payloads containing NaN or Infinity from arithmetic; object graphs that reference themselves; Map/Set/class instances passed through; sparse arrays like [1, , 3]; properties defined with get/set accessors or symbol keys.","commonSituations":"Persisting rich domain or ORM objects directly instead of DTOs; tool results carrying Date fields into entries; accidental circular parent-child links; arrays built with delete or skipped indices; values from third-party libraries (values produced by JSON.parse are always safe).","solutions":["Convert non-plain values before appending: new Date().toISOString() for dates, [...map.entries()] for Maps, spread copies for class instances","Break cycles by storing ids or keys instead of nested object references","Pre-check payloads with the exported assertJsonSerializable(value) to get the same precise reason before any write reaches storage","Replace functions, symbols, and bigint with string serializations or drop them from the payload"],"exampleFix":"// before\nawait session.appendCustomEntry('snapshot', { at: new Date(), files: fileMap });\n// after\nawait session.appendCustomEntry('snapshot', { at: new Date().toISOString(), files: [...fileMap.entries()] });","handlingStrategy":"validation","validationCode":"import { assertJsonSerializable } from './harness/session/session.ts'; // adjust import root\n\nassertJsonSerializable(data); // throws invalid_payload with the exact reason, before any write\nawait session.appendCustomEntry('snapshot', data);","typeGuard":"const isJsonSerializable = (value: unknown): value is JsonValue => {\n  try {\n    assertJsonSerializable(value);\n    return true;\n  } catch {\n    return false;\n  }\n};","tryCatchPattern":"try {\n  await session.appendCustomEntry('snapshot', data);\n} catch (error) {\n  if (error instanceof SessionError && error.code === 'invalid_payload') {\n    // sanitize (dates to ISO strings, maps to arrays) and retry once; fix cycles at the source\n    await session.appendCustomEntry('snapshot', JSON.parse(JSON.stringify(data)));\n  } else {\n    throw error;\n  }\n}","preventionTips":["Normalize Date, Map, Set, and class instances to plain JSON at the boundary","Store ids instead of nested object graphs to avoid cycles","Run assertJsonSerializable on outbound payloads in development builds","Keep durable DTOs separate from runtime objects"],"tags":["serialization","json","validation","session"],"backgroundTag":"json-serialization-failed","analyzedSha":"4af9d21d3b4d664e4a29fcabfec85171077248e3","analyzedAt":"2026-08-24T13:07:14.692Z","schemaVersion":2},"datasetVersion":"2026-08-24T17:17:21.512Z"}