{"record":{"id":"03baedff8c34dba5","repo":"paperclipai/paperclip","slug":"chat-sdk-kind-state-is-not-json-serializable","errorCode":null,"errorMessage":"Chat SDK ${kind} state is not JSON-serializable","messagePattern":"Chat SDK (.+?) state is not JSON-serializable","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/chat-sdk-state.ts","lineNumber":501,"sourceCode":"  }\n\n  private expiryFromTtl(ttlMs: number | undefined): Date | null {\n    if (ttlMs === undefined || ttlMs === 0) return null;\n    assertTtl(\"State TTL\", ttlMs);\n    return new Date(this.now().getTime() + ttlMs);\n  }\n\n  private envelope(kind: StateKind, value: unknown): StateEnvelope {\n    const envelope: StateEnvelope = {\n      kind,\n      schemaVersion: STATE_SCHEMA_VERSION,\n      value,\n    };\n    let serialized: string;\n    try {\n      serialized = JSON.stringify(envelope);\n    } catch (error) {\n      throw new Error(`Chat SDK ${kind} state is not JSON-serializable`, {\n        cause: error,\n      });\n    }\n    if (Buffer.byteLength(serialized) > this.maxValueBytes) {\n      throw new Error(\n        `Chat SDK ${kind} state exceeds the ${this.maxValueBytes}-byte limit`,\n      );\n    }\n    return envelope;\n  }\n\n  private async compareAndSet(\n    key: string,\n    expectedVersion: number | null,\n    kind: StateKind,\n    value: unknown,\n    expiresAt: Date | null,\n  ): Promise<boolean> {","sourceCodeStart":483,"sourceCodeEnd":519,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/chat-sdk-state.ts#L483-L519","documentation":"Before persisting, PaperclipChatSdkStateAdapter serializes the state envelope with JSON.stringify; if that throws (circular references, BigInt, functions, non-serializable objects), the adapter rethrows as this error with the original cause attached. State must round-trip through JSON by contract.","triggerScenarios":"Calling set/state APIs with an object containing circular references, BigInt values, functions, class instances with toJSON throwers, or other JSON-incompatible values as the state payload.","commonSituations":"Storing raw agent SDK response objects that hold circular parent links; BigInt ids from databases; passing Error objects or callbacks in state; version upgrade where payloads gained non-serializable fields.","solutions":["Strip or transform non-serializable fields before storing (JSON.parse(JSON.stringify()) on a safe subset)","Replace BigInt with string, functions with ids, and break circular refs explicitly","Inspect error.cause for the original JSON.stringify failure point"],"exampleFix":"// before\nawait state.set(kind, key, sdkResponse); // contains circular refs\n// after\nawait state.set(kind, key, JSON.parse(JSON.stringify({ id: sdkResponse.id, status: sdkResponse.status })));","handlingStrategy":"validation","validationCode":"function isJsonSafe(v: unknown): boolean { try { JSON.stringify(v); return true; } catch { return false; } }","typeGuard":"function isPlainObject(v: unknown): v is Record<string, unknown> { return typeof v === 'object' && v !== null && !Array.isArray(v) && Object.getPrototypeOf(v) === Object.prototype; }","tryCatchPattern":"try { await state.set(kind, key, value); } catch (err) { if (/not JSON-serializable/.test((err as Error).message)) { console.error('cause:', (err as Error & { cause?: unknown }).cause); value = sanitize(value); return state.set(kind, key, value); } throw err; }","preventionTips":["Never store raw SDK/class objects; store plain DTOs","Replace BigInt with strings before persisting","Check error.cause to find the offending field"],"tags":["serialization","json","validation"],"backgroundTag":"json-serialization-failed","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}