Skip to content

fix(llm): repair \u escapes that lack four hex digits in streaming JSON#88226

Merged
steipete merged 7 commits into
openclaw:mainfrom
coder999999999:fix/repair-json-bad-unicode-escape
May 30, 2026
Merged

fix(llm): repair \u escapes that lack four hex digits in streaming JSON#88226
steipete merged 7 commits into
openclaw:mainfrom
coder999999999:fix/repair-json-bad-unicode-escape

Conversation

@coder999999999

Copy link
Copy Markdown
Contributor

Summary

repairJson in src/llm/utils/json-parse.ts is meant to fix malformed JSON string literals (it escapes raw control chars and doubles backslashes before invalid escapes). But it mishandled a backslash-u that is not followed by four hex digits:

  • The valid \uXXXX path (4-hex check) is skipped, then control falls through to the generic valid-escape branch — and "u" is in VALID_JSON_ESCAPES, so the broken \u is re-emitted un-doubled. The output still fails JSON.parse, defeating the repair.

parseJsonWithRepair only retries when repairJson actually changed the string, so it throws; parseStreamingJson then falls through its fallbacks to an empty object, silently dropping the tool call's arguments.

User impact: parseStreamingJson parses streaming tool-call arguments for both Anthropic and OpenAI (src/llm/providers/anthropic.ts, src/llm/providers/openai-responses-shared.ts, openai-completions.ts). Whenever a model emits a string value containing an unescaped backslash-u — a Windows path (C:\users), LaTeX (\underline), a regex, etc. — the tool is invoked with empty/dropped arguments.

Fix

When a \u is not followed by four hex digits, double the backslash (treat it as an invalid escape), matching how the function already handles other invalid escapes. VALID_JSON_ESCAPES is left intact. Added regression tests (the file had no coverage).

Real behavior proof (required for external PRs)

Behavior addressed: streaming tool-call arguments containing an unescaped backslash-u string were silently reduced to {} because repairJson re-emitted the broken escape instead of repairing it. After this patch the arguments parse and are recovered.

Real environment tested: local macOS checkout on fresh main. The proof drives the real production functions repairJson / parseJsonWithRepair / parseStreamingJson — the exact functions the Anthropic and OpenAI streaming transports call to parse tool-call arguments. No mocks of the functions under test.

Exact steps or command run after this patch: I added regression rows for the invalid-\u cases and the valid \uXXXX case, ran them against the unpatched repairJson (the invalid-\u rows threw / returned {}), then applied the fix and re-ran. Command:

node scripts/run-vitest.mjs src/llm/utils/json-parse.test.ts

Evidence after fix: copied live console output from node scripts/run-vitest.mjs

BEFORE (unpatched repairJson):
  x repairs a \u not followed by four hex digits so the result parses
    AssertionError: expected [Function] to not throw an error but
    'SyntaxError: Bad Unicode escape in JSON at position 13' was thrown
  x recovers streaming tool-call arguments instead of dropping them to {}
  Tests  2 failed | 1 passed (3)

AFTER (this fix):
  Test Files  1 passed (1)
  Tests  3 passed (3)

Observed result after fix: parseStreamingJson('{"cmd":"\underline{x}"}') now returns { cmd: "\underline{x}" } instead of {}, and parseJsonWithRepair('{"path":"C:\users"}') returns { path: "C:\users" }; the valid A escape still decodes to "A".

What was not tested: I did not run a live streaming model emitting such tool arguments end-to-end; the proof drives the exact parser functions the Anthropic/OpenAI transports call, with the offending inputs.

Tests and validation

  • node scripts/run-vitest.mjs src/llm/utils/json-parse.test.ts -> 3 passed (new file; invalid-\u repair, streaming-arg recovery, valid \uXXXX preserved).
  • Whole src/llm/utils/ suite -> 7 files / 34 tests pass (no regressions).
  • Before/after isolation above: the invalid-\u rows fail on the unpatched source and pass with the fix.
  • oxfmt --check clean, oxlint clean, tsgo:core exit 0, tsgo:core:test exit 0, git diff --check clean.

