Skip to content

fix(account-management): clean removal, dead-fallback re-login indicator, OAuth-add label, refresh-error classification#99

Open
iceteaSA wants to merge 2 commits into
cortexkit:mainfrom
iceteaSA:fix/account-management
Open

fix(account-management): clean removal, dead-fallback re-login indicator, OAuth-add label, refresh-error classification#99
iceteaSA wants to merge 2 commits into
cortexkit:mainfrom
iceteaSA:fix/account-management

Conversation

@iceteaSA

@iceteaSA iceteaSA commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Four account-management fixes.

Fixes

1. Prune orphaned per-account state on removal (core/accounts.ts)
saveAccountStateUnlocked only pruned removed-account state on a scoped save; the removal path does a full save, so the state.accounts.<id> block (tokens/quota/refresh-error) was left orphaned in anthropic-auth-state.json. Invisible until the same id is re-added (e.g. re-login reusing a label-derived id), where the stale state would merge onto the new account. The full-save branch now drops any state-account id not present in storage.accounts.

2. Surface a dead-fallback "needs re-login" indicator (sidebar-state.ts, index.ts, tui.tsx)
A fallback whose OAuth refresh token is permanently dead (invalid_grant) is dropped by getUsableFallbackAccounts, so under fallback-first routing it silently degrades to main with no signal. New required needsReauth field on SidebarAccountState, computed from lastRefreshError && refreshBackoffActive && isPermanentRefreshError, rendered as a re-login status in the sidebar. Clears automatically after re-login (keyed on the refresh-token hash).

3. Prompt for a label when adding an OAuth account via /claude-account (core/commands/account.ts, index.ts, tui/command-dialogs.tsx)
The in-modal OAuth add built the account with no label, so it displayed as a raw UUID. Adds --label to add-oauth-finish + a label prompt in the modal (collected at the code step). The account id stays a randomUUID() (OAuth has no natural key); only the display label is set.

4. Classify refresh errors with an explicit permanent flag (core/accounts.ts)
AccountOperationError gains status + permanent (set at construction: permanent = status === 400 for invalid_grant). isPermanentRefreshError precedence: explicit permanentstatus === 400 → a legacy 24h-delay heuristic (back-compat only for errors persisted before the field existed). This keeps a transient (429/5xx/retry-exhausted) error from being mis-flagged as "needs re-login". The discriminators are preserved across the save/load round-trip (normalizeOperationError) so a reload can't undo the classification.

Verification

Each fix has a RED→GREEN test. Gates green: opencode 731 / pi 44 / e2e 2, typecheck clean, biome lint 0 warnings. Single commit off main, scoped to account-management.

Greptile Summary

Four focused account-management fixes: orphaned per-account state is now pruned on full save (preventing stale tokens from merging onto a re-added same-id account); a new needsReauth field surfaces permanently-dead fallback OAuth tokens in the sidebar with a re-login indicator; add-oauth-finish now accepts --label so OAuth accounts no longer display as raw UUIDs; and refresh errors gain explicit status/permanent fields so isPermanentRefreshError correctly distinguishes dead tokens (400 invalid_grant) from transient backoffs without relying on a delay heuristic.

  • Orphan pruning (accounts.ts): the full-save path now drops state.accounts.<id> entries for any id absent from storage.accounts, eliminating the stale-state merge hazard on re-add.
  • needsReauth indicator (sidebar-state.ts, index.ts, tui.tsx): computed from lastRefreshError && refreshBackoffActive && isPermanentRefreshError; rendered as re-login/err tone in AccountBlock and OR'd into the sidebar's degraded signal.
  • OAuth label flow (command-dialogs.tsx, account.ts): a new openOAuthLabelPrompt step is inserted between code-entry and submission; back-navigation now goes URL screen ← code prompt ← label prompt rather than bailing to L1, preserving the live PKCE session.

Confidence Score: 5/5

Safe to merge — all four fixes are tightly scoped, each has a RED→GREEN test, and the orthogonal permanent/status round-trip coverage guards the most fragile migration path.

