From 1574077e8da068d4884d420fd5a822208db4cd63 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Sat, 31 Oct 2026 21:20:57 +0000 Subject: [PATCH 1/3] fix(opencode): OpenCode 1.x text generation gives up after 3 minutes like every other runner The OpenCode 1.x runner awaited session.create and session.prompt with no deadline, so a stuck generation waited for Node fetch's 300 s default and then failed without aborting the server-side session. Put the OpenCode 2 runner's 3-minute timeout on the whole run. Timing out interrupts the in-flight prompt, so the existing session.abort runs. Co-Authored-By: Claude Opus 5.5 --- .../src/server/textGeneration.test.ts | 28 +++++++++++++++++++ .../src/server/textGeneration.ts | 12 ++++++++ 2 files changed, 40 insertions(+) diff --git a/packages/provider-opencode/src/server/textGeneration.test.ts b/packages/provider-opencode/src/server/textGeneration.test.ts index 9b9c2525190c..e24901148534 100644 --- a/packages/provider-opencode/src/server/textGeneration.test.ts +++ b/packages/provider-opencode/src/server/textGeneration.test.ts @@ -357,6 +357,34 @@ it.layer(layerOpenCodeTextGenerationTest)("OpenCodeTextGeneration", (it) => { ), ); + it.effect("gives up and aborts the session when a prompt never settles", () => + withOpenCodeTextGeneration(DEFAULT_OPENCODE_SETTINGS, (textGeneration) => + Effect.gen(function* () { + const started = yield* Deferred.make(); + let sessionAborts = 0; + runtimeMock.state.onPrompt = (signal) => { + Deferred.doneUnsafe(started, Effect.succeed(signal)); + return new Promise(() => {}); + }; + runtimeMock.state.onAbort = () => { + sessionAborts += 1; + return Promise.reject(new Error("abort observed")); + }; + const fiber = yield* textGeneration + .generateCommitMessage(DEFAULT_COMMIT_MESSAGE_INPUT) + .pipe(Effect.forkChild); + const signal = yield* Deferred.await(started); + yield* TestClock.adjust(Duration.minutes(3)); + + const error = yield* Fiber.join(fiber).pipe(Effect.flip); + expect(error).toBeInstanceOf(TextGenerationError); + expect(error.detail).toBe("OpenCode did not finish generating in time."); + expect(signal?.aborted).toBe(true); + expect(sessionAborts).toBe(1); + }), + ), + ); + it.effect("uses an environment-only password for a locally spawned server", () => withOpenCodeTextGeneration( DEFAULT_OPENCODE_SETTINGS, diff --git a/packages/provider-opencode/src/server/textGeneration.ts b/packages/provider-opencode/src/server/textGeneration.ts index eed819e44d62..9b0251c3a51e 100644 --- a/packages/provider-opencode/src/server/textGeneration.ts +++ b/packages/provider-opencode/src/server/textGeneration.ts @@ -10,6 +10,8 @@ import * as TextGenerationOperations from "@t3tools/provider-core/server/textGen import * as OpenCodeRuntime from "./OpenCodeRuntime.ts"; import * as OpenCodeServerOwner from "./OpenCodeServerOwner.ts"; +const GENERATION_TIMEOUT = "3 minutes"; + const OpenCodeTextGenerationOperation = Schema.Literals([ "generateCommitMessage", "generatePrContent", @@ -333,6 +335,16 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration" .pipe(Effect.flatMap(runAgainstServer), Effect.scoped) : serverOwner.withServer(runAgainstServer); const rawOutput = yield* serverOutput.pipe( + Effect.timeoutOrElse({ + duration: GENERATION_TIMEOUT, + orElse: () => + Effect.fail( + new TextGenerationError({ + operation: input.operation, + detail: "OpenCode did not finish generating in time.", + }), + ), + }), Effect.catchTags({ OpenCodeRuntimeError: (cause) => Effect.fail( From d0c75f71d724c962666099f824ddc8dfba271a6f Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Sat, 31 Oct 2026 22:19:35 +0000 Subject: [PATCH 2/3] test(opencode): cover the text generation limit while session.create stalls The 3-minute limit wraps the whole run, so a session.create that never settles must also end with the timeout error, cancel the request and send no prompt. Co-Authored-By: Claude Opus 5.5 --- .../src/server/textGeneration.test.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/provider-opencode/src/server/textGeneration.test.ts b/packages/provider-opencode/src/server/textGeneration.test.ts index e24901148534..49004d676469 100644 --- a/packages/provider-opencode/src/server/textGeneration.test.ts +++ b/packages/provider-opencode/src/server/textGeneration.test.ts @@ -385,6 +385,29 @@ it.layer(layerOpenCodeTextGenerationTest)("OpenCodeTextGeneration", (it) => { ), ); + it.effect("gives up when session creation never settles", () => + withOpenCodeTextGeneration(DEFAULT_OPENCODE_SETTINGS, (textGeneration) => + Effect.gen(function* () { + const started = yield* Deferred.make(); + runtimeMock.state.onSessionCreate = (signal) => { + Deferred.doneUnsafe(started, Effect.succeed(signal)); + return new Promise(() => {}); + }; + const fiber = yield* textGeneration + .generateCommitMessage(DEFAULT_COMMIT_MESSAGE_INPUT) + .pipe(Effect.forkChild); + const signal = yield* Deferred.await(started); + yield* TestClock.adjust(Duration.minutes(3)); + + const error = yield* Fiber.join(fiber).pipe(Effect.flip); + expect(error).toBeInstanceOf(TextGenerationError); + expect(error.detail).toBe("OpenCode did not finish generating in time."); + expect(signal?.aborted).toBe(true); + expect(runtimeMock.state.promptUrls).toEqual([]); + }), + ), + ); + it.effect("uses an environment-only password for a locally spawned server", () => withOpenCodeTextGeneration( DEFAULT_OPENCODE_SETTINGS, From 355d8f25eb171f9c4cfeaa7536c5e103a8d727c5 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Sat, 31 Oct 2026 23:15:24 +0000 Subject: [PATCH 3/3] test(opencode): fold the session.create timeout case into the session-creation test The stalled session.create timeout now runs first inside the existing session-creation test, ahead of its interrupt scenario, instead of as a separate test. Co-Authored-By: Claude Opus 5.5 --- .../src/server/textGeneration.test.ts | 40 ++++++++----------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/packages/provider-opencode/src/server/textGeneration.test.ts b/packages/provider-opencode/src/server/textGeneration.test.ts index 49004d676469..ebb14350c9ec 100644 --- a/packages/provider-opencode/src/server/textGeneration.test.ts +++ b/packages/provider-opencode/src/server/textGeneration.test.ts @@ -244,9 +244,24 @@ const advanceIdleClock = Effect.gen(function* () { }); it.layer(layerOpenCodeTextGenerationTest)("OpenCodeTextGeneration", (it) => { - it.effect("cancels an in-flight session creation before prompting", () => + it.effect("cancels or times out an in-flight session creation before prompting", () => withOpenCodeTextGeneration(EXISTING_SERVER_OPENCODE_SETTINGS, (textGeneration) => Effect.gen(function* () { + const timedOut = yield* Deferred.make(); + runtimeMock.state.onSessionCreate = (signal) => { + Deferred.doneUnsafe(timedOut, Effect.succeed(signal)); + return new Promise(() => {}); + }; + const timedOutFiber = yield* textGeneration + .generateCommitMessage(DEFAULT_COMMIT_MESSAGE_INPUT) + .pipe(Effect.forkChild); + const timedOutSignal = yield* Deferred.await(timedOut); + yield* TestClock.adjust(Duration.minutes(3)); + const timeoutError = yield* Fiber.join(timedOutFiber).pipe(Effect.flip); + expect(timeoutError).toBeInstanceOf(TextGenerationError); + expect(timeoutError.detail).toBe("OpenCode did not finish generating in time."); + expect(timedOutSignal?.aborted).toBe(true); + const started = yield* Deferred.make(); runtimeMock.state.onSessionCreate = (signal) => { Deferred.doneUnsafe(started, Effect.succeed(signal)); @@ -385,29 +400,6 @@ it.layer(layerOpenCodeTextGenerationTest)("OpenCodeTextGeneration", (it) => { ), ); - it.effect("gives up when session creation never settles", () => - withOpenCodeTextGeneration(DEFAULT_OPENCODE_SETTINGS, (textGeneration) => - Effect.gen(function* () { - const started = yield* Deferred.make(); - runtimeMock.state.onSessionCreate = (signal) => { - Deferred.doneUnsafe(started, Effect.succeed(signal)); - return new Promise(() => {}); - }; - const fiber = yield* textGeneration - .generateCommitMessage(DEFAULT_COMMIT_MESSAGE_INPUT) - .pipe(Effect.forkChild); - const signal = yield* Deferred.await(started); - yield* TestClock.adjust(Duration.minutes(3)); - - const error = yield* Fiber.join(fiber).pipe(Effect.flip); - expect(error).toBeInstanceOf(TextGenerationError); - expect(error.detail).toBe("OpenCode did not finish generating in time."); - expect(signal?.aborted).toBe(true); - expect(runtimeMock.state.promptUrls).toEqual([]); - }), - ), - ); - it.effect("uses an environment-only password for a locally spawned server", () => withOpenCodeTextGeneration( DEFAULT_OPENCODE_SETTINGS,