Copilot AI review requested due to automatic review settings May 30, 2026 04:13
@openclaw-barnacle openclaw-barnacle Bot added size: XS proof: supplied External PR includes structured after-fix real behavior proof. labels May 30, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR addresses two edge cases where formatting/parsing could produce invalid or non-parseable outputs: token counts rounding to 1000k, and JSON strings containing invalid \u escapes.

Changes:

  • Prevent formatTokenShort from emitting "1000k" by falling through to the million formatter at the rollover boundary.
  • Make repairJson handle invalid \u escapes by escaping the backslash so the JSON becomes parseable.
  • Add targeted unit tests covering both boundary/repair behaviors (including a streaming JSON scenario).

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
src/shared/subagents-format.ts Avoids "1000k" by falling through to "1m" when rounding thousands reaches 1000.
src/shared/subagents-format.test.ts Adds tests for the thousands→millions rollover boundary.
src/llm/utils/json-parse.ts Fixes repairJson to escape invalid \u sequences so parsing succeeds.
src/llm/utils/json-parse.test.ts Adds tests for invalid/valid \u handling and streaming args recovery.

@clawsweeper

clawsweeper Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed May 30, 2026, 3:39 PM ET / 19:39 UTC.

Summary
The PR fixes invalid short \u escape repair in streaming JSON parsing, adds parser regression tests, and includes maintainer-pushed CI shard/test cleanup changes.

PR surface: Source +5, Tests +147, Other +146. Total +298 across 7 files.

Reproducibility: yes. Source inspection shows current main falls through from an invalid short \u to the generic valid-escape branch, and the PR’s regression rows describe the failing parser and streaming-argument cases.

Review metrics: 1 noteworthy metric.

  • Node CI shard plan: 2 planner areas changed. The branch changes command-doctor sharding and core-runtime infra/hooks/secrets sharding, which affects CI coverage and check naming before merge.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🦞 diamond lobster
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • Confirm fresh PR-head CI or focused shard-plan proof for the CI planner changes.

Risk before merge

  • [P1] The merge result changes Node CI shard planning in addition to the parser repair; a bad include mapping could skip, duplicate, or rename checks, so fresh PR-head CI or focused test/scripts/ci-node-test-plan.test.ts proof should be part of the merge decision.

Maintainer options:

  1. Verify shard plan before merge (recommended)
    Wait for fresh PR-head CI or focused shard-plan proof showing the affected command and infra tests are covered exactly once.
  2. Accept maintainer-owned CI split
    If maintainers already verified the shard matrix outside this review, they can accept the automation risk and record that proof when landing.
  3. Split parser fix from CI work
    If CI shard confidence is unavailable, keep the parser repair separate and move the shard-planner work to a dedicated PR.

Next step before merge

  • [P2] Human review is appropriate because the remaining concern is maintainer verification of CI automation changes, not a narrow code repair.

Security
Cleared: No concrete security or supply-chain regression was found; the CI planner changes affect automation reliability, not permissions, secrets, dependencies, or downloaded code execution.

Review details

Best possible solution:

Land the parser repair once maintainers are satisfied that the CI shard split is intentional and freshly verified.

Do we have a high-confidence way to reproduce the issue?

Yes. Source inspection shows current main falls through from an invalid short \u to the generic valid-escape branch, and the PR’s regression rows describe the failing parser and streaming-argument cases.

Is this the best way to solve the issue?

Yes for the parser repair: treating a short \u as an invalid escape matches the existing invalid-escape repair path and keeps valid \uXXXX behavior intact. The CI shard changes are separate automation work that should be verified before merge rather than repaired by ClawSweeper.

AGENTS.md: found and applied where relevant.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 37c6e2dfa0bf.

Label changes

Label changes:

  • add proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes copied before/after terminal output driving the production parser functions for the central streaming-argument repair.

Label justifications:

  • P2: The parser bug can drop malformed streaming tool-call arguments, but the fix is bounded to a shared LLM utility and regression tests.
  • merge-risk: 🚨 automation: The PR changes CI shard generation, so green unit tests alone do not prove check coverage and matrix behavior remain correct.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body includes copied before/after terminal output driving the production parser functions for the central streaming-argument repair.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes copied before/after terminal output driving the production parser functions for the central streaming-argument repair.
Evidence reviewed

PR surface:

Source +5, Tests +147, Other +146. Total +298 across 7 files.

