fix: guard against malformed tool result text blocks crashing context truncation#34980
fix: guard against malformed tool result text blocks crashing context truncation#34980coffeexcoin wants to merge 0 commit intoopenclaw:mainfrom
Conversation
Greptile SummaryThis PR fixes a crash loop caused by plugin tool handlers returning Changes:
Minor gap worth noting: Confidence Score: 4/5
Last reviewed commit: 6e2b387 |
Additional Comments (1)
const capped = capToolResultSize(persistMessage(normalizedToolResult));
const persisted = applyBeforeWriteHook(
persistToolResult(capped, { ... }),
);For full coverage, consider calling This is a low-probability gap (plugins would have to deliberately produce malformed content through these hooks), but worth noting given the PR's stated security-hardening goal. |
6e2b387 to
3ffd367
Compare
There was a problem hiding this comment.
💡 Codex Review
https://github.com/openclaw/openclaw/blob/4571f245377e9dd85b4f094487a95a7322711660/src/agents/session-tool-result-guard.ts#L227-L229
Re-sanitize tool results after persistence hooks rewrite them
In deployments that use tool_result_persist or before_message_write, this ordering still lets malformed {type:"text"} blocks reach JSONL. capToolResultSize() is the only place that now sanitizes tool-result content, but it runs before both hook chains here, and the hook contracts in src/plugins/types.ts:1725-1750 explicitly allow either hook to replace the message entirely. If a hook returns content: [{type: "text"}], the malformed block bypasses the new guard and the next context-pruning pass will still hit block.text.length in src/agents/pi-extensions/context-pruning/pruner.ts:114-160, recreating the same crash loop for those sessions.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
4571f24 to
7fb142d
Compare
…ar estimation
A plugin tool handler returning undefined produces {type: "text"} (no text
property) in the session JSONL. This crashes the context truncation pipeline
with TypeError on every subsequent message, locking the session into an
unrecoverable crash loop.
Guard all four crash sites:
- isTextBlock type guard in tool-result-char-estimator.ts
- collectTextSegments in pruner.ts
- collectPrunableToolResultSegments in pruner.ts
- estimateTextAndImageChars in pruner.ts
Add regression tests for both the char estimator and the context pruner
covering malformed toolResult content blocks.
Closes openclaw#34979
Based on prior work by @alvinttang (openclaw#39331) and @coffeexcoin (openclaw#34980).
Co-Authored-By: alvinttang <alvinttang@users.noreply.github.com>
Co-Authored-By: coffeexcoin <coffeexcoin@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Harden context pruning and tool-result character estimation against malformed `{ type: "text" }` blocks created by void/undefined tool handler results.
- Require text blocks to carry a string before using `.length` in the tool-result estimator.
- Guard context-pruning text/image loops against malformed and null content entries.
- Serialize malformed non-string text blocks for pruning size accounting so they cannot bypass trimming as zero-sized.
- Add regression coverage for malformed text blocks, null entries, and non-string text payloads.
Closes #34979.
Maintainer verification:
- `pnpm test src/agents/pi-embedded-runner/tool-result-char-estimator.test.ts src/agents/pi-hooks/context-pruning/pruner.test.ts`
- `pnpm check:changed`
- GitHub checks passed, including the OpenAI / Opus 4.6 parity gate.
Based on prior work by #39331 and #34980.
Co-authored-by: Charles Dusek <cgdusek@gmail.com>
Co-authored-by: alvinttang <alvinttang@users.noreply.github.com>
Co-authored-by: coffeexcoin <coffeexcoin@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
… tool-result pruning entries Three entries were missing co-credits I should have preserved: - Diagnostics/OTEL exec-process spans (#71451): @vincentkoc implemented, but @jlapenna's #70424 proposed the broader tracing work this entry builds on. Now credits both. - Diagnostics/OTEL preloaded SDK (#71450): same pattern — credits @vincentkoc and @jlapenna. - Agents/tool-result pruning (#51267): @cgdusek's PR explicitly built on prior work in #39331 by @alvinttang and #34980 by @coffeexcoin. Now credits all three.
Harden context pruning and tool-result character estimation against malformed `{ type: "text" }` blocks created by void/undefined tool handler results.
- Require text blocks to carry a string before using `.length` in the tool-result estimator.
- Guard context-pruning text/image loops against malformed and null content entries.
- Serialize malformed non-string text blocks for pruning size accounting so they cannot bypass trimming as zero-sized.
- Add regression coverage for malformed text blocks, null entries, and non-string text payloads.
Closes openclaw#34979.
Maintainer verification:
- `pnpm test src/agents/pi-embedded-runner/tool-result-char-estimator.test.ts src/agents/pi-hooks/context-pruning/pruner.test.ts`
- `pnpm check:changed`
- GitHub checks passed, including the OpenAI / Opus 4.6 parity gate.
Based on prior work by openclaw#39331 and openclaw#34980.
Co-authored-by: Charles Dusek <cgdusek@gmail.com>
Co-authored-by: alvinttang <alvinttang@users.noreply.github.com>
Co-authored-by: coffeexcoin <coffeexcoin@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
… tool-result pruning entries Three entries were missing co-credits I should have preserved: - Diagnostics/OTEL exec-process spans (openclaw#71451): @vincentkoc implemented, but @jlapenna's openclaw#70424 proposed the broader tracing work this entry builds on. Now credits both. - Diagnostics/OTEL preloaded SDK (openclaw#71450): same pattern — credits @vincentkoc and @jlapenna. - Agents/tool-result pruning (openclaw#51267): @cgdusek's PR explicitly built on prior work in openclaw#39331 by @alvinttang and openclaw#34980 by @coffeexcoin. Now credits all three.
Harden context pruning and tool-result character estimation against malformed `{ type: "text" }` blocks created by void/undefined tool handler results.
- Require text blocks to carry a string before using `.length` in the tool-result estimator.
- Guard context-pruning text/image loops against malformed and null content entries.
- Serialize malformed non-string text blocks for pruning size accounting so they cannot bypass trimming as zero-sized.
- Add regression coverage for malformed text blocks, null entries, and non-string text payloads.
Closes openclaw#34979.
Maintainer verification:
- `pnpm test src/agents/pi-embedded-runner/tool-result-char-estimator.test.ts src/agents/pi-hooks/context-pruning/pruner.test.ts`
- `pnpm check:changed`
- GitHub checks passed, including the OpenAI / Opus 4.6 parity gate.
Based on prior work by openclaw#39331 and openclaw#34980.
Co-authored-by: Charles Dusek <cgdusek@gmail.com>
Co-authored-by: alvinttang <alvinttang@users.noreply.github.com>
Co-authored-by: coffeexcoin <coffeexcoin@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
… tool-result pruning entries Three entries were missing co-credits I should have preserved: - Diagnostics/OTEL exec-process spans (openclaw#71451): @vincentkoc implemented, but @jlapenna's openclaw#70424 proposed the broader tracing work this entry builds on. Now credits both. - Diagnostics/OTEL preloaded SDK (openclaw#71450): same pattern — credits @vincentkoc and @jlapenna. - Agents/tool-result pruning (openclaw#51267): @cgdusek's PR explicitly built on prior work in openclaw#39331 by @alvinttang and openclaw#34980 by @coffeexcoin. Now credits all three.
Harden context pruning and tool-result character estimation against malformed `{ type: "text" }` blocks created by void/undefined tool handler results.
- Require text blocks to carry a string before using `.length` in the tool-result estimator.
- Guard context-pruning text/image loops against malformed and null content entries.
- Serialize malformed non-string text blocks for pruning size accounting so they cannot bypass trimming as zero-sized.
- Add regression coverage for malformed text blocks, null entries, and non-string text payloads.
Closes openclaw#34979.
Maintainer verification:
- `pnpm test src/agents/pi-embedded-runner/tool-result-char-estimator.test.ts src/agents/pi-hooks/context-pruning/pruner.test.ts`
- `pnpm check:changed`
- GitHub checks passed, including the OpenAI / Opus 4.6 parity gate.
Based on prior work by openclaw#39331 and openclaw#34980.
Co-authored-by: Charles Dusek <cgdusek@gmail.com>
Co-authored-by: alvinttang <alvinttang@users.noreply.github.com>
Co-authored-by: coffeexcoin <coffeexcoin@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
… tool-result pruning entries Three entries were missing co-credits I should have preserved: - Diagnostics/OTEL exec-process spans (openclaw#71451): @vincentkoc implemented, but @jlapenna's openclaw#70424 proposed the broader tracing work this entry builds on. Now credits both. - Diagnostics/OTEL preloaded SDK (openclaw#71450): same pattern — credits @vincentkoc and @jlapenna. - Agents/tool-result pruning (openclaw#51267): @cgdusek's PR explicitly built on prior work in openclaw#39331 by @alvinttang and openclaw#34980 by @coffeexcoin. Now credits all three.
Harden context pruning and tool-result character estimation against malformed `{ type: "text" }` blocks created by void/undefined tool handler results.
- Require text blocks to carry a string before using `.length` in the tool-result estimator.
- Guard context-pruning text/image loops against malformed and null content entries.
- Serialize malformed non-string text blocks for pruning size accounting so they cannot bypass trimming as zero-sized.
- Add regression coverage for malformed text blocks, null entries, and non-string text payloads.
Closes openclaw#34979.
Maintainer verification:
- `pnpm test src/agents/pi-embedded-runner/tool-result-char-estimator.test.ts src/agents/pi-hooks/context-pruning/pruner.test.ts`
- `pnpm check:changed`
- GitHub checks passed, including the OpenAI / Opus 4.6 parity gate.
Based on prior work by openclaw#39331 and openclaw#34980.
Co-authored-by: Charles Dusek <cgdusek@gmail.com>
Co-authored-by: alvinttang <alvinttang@users.noreply.github.com>
Co-authored-by: coffeexcoin <coffeexcoin@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
… tool-result pruning entries Three entries were missing co-credits I should have preserved: - Diagnostics/OTEL exec-process spans (openclaw#71451): @vincentkoc implemented, but @jlapenna's openclaw#70424 proposed the broader tracing work this entry builds on. Now credits both. - Diagnostics/OTEL preloaded SDK (openclaw#71450): same pattern — credits @vincentkoc and @jlapenna. - Agents/tool-result pruning (openclaw#51267): @cgdusek's PR explicitly built on prior work in openclaw#39331 by @alvinttang and openclaw#34980 by @coffeexcoin. Now credits all three.
Harden context pruning and tool-result character estimation against malformed `{ type: "text" }` blocks created by void/undefined tool handler results.
- Require text blocks to carry a string before using `.length` in the tool-result estimator.
- Guard context-pruning text/image loops against malformed and null content entries.
- Serialize malformed non-string text blocks for pruning size accounting so they cannot bypass trimming as zero-sized.
- Add regression coverage for malformed text blocks, null entries, and non-string text payloads.
Closes openclaw#34979.
Maintainer verification:
- `pnpm test src/agents/pi-embedded-runner/tool-result-char-estimator.test.ts src/agents/pi-hooks/context-pruning/pruner.test.ts`
- `pnpm check:changed`
- GitHub checks passed, including the OpenAI / Opus 4.6 parity gate.
Based on prior work by openclaw#39331 and openclaw#34980.
Co-authored-by: Charles Dusek <cgdusek@gmail.com>
Co-authored-by: alvinttang <alvinttang@users.noreply.github.com>
Co-authored-by: coffeexcoin <coffeexcoin@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
… tool-result pruning entries Three entries were missing co-credits I should have preserved: - Diagnostics/OTEL exec-process spans (openclaw#71451): @vincentkoc implemented, but @jlapenna's openclaw#70424 proposed the broader tracing work this entry builds on. Now credits both. - Diagnostics/OTEL preloaded SDK (openclaw#71450): same pattern — credits @vincentkoc and @jlapenna. - Agents/tool-result pruning (openclaw#51267): @cgdusek's PR explicitly built on prior work in openclaw#39331 by @alvinttang and openclaw#34980 by @coffeexcoin. Now credits all three.
Summary
undefinedproduced a content block{type: "text"}with notextproperty. This got persisted to session JSONL, and every subsequent context truncation attempt crashed withTypeError: Cannot read properties of undefined (reading 'length')atestimateMessageChars, causing an unresponsive crash loop.void/undefinedfrom a tool handler can trigger this, bricking the session until manual reset.isTextBlocktype guard to requiretypeof text === "string", and addedsanitizeToolResultContentBlocksat the persistence boundary to fix malformed blocks before they reach the session JSONL.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
Fixes #34979
User-visible / Behavior Changes
textproperty.{type: "text"}content blocks are silently normalized to{type: "text", text: ""}before persistence.Security Impact (required)
NoNoNoNoNoRepro + Verification
Environment
Steps
undefined(e.g.return;with no value)Expected
Actual (before fix)
TypeError: Cannot read properties of undefined (reading 'length')on every subsequent message, crash loop until session reset.Evidence
New tests (15 total across 3 files) reproduce the exact crash scenario —
{type: "text"}with notextkey passed throughestimateMessageChars, context guard pipeline, and persistence guard. All 45 tests pass (15 new + 30 existing).Human Verification (required)
pnpm vitest runon all 3 changed test files — 45/45 pass. Ranpnpm tsgo— no new type errors.{type: "text"}(no key),{type: "text", text: undefined},{type: "text", text: null},{type: "text", text: 42}, mixed valid+malformed blocks in same content array.pnpm testsuite.Compatibility / Migration
YesNoNoFailure Recovery (if this breaks)
tool-result-char-estimator.ts,session-tool-result-guard.ts). The old behavior returns (crashes on malformed blocks).src/agents/pi-embedded-runner/tool-result-char-estimator.ts,src/agents/session-tool-result-guard.tsisTextBlockchange could cause valid blocks to be estimated viaestimateUnknownChars(slightly different char count, no crash).Risks and Mitigations
isTextBlocknow rejects blocks wheretextis not a string, which changes the estimation path fromblock.text.lengthtoestimateUnknownChars(block)(JSON serialization). This could produce slightly different char estimates for blocks that previously had non-stringtextvalues.