The changes are well-bounded: the orphan-pruning only touches the full-save branch, the needsReauth field defaults to false so existing serialized state is safe, and the error-classification logic is protected by explicit permanent checks that take precedence over the legacy heuristic. No auth or data-loss regressions are introduced.

No files require special attention; test coverage across the four fixes is thorough.

Important Files Changed

Filename Overview
packages/core/src/accounts.ts Adds orphan-state pruning on full save, new status/permanent fields on AccountOperationError, buildRefreshOperationError fix to only set permanent=true for 400 invalid_grant, and the new isPermanentRefreshError helper with a three-level precedence chain. Logic is correct and round-trip-tested.
packages/core/src/commands/account.ts Adds --label parsing to add-oauth-finish via a greedy regex that handles multi-word labels and the opaque OAuth code/state payload. USAGE_TEXT is not updated to document the new flag.
packages/opencode/src/index.ts Computes needsReauth from lastRefreshError && refreshBackoffActive && isPermanentRefreshError and threads it into SidebarAccountState; also assigns label from the new add-oauth-finish --label action.
packages/opencode/src/sidebar-state.ts Adds required needsReauth: boolean to SidebarAccountState with a safe default-false normalization in normalizeSidebarState.
packages/opencode/src/tui.tsx Threads needsReauth into AccountBlock (re-login status word, err tone), adds a needsReauth() derived signal on QuotaSidebar, and ORs it into the degraded indicator.
packages/opencode/src/tui/command-dialogs.tsx Inserts a new openOAuthLabelPrompt step between code entry and submission; fixes back-navigation so cancelling goes to the URL screen (code step) rather than L1, preventing PKCE session invalidation.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant U as User
    participant D as Dialog
    participant P as parseAccountCommandAction
    participant A as AnthropicAuthPlugin
    participant S as Sidebar

    U->>D: add-oauth-start
    D->>D: openOAuthUrlScreen(oauthUrl)
    U->>D: confirm → openOAuthCodePrompt(oauthUrl)
    U->>D: paste code → openOAuthLabelPrompt(code, oauthUrl)
    U->>D: enter label (optional)
    D->>P: "add-oauth-finish {code} [--label {label}]"
    P-->>A: "{ type: add-oauth-finish, code, label? }"
    A->>A: create OAuthAccount (randomUUID id, label)
    A->>S: "SidebarAccountState { needsReauth: false }"

    Note over A,S: On 400 invalid_grant refresh error
    A->>A: "buildRefreshOperationError → permanent=true"
    A->>S: "SidebarAccountState { needsReauth: true }"
    S-->>U: AccountBlock shows re-login (err tone)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant U as User
    participant D as Dialog
    participant P as parseAccountCommandAction
    participant A as AnthropicAuthPlugin
    participant S as Sidebar

    U->>D: add-oauth-start
    D->>D: openOAuthUrlScreen(oauthUrl)
    U->>D: confirm → openOAuthCodePrompt(oauthUrl)
    U->>D: paste code → openOAuthLabelPrompt(code, oauthUrl)
    U->>D: enter label (optional)
    D->>P: "add-oauth-finish {code} [--label {label}]"
    P-->>A: "{ type: add-oauth-finish, code, label? }"
    A->>A: create OAuthAccount (randomUUID id, label)
    A->>S: "SidebarAccountState { needsReauth: false }"

    Note over A,S: On 400 invalid_grant refresh error
    A->>A: "buildRefreshOperationError → permanent=true"
    A->>S: "SidebarAccountState { needsReauth: true }"
    S-->>U: AccountBlock shows re-login (err tone)
Loading

Comments Outside Diff (1)

  1. packages/opencode/src/tui/command-dialogs.tsx, line 575-580 (link)

    P2 Cancelling the label dialog loses the already-entered OAuth code

    onCancel in the label dialog jumps straight back to L1 (buildL1()), discarding the OAuth code the user just pasted. Since OAuth authorization codes are single-use and typically short-lived, the user would need to restart the entire OAuth flow (re-trigger add-oauth-start and open a new browser session) just because they changed their mind on the label. Going back to the code-entry step instead would be a safer fallback.

