earendil-works/pi · error · SessionError
invalid_payload
invalid_payload
Error message
Durable payload ${reason} What it means
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.
Source
Thrown at packages/agent/src/harness/session/session.ts:27
LaneRecord,
LogItem,
LogOptions,
NewRecord,
OperationStartedRecord,
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) {View on GitHub (pinned to 4af9d21d3b)
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
Example fix
// before
await session.appendCustomEntry('snapshot', { at: new Date(), files: fileMap });
// after
await session.appendCustomEntry('snapshot', { at: new Date().toISOString(), files: [...fileMap.entries()] }); Defensive patterns
Strategy: validation
Validate before calling
import { assertJsonSerializable } from './harness/session/session.ts'; // adjust import root
assertJsonSerializable(data); // throws invalid_payload with the exact reason, before any write
await session.appendCustomEntry('snapshot', data); Type guard
const isJsonSerializable = (value: unknown): value is JsonValue => {
try {
assertJsonSerializable(value);
return true;
} catch {
return false;
}
}; Try / catch
try {
await session.appendCustomEntry('snapshot', data);
} catch (error) {
if (error instanceof SessionError && error.code === 'invalid_payload') {
// sanitize (dates to ISO strings, maps to arrays) and retry once; fix cycles at the source
await session.appendCustomEntry('snapshot', JSON.parse(JSON.stringify(data)));
} else {
throw error;
}
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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).
Related errors
AI-assisted analysis of earendil-works/pi@4af9d21d3b (2026-08-24).
Data as JSON: /api/errors/fe225cfa032b6245.
Report an issue: GitHub.