# Files changed · PR \#23908 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/23908/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[fix\(opencode\): actionable error when a session outgrows the model window](https://git.cafe/pingdotgg/t3code/pull/23908.md)

Version: 6

Head commit: ba63cde9cd35f4b927d861512200c945803822cf

Base commit: 51273020db3245f4ec29514a419d29b5dcb012dc

Comparison base: 51273020db3245f4ec29514a419d29b5dcb012dc

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## packages/provider-opencode/src/server/v2/adapter.ts

Status: modified; +26; −1

```diff
@@ -56,6 +56,7 @@
 import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
 import * as OpenCode2Client from "./OpenCode2Client.ts";
 import * as OpenCode2Server from "./OpenCode2Server.ts";
+import { oversizedSessionMessage } from "./oversizedSession.ts";
 import * as OpenCodeRuntime from "../OpenCodeRuntime.ts";
 import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
 import { buildRuntimeInstructions } from "@t3tools/provider-core/server/runtimeInstructions";
@@ -3708,9 +3709,33 @@
               state.active === turn
                 ? Effect.gen(function* () {
                     if (!CLEAR_PROMPT_REJECTIONS.has(cause._tag)) markUnsettled(state);
+                    // A prompt the session outgrew reads as a bare
+                    // InvalidRequest; say what actually happened instead of
+                    // the provider's generic text. Anything without size
+                    // evidence keeps the original error.
+                    const oversized = oversizedSessionMessage({
+                      causeTag: cause._tag,
+                      lastStepInputTokens:
+                        turn.lastStep === undefined
+                          ? undefined
+                          : turn.lastStep.input +
+                            turn.lastStep.cache.read +
+                            turn.lastStep.cache.write,
+                      windowTokens: windowOf(
+                        directoryOf(turnInput.runtimePolicy.cwd),
+                        turnInput.modelSelection.model,
+                      ),
+                    });
                     yield* finishTurn(state, {
                       status: "failed",
+                      failure:
+                        oversized === null
+                          ? makeProviderFailure({ cause, class: "provider_error" })
+                          : makeProviderFailure({
+                              cause,
-                      failure: makeProviderFailure({ cause, class: "provider_error" }),
+                              class: "provider_error",
+                              message: oversized,
+                            }),
                     });
                   })
                 : Effect.void,
```

## packages/provider-opencode/src/server/v2/oversizedSession.test.ts

Status: added; +77; −0

```diff
@@ -1,0 +1,77 @@
+import { describe, expect, it } from "@effect/vitest";
+
+import { oversizedSessionMessage } from "./oversizedSession.ts";
+
+describe("oversizedSessionMessage", () => {
+  it("maps a window-exceeding InvalidRequest to the actionable message", () => {
+    expect(
+      oversizedSessionMessage({
+        causeTag: "InvalidRequestError",
+        lastStepInputTokens: 8_700_000,
+        windowTokens: 1_000_000,
+      }),
+    ).toBe(
+      "This session has grown past the model's context window " +
+        "(8,700,000 tokens used of 1,000,000). " +
+        "Fork from an earlier completed run or start a fresh thread; " +
+        "retrying this run will fail the same way.",
+    );
+  });
+
+  it("fires exactly at the window boundary", () => {
+    expect(
+      oversizedSessionMessage({
+        causeTag: "InvalidRequestError",
+        lastStepInputTokens: 200_000,
+        windowTokens: 200_000,
+      }),
+    ).not.toBeNull();
+  });
+
+  it("ignores cumulative totals when the last step fits the window", () => {
+    expect(
+      oversizedSessionMessage({
+        causeTag: "InvalidRequestError",
+        lastStepInputTokens: 80_000,
+        windowTokens: 200_000,
+      }),
+    ).toBeNull();
+  });
+
+  it("leaves other rejections alone", () => {
+    expect(
+      oversizedSessionMessage({
+        causeTag: "ConflictError",
+        lastStepInputTokens: 8_700_000,
+        windowTokens: 1_000_000,
+      }),
+    ).toBeNull();
+  });
+
+  it("leaves below-window requests alone", () => {
+    expect(
+      oversizedSessionMessage({
+        causeTag: "InvalidRequestError",
+        lastStepInputTokens: 50_000,
+        windowTokens: 1_000_000,
+      }),
+    ).toBeNull();
+  });
+
+  it("leaves unknown windows and prompt-less failures alone", () => {
+    expect(
+      oversizedSessionMessage({
+        causeTag: "InvalidRequestError",
+        lastStepInputTokens: 8_700_000,
+        windowTokens: undefined,
+      }),
+    ).toBeNull();
+    expect(
+      oversizedSessionMessage({
+        causeTag: "InvalidRequestError",
+        lastStepInputTokens: undefined,
+        windowTokens: 1_000_000,
+      }),
+    ).toBeNull();
+  });
+});
```

## packages/provider-opencode/src/server/v2/oversizedSession.ts

Status: added; +34; −0

```diff
@@ -1,0 +1,34 @@
+/**
+ * Why an OpenCode prompt was refused as oversized.
+ *
+ * OpenCode reports a session that outgrew the model's context window as a
+ * bare InvalidRequest ("The request contains invalid parameters"), which
+ * reads as a bug in the request. Retrying the same run then fails
+ * identically, because the history that broke the request is still there.
+ * This maps that case to an actionable message: fork from an earlier
+ * completed run or start a fresh thread.
+ *
+ * Only fires with per-request size evidence: the last completed step's
+ * input is the live context size, and it must already meet the known
+ * window. Cumulative turn totals are deliberately not used — several steps
+ * that each fit the window can sum past it, and a later unrelated
+ * rejection must keep its original error. Unknown windows and prompt-less
+ * failures are left alone for the same reason.
+ */
+export const oversizedSessionMessage = (input: {
+  readonly causeTag: string | undefined;
+  readonly lastStepInputTokens: number | undefined;
+  readonly windowTokens: number | undefined;
+}): string | null => {
+  if (input.causeTag !== "InvalidRequestError") return null;
+  if (input.windowTokens === undefined || input.lastStepInputTokens === undefined) return null;
+  if (input.lastStepInputTokens < input.windowTokens) return null;
+  const used = input.lastStepInputTokens.toLocaleString("en-US");
+  const window = input.windowTokens.toLocaleString("en-US");
+  return (
+    `This session has grown past the model's context window ` +
+    `(${used} tokens used of ${window}). ` +
+    `Fork from an earlier completed run or start a fresh thread; ` +
+    `retrying this run will fail the same way.`
+  );
+};
```
