Skip to content

fix(llm): trust control escapes in JSON strings that escape backslashes#96949

Closed
Bartok9 wants to merge 1 commit into
openclaw:mainfrom
Bartok9:salvage/96737-json-control-escape
Closed

fix(llm): trust control escapes in JSON strings that escape backslashes#96949
Bartok9 wants to merge 1 commit into
openclaw:mainfrom
Bartok9:salvage/96737-json-control-escape

Conversation

@Bartok9

@Bartok9 Bartok9 commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

Salvages #96737 by @ly-wang19 onto current main — clean single-commit rebase, original authorship preserved. The original was auto-closed by openclaw-barnacle for the author's >20-active-PR cap, not on merits; the bug is still live on main.

repairJson — the tolerant parser behind parseStreamingJson/parseJsonWithRepair used for model tool-call arguments — corrupts genuine control escapes (\n, \t, …) that legitimately follow an escaped Windows path inside a JSON string. A real newline in a multi-line tool-call argument becomes a literal backslash-n.

Root Cause

Symptom — A tool-call argument like {"text":"Saved to C:\\Users\\me\nDone."} (valid JSON: \\ are escaped separators, \n is a real newline) comes back with the \n rewritten to a literal \\n, breaking multi-line shell commands and any value that mixes a Windows path with a real control char.

Root cause — In src/llm/utils/json-parse.ts, repairJson() has a heuristic that, when it sees a control-escape char after text that looksLikeWindowsPathPrefix, assumes the model forgot to escape a backslash and rewrites \\\. But once a string has already contained a properly escaped backslash (\\), the model has demonstrated it escapes correctly, so subsequent \n/\t are genuine control escapes — the heuristic fires anyway and corrupts them.

Evidence — On current main, line 82 guards only on looksLikeWindowsPathPrefix(stringValuePrefix), with no "already saw a correct escape" condition. The new regression test fails against this main source (see Real behavior proof).

Fix + why this level — Track sawEscapedBackslash per string; once a correctly escaped \\ is seen, suppress the Windows-path rewrite for the remainder of that string. Reset on string open/close. This is the right level because the false positive is intrinsic to the repair heuristic's assumption — downstream consumers can't distinguish a real newline from a corrupted one after the fact.

Scope / risk — Touches only the Windows-path repair branch in repairJson. Strings that never contain a correct \\ still get the original repair behavior (the genuinely-malformed case it was designed for). Reset on string boundaries prevents cross-string leakage.

Evidence

  • Regression test (new): src/llm/utils/json-parse.test.ts → "preserves a real control escape after an escaped Windows path" — fails without the fix.
  • Captured run on this branch:

WITH fix:

 ✓ src/llm/utils/json-parse.test.ts (15 tests) 82ms
 Test Files  1 passed (1)
      Tests  15 passed (15)

WITHOUT fix (test against current main source for json-parse.ts):

 Test Files  1 failed (1)
      Tests  1 failed | 14 passed (15)

Changes from original

  • Rebased onto current main (clean single commit, no conflicts).
  • No code changes from @ly-wang19's original.

Credit: Salvage of #96737 by @ly-wang19 — rebased onto current main. Original closed by the >20-active-PR queue cap, not on merits.

repairJson has a Windows-path heuristic: inside a string, a backslash before a
control-escape letter (b/f/n/r/t) whose decoded prefix looks like a Windows path
is treated as an unescaped path separator and doubled, so a model that emits a
raw path like {"path":"C:\new\file"} is repaired to the literal path instead of
a newline/formfeed. That bet is deliberate and test-pinned for unescaped paths.

But the heuristic inspected only the DECODED prefix, which is identical whether
the wire used escaped "\\" (valid) or raw "\" (malformed). So for a string that
properly escapes its path — e.g. JSON.stringify({text:"C:\\Users\\me\nDone."}),
where the path uses "\\" and the \n is a genuine newline — it still fired and
turned the real newline into a literal backslash-n. OpenAI/Azure/ChatGPT
Responses transports feed parseStreamingJson straight into tool calls with no
JSON.parse-first guard, so a model emitting a Bash/Write/Edit argument with a
properly escaped Windows path plus multi-line content ran a corrupted command.

Fix: once a string contains a properly escaped backslash ("\\"), the model
demonstrably escapes backslashes, so its later \n/\t/etc. are genuine control
escapes — skip the path heuristic for the rest of that string. Strings with no
escaped backslash (the pinned unescaped-path cases) repair exactly as before;
this only adds positive escaping evidence as a suppression signal. Add a
regression test asserting an escaped path followed by a real newline/tab
round-trips to JSON.parse.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@openclaw-barnacle openclaw-barnacle Bot added size: XS triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. labels Jun 26, 2026
@clawsweeper

