diff --git a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts index bb52bb92daea3..fe19891627d72 100644 --- a/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts @@ -584,11 +584,32 @@ 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: 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"); assert.deepEqual(ended?.failure, { class: failureClass, @@ -3036,7 +3057,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, @@ -3218,7 +3239,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 ae74eb2908e86..ec21e6c6db7c6 100644 --- a/packages/provider-opencode/src/server/v2/adapter.ts +++ b/packages/provider-opencode/src/server/v2/adapter.ts @@ -517,6 +517,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"; @@ -1098,7 +1101,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), @@ -2159,10 +2162,7 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr // 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 claudeLimit = claudeUsageLimit(message); - const usageLimit = - (type === "provider.quota" && status !== 402) || - type === "provider.rate-limit" || - claudeLimit !== undefined; + const usageLimit = isUsageLimit(type, status) || claudeLimit !== undefined; return yield* endExecution(state, turn, { status: "failed", failure: makeProviderFailure({