Reviews (2): Last reviewed commit: "fix(account-management): only classify 4..." | Re-trigger Greptile

…tor, OAuth-add label, refresh-error classification

- Prune orphaned per-account state on removal (full-save path skipped the state delete) — orphaned accounts.<id> state would resurface on same-id re-add.
- Surface a dead-fallback 'needs re-login' indicator in the sidebar (was silently degrading fallback-first to main with no signal). New needsReauth field gated on refreshBackoffActive && isPermanentRefreshError.
- Prompt for a label when adding an OAuth account via /claude-account (was naming it a raw UUID); --label on add-oauth-finish, id stays UUID.
- Classify refresh errors with an explicit permanent flag (status===400 invalid_grant = dead → re-login); retry-exhausted/transient errors non-permanent; 24h-delay heuristic kept only as legacy back-compat. status/permanent preserved across save/load (normalizeOperationError) so the round-trip can't undo the classification.
Comment thread packages/core/src/accounts.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 11 files

Confidence score: 3/5

  • In packages/opencode/src/tui/command-dialogs.tsx, cancelling at the OAuth label prompt returns users to buildL1, so a retry requires re-running add-oauth-start and can overwrite the in-progress PKCE/session state; merging as-is risks broken or confusing OAuth setup flows — keep cancellation within the current OAuth dialog path (or preserve pending session state) before merging.
  • In packages/core/src/accounts.ts, treating permanent: status === 400 as a dead-token signal can misclassify valid retryable token-endpoint failures (for example non-invalid_grant 400s), which may cause unnecessary token invalidation and forced re-authentication — gate permanent failure on error type (e.g., invalid_grant) rather than HTTP status alone before merging.
Architecture diagram
sequenceDiagram
    participant U as User (TUI)
    participant D as command‑dialogs.tsx
    participant P as parseAccountCommandAction
    participant I as index.ts (Plugin)
    participant A as accounts.ts
    participant S as sidebar‑state.ts
    participant V as tui.tsx (AccountBlock)

    Note over U,V: OAuth account addition with label (fix 3)

    U->>D: add-oauth-start → paste OAuth code
    D->>D: openOAuthCodePrompt()
    U->>D: submit code
    D->>D: NEW: openOAuthLabelPrompt(code)
    U->>D: submit label (optional)
    D->>P: NEW: "add-oauth-finish {code} --label {label}"
    P-->>I: NEW: { type: 'add-oauth-finish', code, label? }
    I->>A: create OAuthAccount { id: randomUUID(), label }
    A->>A: saveAccounts() full save
    A->>A: NEW: prune orphaned state ids not in storage.accounts (fix 1)
    A-->>I: success
    I-->>D: toast + refresh sidebar

    Note over I,V: Sidebar state refresh (fix 2 & 4)

    I->>A: refreshBackoffActive() + isPermanentRefreshError()
    A-->>I: NEW: needsReauth = (lastRefreshError != null && backoff active && permanent)
    I->>S: SidebarAccountState { needsReauth }
    S-->>V: render "re‑login" / err tone (fix 2)

    Note over A,A: Error classification (fix 4)

    A->>A: buildRefreshOperationError()
    Note over A: NEW: sets permanent true only if status === 400
    A->>A: isPermanentRefreshError()
    Note over A: precedence: permanent flag → status === 400 → legacy 24h heuristic
    A->>A: normalizeOperationError() preserves status & permanent across save/load

    Note over V,V: Fallback degradation

    V->>V: CHANGED: degraded() includes any fallback with needsReauth
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/opencode/src/tui/command-dialogs.tsx Outdated
Comment thread packages/core/src/accounts.ts Outdated
…ly dead; preserve OAuth session when cancelling the label prompt
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant