Skip to content

fix: read runtime state when config file is absent#98

Merged
ualtinok merged 1 commit into
cortexkit:mainfrom
jonmast:fix/load-accounts-state-without-config
Jun 26, 2026
Merged

fix: read runtime state when config file is absent#98
ualtinok merged 1 commit into
cortexkit:mainfrom
jonmast:fix/load-accounts-state-without-config

Conversation

@jonmast

@jonmast jonmast commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

loadAccounts treated the config file (anthropic-auth.json) as a
gatekeeper: if it did not exist, it returned null without ever reading
the runtime state file (anthropic-auth-state.json). But the two files are
written independently, and runtime-only flows — notably main-OAuth
refresh with no fallback accounts — write the state file while never
creating the config file. As a result, for any user without fallback
accounts the config file never exists, so every reader got null and the
state file's refresh-lease, quota, and error data was invisible.

This broke the post-write lease verification as well as the backoff
check and lease-skip check in the same refresh path, all of which
silently received null storage.

Fix loadAccounts to read both files and return null only when neither
exists; when config is absent but state is present, synthesize an empty
config shell and merge state in as usual. This matches the documented
two-file design where the state file holds independent runtime data.

Also extract the repeated empty-storage literal into a shared
createEmptyStorage() factory in core and replace the 18 inline
duplicates across accounts.ts, routing.ts, index.ts, and pi/commands.ts.
The CLI's richer defaultStorage() (config bootstrap with seeded
defaults) is intentionally left separate.

Adds regression tests covering: null when neither file exists, runtime
state visible when config is absent, and a lease written via
saveAccountState being readable by loadAccounts without a config file.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Fixes loadAccounts to read runtime state when the config file is missing, restoring OAuth refresh/backoff behavior for users without fallback accounts. Adds createEmptyStorage() to dedupe empty-storage literals and includes regression tests.

  • Bug Fixes

    • loadAccounts now returns null only when neither config nor state exists.
    • When config is absent but state exists, synthesize an empty config and merge state.
    • Restores post-write lease verification, backoff, and lease-skip checks in the OAuth refresh path.
    • Adds tests for: no files, state-only reads, and reading a lease saved via saveAccountState without a config file.
  • Refactors

    • Introduced createEmptyStorage() in core and replaced inline defaults across accounts.ts, routing.ts, opencode/index.ts, and pi/commands.ts.

Written for commit 6d8d69f. Summary will update on new commits.

Review in cubic

Greptile Summary

This PR fixes loadAccounts so it reads runtime state even when the config file (anthropic-auth.json) is absent, which was silently causing null storage for users without fallback accounts. It also extracts the repeated inline empty-storage literal into a shared createEmptyStorage() factory.

  • Core logic fix (accounts.ts): loadAccounts now reads both files before deciding, returns null only when neither exists, and synthesizes an empty config shell to merge state into when only the state file is present.
  • Deduplication (accounts.ts, routing.ts, index.ts, pi/commands.ts): replaces 18 copies of { version: 1, accounts: [] } (some of which were missing the main field) with createEmptyStorage(), which always includes main: { type: 'opencode', provider: 'anthropic' }.
  • Regression tests (accounts.test.ts): three new tests cover null-when-neither, state-visible-without-config, and round-trip lease write/read without a config file.

Confidence Score: 5/5

Safe to merge — the fix is well-scoped, regression-tested, and the deduplication is a straightforward mechanical refactor.

The loadAccounts change is the only logic mutation; the rest of the PR replaces identical inline literals with the new factory. The fix is logically sound: mergeConfigAndState already handles a null/absent state value, and normalizeStorage correctly validates the merged output before returning it. The three new tests directly exercise each changed code path. No callers are broken by the behavioral change where some old fallbacks were missing the main field.

No files require special attention.

Important Files Changed

