Skip to content

fix(git): replace mutating Stream.runFold with Stream.runForEach#25867

Open
stephanschielke wants to merge 6 commits intoanomalyco:devfrom
stephanschielke:fix/git-runfold-readonly-mutation
Open

fix(git): replace mutating Stream.runFold with Stream.runForEach#25867
stephanschielke wants to merge 6 commits intoanomalyco:devfrom
stephanschielke:fix/git-runfold-readonly-mutation

Conversation

@stephanschielke
Copy link
Copy Markdown

@stephanschielke stephanschielke commented May 5, 2026

Issue for this PR

Closes #25873
Related: #25835

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Two fixes:

  1. packages/opencode/src/git/index.ts -- replaces Stream.runFold with mutable accumulator with Stream.runForEach + local variables (original change in this PR).

  2. packages/opencode/src/snapshot/index.ts (new commit) -- replaces Stream.mkUint8Array(handle.stdout) with an explicit Stream.runForEach + local variable collector. This is the actual crash site.

Root cause (fully confirmed)

TL;DR: effect@4.0.0-beta.59 changed Stream.mkUint8Array to use a mutable Channel.runFold accumulator. In Bun --compile --minify binaries, JSC freezes objects repeatedly passed to a loop step callback as a deoptimization guard. The second fold iteration receives a frozen accumulator and throws.

Full chain:

  1. effect@4.0.0-beta.59 (bumped in fix(vcs): avoid unbounded diff memory usage #25581) changed Stream.mkUint8Array from returning a new Uint8Array on each iteration to mutating a shared { bytes, arrays } accumulator inside Channel.runFold.

  2. snapshot/index.ts line 577 calls Stream.mkUint8Array(handle.stdout) to collect output from git cat-file --batch.

  3. processor.ts calls snapshot.track() at case "start-step" -- after the first tool call completes and before the second starts.

  4. Result: the second tool call in any session causes snapshot.track() to run git cat-file --batch, which calls Stream.mkUint8Array, which on the second chunk throws Attempted to assign to readonly property / Attempting to define property on object that is not extensible.

Why it appeared "git-specific": The original failing session had 35 tool calls in Step 1 (no snapshot needed yet), then git commands in Step 2. snapshot.track() fires at the step boundary, so the first git call in Step 2 triggered it. With fewer tools per step, the crash appears on call #2 regardless of tool type -- confirmed in Round 3 testing where a task tool call (no git, no subprocess) crashed as the second call.

Why bun run does not reproduce: JSC interprets object shapes differently when compiling with --minify. The freeze behavior is a JSC compiled-mode optimization, not present in interpreted mode.

Evidence:

Why the original git/index.ts fix was correct but incomplete

The git/index.ts Stream.runFold fix in the original commit is correct -- that mutable accumulator was also broken. But git/index.ts is only reached after execute() is called, which happens after snapshot.track() already crashed. So both fixes are needed.

How to reproduce

  1. Build opencode with bun run build --single (compiled + minified)
  2. Start with opencode serve
  3. Use the local-anthropic agent (LM Studio Anthropic API) to trigger two consecutive tool calls
  4. Second tool call fails with Attempted to assign to readonly property / Attempting to define property on object that is not extensible

Does NOT reproduce:

  • With bun run (interpreted mode)
  • With compiled but non-minified binary (bun build --compile without --minify)
  • With effect 4.0.0-beta.57 (last good version)
  • With effect 4.0.0-beta.60 (does NOT fix Stream.mkUint8Array)

How did you verify your code works?

  1. Confirmed root cause via Effect repo git diff (beta.57..beta.59, 255 lines, only change to Stream.mkUint8Array in Stream.ts).
  2. Verified via SQLite DB queries: all error tool calls have fully parsed input (secureJsonParse succeeded, throw site is post-parse).
  3. Round 3 reproduction: local-anthropic agent on broken binary reproduces on demand -- call Roadmap & Existing Issues #2 fails regardless of tool type.
  4. packages/opencode/src/snapshot/index.ts LSP diagnostics: clean after fix.
  5. No other Stream.mkUint8Array calls in the codebase (confirmed via grep).

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

The collect() function in Git.run() mutates the fold accumulator in-place
(acc.bytes += ..., acc.truncated = ..., acc.chunks.push(...)). Since
effect@4.0.0-beta.59 (bumped in v1.14.34), Stream.runFold may treat
accumulators as readonly, causing 'Attempted to assign to readonly property'
on every git subprocess invocation.

Replace with Stream.runForEach + local mutable state which is idiomatic
for side-effectful stream consumption and avoids accumulator immutability
assumptions entirely.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@github-actions github-actions Bot added needs:compliance This means the issue will auto-close after 2 hours. and removed needs:compliance This means the issue will auto-close after 2 hours. needs:issue labels May 5, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

Thanks for updating your PR! It now meets our contributing guidelines. 👍

…utable fold accumulator crash in Bun compiled binaries
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bash tool fails with 'Attempted to assign to readonly property' in v1.14.34

1 participant