Skip to content

fix(json-parse): exclude code-context tails from Windows-path heuristic (#93139)#95982

Open
louria wants to merge 1 commit into
openclaw:mainfrom
louria:fix/repair-json-windows-path-false-positive
Open

fix(json-parse): exclude code-context tails from Windows-path heuristic (#93139)#95982
louria wants to merge 1 commit into
openclaw:mainfrom
louria:fix/repair-json-windows-path-false-positive

Conversation

@louria

@louria louria commented Jun 23, 2026

Copy link
Copy Markdown

What Problem This Solves

OpenClaw agent cron jobs that emit multi-line Python (or any code block whose opener ends in :) through the exec tool started failing reliably after 2026.6.6/2026.6.9 with errors like:

SyntaxError: unexpected character after line continuation character

…and the cron error log surfaced fragments of the agent's own Python being misinterpreted as tool/shell names:

🛠️ # Maybe the user messages aren't ones I'm filtering correct…les[r] = roles.get(r, 0) + 1 print(f" roles: {roles}") PYEOF failed
🛠️ run python3 inline script (heredoc) failed
📝 Edit: in /tmp/write_reading.py failed

Tracked in issue #93139 (also #93139 comment thread with v2026.6.6 and v2026.6.9 reproducers).

On one production host, this regression took the daily/Notion-sync/language-learning cron failure rate from 0/211 (0.0%) in the 12 days before the upgrade to 8/27 (29.6%) in the 1.5 days after.

Root Cause

repairJson in src/llm/utils/json-parse.ts includes a Windows-path heuristic (looksLikeWindowsPathPrefix) so that when a model emits an unescaped path like C:\bin\app.exe inside a JSON string, the backslash escape sequences (\b, \t, \n, …) are not greedily re-interpreted as JSON control escapes.

The heuristic uses this regex on the last 160 chars of the in-progress string value:

/(?:^|[^A-Za-z0-9])[A-Za-z]:(?:[\\/][^"\\/:*?<>|\r\n]*)*$/

The trailing * makes the path segment optional — so any tail ending in <non-alphanum><letter>: matches. That happens to also describe Python block openers:

Tail Should match? Old regex result
C:\Users yes (Windows path) ✅ true
if x: no (Python if) ❌ true
for x: no (Python for) ❌ true
r: no (single-letter var) ❌ true
x: no (indented) ❌ true

When the false positive fires, the next \n after the : is treated as the start of a path segment and gets emitted as a literal backslash-n (two characters) into the parsed JSON value. The agent then sees its own multi-line Python turn into a garbled one-liner and the shell rejects it.

Byte-level repro

od -c of the corrupted heredoc payload (with this PR not applied):

0000220   n   e   (   )  \n   i   f       r   :   \   n
                                                  ^^^^^^
                                                  two SEPARATE chars
                                                  0x5C 0x6E here

Every other newline in the same string is the genuine \n (one byte, 0x0A). Only the one immediately after : gets doubled.

Why This Change Was Made

The fix is a single guard at the top of looksLikeWindowsPathPrefix: if the tail contains any code-context marker — (, ), =, ;, {, }, ', ,, <, >, | — bail out and don't treat the next backslash-escape as a path char.

Those characters do not appear in real Windows path tails, but appear in essentially every Python / JS / shell block opener that ends with : (def foo(x):, if (cond):, try:, for f in *.py:, python3 << EOF, etc.).

I chose this over tightening the regex to require an actual filename segment ([A-Za-z]:[\\/][A-Za-z0-9._\-]+) because that approach broke the existing test fixtures that depend on C:\bin (single character after the separator that's a valid \b JSON escape) staying matched. The code-context guard preserves all existing fixtures unchanged.

User Impact

  • Before: agent cron jobs emitting any multi-line Python through exec heredocs / write tool fail unpredictably, ~30% in observed production; each failure burns thousands of output tokens as the agent tries to self-debug the corrupted output.
  • After: Python if: / for: / try: / def f(): blocks round-trip correctly through tool-call JSON repair. Windows paths still take the existing repair path.

Evidence

Tests

All 14 existing test cases in src/llm/utils/json-parse.test.ts continue to pass (zero changes needed), and 7 new regression tests cover the bug:

$ node scripts/run-vitest.mjs run --config test/vitest/vitest.unit.config.ts \
    src/llm/utils/json-parse.test.ts

 ✓ unit src/llm/utils/json-parse.test.ts (21 tests) 181ms

 Test Files  1 passed (1)
      Tests  21 passed (21)
   Duration  1.25s

New regression tests cover:

  • Python if x: block in a heredoc (the headline bug, with both parseJsonWithRepair and parseStreamingJson assertions)
  • Python try: / except Exception as e: chains
  • Python def foo(x): (parens in signature)
  • Python while True: (no parens / no identifier after the keyword)
  • The specific if r: reproducer captured in the issue's trajectory dumps
  • Bash for f in *.py:\n echo $f
  • Mixed: "see C:\\Users for path; if x:\n do_stuff()" — the path part stays as C:\Users, the code part gets a real newline.

Lint

$ node_modules/.bin/oxlint src/llm/utils/json-parse.ts src/llm/utils/json-parse.test.ts
Found 0 warnings and 0 errors.
Finished in 67ms on 2 files with 208 rules using 2 threads.

Format

$ node_modules/.bin/oxfmt --check src/llm/utils/json-parse.ts src/llm/utils/json-parse.test.ts
Checking formatting...
Finished in 40ms on 2 files using 2 threads.

(oxfmt was run on the test file to align with project style after I added the new describe block.)

Backward Compatibility Smoke

Every parseJsonWithRepair / parseStreamingJson fixture in the existing test file — including all five C:\bin\app.exe, C:\temp\x, C:\new\file, D:\reports\q, C:\users\bob Windows paths — passes after the fix. The Windows-path repair pathway is preserved verbatim; the only behavioral change is that obvious code tails no longer trip it.

Notes for Reviewers

  • Touched files: src/llm/utils/json-parse.ts (9-line guard), src/llm/utils/json-parse.test.ts (51-line describe block of regression tests).
  • No changes to public exports, types, or other call sites.
  • CHANGELOG.md deliberately untouched per CONTRIBUTING.md.
  • I left "Allow edits by maintainers" enabled on this branch.

The repairJson helper detects unescaped backslashes in Windows path
strings (\bin, \users) and preserves them as literal characters
instead of rewriting them as JSON escape sequences. The detector keys
off a tail ending in `<non-alphanum><letter>:`, which matches genuine
drive letters like `C:` but also matches Python block openers like
`if x:`, `for x:`, `try:`, `def foo(x):`. When the next character
was a real newline, the model output got rewritten so the shell saw a
literal `\n` instead of an actual newline.

This broke any agent tool call that carried multi-line Python in a
heredoc, with the symptom that the shell parser threw
`SyntaxError: unexpected character after line continuation character`
and the cron agent burned thousands of output tokens trying to work
around the corruption.

Fix: reject any tail that contains code-context markers like parens,
braces, equals, semicolons, single-quotes, commas, shell redirects, or
pipes. Real Windows path tails do not contain those characters, but
nearly every Python/JS/shell block opener does.

Adds regression tests for the failing patterns and a mixed-content test
that confirms a Windows path adjacent to code still survives.

Refs openclaw#93139
@clawsweeper

clawsweeper Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed June 30, 2026, 5:46 PM ET / 21:46 UTC.

Summary
The PR adds a code-context punctuation guard to the JSON repair Windows-path heuristic and regression tests for Python and shell block-opener newlines.

PR surface: Source +9, Tests +51. Total +60 across 2 files.

Reproducibility: yes. Source-level reproduction is high confidence: the current predicate matches code-like if x: and if r: tails as Windows paths, and the linked issue has v2026.6.9 byte-level failures after Python block colons. I did not run tests because this was a read-only review.

Review metrics: 1 noteworthy metric.

  • Shared parser consumers: 8 runtime files plus 1 SDK export. The helper parses streamed tool-call arguments across provider/runtime paths and plugin-facing SDK export, so a heuristic regression is not isolated to the reported cron path.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #93139
Summary: This PR is a candidate fix for the open JSON/newline corruption issue; adjacent parser PRs touch the same heuristic but address different false-positive classes.

Members:

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

Merge readiness
Overall: 🧂 unranked krab
Proof: 🧂 unranked krab
Patch quality: 🧂 unranked krab
Result: blocked until real behavior proof is added.

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

Rank-up moves:

  • [P1] Repair the predicate and add regressions for punctuation-before-path and bare single-letter code-opener cases.
  • [P1] Add redacted real OpenClaw agent/tool-run proof for the affected multiline Python payload path.

Proof guidance:

  • [P1] Needs real behavior proof before merge: Unit, lint, format, and compatibility-smoke output are present, but the contributor still needs redacted terminal output, logs, or a recording from a real OpenClaw agent/tool run; after updating the PR body, ClawSweeper should re-review automatically or a maintainer can comment @clawsweeper re-review.

Risk before merge

  • [P1] Merging as-is can break existing unescaped Windows-path repair for strings where punctuation appears before a genuine path, such as see, C:\new or Bob's C:\new.
  • [P1] Merging as-is still leaves bare single-letter code opener tails like if x: and if r: classified as Windows path prefixes.
  • [P1] The PR body has unit/lint/format output, but no after-fix real OpenClaw agent or tool run showing the reported heredoc/write workflow now succeeds.

Maintainer options:

  1. Repair the predicate before merge (recommended)
    Limit code-context rejection to the matched candidate suffix or an equivalent narrower signal, and add regressions for punctuation-before-path plus bare code-opener cases.
  2. Coordinate with adjacent parser work
    Review this branch together with fix(llm): trust control escapes in JSON strings that escape backslashes #96748 so both Windows-path heuristic changes share one compatible parser rule.
  3. Pause if the compatibility rule is undecided
    If maintainers are not ready to choose the shared parser contract, hold this PR until the Windows-path compatibility tradeoff is explicit.

Next step before merge

  • [P1] The code blockers are mechanically repairable, but missing real behavior proof keeps this as a human-only merge blocker rather than a ClawSweeper repair marker.

Security
Cleared: The diff only changes local JSON parser logic and colocated tests; no dependency, workflow, secret, or supply-chain surface changed.

Review findings

  • [P1] Scope the marker check to the candidate suffix — src/llm/utils/json-parse.ts:123-125
  • [P2] Cover bare single-letter code openers — src/llm/utils/json-parse.ts:123-125
Review details

Best possible solution:

Keep the fix in the shared JSON parser, but revise the predicate so it scopes code-context rejection to the actual candidate suffix, preserves existing Windows-path repair, covers bare code openers, and is backed by real OpenClaw tool-run proof.

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

Yes. Source-level reproduction is high confidence: the current predicate matches code-like if x: and if r: tails as Windows paths, and the linked issue has v2026.6.9 byte-level failures after Python block colons. I did not run tests because this was a read-only review.

Is this the best way to solve the issue?

No. The parser helper is the right layer, but the proposed whole-tail punctuation guard is too broad for existing Windows-path repair and too narrow for bare single-letter code-opener tails.

Full review comments:

  • [P1] Scope the marker check to the candidate suffix — src/llm/utils/json-parse.ts:123-125
    The new guard scans the whole 160-character tail before the Windows-path regex. That disables repair for real unescaped paths when punctuation appears earlier in the same string, such as see, C:\new or Bob's C:\new, so limit the rejection to the candidate suffix or add an equivalent path-preserving check.
    Confidence: 0.91
  • [P2] Cover bare single-letter code openers — src/llm/utils/json-parse.ts:123-125
    The PR says if x: and if r: should stop matching as Windows paths, but this guard does not fire for those tails when no marker punctuation appears earlier. The existing regex still returns true and the following \n is still emitted literally, so extend the predicate and add focused coverage.
    Confidence: 0.84

Overall correctness: patch is incorrect
Overall confidence: 0.9

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 3811001d2783.

Label changes

Label justifications:

  • P1: The linked regression affects real agent tool-call workflows and this PR touches a shared parser used across provider runtimes.
  • merge-risk: 🚨 compatibility: The diff can change existing Windows-path repair behavior for strings where punctuation appears earlier in the same tail before a genuine drive path.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🧂 unranked krab and patch quality is 🧂 unranked krab.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: Unit, lint, format, and compatibility-smoke output are present, but the contributor still needs redacted terminal output, logs, or a recording from a real OpenClaw agent/tool run; after updating the PR body, ClawSweeper should re-review automatically or a maintainer can comment @clawsweeper re-review.
Evidence reviewed

PR surface:

Source +9, Tests +51. Total +60 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 9 0 +9
Tests 1 51 0 +51
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 60 0 +60

Acceptance criteria:

  • [P1] node scripts/run-vitest.mjs src/llm/utils/json-parse.test.ts.
  • [P1] node_modules/.bin/oxfmt --check src/llm/utils/json-parse.ts src/llm/utils/json-parse.test.ts.
  • [P1] node_modules/.bin/oxlint src/llm/utils/json-parse.ts src/llm/utils/json-parse.test.ts.

What I checked:

  • Repository policy applied: Root AGENTS.md was read fully, and the agent/plugin-SDK scoped guides were read because the shared parser feeds agent transports and is exported through the SDK. (AGENTS.md:1, 3811001d2783)
  • Current main parser behavior: Current main still routes JSON control escapes through looksLikeWindowsPathPrefix before accepting them as normal JSON escapes, and the path regex allows the optional path segment that misclassifies code-like single-letter colon tails. (src/llm/utils/json-parse.ts:82, 3811001d2783)
  • Latest release behavior: The latest release tag v2026.6.11 points at e085fa1 and contains the same Windows-path control-escape branch and regex. (src/llm/utils/json-parse.ts:82, e085fa1a3ffd)
  • Existing compatibility contract: Adjacent current tests pin repair of unescaped Windows paths such as C:\new\file, so this PR must not disable that behavior when ordinary punctuation appears earlier in the same string. (src/llm/utils/json-parse.test.ts:17, 3811001d2783)
  • PR diff guard: The PR adds a marker-character guard before the existing Windows-path regex and applies it to the entire 160-character tail, not just the candidate suffix. (src/llm/utils/json-parse.ts:115, bf74d346f3f5)
  • Predicate probe: A read-only Node probe showed current main and the PR both match bare if x: and if r:, while the PR stops matching genuine path tails with earlier punctuation such as see, C:\new and Bob's C:\new. (src/llm/utils/json-parse.ts:115, bf74d346f3f5)

Likely related people:

  • steipete: Commit bb4ae91 added the Windows-path escape preservation behavior, and merged PR Fix live model inference edge cases #88946 carried it into main. (role: introduced behavior; confidence: high; commits: bb4ae914da48, 9ead0ae9219e; files: src/llm/utils/json-parse.ts, src/llm/utils/json-parse.test.ts)
  • vincentkoc: Commit b579c0a recently changed parseStreamingJson normalization in the same parser utility. (role: recent adjacent parser contributor; confidence: medium; commits: b579c0a65be1; files: src/llm/utils/json-parse.ts, src/llm/utils/json-parse.test.ts)
  • coder999999999: Commit adcac40 added nearby invalid-unicode escape repair coverage in the same JSON parser utility. (role: earlier parser repair contributor; confidence: medium; commits: adcac404e147; files: src/llm/utils/json-parse.ts, src/llm/utils/json-parse.test.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: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jun 23, 2026
@clawsweeper clawsweeper Bot added P1 High-priority user-facing bug, regression, or broken workflow. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. labels Jun 27, 2026
@salad-maker

Copy link
Copy Markdown

Ran an independent verification of the looksLikeWindowsPathPrefix guard against a real byte-level capture from our own OpenClaw agent session (not the issue's examples), plus the two regressions ClawSweeper's review flagged. Small standalone script, no test framework needed:

function looksLikeWindowsPathPrefixOLD(prefix) {
  const tail = prefix.slice(-160);
  return /(?:^|[^A-Za-z0-9])[A-Za-z]:(?:[\\/][^"\\/:*?<>|\r\n]*)*$/.test(tail);
}
function looksLikeWindowsPathPrefixPR(prefix) {
  const tail = prefix.slice(-160);
  if (/[()=;{}',<>|]/.test(tail)) return false;
  return /(?:^|[^A-Za-z0-9])[A-Za-z]:(?:[\\/][^"\\/:*?<>|\r\n]*)*$/.test(tail);
}

const cases = [
  ["real capture: with p.open('w') as f:", "with p.open('w') as f:"],
  ["bare if x:", "if x:"],
  ["bare if r:", "if r:"],
  ["punctuation-before-path: see, C:\\new", "see, C:\\new"],
  ["punctuation-before-path: Bob's C:\\new", "Bob's C:\\new"],
  ["genuine simple path: C:\\Users\\x", "C:\\Users\\x"],
  ["for item in d:", "for item in d:"],
];
for (const [label, prefix] of cases) {
  console.log(label, "| OLD:", looksLikeWindowsPathPrefixOLD(prefix), "| PR:", looksLikeWindowsPathPrefixPR(prefix));
}

Results:

Case Should corrupt? OLD (main) PR #95982
with p.open('w') as f: (our real capture) no true (bug) false (fixed)
if r: (bare, no punctuation) no true (bug) true (still broken)
for item in d: (bare, no punctuation) no true (bug) true (still broken)
see, C:\new (genuine path, punctuation earlier) yes, protect it true (correct) false (new regression)
Bob's C:\new (genuine path, punctuation earlier) yes, protect it true (correct) false (new regression)
C:\Users\x (plain genuine path) yes, protect it true (correct) true (still correct)

This confirms the fix as-written closes the specific reproducer we hit (a with ... : block header in a write tool call), but confirms both gaps @clawsweeper's review already flagged:

  1. Bare single-letter code openers with no adjacent punctuation (if r:, for item in d:, while x:) still false-positive as Windows paths and still corrupt the following \n.
  2. The whole-160-char-tail punctuation guard is too broad — it now strips path protection from genuine Windows paths that happen to have ordinary punctuation earlier in the same string (a comma, apostrophe, etc. before the drive letter).

For what it's worth, our own real-world capture (an agent-generated write tool call for a Python file) hit case 1's class of bug (with p.open('w') as f: → matched by punctuation guard so it's fixed here) but the same regex family risk clearly extends to any bare if/for/while/try colon-terminated line, which is extremely common in generated Python. A narrower fix scoped to the actual candidate suffix (as the review suggests) rather than the whole tail would likely close both gaps without the new path-protection regression.

Happy to run further cases against a revised patch if useful.

@salad-maker

Copy link
Copy Markdown

Follow-up with live-deployment results, since our earlier comment was module-level only.

We hand-applied this PR's guard to the minified json-parse chunk of a production OpenClaw 2026.6.11 install (same logic, adapted to the shipped bundle) and have been running it live.

Results so far:

  • Sanity probes: agent-generated write tool calls containing with open(...) as f: / if data: lines now come through byte-exact — newlines preserved, output is valid Python.
  • No regressions observed in normal traffic (multi-model, tool-heavy agent sessions) since deployment.

One methodology caveat worth stating honestly: the corruption only occurs when streamed tool-call JSON actually hits the repair path, which is nondeterministic in live traffic. Our original corrupted capture happened organically, but we could not force the repair path on demand — an identical probe against the stock bundle also came through clean. So live clean probes alone aren't conclusive; combined with the module-level before/after above (stock corrupts the capture, patched preserves it, same shipped bundle), we consider the fix verified for our reproducer class.

The two gaps flagged earlier (bare if r:-style openers with no adjacent punctuation still matching, and the tail-wide punctuation guard dropping path protection when ordinary punctuation appears earlier in the string) still apply as of this deployment.

@openclaw-barnacle

Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label Jul 25, 2026
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. P1 High-priority user-facing bug, regression, or broken workflow. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: S stale Marked as stale due to inactivity status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants