From 0a1a9064f39a27f9c39f10211b8a8d36687a609d Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Wed, 1 Apr 2026 00:23:08 +0800 Subject: [PATCH 1/2] fix: wrap validateRuntimeState in try-catch to prevent JSON parse errors from crashing the pipeline The state validator was throwing an uncaught exception when the LLM output contains invalid JSON characters (e.g. control chars, unescaped sequences), causing the entire write pipeline to crash with 'State validator returned invalid JSON'. This wraps the entire function in a try-catch so that any such errors are returned as a validation issue rather than crashing. --- packages/core/src/state/state-validator.ts | 128 +++++++++++---------- 1 file changed, 69 insertions(+), 59 deletions(-) diff --git a/packages/core/src/state/state-validator.ts b/packages/core/src/state/state-validator.ts index 81846e4f..4391b8d4 100644 --- a/packages/core/src/state/state-validator.ts +++ b/packages/core/src/state/state-validator.ts @@ -17,74 +17,84 @@ export function validateRuntimeState(input: { readonly hooks: unknown; readonly chapterSummaries: unknown; }): RuntimeStateValidationIssue[] { - const issues: RuntimeStateValidationIssue[] = []; + try { + const issues: RuntimeStateValidationIssue[] = []; - const manifest = parseOrIssue( - StateManifestSchema, - input.manifest, - issues, - "invalid_manifest", - "manifest", - ); - const currentState = parseOrIssue( - CurrentStateStateSchema, - input.currentState, - issues, - "invalid_current_state", - "currentState", - ); - const hooks = parseOrIssue( - HooksStateSchema, - input.hooks, - issues, - "invalid_hooks_state", - "hooks", - ); - const chapterSummaries = parseOrIssue( - ChapterSummariesStateSchema, - input.chapterSummaries, - issues, - "invalid_chapter_summaries_state", - "chapterSummaries", - ); + const manifest = parseOrIssue( + StateManifestSchema, + input.manifest, + issues, + "invalid_manifest", + "manifest", + ); + const currentState = parseOrIssue( + CurrentStateStateSchema, + input.currentState, + issues, + "invalid_current_state", + "currentState", + ); + const hooks = parseOrIssue( + HooksStateSchema, + input.hooks, + issues, + "invalid_hooks_state", + "hooks", + ); + const chapterSummaries = parseOrIssue( + ChapterSummariesStateSchema, + input.chapterSummaries, + issues, + "invalid_chapter_summaries_state", + "chapterSummaries", + ); - if (hooks) { - const seen = new Set(); - for (const hook of hooks.hooks) { - if (seen.has(hook.hookId)) { - issues.push({ - code: "duplicate_hook_id", - message: `duplicate hook id: ${hook.hookId}`, - path: `hooks.${hook.hookId}`, - }); + if (hooks) { + const seen = new Set(); + for (const hook of hooks.hooks) { + if (seen.has(hook.hookId)) { + issues.push({ + code: "duplicate_hook_id", + message: `duplicate hook id: ${hook.hookId}`, + path: `hooks.${hook.hookId}`, + }); + } + seen.add(hook.hookId); } - seen.add(hook.hookId); } - } - if (chapterSummaries) { - const seen = new Set(); - for (const row of chapterSummaries.rows) { - if (seen.has(row.chapter)) { - issues.push({ - code: "duplicate_summary_chapter", - message: `duplicate summary chapter: ${row.chapter}`, - path: `chapterSummaries.${row.chapter}`, - }); + if (chapterSummaries) { + const seen = new Set(); + for (const row of chapterSummaries.rows) { + if (seen.has(row.chapter)) { + issues.push({ + code: "duplicate_summary_chapter", + message: `duplicate summary chapter: ${row.chapter}`, + path: `chapterSummaries.${row.chapter}`, + }); + } + seen.add(row.chapter); } - seen.add(row.chapter); } - } - if (manifest && currentState && currentState.chapter > manifest.lastAppliedChapter) { - issues.push({ - code: "current_state_ahead_of_manifest", - message: `current state chapter ${currentState.chapter} exceeds manifest ${manifest.lastAppliedChapter}`, - path: "currentState.chapter", - }); - } + if (manifest && currentState && currentState.chapter > manifest.lastAppliedChapter) { + issues.push({ + code: "current_state_ahead_of_manifest", + message: `current state chapter ${currentState.chapter} exceeds manifest ${manifest.lastAppliedChapter}`, + path: "currentState.chapter", + }); + } - return issues; + return issues; + } catch (error) { + return [ + { + code: "validator_crash", + message: String(error), + path: "", + }, + ]; + } } function parseOrIssue( From 9b8a12b22bf2d075e4d5d4e30d76f903d88f9bda Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Wed, 1 Apr 2026 00:32:00 +0800 Subject: [PATCH 2/2] fix: sanitize JSON before parsing in settler-delta-parser Add sanitizeJSON() to strip control characters (\x00-\x1F\x7F) and trailing commas from LLM output before passing to JSON.parse. This prevents the parser from crashing when MiniMax or other providers emit invalid JSON sequences, allowing state settlement to proceed normally. --- packages/core/src/agents/settler-delta-parser.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/core/src/agents/settler-delta-parser.ts b/packages/core/src/agents/settler-delta-parser.ts index e3515bcd..eb5e616b 100644 --- a/packages/core/src/agents/settler-delta-parser.ts +++ b/packages/core/src/agents/settler-delta-parser.ts @@ -8,6 +8,12 @@ export interface SettlerDeltaOutput { readonly runtimeStateDelta: RuntimeStateDelta; } +function sanitizeJSON(str: string): string { + return str + .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "") + .replace(/,\s*([}\]])/g, "$1"); +} + export function parseSettlerDeltaOutput(content: string): SettlerDeltaOutput { const extract = (tag: string): string => { const regex = new RegExp( @@ -25,7 +31,7 @@ export function parseSettlerDeltaOutput(content: string): SettlerDeltaOutput { const jsonPayload = stripCodeFence(rawDelta); let parsed: unknown; try { - parsed = JSON.parse(jsonPayload); + parsed = JSON.parse(sanitizeJSON(jsonPayload)); } catch (error) { throw new Error(`runtime state delta is not valid JSON: ${String(error)}`); }