From 497b397e873f96d6fde3d8a44b3318e1ee1cbef4 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Fri, 29 May 2026 23:14:55 +0900 Subject: fix(claude): eliminate /home mount race that blanks Claude credentials at boot On hosts where /home is a separate filesystem, the dispatch-api service could start before /home was mounted. The API's first DB access then failed (EACCES: mkdir '/home/tradam'), Claude account discovery silently caught the error and left claudeAccounts empty, and -- because discovery only ran in the constructor -- it stayed empty for the whole process lifetime. Every Claude message then fell back to the deepseek-v4-flash / empty-key defaults, producing a 401 'Missing API key' from OpenCode Zen. Fixes: - s6 run script waits (capped ~30s) for /home/tradam before exec'ing bun; passes instantly where /home is on the root filesystem. - systemd unit gains RequiresMountsFor=/home and After=...home.mount. - agent-manager re-runs _refreshClaudeAccounts() on config hot-reload and lazily on an empty cache in the Anthropic path, so a process that lost the boot race self-heals on the next request instead of staying broken. --- packages/api/src/agent-manager.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) (limited to 'packages/api/src') diff --git a/packages/api/src/agent-manager.ts b/packages/api/src/agent-manager.ts index c09a607..c873388 100644 --- a/packages/api/src/agent-manager.ts +++ b/packages/api/src/agent-manager.ts @@ -244,6 +244,10 @@ export class AgentManager { } // Update model registry with new config this._initModelRegistry(newConfig); + // Re-discover Claude accounts: a config reload may accompany freshly + // imported credentials, and (critically) lets a process that failed + // account discovery at boot recover without a full restart. + this._refreshClaudeAccounts(); // Invalidate cached agents so next message uses updated config for (const tabAgent of this.tabAgents.values()) { tabAgent.agent = null; @@ -575,11 +579,21 @@ export class AgentManager { if (key.provider === "anthropic") { // Anthropic provider: resolve credentials from Claude accounts const credFile = key.credentials_file; - const account = + const findAccount = () => this.claudeAccounts.find((a) => a.id === effectiveKeyId) ?? (credFile ? this.claudeAccounts.find((a) => a.source === credFile) : this.claudeAccounts[0]); + let account = findAccount(); + // Self-heal: account discovery runs once at construction and can + // fail at boot (e.g. the data dir isn't mounted yet and + // getDatabase() throws EACCES), leaving claudeAccounts empty for + // the process lifetime. If the lookup fails, re-run discovery now + // that the DB is reachable and retry before giving up. + if (!account) { + this._refreshClaudeAccounts(); + account = findAccount(); + } if (account) { const creds = refreshAccountCredentials(account); if (creds && creds.expiresAt > Date.now() + 60_000) { -- cgit v1.2.3