Skip to content

Commit 73098c4

Browse files
committed
Keep credential failures on login copy
Auth and OAuth-refresh send failures were still suggesting /model. Keep that copy on login, drop the fused task coverage after spawn/wait became the only path, and document the no-failover behavior.
1 parent 3f44694 commit 73098c4

6 files changed

Lines changed: 26 additions & 39 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename
3131

3232
- Cancelling a `task` or `wait_agents` worker reports wait status `interrupted`,
3333
not `failed`.
34+
- Inference no longer fails over to a backup provider. A selected-provider
35+
failure stays on that provider; switch with `/model`.
3436

3537
### Fixed
3638

@@ -44,6 +46,8 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename
4446
longer swallow `--*` or `-h` as their option values.
4547
- `wait_agents` no longer collects a stale completed or interrupted stamp
4648
when a followup is already in flight.
49+
- Credential-refresh and auth send failures tell the user to log in again
50+
instead of suggesting `/model`.
4751

4852
## [0.3.11] - 2026-08-31
4953

src/exec/runner.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import { createAgentWithLiveToolDispatch } from "../agent/live-tool-dispatch.js"
6363
import { liveTelemetry } from "../telemetry/singleton.js";
6464
import { createTurnObserver } from "../telemetry/ai-observability.js";
6565
import {
66+
CREDENTIAL_FAILURE_USER_MESSAGE,
6667
isResolvedProviderFailureError,
6768
terminalProviderFailureMessage,
6869
} from "../inference-error-message.js";
@@ -136,11 +137,10 @@ export function execUserFailureMessage(
136137
err: unknown,
137138
providerFailureObserved: boolean,
138139
): string {
139-
if (
140-
providerFailureObserved ||
141-
isResolvedProviderFailureError(err) ||
142-
(err instanceof Error && err.name === SELECTED_PROVIDER_FAILURE)
143-
) {
140+
if (err instanceof Error && err.name === SELECTED_PROVIDER_FAILURE) {
141+
return CREDENTIAL_FAILURE_USER_MESSAGE;
142+
}
143+
if (providerFailureObserved || isResolvedProviderFailureError(err)) {
144144
return terminalProviderFailureMessage(
145145
config.providerName,
146146
config.settings?.providers[config.providerName]?.name,

src/subagent/run-resolved-provider-failure.test.ts

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@ import {
1111
type ResolvedProviderFailureError,
1212
} from "../inference-error-message.js";
1313
import { createPermissionGate } from "../permission/gate.js";
14-
import { createFleetRecords, createSpawnAgentTool, createWaitAgentsTool } from "./agent-fleet.js";
14+
import { createFleetMailbox, createSpawnAgentTool, createWaitAgentsTool } from "./agent-fleet.js";
1515
import { createSubAgentSessionStore } from "./session-store.js";
16-
import { createTaskTool } from "./task-tool.js";
1716
import type { RunSubAgentParams, RunSubAgentResult } from "./types.js";
1817

1918
const RAW_DIAGNOSTIC = "POST https://provider.invalid returned secret response body";
@@ -130,33 +129,10 @@ describe("resolved sub-agent provider failures", () => {
130129
expect(observed.some((event) => JSON.stringify(event).includes(RAW_DIAGNOSTIC))).toBe(true);
131130
});
132131

133-
test("fused task returns only the safe message for the resolved reply sequence", async () => {
134-
await withResolvedProviderRun(async (run, cwd) => {
135-
const sessions = createSubAgentSessionStore();
136-
const task = createTaskTool({
137-
...runParams(cwd),
138-
getWorkdirBase: () => join(cwd, ".ctx"),
139-
sessions,
140-
fleetRecords: createFleetRecords(),
141-
run,
142-
});
143-
const result = await callTool(task, "task", {
144-
description: "provider failure",
145-
prompt: "trigger it",
146-
intent: "explore",
147-
});
148-
149-
expect(result.isError).toBe(true);
150-
expect(result.content).toBe(SAFE_MESSAGE);
151-
expect(String(result.content)).not.toContain(RAW_DIAGNOSTIC);
152-
expect(sessions.list()[0]?.error).toBe(SAFE_MESSAGE);
153-
});
154-
});
155-
156132
test("split spawn_agent and wait_agents return only the safe message", async () => {
157133
await withResolvedProviderRun(async (run, cwd) => {
158134
const sessions = createSubAgentSessionStore();
159-
const fleetRecords = createFleetRecords();
135+
const fleetRecords = createFleetMailbox(sessions);
160136
const deps = {
161137
...runParams(cwd),
162138
getWorkdirBase: () => join(cwd, ".ctx"),

src/tui/runner.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ import {
217217
} from "./session-chrome.js";
218218
import { ingestOperatorPrompt } from "./prompt-attachments.js";
219219
import {
220+
CREDENTIAL_FAILURE_USER_MESSAGE,
220221
isResolvedProviderFailureError,
221222
terminalProviderFailureMessage,
222223
} from "../inference-error-message.js";
@@ -619,11 +620,10 @@ export function tuiSendFailureMessage(
619620
providerFailureObserved: boolean,
620621
attempt: InferenceAttemptIdentity,
621622
): string {
622-
if (
623-
failureKind !== "auth" &&
624-
!providerFailureObserved &&
625-
!isResolvedProviderFailureError(error)
626-
) {
623+
if (failureKind === "auth") {
624+
return CREDENTIAL_FAILURE_USER_MESSAGE;
625+
}
626+
if (!providerFailureObserved && !isResolvedProviderFailureError(error)) {
627627
return error instanceof Error ? error.message : String(error);
628628
}
629629
const providerId = isResolvedProviderFailureError(error) ? error.providerId : attempt.providerId;

tests/unit/exec/runner.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,7 @@ describe("selected provider refresh failures", () => {
5858
} catch (err) {
5959
expect(formatCaughtError(err)).toBe(rawDiagnostic);
6060
const userMessage = execUserFailureMessage(config, err, false);
61-
expect(userMessage).toBe(
62-
'Codex Provider failed. Try again or switch with "/model" and select another.',
63-
);
61+
expect(userMessage).toBe("Authentication failed — log in again.");
6462
expect(userMessage).not.toContain(rawDiagnostic);
6563
}
6664
});

tests/unit/tui/runner.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ test("TUI send failures retain the in-flight provider identity across model swit
5858
).toBe('Codex Provider failed. Try again or switch with "/model" and select another.');
5959
});
6060

61+
test("TUI auth failures tell the user to log in again instead of switching models", () => {
62+
expect(
63+
tuiSendFailureMessage(new Error("401 refresh token rejected"), "auth", false, {
64+
providerId: "codex/work",
65+
displayLabel: "Codex",
66+
}),
67+
).toBe("Authentication failed — log in again.");
68+
});
69+
6170
test("loadLocalSettingsWriteBase distinguishes absent from unreadable", async () => {
6271
// Absent → empty base (safe to write a single key).
6372
expect(await loadLocalSettingsWriteBase("/nope", async () => null)).toEqual({});

0 commit comments

Comments
 (0)