From 7183f5b7a962ddf52592f0f734fab4aff25961ce Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Tue, 30 Jun 2026 11:45:56 +0200 Subject: [PATCH 1/3] test(cli): deflake prompt cancellation race --- packages/opencode/test/session/prompt.test.ts | 47 ++++++++++++------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 285546a1cf6..1c2bb9bd8a8 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -160,11 +160,32 @@ const status = SessionStatus.layer.pipe(Layer.provideMerge(Bus.layer)) const run = SessionRunState.layer.pipe(Layer.provide(status)) const infra = Layer.mergeAll(NodeFileSystem.layer, CrossSpawnSpawner.defaultLayer) -const processorCreateStarted: Array<() => void> = [] +const agent: AgentSvc.Info = { + name: "build", + mode: "primary", + native: true, + permission: Permission.fromConfig({ "*": "allow" }), + model: ref, + options: {}, +} +const fastAgents = Layer.mock(AgentSvc.Service)({ + get: () => Effect.succeed(agent), + list: () => Effect.succeed([agent]), + defaultInfo: () => Effect.succeed(agent), + defaultAgent: () => Effect.succeed(agent.name), + guardRequirements: () => Effect.void, +}) + +const processorCreateStarted: Deferred.Deferred[] = [] const blockingProcessor = Layer.succeed( SessionProcessor.Service, SessionProcessor.Service.of({ - create: () => Effect.sync(() => processorCreateStarted.shift()?.()).pipe(Effect.andThen(Effect.never)), + create: () => + Effect.gen(function* () { + const started = processorCreateStarted.shift() + if (started) yield* Deferred.succeed(started, undefined).pipe(Effect.ignore) + return yield* Effect.never + }), }), ) @@ -174,7 +195,7 @@ function makePrompt(input?: { processor?: "blocking" }) { Snapshot.defaultLayer, LLM.defaultLayer, Env.defaultLayer, - AgentSvc.defaultLayer, + input?.processor === "blocking" ? fastAgents : AgentSvc.defaultLayer, Command.defaultLayer, Permission.defaultLayer, Plugin.defaultLayer, @@ -357,14 +378,6 @@ const deferredAsPromise = (deferred: Deferred.Deferred): PromiseLike => }, }) -function defer() { - let resolve!: (value: T | PromiseLike) => void - const promise = new Promise((done) => { - resolve = done - }) - return { promise, resolve } -} - const succeedVoid = (deferred: Deferred.Deferred) => { Effect.runSync(Deferred.succeed(deferred, void 0).pipe(Effect.ignore)) } @@ -1149,10 +1162,10 @@ raceNoLLMServer.instance( parts: [{ type: "text", text: "first" }], }) - const firstCreate = defer() - processorCreateStarted.push(firstCreate.resolve) + const firstCreate = yield* Deferred.make() + processorCreateStarted.push(firstCreate) const first = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild) - yield* Effect.promise(() => firstCreate.promise) + yield* awaitWithTimeout(Deferred.await(firstCreate), "processor.create did not start for first turn") yield* prompt.cancel(chat.id) const firstExit = yield* Fiber.await(first) @@ -1175,10 +1188,10 @@ raceNoLLMServer.instance( parts: [{ type: "text", text: "second" }], }) - const secondCreate = defer() - processorCreateStarted.push(secondCreate.resolve) + const secondCreate = yield* Deferred.make() + processorCreateStarted.push(secondCreate) const second = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild) - yield* Effect.promise(() => secondCreate.promise) + yield* awaitWithTimeout(Deferred.await(secondCreate), "processor.create did not start for second turn") yield* prompt.cancel(chat.id) const secondExit = yield* Fiber.await(second) From 06e346b89a7a44b3505c6a82af25490aaf81d8c1 Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Tue, 30 Jun 2026 12:06:19 +0200 Subject: [PATCH 2/3] test(cli): annotate prompt flake fixture --- packages/opencode/test/session/prompt.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 1c2bb9bd8a8..9634838f725 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -160,6 +160,7 @@ const status = SessionStatus.layer.pipe(Layer.provideMerge(Bus.layer)) const run = SessionRunState.layer.pipe(Layer.provide(status)) const infra = Layer.mergeAll(NodeFileSystem.layer, CrossSpawnSpawner.defaultLayer) +// kilocode_change start const agent: AgentSvc.Info = { name: "build", mode: "primary", @@ -188,6 +189,7 @@ const blockingProcessor = Layer.succeed( }), }), ) +// kilocode_change end function makePrompt(input?: { processor?: "blocking" }) { const deps = Layer.mergeAll( @@ -195,7 +197,7 @@ function makePrompt(input?: { processor?: "blocking" }) { Snapshot.defaultLayer, LLM.defaultLayer, Env.defaultLayer, - input?.processor === "blocking" ? fastAgents : AgentSvc.defaultLayer, + input?.processor === "blocking" ? fastAgents : AgentSvc.defaultLayer, // kilocode_change Command.defaultLayer, Permission.defaultLayer, Plugin.defaultLayer, @@ -1162,10 +1164,12 @@ raceNoLLMServer.instance( parts: [{ type: "text", text: "first" }], }) + // kilocode_change start const firstCreate = yield* Deferred.make() processorCreateStarted.push(firstCreate) const first = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild) yield* awaitWithTimeout(Deferred.await(firstCreate), "processor.create did not start for first turn") + // kilocode_change end yield* prompt.cancel(chat.id) const firstExit = yield* Fiber.await(first) @@ -1188,10 +1192,12 @@ raceNoLLMServer.instance( parts: [{ type: "text", text: "second" }], }) + // kilocode_change start const secondCreate = yield* Deferred.make() processorCreateStarted.push(secondCreate) const second = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild) yield* awaitWithTimeout(Deferred.await(secondCreate), "processor.create did not start for second turn") + // kilocode_change end yield* prompt.cancel(chat.id) const secondExit = yield* Fiber.await(second) From 96f9de4b800ea653b043a6711595d01ca3cb436e Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Tue, 30 Jun 2026 12:27:40 +0200 Subject: [PATCH 3/3] fix(cli): flush persistent process logs before readiness --- .../src/kilocode/background-process/runner.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/opencode/src/kilocode/background-process/runner.ts b/packages/opencode/src/kilocode/background-process/runner.ts index 89b7f71891f..fe8488158d5 100644 --- a/packages/opencode/src/kilocode/background-process/runner.ts +++ b/packages/opencode/src/kilocode/background-process/runner.ts @@ -64,23 +64,23 @@ export namespace BackgroundProcessRunner { let size = (await file.stat()).size let queue = Promise.resolve() const append = (chunk: Buffer) => { - if (!process.stdout.destroyed) process.stdout.write(chunk) queue = queue.then(async () => { if (size + chunk.length <= MAX) { await file.write(chunk) size += chunk.length - return + } else { + await file.close() + const source = Bun.file(input.log) + const old = size + ? Buffer.from(await source.slice(Math.max(0, size - KEEP), size).arrayBuffer()) + : Buffer.alloc(0) + const next = Buffer.concat([old, chunk]) + const tail = next.subarray(Math.max(0, next.length - KEEP)) + await Filesystem.write(input.log, tail, MODE) + file = await open(input.log, "a", MODE) + size = tail.length } - await file.close() - const source = Bun.file(input.log) - const old = size - ? Buffer.from(await source.slice(Math.max(0, size - KEEP), size).arrayBuffer()) - : Buffer.alloc(0) - const next = Buffer.concat([old, chunk]) - const tail = next.subarray(Math.max(0, next.length - KEEP)) - await Filesystem.write(input.log, tail, MODE) - file = await open(input.log, "a", MODE) - size = tail.length + if (!process.stdout.destroyed) process.stdout.write(chunk) }) } return {