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

  1. Never use the reserved key "$paperclipRunEventJsonV1" in your own payload fields; rename the application field
  2. 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)
  3. Re-encode the payload with encodeRunEventPayload and write it back before decoding
  4. 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

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


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)