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

  1. Convert non-plain values before appending: new Date().toISOString() for dates, [...map.entries()] for Maps, spread copies for class instances
  2. Break cycles by storing ids or keys instead of nested object references
  3. Pre-check payloads with the exported assertJsonSerializable(value) to get the same precise reason before any write reaches storage
  4. 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

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.