fix: read runtime state when config file is absent#98
Merged
ualtinok merged 1 commit intoJun 26, 2026
Merged
Conversation
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.
There was a problem hiding this comment.
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
loadAccountsto returnnullonly 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Summary by cubic
Fixes
loadAccountsto read runtime state when the config file is missing, restoring OAuth refresh/backoff behavior for users without fallback accounts. AddscreateEmptyStorage()to dedupe empty-storage literals and includes regression tests.Bug Fixes
loadAccountsnow returns null only when neither config nor state exists.saveAccountStatewithout a config file.Refactors
createEmptyStorage()in core and replaced inline defaults acrossaccounts.ts,routing.ts,opencode/index.ts, andpi/commands.ts.Written for commit 6d8d69f. Summary will update on new commits.
Greptile Summary
This PR fixes
loadAccountsso 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 sharedcreateEmptyStorage()factory.accounts.ts):loadAccountsnow reads both files before deciding, returnsnullonly when neither exists, and synthesizes an empty config shell to merge state into when only the state file is present.accounts.ts,routing.ts,index.ts,pi/commands.ts): replaces 18 copies of{ version: 1, accounts: [] }(some of which were missing themainfield) withcreateEmptyStorage(), which always includesmain: { type: 'opencode', provider: 'anthropic' }.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
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]%%{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]Reviews (1): Last reviewed commit: "fix: read runtime state when config file..." | Re-trigger Greptile