Description
In v1.14.34+, the compiled + minified opencode binary fails tool execution with:
TypeError: Attempted to assign to readonly property
or:
TypeError: Attempting to define property on object that is not extensible
Also reported in #25835.
Root cause (confirmed)
Two contributing factors:
-
Effect change: effect@4.0.0-beta.58 (Effect-TS/effect-smol#2098, "generate binary arrays from streams with less copying") changed Stream.mkUint8Array from an immutable Channel.runFold (new Uint8Array per iteration) to a mutable accumulator (acc.bytes += ...; acc.arrays.push(...); return acc). The bump from beta.57 to beta.59 was a routine chore (#25524) with no feature dependency.
-
Bun compilation behavior: In bun build --compile --minify binaries, JSC makes the fold accumulator object non-extensible between iterations. The second chunk throws. This is consistent with known Bun --compile --minify issues (oven-sh/bun#13394, oven-sh/bun#12655).
Throw site: snapshot/index.ts:577 calls Stream.mkUint8Array(handle.stdout) to collect output from git cat-file --batch. processor.ts calls snapshot.track() at case "start-step" (after the first tool call completes, before the second starts). The error propagates as the next tool call's error.
Reproduction
| Scenario |
Result |
bun run (interpreted) |
✅ Works |
bun build --compile (no minify) |
✅ Works |
bun build --compile --minify + effect@4.0.0-beta.57 |
✅ Works |
bun build --compile --minify + effect@4.0.0-beta.58 or later |
❌ Crashes on second tool call |
Steps:
- Build opencode with
bun run build --single using effect@4.0.0-beta.58 or later
- Start with
opencode serve
- Trigger two consecutive tool calls in a session
- Second tool call fails with
Attempted to assign to readonly property
Why it appeared "git-specific": The original failing session had 35 tool calls in Step 1 (no snapshot needed). Git commands started in Step 2. snapshot.track() fires at the step boundary. Testing with fewer tools per step confirmed a task tool (no git, no subprocess) also fails as the second call.
Evidence
- Effect diff
4.0.0-beta.57..4.0.0-beta.59: exactly one change to Stream.mkUint8Array in Stream.ts (Effect-TS/effect-smol#2098)
opencode.db: all 4 original errors have fully parsed tool input (secureJsonParse succeeded), ruling out AI SDK JSON parsing
- Error timestamps: 12ms elapsed (too fast for subprocess spawn), confirming the throw is in Effect/snapshot machinery
- Reproduction:
local-anthropic agent on broken binary, first call succeeded, second call (task tool, not git) failed
False leads eliminated
secureJsonParse finally block in @ai-sdk/provider-utils (DB proves parsing succeeded)
tool.ts wrap() mutation (runs at startup, not tool-call time)
- "Only git commands fail" (disproved by
task tool failing)
- Bun minifier as sole cause (the mutable pattern is the trigger, Bun's
--compile --minify object freezing is the environmental factor)
Fix
PR #25867:
snapshot/index.ts: replace Stream.mkUint8Array with Stream.runForEach + local vars (actual crash site)
git/index.ts: replace mutable Stream.runFold with Stream.runForEach + local vars
- Revert
effect from 4.0.0-beta.59 to 4.0.0-beta.57 (last known-good)
Upstream
Once the upstream fix merges, the runForEach workarounds can be reverted and effect bumped again.
Description
In v1.14.34+, the compiled + minified opencode binary fails tool execution with:
or:
Also reported in #25835.
Root cause (confirmed)
Two contributing factors:
Effect change:
effect@4.0.0-beta.58(Effect-TS/effect-smol#2098, "generate binary arrays from streams with less copying") changedStream.mkUint8Arrayfrom an immutableChannel.runFold(newUint8Arrayper iteration) to a mutable accumulator (acc.bytes += ...; acc.arrays.push(...); return acc). The bump frombeta.57tobeta.59was a routine chore (#25524) with no feature dependency.Bun compilation behavior: In
bun build --compile --minifybinaries, JSC makes the fold accumulator object non-extensible between iterations. The second chunk throws. This is consistent with known Bun--compile --minifyissues (oven-sh/bun#13394, oven-sh/bun#12655).Throw site:
snapshot/index.ts:577callsStream.mkUint8Array(handle.stdout)to collect output fromgit cat-file --batch.processor.tscallssnapshot.track()atcase "start-step"(after the first tool call completes, before the second starts). The error propagates as the next tool call's error.Reproduction
bun run(interpreted)bun build --compile(no minify)bun build --compile --minify+effect@4.0.0-beta.57bun build --compile --minify+effect@4.0.0-beta.58or laterSteps:
bun run build --singleusingeffect@4.0.0-beta.58or lateropencode serveAttempted to assign to readonly propertyWhy it appeared "git-specific": The original failing session had 35 tool calls in Step 1 (no snapshot needed). Git commands started in Step 2.
snapshot.track()fires at the step boundary. Testing with fewer tools per step confirmed atasktool (no git, no subprocess) also fails as the second call.Evidence
4.0.0-beta.57..4.0.0-beta.59: exactly one change toStream.mkUint8ArrayinStream.ts(Effect-TS/effect-smol#2098)opencode.db: all 4 original errors have fully parsed tool input (secureJsonParsesucceeded), ruling out AI SDK JSON parsinglocal-anthropicagent on broken binary, first call succeeded, second call (tasktool, not git) failedFalse leads eliminated
secureJsonParsefinallyblock in@ai-sdk/provider-utils(DB proves parsing succeeded)tool.tswrap()mutation (runs at startup, not tool-call time)tasktool failing)--compile --minifyobject freezing is the environmental factor)Fix
PR #25867:
snapshot/index.ts: replaceStream.mkUint8ArraywithStream.runForEach+ local vars (actual crash site)git/index.ts: replace mutableStream.runFoldwithStream.runForEach+ local varseffectfrom4.0.0-beta.59to4.0.0-beta.57(last known-good)Upstream
mkUint8Arrayto immutableChannel.runFold)Once the upstream fix merges, the
runForEachworkarounds can be reverted and effect bumped again.