mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
* fix(accounts): free the account queue when a sign-in is abandoned Closing Settings mid sign-in left the `codex login` / `claude auth login` child running, and every account mutation shares one FIFO queue, so the next Add Account sat behind it for the login's whole deadline and then inherited the abandoned call's timeout toast. Cancel the pending login before enqueueing the next add or reauth (never inside the queue the abandoned login owns), give Codex the cancel handle and Cancel button Claude already had, and stop reporting a cancellation as a failure. Also surface the sign-in link Codex prints, with copy and open, so the flow can be finished in a private window or another browser profile. * test(accounts): drop the bare casts CI's changed-code gate rejects The service doubles still need a cast; one documented helper per file carries the SAFETY rationale instead of nine bare `as never`s. * fix(codex): a cancel must not discard a sign-in that already succeeded The Windows post-auth watcher gives a lingering codex login five seconds to exit after it writes auth.json. A cancel arriving in that window rejected the login, and the caller's rollback then deleted the managed home that had just authenticated. Refuse the cancel once new credential bytes exist: there is nothing left to cancel, and the close handler already treats that state as success. Found by review of #21372. * fix(codex): keep a refused cancel cancellable, and require the sign-in notice Review of the auth-aware cancel guard found two holes it opened: - The outer handle latched `cancelled` before asking the session, so a refusal killed cancellation for the rest of the deadline. On a host with no post-auth watcher that reinstated the very stall this PR removes. Latch only when the cancel is accepted. - WSL never reads a pre-spawn baseline, so the guard read the auth.json that was already there and refused from the first click, making a WSL reauthentication uncancellable. Require a baseline before refusing. Also from review: publish the sign-in link from a stdout-only buffer, so an interleaved stderr chunk cannot truncate it; require codex's own "navigate to this URL" notice rather than offering the first link in the output; hide the notice in a remote account scope, where it would name a login running on this desktop; and share the cancellation message instead of matching a duplicated literal. The Claude case joins the login-process suite that already owns the two neighbouring cancel cases, and the auth-snapshot helpers move out of the session file, which the additions pushed over the line cap. * refactor(codex): cut the sign-in-link plumbing to its smallest form Review found the change correct but larger than it needs to be: - The pending-link store was a class with one permanent subscriber, a never-called unsubscribe and a try/catch that could not fire. It is a field and a listener set on the service, beside the cancel handle it already owned — and the service now clears both in one place. - The optional login-session dependencies were always supplied. - The parser's https check could not fail; the pattern already fixed the scheme. The renderer's unmount guard inside a synchronous IPC listener could not fire either. - The broadcast channel and the cancellation message are single sources of truth in src/shared now, rather than exported next to a hardcoded copy of themselves. - The duplicated seven-line rationale in both services says the same thing in three, including why only add and reauthenticate supersede. - The codex suite reuses its own factory, and unmocks once. Also reverts four reformat hunks the formatter pulled in around edits. * fix(accounts): free the queue for a switch, not only for another add Switching or removing an account shares the mutation queue an abandoned sign-in was holding, so the commonest thing a user does after giving up — pick a different account — still spun for the whole deadline while Add recovered instantly. Both now supersede, as does the Claude side. Every caller is a person: the two IPC handlers and the mobile RPC methods. No poll, sync or CLI path reaches them, and a sign-in that already wrote credentials refuses the cancel, so a switch cannot discard one that succeeded. Also from review: the Cancel button regains the gap its Claude twin has (layout is allowed by the design-system rule; only the colour override was not), and the URL subscription says what it is — registration for the process's lifetime, with no teardown to hand back.
76 lines
2.5 KiB
TypeScript
76 lines
2.5 KiB
TypeScript
/** Carries the sign-in link of an in-flight `codex login` from main to every window. */
|
|
export const CODEX_PENDING_LOGIN_URL_CHANGED_CHANNEL = 'codexAccounts:pendingLoginUrlChanged'
|
|
|
|
/** The rejection a cancelled `codex login` produces; the Accounts pane reads it to keep a cancellation out of the error toast. */
|
|
export const CODEX_LOGIN_CANCELLED_MESSAGE = 'Codex sign-in was cancelled.'
|
|
|
|
const CODEX_AUTH_ERROR_PATTERNS = [
|
|
/access token could not be refreshed/i,
|
|
/authentication session could not be refreshed/i,
|
|
/refresh token (?:has expired|was already used|was revoked)/i,
|
|
/you have since logged out or signed in to another account/i,
|
|
/please (?:log out and )?sign in again/i,
|
|
/please reauthenticate/i,
|
|
/not logged in/i,
|
|
/sign in with chatgpt/i,
|
|
/token data is not available/i,
|
|
/auth (?:is missing|tokens are missing|does not expose)/i,
|
|
// Why: app-server rejects account/rateLimits/read with this when auth.json
|
|
// holds only an API key; without classification the fetcher falls through to
|
|
// a hidden PTY probe that can only time out (15s) on every refresh.
|
|
/chatgpt authentication required/i
|
|
]
|
|
const ANSI_ESCAPE_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*[a-zA-Z]`, 'g')
|
|
|
|
export function isCodexAuthError(error: string | null | undefined): boolean {
|
|
const message = error?.trim()
|
|
if (!message) {
|
|
return false
|
|
}
|
|
return CODEX_AUTH_ERROR_PATTERNS.some((pattern) => pattern.test(message))
|
|
}
|
|
|
|
export function extractCodexAuthError(output: string | null | undefined): string | null {
|
|
if (!output) {
|
|
return null
|
|
}
|
|
|
|
let cleanPrefix = ''
|
|
for (const rawLine of iterateCodexOutputLines(output)) {
|
|
const line = rawLine.replace(ANSI_ESCAPE_RE, '').trim()
|
|
if (!line) {
|
|
continue
|
|
}
|
|
if (isCodexAuthError(line)) {
|
|
return line.slice(0, 4_000)
|
|
}
|
|
if (cleanPrefix.length < 4_000) {
|
|
cleanPrefix = cleanPrefix ? `${cleanPrefix}\n${line}` : line
|
|
cleanPrefix = cleanPrefix.slice(0, 4_000)
|
|
}
|
|
}
|
|
|
|
return isCodexAuthError(cleanPrefix) ? cleanPrefix : null
|
|
}
|
|
|
|
function* iterateCodexOutputLines(output: string): Generator<string> {
|
|
let lineStart = 0
|
|
|
|
for (let index = 0; index < output.length; index++) {
|
|
const code = output.charCodeAt(index)
|
|
if (code !== 10 && code !== 13) {
|
|
continue
|
|
}
|
|
|
|
yield output.slice(lineStart, index)
|
|
if (code === 13 && output.charCodeAt(index + 1) === 10) {
|
|
index++
|
|
}
|
|
lineStart = index + 1
|
|
}
|
|
|
|
if (lineStart <= output.length) {
|
|
yield output.slice(lineStart)
|
|
}
|
|
}
|