Skip to content

extractJson gives up when provider output has an unbalanced "{" before the JSON object #237

Closed

Description

@notlakshitha

Summary

extractJson (src/provider-json.ts) is the fallback parser for codex and pi provider output. It scans from the first { and tracks brace depth. If that brace is never closed, which happens when the model's prose mentions a lone { before the real JSON, the inner loop runs to the end of the text with depth !== 0. The function then sets firstBrace = -1 (provider-json.ts:49-51) and returns null, without trying any later {.

A valid JSON object that follows is ignored. parseCodexJson throws codex provider produced unparseable JSON output, and the pi provider fails the same way.

This case is distinct from #30, which added fence and first-object extraction, and from #71, which asks for raw-output logging. The extraction added for #30 only recovers when the first { is balanced.

Steps to Reproduce

Run from the repository root (v0.8.2, commit 366127f) with npx tsx:

import { extractJson } from "./src/provider-json.ts";
console.log(extractJson('Plan: wrap keys in { braces.\n{"ok":true}'));
console.log(extractJson('Looking at fn() { ... the result:\n{"findings":[]}'));
console.log(extractJson('I will output {draft} then {"ok":1}'));

Output:

null
null
{ ok: 1 }

The third case works because {draft} is balanced and the parse failure moves on to the next brace. The first two contain an unclosed { and return null.

Expected Behavior

{ ok: true } and { findings: [] }. An unbalanced brace in the text before the JSON should be skipped, just as an unparseable balanced block already is.

Actual Behavior

null. The review or fix step fails with malformed-output even though the provider returned a valid object.

Possible Solution

  1. When the scan from a given { ends unbalanced, retry from the next { after it instead of stopping.
  2. Add extractJson tests with an unbalanced { in leading prose.
    if (depth !== 0) {
      // Unbalanced "{" (e.g. in prose): retry from the next brace instead of giving up.
      firstBrace = text.indexOf("{", firstBrace + 1);
    }

With this change, both failing inputs above return the expected objects, 'no json { here' and '{{{{' still return null, and src/provider.test.ts passes (140/140). The loop still terminates, because firstBrace strictly increases.

I'm happy to send a PR.

Activity

  1. added
    clawsweeper:intake:bugPreliminary submission classification; full source review pending.
    P2Normal priority bug or improvement with limited blast radius.
    clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.
    clawsweeper:queueable-fixClawSweeper marked this issue as an existing queue_fix_pr work candidate.
    clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.
    impact:otherThis issue has meaningful maintainer-visible impact outside the owned taxonomy.
    issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.
    no-staleExempts this issue from stale automation.
    and removed
    clawsweeper:intake:bugPreliminary submission classification; full source review pending.
    on Oct 18, 2026
  2. clawsweeper commented on Oct 18, 2026

    @clawsweeper
    Contributor

    Codex review: this still needs some work.

    Summary
    This issue describes a current parser defect, and #238 explicitly closes it but remains unmerged.

    Reproducibility: Both reported inputs deterministically reach current main's unmatched-brace termination branch; no runtime reproduction or tests were executed during this read-only review.

    Root-cause cluster
    Relationship: canonical
    Canonical: #237
    Summary: This issue is the concrete unmatched-preamble report, with an open PR explicitly proposing its repair.

    Members:

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

    Next step
    Review #238 against the reported regressions and existing parsing safeguards, including its newly supplied CLI transcript.

    Review details

    Best possible solution:

    Recover JSON in the shared extractor while preserving balanced-candidate priority, nested-draft exclusion, and downstream schema validation.

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

    Both reported inputs deterministically reach current main's unmatched-brace termination branch; no runtime reproduction or tests were executed during this read-only review.

    Is this the best way to solve the issue?

    Repairing the shared extractor fits its established tolerant-parsing contract, with regressions needed to protect candidate selection and avoid excessive rescanning.

    AGENTS.md: found and applied where relevant.

    Codex review notes: model internal, reasoning medium; reviewed against 610a26db8437.

    Label changes

    Label changes:

    No label changes.

    Label justifications:

    • P2: A specific provider-response pattern interrupts review or repair despite usable JSON, with no urgent regression established.
    • impact:other: Shared response extraction rejects usable provider results and fails CLI workflow steps.
    Evidence reviewed

    What I checked:

    • Current main retains the defect: GitHub main matches the checkout; an unmatched opening brace leaves depth nonzero, sets firstBrace to -1, and prevents examination of a later valid object. (src/provider-json.ts:49, 610a26db8437)
    • Production failure path: Pi passes stdout to extractJson and throws malformed-output on null; Codex reaches the same extractor through parseCodexJson. (src/providers/pi.ts:113, 610a26db8437)
    • Existing recovery safeguards: Tests require balanced malformed candidates to be skipped without selecting their nested draft objects; they do not cover unmatched leading prose followed by valid JSON. (src/provider.test.ts:156, 610a26db8437)
    • Open repair owns the work: fix: parse provider JSON after unmatched prose #238 explicitly closes this issue and remains open and unmerged; its diff adds both reported regression inputs, and its updated body supplies a deterministic provider-fixture CLI transcript beyond the unit-test evidence discussed earlier. (20008175b096)
    • Verified parsing history: The REST commit patch establishes balanced-candidate retries, unmatched-candidate termination, and nested-draft exclusion coverage; earlier commits trace helper extraction and Codex fallback integration to steipete. (src/provider-json.ts:50, 429cc9cbb9fd)
    • Released source is affected: The v0.8.2 source contains the same unmatched-candidate termination branch. (src/provider-json.ts:49, b8565de5bb68)
    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; those execution lanes are retired.
    • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

    Reviewed October 19, 2026, 3:23 PM ET / 19:23 UTC.

  3. added
    clawsweeper:intake:bugPreliminary submission classification; full source review pending.
    and removed
    clawsweeper:intake:bugPreliminary submission classification; full source review pending.
    on Oct 18, 2026
  4. 7 remaining items

  5. added
    clawsweeper:intake:bugPreliminary submission classification; full source review pending.
    and removed
    clawsweeper:intake:bugPreliminary submission classification; full source review pending.
    on Oct 19, 2026
  6. steipete commented on Oct 19, 2026

    @steipete
    Contributor

    Fixed in #238. Both reported inputs returned null on the previous parser and now return the expected objects.

    Verified the built CLI through the normal Codex adapter with a deterministic provider executable: the same response containing an unmatched prose brace failed with exit 8 / malformed-output using the old parser, then completed one review with zero findings using the fixed parser. No live provider credentials were involved.

    The full validation gate passed on Linux: typecheck, lint, formatting, 970 tests passed (2 skipped), coverage, build, and package smoke. Exact-head GitHub CI also passed, including Windows execution and Node 22/24 runtime checks: https://github.com/openclaw/clawpatch/actions/runs/39314280709.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal priority bug or improvement with limited blast radius.clawsweeper:linked-pr-openClawSweeper found an open linked pull request for this issue.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:otherThis issue has meaningful maintainer-visible impact outside the owned taxonomy.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions