Recognize sign-in failures for every agent and fall through
Agents / agent (pull_request) Has been cancelled
Check / deno (pull_request) Failing after 35s

Codex re-runs device login on revoked, expired, or reused sessions and
token_revoked/401 errors, not only invalid_refresh_token. Claude and
OpenCode credential rejections are classified too. Sign-in failures fall
through to the next profile, and the failure comment names each tried
profile with a fixed reason label.
This commit is contained in:
Danny Kim
2026-10-12 11:38:21 +09:00
parent ea20f6d629
commit 3565988249
11 changed files with 411 additions and 36 deletions
+34 -9
View File
@@ -4,6 +4,8 @@ import { TextLineStream } from "jsr:@std/streams@1/text-line-stream";
import {
type CodexAuthentication,
codexAuthentication,
isClaudeAuthFailure,
isCodexAuthFailure,
persistCodexAuth,
retryInvalidToken,
stageCodexAuth,
@@ -14,7 +16,9 @@ import {
readProfileFile,
} from "./isolation.ts";
import {
AgentAuthError,
agentEnvironment,
type ProfileFailure,
runProfiles,
sanitizedEnvironment,
withProfile,
@@ -22,6 +26,7 @@ import {
import {
collectOpenCodeOutput,
openCodeArgs,
openCodeError,
parseOpenCodeEvents,
} from "./opencode.ts";
import {
@@ -195,7 +200,12 @@ async function runClaude(
stderr: "piped",
}).spawn();
let result:
| { is_error?: boolean; result?: string; subtype: string }
| {
is_error?: boolean;
result?: string;
subtype: string;
api_error_status?: number | null;
}
| undefined;
let error = "";
const copyError = claude.stderr.pipeThrough(new TextDecoderStream()).pipeTo(
@@ -220,7 +230,13 @@ async function runClaude(
}
const [status] = await Promise.all([claude.status, copyError]);
if (result?.is_error) {
throw new Error(result.result ?? `claude ended with ${result.subtype}`);
const failure = new Error(
result.result ?? `claude ended with ${result.subtype}`,
);
if (isClaudeAuthFailure(result)) {
throw new AgentAuthError("claude sign-in failed", { cause: failure });
}
throw failure;
}
if (result?.result === undefined) {
throw new Error(
@@ -257,12 +273,14 @@ async function codexDeviceLogin(
status.then(() => true),
new Promise<boolean>((resolve) => setTimeout(() => resolve(false), 100)),
]);
if (exited) throw new Error("AI bot login produced no instructions");
if (exited) {
throw new AgentAuthError("AI bot login produced no instructions");
}
}
await postAuthorComment(withProfile(shown, profile));
const [result] = await Promise.all([status, drained]);
if (!result.success) {
throw new Error(`codex login exited with ${result.code}`);
throw new AgentAuthError(`codex login exited with ${result.code}`);
}
}
@@ -347,7 +365,8 @@ async function runCodex(
error += decoder.decode(chunk, { stream: true });
}
error += decoder.decode();
return { status: await codex.status, error };
// Codex echoes the prompt, whose comments may quote auth errors.
return { status: await codex.status, error: error.replaceAll(prompt, "") };
};
const result = authentication.kind === "api-key"
? await attempt()
@@ -356,9 +375,13 @@ async function runCodex(
await codexDeviceLogin(profile, environment);
});
if (!result.status.success) {
throw new Error(
const failure = new Error(
result.error.trim() || `codex exited with ${result.status.code}`,
);
if (isCodexAuthFailure(result.error)) {
throw new AgentAuthError("codex sign-in failed", { cause: failure });
}
throw failure;
}
return outputPath;
}
@@ -393,7 +416,8 @@ async function runOpenCode(
const [status, streams] = await Promise.all([process.status, output]);
const error = streams.stderr.trim();
if (!status.success) {
throw new Error(error || `opencode exited with ${status.code}`);
throw openCodeError(streams.stdout) ??
new Error(error || `opencode exited with ${status.code}`);
}
return parseOpenCodeEvents(streams.stdout);
}
@@ -455,14 +479,15 @@ async function writeOutcome(outcome: RuntimeOutcome): Promise<void> {
await Deno.chmod(MANIFEST.outcomeFile, 0o600);
}
const failures: ProfileFailure[] = [];
try {
await configureGitAuthor();
await writeOutcome({
ok: true,
...await runProfiles(MANIFEST.profiles, runProfile),
...await runProfiles(MANIFEST.profiles, runProfile, failures),
});
} catch (error) {
console.error(error);
await writeOutcome({ ok: false });
await writeOutcome({ ok: false, failures });
throw error;
}