View PR surface stats
Area Files Added Removed Net
Source 2 7 2 +5
Tests 4 168 21 +147
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 1 155 9 +146
Total 7 330 32 +298

Acceptance criteria:

  • [P1] node scripts/run-vitest.mjs src/llm/utils/json-parse.test.ts.
  • [P1] node scripts/run-vitest.mjs test/scripts/ci-node-test-plan.test.ts.
  • [P1] fresh PR-head CI for Node shard matrix.

What I checked:

  • Repository policy read: Root AGENTS.md and scoped guides for scripts, agents, embedded runner tests, and plugins were read; the review applied the automation-risk and plugin-public-contract guidance to the touched paths. (AGENTS.md:19, 37c6e2dfa0bf)
  • Current-main bug path: On current main, an invalid \u reaches the later VALID_JSON_ESCAPES branch because u is listed as a valid escape, so the malformed escape can be re-emitted unchanged. (src/llm/utils/json-parse.ts:60, 37c6e2dfa0bf)
  • Streaming parser call sites: parseStreamingJson is used by OpenAI, Anthropic, Mistral, embedded agent, and runtime proxy streaming paths, so malformed tool-call argument repair is a shared LLM runtime surface. (src/llm/providers/openai-responses-shared.ts:602, 37c6e2dfa0bf)
  • Proposed parser fix: The merge result treats \u not followed by four hex digits as an invalid escape and doubles the backslash before continuing. (src/llm/utils/json-parse.ts:67, ad25cea0c50e)
  • Regression coverage: The added tests cover invalid short \u, valid \uXXXX, and streaming argument recovery instead of {}. (src/llm/utils/json-parse.test.ts:4, ad25cea0c50e)
  • Automation surface changed: The merge result splits command doctor shards and core-runtime infra/hooks/secrets shards, which changes CI check planning beyond the parser repair. (scripts/lib/ci-node-test-plan.mjs:121, ad25cea0c50e)

