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 85ad337a9a5a9736ae90b64ba4abb5cb17d8670e Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Wed, 1 Apr 2026 01:40:48 +0800 Subject: [PATCH 2/2] fix: only treat structural issues as fatal in applyRuntimeStateDelta Filter warnings (missing_state_change, unsupported_change, temporal_impossibility, validator_crash) before throwing, allowing the pipeline to continue when the LLM reports content-level contradictions that are not structural errors. --- packages/core/src/state/state-reducer.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/core/src/state/state-reducer.ts b/packages/core/src/state/state-reducer.ts index 3370b0ee..d73c3c23 100644 --- a/packages/core/src/state/state-reducer.ts +++ b/packages/core/src/state/state-reducer.ts @@ -67,8 +67,14 @@ export function applyRuntimeStateDelta(params: { }; const issues = validateRuntimeState(next); - if (issues.length > 0) { - throw new Error(issues.map((issue) => `${issue.code}: ${issue.message}`).join("; ")); + const fatalIssues = issues.filter( + (issue) => + !["missing_state_change", "unsupported_change", "temporal_impossibility", "validator_crash"].includes( + issue.code, + ), + ); + if (fatalIssues.length > 0) { + throw new Error(fatalIssues.map((issue) => `${issue.code}: ${issue.message}`).join("; ")); } return next;