Filename Overview
packages/core/src/accounts.ts Core fix: loadAccounts now reads state file unconditionally and synthesizes an empty config when only state is present; createEmptyStorage() factory extracted and exported cleanly.
packages/core/src/routing.ts Mechanical substitution of inline empty-storage literal with createEmptyStorage(); no logic change.
packages/opencode/src/index.ts Replaces 10 inline empty-storage literals with createEmptyStorage(); some old literals were missing the main field which createEmptyStorage now always includes — an improvement.
packages/opencode/src/tests/accounts.test.ts Three new regression tests covering null-when-neither, state-visible-without-config, and lease round-trip without a config file — all exercising the fixed code path.
packages/pi/src/commands.ts Single literal replaced with createEmptyStorage(); the old literal was missing main, which is now correctly included.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[loadAccounts called] --> B[readJsonIfPresent config file]
    A --> C[readJsonIfPresent state file]
    B --> D{config.exists?}
    C --> E{state.exists?}
    D -- No --> F{state.exists?}
    E -- No --> F
    D -- Yes --> G[use config.value]
    E -- Yes --> H[use state.value]
    F -- No --> I[return null]
    F -- Yes --> J[synthesize createEmptyStorage as configValue]
    G --> K[mergeConfigAndState configValue + state.value]
    J --> K
    H --> K
    K --> L[normalizeStorage merged result]
    L --> M[return AccountStorage]
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"}}}%%
flowchart TD
    A[loadAccounts called] --> B[readJsonIfPresent config file]
    A --> C[readJsonIfPresent state file]
    B --> D{config.exists?}
    C --> E{state.exists?}
    D -- No --> F{state.exists?}
    E -- No --> F
    D -- Yes --> G[use config.value]
    E -- Yes --> H[use state.value]
    F -- No --> I[return null]
    F -- Yes --> J[synthesize createEmptyStorage as configValue]
    G --> K[mergeConfigAndState configValue + state.value]
    J --> K
    H --> K
    K --> L[normalizeStorage merged result]
    L --> M[return AccountStorage]
Loading

Reviews (1): Last reviewed commit: "fix: read runtime state when config file..." | Re-trigger Greptile

loadAccounts treated the config file (anthropic-auth.json) as a
gatekeeper: if it did not exist, it returned null without ever reading
the runtime state file (anthropic-auth-state.json). But the two files are
written independently, and runtime-only flows — notably main-OAuth
refresh with no fallback accounts — write the state file while never
creating the config file. As a result, for any user without fallback
accounts the config file never exists, so every reader got null and the
state file's refresh-lease, quota, and error data was invisible.

This broke the post-write lease verification as well as the backoff
check and lease-skip check in the same refresh path, all of which
silently received null storage.

Fix loadAccounts to read both files and return null only when neither
exists; when config is absent but state is present, synthesize an empty
config shell and merge state in as usual. This matches the documented
two-file design where the state file holds independent runtime data.

Also extract the repeated empty-storage literal into a shared
createEmptyStorage() factory in core and replace the 18 inline
duplicates across accounts.ts, routing.ts, index.ts, and pi/commands.ts.
The CLI's richer defaultStorage() (config bootstrap with seeded
defaults) is intentionally left separate.

Adds regression tests covering: null when neither file exists, runtime
state visible when config is absent, and a lease written via
saveAccountState being readable by loadAccounts without a config file.
Copilot AI review requested due to automatic review settings June 24, 2026 17:23

Copilot AI 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.

Pull request overview

Fixes account-store loading so runtime state (anthropic-auth-state.json) is read even when the config file (anthropic-auth.json) doesn’t exist, which is required for runtime-only OAuth refresh flows (no fallback accounts). Also centralizes the “empty storage” default into a shared factory to avoid repeated literals.

Changes:

  • Update loadAccounts to return null only when both config and state files are absent; when config is missing but state exists, merge state into a synthesized empty config.
  • Introduce createEmptyStorage() in core and replace repeated inline { version: 1, accounts: [] } / similar defaults across packages.
  • Add regression tests covering state-only loads and lease visibility without a config file.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
packages/core/src/accounts.ts Adds createEmptyStorage() and fixes loadAccounts() to read state even when config is missing.
packages/core/src/routing.ts Uses createEmptyStorage() instead of duplicating an empty-storage literal.
packages/opencode/src/index.ts Replaces repeated empty-storage literals with createEmptyStorage() in multiple plugin paths.
packages/pi/src/commands.ts Uses createEmptyStorage() when loadAccounts() returns null.
packages/opencode/src/tests/accounts.test.ts Adds regression tests for state-only scenarios and lease visibility without config.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@ualtinok ualtinok merged commit 55550e2 into cortexkit:main Jun 26, 2026
3 checks passed
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.

3 participants