Likely related people:

  • Peter Steinberger: Authored the current main CI commit and several PR-branch commits touching the CI shard planner, MCP test timing, and plugin hook type cleanup; the timeline also shows assignment to steipete. (role: recent area contributor and likely follow-up owner; confidence: high; commits: 37c6e2dfa0bf, 31cb7e253a26, a5f57f609b39; files: scripts/lib/ci-node-test-plan.mjs, test/scripts/ci-node-test-plan.test.ts, src/agents/agent-bundle-mcp-runtime.test.ts)
  • Shakker: Local blame on the current-main checkout attributes the existing repairJson escape handling and CI planner scaffolding to the shallow grafted main commit; because the checkout history is shallow, this is a routing hint rather than full provenance. (role: current-main source introducer in available history; confidence: low; commits: 7f48ee1e5745; files: src/llm/utils/json-parse.ts, scripts/lib/ci-node-test-plan.mjs, src/plugins/host-hook-turn-types.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. labels May 30, 2026
@coder999999999
coder999999999 force-pushed the fix/repair-json-bad-unicode-escape branch from 51d8c26 to 17fd128 Compare May 30, 2026 04:25
@coder999999999

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

I split this PR down to a single purpose. The formatTokenShort rollover change you flagged ("two user-visible behaviors changed", formatter proof missing) was committed here by accident — it's already its own PR (#88209). This branch now contains only the repairJson \u-escape fix and its regression tests (2 files, +27).

Your prior review already judged the parser proof sufficient ("Copied terminal output sufficiently supports the parser repair"); the gold-shrimp/needs-proof rating was driven by the formatter behavior, which is now removed. Please re-review the current single-purpose head (17fd128a5d).

@clawsweeper

clawsweeper Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@coder999999999
coder999999999 force-pushed the fix/repair-json-bad-unicode-escape branch from 17fd128 to 7610282 Compare May 30, 2026 04:33
@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels May 30, 2026
@coder999999999
coder999999999 force-pushed the fix/repair-json-bad-unicode-escape branch from 7610282 to 18c802b Compare May 30, 2026 15:22
@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 30, 2026
@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. and removed rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. labels May 30, 2026
@steipete steipete self-assigned this May 30, 2026
@steipete
steipete force-pushed the fix/repair-json-bad-unicode-escape branch from 18c802b to 845c0cd Compare May 30, 2026 18:53
@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 30, 2026
coder999999999 and others added 2 commits May 30, 2026 19:56
repairJson treated a backslash-u not followed by four hex digits as a valid
escape ("u" is in VALID_JSON_ESCAPES) and re-emitted it un-doubled, so the
output still failed JSON.parse — defeating the repair. parseStreamingJson then
fell through to an empty object, silently dropping tool-call arguments.

This hits the streaming tool-call argument path for both Anthropic and OpenAI
(parseStreamingJson) whenever a model emits an unescaped backslash-u string,
e.g. a Windows path (C:\users), LaTeX (\underline), or a regex.

Double the backslash for an invalid \u, matching the other invalid-escape
handling. Add regression tests (the file had no coverage).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 30, 2026
@steipete
steipete force-pushed the fix/repair-json-bad-unicode-escape branch from 845c0cd to c8f0af6 Compare May 30, 2026 18:59
@openclaw-barnacle openclaw-barnacle Bot added the agents Agent runtime and tooling label May 30, 2026
@openclaw-barnacle openclaw-barnacle Bot added scripts Repository scripts size: M and removed size: XS labels May 30, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 30, 2026
@clawsweeper clawsweeper Bot added rating: 🌊 off-meta tidepool PR readiness rating does not apply to this item. and removed rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. labels May 30, 2026
@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. merge-risk: 🚨 automation 🚨 May affect CI, automerge, proof capture, label sync, or maintainer automation. and removed rating: 🌊 off-meta tidepool PR readiness rating does not apply to this item. labels May 30, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 30, 2026
@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 30, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 30, 2026
@steipete

Copy link
Copy Markdown
Contributor

Verification before merge:

Behavior addressed: invalid streaming JSON Unicode escapes are repaired without corrupting valid \uXXXX sequences; CI node doctor/infra shards are split so formerly long shards complete under 3 minutes.
Real environment tested: local macOS checkout; GitHub Actions CI on head 814036172c3917b6414cdcd7a1ab82660d485e0d.
Exact steps or command run after this patch:

  • node scripts/run-vitest.mjs src/llm/utils/json-parse.test.ts
  • node scripts/run-vitest.mjs test/scripts/ci-node-test-plan.test.ts
  • node scripts/run-vitest.mjs src/plugins/contracts/extension-runtime-dependencies.contract.test.ts -t "extensions/skill-workshop does not keep unused direct runtime dependencies"
  • node scripts/run-vitest.mjs src/infra/restart.test.ts
  • node scripts/run-vitest.mjs src/infra/process-respawn.test.ts src/infra/replace-file.test.ts src/infra/resolve-system-bin.test.ts src/infra/restart-coordinator.test.ts src/infra/restart-handoff.test.ts src/infra/restart-intent.test.ts src/infra/restart-sentinel.test.ts src/infra/restart-stale-pids.test.ts src/infra/restart.deferral-timeout.test.ts src/infra/restart.test.ts src/infra/retry-policy.test.ts src/infra/retry.retry-after-lower-bound.test.ts src/infra/retry.test.ts src/infra/run-node.test.ts src/infra/runtime-guard.test.ts src/infra/runtime-status.test.ts
  • mise exec [email protected] -- pnpm lint --threads=8
  • pnpm deadcode:dependencies
  • git diff --check
  • .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main and focused local autoreview reruns after follow-up edits
    Evidence after fix: CI run https://github.com/openclaw/openclaw/actions/runs/26693267521 passed. Former blocker shards now pass under 3 minutes, including checks-node-agentic-commands-doctor-misc-platform at 29s and all checks-node-core-runtime-infra-misc-* shards at 28-36s.
    Observed result after fix: CI green; required checks pass.
    What was not tested: no live provider/channel run; this PR touches JSON repair utilities, test mocks, and CI shard planning only.

@steipete
steipete merged commit adcac40 into openclaw:main May 30, 2026
131 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling merge-risk: 🚨 automation 🚨 May affect CI, automerge, proof capture, label sync, or maintainer automation. P2 Normal backlog priority with limited blast radius. proof: supplied External PR includes structured after-fix real behavior proof. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. scripts Repository scripts size: M status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants