From 1e1dca64f2ccd954fd943eff65f2f34e280fe18c Mon Sep 17 00:00:00 2001 From: Stephan Schielke Date: Tue, 5 May 2026 13:36:24 +0100 Subject: [PATCH 1/2] fix(git): replace mutating Stream.runFold with Stream.runForEach 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. --- packages/opencode/src/git/index.ts | 36 ++++++++++++++++-------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/opencode/src/git/index.ts b/packages/opencode/src/git/index.ts index fff1d70b2a41..6a4129110d2d 100644 --- a/packages/opencode/src/git/index.ts +++ b/packages/opencode/src/git/index.ts @@ -114,23 +114,25 @@ export const layer = Layer.effect( }) const handle = yield* spawner.spawn(proc) const collect = (stream: typeof handle.stdout) => - Stream.runFold( - stream, - () => ({ chunks: [] as Uint8Array[], bytes: 0, truncated: false }), - (acc, chunk) => { - if (opts.maxOutputBytes === undefined) { - acc.chunks.push(chunk) - acc.bytes += chunk.length - return acc - } - - const remaining = opts.maxOutputBytes - acc.bytes - if (remaining > 0) acc.chunks.push(remaining >= chunk.length ? chunk : chunk.slice(0, remaining)) - acc.bytes += chunk.length - acc.truncated = acc.truncated || acc.bytes > opts.maxOutputBytes - return acc - }, - ).pipe(Effect.map((x) => ({ buffer: Buffer.concat(x.chunks), truncated: x.truncated }))) + Effect.gen(function* () { + const chunks: Uint8Array[] = [] + let bytes = 0 + let truncated = false + yield* Stream.runForEach(stream, (chunk) => + Effect.sync(() => { + if (opts.maxOutputBytes === undefined) { + chunks.push(chunk) + bytes += chunk.length + } else { + const remaining = opts.maxOutputBytes - bytes + if (remaining > 0) chunks.push(remaining >= chunk.length ? chunk : chunk.slice(0, remaining)) + bytes += chunk.length + truncated = truncated || bytes > opts.maxOutputBytes + } + }), + ) + return { buffer: Buffer.concat(chunks), truncated } + }) const [stdout, stderr] = yield* Effect.all([collect(handle.stdout), collect(handle.stderr)], { concurrency: 2 }) return { exitCode: yield* handle.exitCode, From 2a5c51791f58c98943230dc1952c1fe6a15deed8 Mon Sep 17 00:00:00 2001 From: Stephan Schielke Date: Wed, 6 May 2026 07:34:23 +0100 Subject: [PATCH 2/2] fix(snapshot): replace Stream.mkUint8Array with runForEach to avoid mutable fold accumulator crash in Bun compiled binaries --- packages/opencode/src/snapshot/index.ts | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index ea30f5afc7ca..9e8634376339 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -573,8 +573,28 @@ export const layer: Layer.Layer< stdin: Stream.make(new TextEncoder().encode(refs.map((item) => item.ref).join("\n") + "\n")), }) const handle = yield* spawner.spawn(proc) + const collectUint8 = (stream: typeof handle.stdout) => { + const chunks: Uint8Array[] = [] + let bytes = 0 + return Stream.runForEach(stream, (chunk) => + Effect.sync(() => { + chunks.push(chunk) + bytes += chunk.length + }), + ).pipe( + Effect.map(() => { + const result = new Uint8Array(bytes) + let offset = 0 + for (const chunk of chunks) { + result.set(chunk, offset) + offset += chunk.length + } + return result + }), + ) + } const [out, err] = yield* Effect.all( - [Stream.mkUint8Array(handle.stdout), Stream.mkString(Stream.decodeText(handle.stderr))], + [collectUint8(handle.stdout), Stream.mkString(Stream.decodeText(handle.stderr))], { concurrency: 2 }, ) const code = yield* handle.exitCode