clawsweeper Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Thanks for the context here. I swept through the related work, and this is now duplicate or superseded.

This PR should close as a duplicate: the same parser fix is already open in a proof-positive, clean canonical PR by the original author, so keeping this salvage branch open adds review noise without unique code or evidence.

Root-cause cluster
Relationship: duplicate
Canonical: #96748
Summary: This PR duplicates the open canonical escaped-backslash parser fix; adjacent parser PRs cover different Windows-path false positives and do not replace the canonical fix.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Canonical path: Close this duplicate and review, land, or reject #96748 as the canonical fix path.

So I’m closing this here and keeping the remaining discussion on #96748.

Review details

Best possible solution:

Close this duplicate and review, land, or reject #96748 as the canonical fix path.

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

Yes. Source inspection shows current main and v2026.6.10 still run the Windows-path heuristic before valid escape preservation, and the canonical same-patch PR includes fail-before/pass-after terminal proof against the exported parser functions.

Is this the best way to solve the issue?

No for this PR as a landing path. The code change is a plausible shared-parser fix, but the same patch is already open with proof and maintainer-ready review at #96748.

Security review:

Security review cleared: No concrete security or supply-chain concern was found; the diff only changes local parser logic and colocated tests.

AGENTS.md: found and applied where relevant.

What I checked:

  • Repository policy read: Root AGENTS.md was read fully and applied; it requires whole-path PR review, related-item checks, proof assessment, and compatibility review for shared runtime/parser behavior. (AGENTS.md:1, 4fc504d321b6)
  • Current main still has the reported parser behavior: Current main runs the Windows-path control-escape heuristic before the valid-escape branch and has no escaped-backslash trust flag, so the bug is not already fixed on main. (src/llm/utils/json-parse.ts:82, 4fc504d321b6)
  • Latest release still has the same parser behavior: The latest release tag v2026.6.10 contains the same heuristic ordering without the escaped-backslash guard, so the fix has not shipped. (src/llm/utils/json-parse.ts:82, aa69b12d0086)
  • Shared caller surface checked: parseStreamingJson feeds OpenAI-style completed tool-call arguments, confirming the shared parser is the relevant owner boundary rather than a provider-specific branch. (src/agents/openai-transport-stream.ts:1550, 4fc504d321b6)
  • Canonical open PR owns the same patch: The canonical PR is open, cleanly mergeable, proof-labeled, and has the same two-file escaped-backslash patch as this PR. (src/llm/utils/json-parse.ts:35, dc5f774cd92b)
  • Current PR duplicates the canonical diff: The current PR diff matches the canonical escaped-backslash patch and regression test, with no unique behavior beyond the re-submission. (src/llm/utils/json-parse.ts:35, 3478611c6cdd)

Likely related people:

  • steipete: PR Fix live model inference edge cases #88946 / commit 9ead0ae added the Windows-path control-escape preservation behavior and regression tests this PR narrows. (role: introduced current Windows-path heuristic; confidence: high; commits: 9ead0ae9219e; files: src/llm/utils/json-parse.ts, src/llm/utils/json-parse.test.ts)
  • vincentkoc: Commit b579c0a normalized parseStreamingJson return behavior in the same parser utility shortly after the Windows-path heuristic landed. (role: recent adjacent parser contributor; confidence: medium; commits: b579c0a65be1; files: src/llm/utils/json-parse.ts, src/llm/utils/json-parse.test.ts)
  • coder999999999: Merged PR fix(llm): repair \u escapes that lack four hex digits in streaming JSON #88226 added nearby invalid-unicode escape repair and tests in the same utility. (role: earlier parser repair contributor; confidence: medium; commits: adcac404e147; files: src/llm/utils/json-parse.ts, src/llm/utils/json-parse.test.ts)

Codex review notes: model internal, reasoning high; reviewed against 4fc504d321b6.

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jun 26, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. label Jun 26, 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. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jun 26, 2026
@Bartok9

Bartok9 commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

Closing as a duplicate of the canonical #96748 — same two-file escaped-backslash control-escape patch by the original fix author, open and proof-positive. No unique behavior here beyond the re-submission, so deferring to #96748 as the landing path. Thanks @clawsweeper for the cross-reference.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: XS 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.

2 participants