From d181dbda7390f01f56198fd84780c05a6d93b7d7 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Sat, 31 Oct 2026 21:03:56 +0000 Subject: [PATCH 1/2] fix(opencode): OpenCode 2 retry rows keep the usage-limit class A retry scheduled for a rate limit or a 429 quota showed as a provider error while it ran, and only the terminal failure row was classed as a usage limit. Retry rows now share the terminal row's classification, so an out-of-funds 402 quota stays a provider error and the row stays retryable. Co-Authored-By: Claude Opus 5.5 --- .../Adapters/OpenCode2AdapterV2.test.ts | 24 +++++++++++++++---- .../src/server/v2/adapter.ts | 8 ++++--- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts index bb26a5dd53a30..af2f85a5d97a6 100644 --- a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts @@ -568,11 +568,27 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { const { runtime, thread } = yield* resumed([ out("session.prompt", { sessionID: SESSION, text: "" }), promptAccepted, + event("session.retry.scheduled", { + sessionID: SESSION, + assistantMessageID: "msg_0eb735d5b001oAFVeY5jz3WD4Z", + attempt: 2, + at: 2_001, + error, + }), event("session.execution.failed", { sessionID: SESSION, error }), ]); - const terminal = yield* terminalOf(runtime).pipe(Effect.forkScoped); + const collected = yield* runtime.events.pipe( + Stream.takeUntil((event) => event.type === "turn.terminal"), + Stream.runCollect, + Effect.forkScoped, + ); yield* runtime.startTurn(turnInput(thread)); - const ended = yield* Fiber.join(terminal); + const seen = yield* Fiber.join(collected); + assert.deepInclude(errorRows(seen)[0], { + status: "running", + failure: { class: failureClass, code: error.type, message: error.message, retryable: true }, + }); + const ended = seen.find((event) => event.type === "turn.terminal"); assert.equal(ended?.status, "failed"); assert.deepEqual(ended?.failure, { class: failureClass, @@ -2934,7 +2950,7 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { error: rateLimited, }); const retrying = { - class: "provider_error", + class: "usage_limit", message: rateLimited.message, code: rateLimited.type, retryable: true, @@ -3116,7 +3132,7 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { ], gate, ); - const failed = { ...retrying, class: "usage_limit" as const, retryable: null }; + const failed = { ...retrying, retryable: null }; const lastRetry = { attempt: 2, maxAttempts: null, retryDelayMs: 4_000 }; const rows = errorRows(seen); assert.deepEqual( diff --git a/packages/provider-opencode/src/server/v2/adapter.ts b/packages/provider-opencode/src/server/v2/adapter.ts index 74695bf6061aa..c237d9b6ebd70 100644 --- a/packages/provider-opencode/src/server/v2/adapter.ts +++ b/packages/provider-opencode/src/server/v2/adapter.ts @@ -512,6 +512,9 @@ const reportOutcome = (state: string | undefined) => const subagentOutput = (text: string) => /^]*>\n?([\s\S]*?)\n?<\/subagent>$/.exec(text.trim())?.[1] ?? text; +const isUsageLimit = (type: string, status: number | undefined) => + (type === "provider.quota" && status !== 402) || type === "provider.rate-limit"; + const isContinuation = (turnInput: ProviderAdapter.ProviderAdapterV2TurnInput) => turnInput.message.createdBy === "agent" && turnInput.message.creationSource === "provider"; @@ -1076,7 +1079,7 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr failure: makeProviderFailure({ message: readableProviderErrorMessage(error.message, error.status, error.response?.body), code: error.type, - class: "provider_error", + class: isUsageLimit(error.type, error.status) ? "usage_limit" : "provider_error", retryable: true, }), startedAt: turn.retry?.startedAt ?? (yield* DateTime.now), @@ -2129,8 +2132,7 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr const { type, message, status, response } = event.data.error; // OpenCode types an empty balance as provider.quota too, but it answers // that with HTTP 402 and it never resets. Usage windows and budgets are 429. - const usageLimit = - (type === "provider.quota" && status !== 402) || type === "provider.rate-limit"; + const usageLimit = isUsageLimit(type, status); return yield* endExecution(state, turn, { status: "failed", failure: makeProviderFailure({ From 278a992f209a389fc7abccad391bfd5a9d731329 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 2 Nov 2026 22:09:55 +0000 Subject: [PATCH 2/2] test(opencode): preserve Claude retry classification after merge --- .../orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts index dc5794d7a0767..fe19891627d72 100644 --- a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts @@ -602,7 +602,12 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { const seen = yield* Fiber.join(collected); assert.deepInclude(errorRows(seen)[0], { status: "running", - failure: { class: failureClass, code: error.type, message: error.message, retryable: true }, + failure: { + class: error.type === "provider.unknown" ? "provider_error" : failureClass, + code: error.type, + message: error.message, + retryable: true, + }, }); const ended = seen.find((event) => event.type === "turn.terminal"); assert.equal(ended?.status, "failed");