{"record":{"id":"18b8e2503dcd06ea","repo":"eyaltoledano/claude-task-master","slug":"failed-to-generate-valid-json-from-workflow-state","errorCode":null,"errorMessage":"Failed to generate valid JSON from workflow state","messagePattern":"Failed to generate valid JSON from workflow state","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/tm-core/src/modules/workflow/managers/workflow-state-manager.ts","lineNumber":144,"sourceCode":"\n\t/**\n\t * Save workflow state to disk\n\t * Uses steno for atomic writes and automatic queueing of concurrent saves\n\t */\n\tasync save(state: WorkflowState): Promise<void> {\n\t\ttry {\n\t\t\t// Ensure writer is initialized (creates directory if needed)\n\t\t\tawait this.ensureWriter();\n\n\t\t\t// Serialize and validate JSON\n\t\t\tconst jsonContent = JSON.stringify(state, null, 2);\n\n\t\t\t// Validate that the JSON is well-formed by parsing it back\n\t\t\ttry {\n\t\t\t\tJSON.parse(jsonContent);\n\t\t\t} catch (parseError) {\n\t\t\t\tthis.logger.error('Generated invalid JSON:', jsonContent);\n\t\t\t\tthrow new Error('Failed to generate valid JSON from workflow state');\n\t\t\t}\n\n\t\t\t// Write using steno (handles queuing and atomic writes automatically)\n\t\t\tawait this.writer!.write(jsonContent + '\\n');\n\n\t\t\tthis.logger.debug(`Saved workflow state (${jsonContent.length} bytes)`);\n\t\t} catch (error: any) {\n\t\t\tthrow new Error(`Failed to save workflow state: ${error.message}`);\n\t\t}\n\t}\n\n\t/**\n\t * Create a backup of current state\n\t */\n\tasync createBackup(): Promise<void> {\n\t\ttry {\n\t\t\tconst exists = await this.exists();\n\t\t\tif (!exists) {","sourceCodeStart":126,"sourceCodeEnd":162,"githubUrl":"https://github.com/eyaltoledano/claude-task-master/blob/c0c98d367c55296bfe69e65680625b6db437af02/packages/tm-core/src/modules/workflow/managers/workflow-state-manager.ts#L126-L162","documentation":"WorkflowStateManager.save() serializes the workflow state to JSON, then defensively parses it back with JSON.parse to prove the string is well-formed before writing. If the round-trip parse fails, the state object contains data JSON.stringify cannot serialize safely (circular refs, BigInt, etc.), so save aborts without touching the file rather than persisting corrupt state.","triggerScenarios":"Calling save() (directly or via startWorkflow, resumeWorkflow, restoreBackup) with a WorkflowState containing circular object references, BigInt values, or a toJSON that throws, producing a string JSON.parse rejects.","commonSituations":"Custom tooling injects non-serializable objects into workflow context (e.g. a Node.js stream, class instance with circular parent refs, or a BigInt from a JSON-LD parser) before starting or resuming a workflow.","solutions":["Inspect the logged jsonContent (printed via logger.error) to find the malformed portion of the serialized state.","Remove or sanitize non-serializable values (circular refs, BigInt) from the WorkflowState object before calling save.","If a custom field was added to state, convert it to plain JSON-safe data (e.g. String(bigintValue), plain object copy) before persistence.","Check for bugs in any override of toJSON on context objects."],"exampleFix":"// before\ncontext.session = someCircularSessionObject;\nawait manager.save(state);\n// after\ncontext.session = JSON.parse(JSON.stringify(someCircularSessionObject));\nawait manager.save(state);","handlingStrategy":"validation","validationCode":"function isJsonSafe(value, seen = new Set()) {\n  if (value === null || typeof value !== 'object') {\n    if (typeof value === 'bigint') return false;\n    return true;\n  }\n  if (seen.has(value)) return false;\n  seen.add(value);\n  return Object.values(value).every((v) => isJsonSafe(v, seen));\n}\n// before save: if (!isJsonSafe(state)) sanitize state;","typeGuard":"function isPlainJsonSafe<T>(v: T): boolean {\n  try { JSON.parse(JSON.stringify(v)); return true; } catch { return false; }\n}","tryCatchPattern":"try {\n  await manager.save(state);\n} catch (e) {\n  if (e.message.includes('valid JSON')) {\n    state = JSON.parse(JSON.stringify(state, safeReplacer()));\n    await manager.save(state);\n  } else throw e;\n}","preventionTips":["Keep WorkflowState limited to plain JSON data — no class instances with circular refs","Use a replacer function (skip keys, convert BigInt) when serializing custom context","Unit-test save/restore round-trip for any new state field","Avoid storing streams, timers, or DOM/Node handles in workflow context"],"tags":["json","serialization","workflow"],"backgroundTag":"json-serialization-failed","analyzedSha":"c0c98d367c55296bfe69e65680625b6db437af02","analyzedAt":"2026-08-29T02:56:26.071Z","schemaVersion":2},"datasetVersion":"2026-08-29T07:17:48.351Z"}