paperclipai/paperclip · error
Invalid run-event payload encoding
Error message
Invalid run-event payload encoding
What it means
decodeRunEventPayload decodes the JSONB run-event payload stored by encodeRunEventPayload. When a stored payload contains the reserved on-disk marker key "$paperclipRunEventJsonV1", its value must be a string holding the double-encoded original JSON; if the marker's value is any other type (number, object, null, boolean), the encoding contract is violated and this error is thrown instead of returning corrupted data.
Solutions
- Never use the reserved key "$paperclipRunEventJsonV1" in your own payload fields; rename the application field
- Fix or delete the malformed row so the marker value is the JSON-string produced by encodeRunEventPayload (or drop the marker entirely for plain payloads)
- Re-encode the payload with encodeRunEventPayload and write it back before decoding
- If migrating data, ensure the writer that produces the marker runs the same package version as the reader
Example fix
// before (malformed stored payload)
{ "$paperclipRunEventJsonV1": { "nested": true } }
// after (valid: marker holds the original JSON as a string)
{ "$paperclipRunEventJsonV1": "{\"nested\":true}" }
// or simply remove the reserved key from application payloads Defensive patterns
Strategy: type-guard
Validate before calling
const marker = "$paperclipRunEventJsonV1";
function isDecodablePayload(p: unknown): boolean {
if (typeof p !== "object" || p === null || Array.isArray(p)) return false;
if (!Object.hasOwn(p, marker)) return true;
const v = (p as Record<string, unknown>)[marker];
if (typeof v !== "string") return false;
const orig: unknown = JSON.parse(v);
return orig !== null && typeof orig === "object" && !Array.isArray(orig);
} Type guard
function isValidEncodedMarker(v: unknown): v is string {
if (typeof v !== "string") return false;
try {
const orig: unknown = JSON.parse(v);
return orig !== null && typeof orig === "object" && !Array.isArray(orig);
} catch { return false; }
} Try / catch
try {
const payload = decodeRunEventPayload(raw);
handle(payload);
} catch (err) {
if (err instanceof Error && err.message === "Invalid run-event payload encoding") {
// treat row as corrupt: quarantine payloadId and log raw value
await quarantineRunEvent(raw);
} else throw err;
} Prevention
- Never use the reserved key "$paperclipRunEventJsonV1" as an application payload field
- Only pass Record<string, unknown> payloads through encodeRunEventPayload; wrap anything else in an object
- Write rows only via the runEventPayload Drizzle custom type, not raw SQL with hand-built JSON
- Keep writer and reader on the same package version when the payload format evolves
When it happens
Trigger: Calling decodeRunEventPayload (directly or via the runEventPayload custom Drizzle type's fromDriver) on a payload object or JSON string where payload["$paperclipRunEventJsonV1"] exists but is not a string — e.g. hand-written DB rows, migrated data, or code that stored an object under the reserved key.
Common situations: Manual SQL inserts into heartbeat_run_events that copied the marker key without the encoded string; a custom ETL/migration rewriting payloads; application code accidentally writing a field named $paperclipRunEventJsonV1 into its payload, triggering the encode path and confusing the decoder; version drift between writer and reader of the payload format.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
- Chat SDK state exceeds the -byte limit
- Chat SDK state is not JSON-serializable
- Cloud runtime identity has an invalid
- Cloud runtime identity is already claimed by another…
- Completion report has no bound task.
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/aa2c21e72db5c065.
Report an issue: GitHub.
Appendix: source
Thrown at packages/db/src/run-event-payload.ts:42
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
projectString(key), project(entry),
]));
}
return value;
}
const projection = project(original) as Record<string, unknown>;
if (!needsEncoding) return originalJson;
// JSON.stringify escapes the original JSON a second time: PostgreSQL receives
// literal backslashes, not an unsupported U+0000. Decode before hashing/replay.
return JSON.stringify({ ...projection, [originalJsonKey]: originalJson });
}
export function decodeRunEventPayload(value: string | Record<string, unknown>): Record<string, unknown> {
const payload = typeof value === "string" ? JSON.parse(value) as Record<string, unknown> : value;
if (Object.hasOwn(payload, originalJsonKey)) {
const originalJson = payload[originalJsonKey];
if (typeof originalJson !== "string") throw new Error("Invalid run-event payload encoding");
const original: unknown = JSON.parse(originalJson);
if (original === null || typeof original !== "object" || Array.isArray(original)) {
throw new Error("Invalid run-event payload encoding");
}
return original as Record<string, unknown>;
}
return payload;
}
// The SQL type stays JSONB; existing rows and SQL routing queries are unchanged.
export const runEventPayload = customType<{
data: Record<string, unknown>;
driverData: string;
}>({
dataType: () => "jsonb",
toDriver: encodeRunEventPayload,
fromDriver: decodeRunEventPayload,
});View on GitHub (pinned to 3f1d897a7c)