fix: Convert sync file I/O to async in credential handler and account setup

Removes blocking readFileSync, writeFileSync, and mkdirSync calls from the
agent spawn hot path, replacing them with async fs/promises equivalents to
avoid stalling the Node.js event loop during credential operations.
This commit is contained in:
Lukas May
2026-03-04 12:25:05 +01:00
parent bd0aec4499
commit a2afc2e1fd
3 changed files with 17 additions and 16 deletions

View File

@@ -6,7 +6,8 @@
* ensuring they're fresh, and marking accounts as exhausted on failure.
*/
import { readFileSync, existsSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import type { AccountRepository } from '../db/repositories/account-repository.js';
import type { AccountCredentialManager } from './credentials/types.js';
@@ -50,9 +51,9 @@ export class CredentialHandler {
* Write account credentials from DB to the convention-based config directory.
* Must be called before ensureCredentials so the files exist on disk.
*/
writeCredentialsToDisk(account: Account, configDir: string): void {
async writeCredentialsToDisk(account: Account, configDir: string): Promise<void> {
if (account.configJson && account.credentials) {
setupAccountConfigDir(configDir, {
await setupAccountConfigDir(configDir, {
configJson: JSON.parse(account.configJson),
credentials: account.credentials,
});
@@ -70,7 +71,7 @@ export class CredentialHandler {
if (!this.accountRepository) return;
try {
const credPath = join(configDir, '.credentials.json');
const credentials = readFileSync(credPath, 'utf-8');
const credentials = await readFile(credPath, 'utf-8');
await this.accountRepository.updateCredentials(accountId, credentials);
log.debug({ accountId }, 'persisted refreshed credentials back to DB');
} catch (err) {
@@ -97,11 +98,11 @@ export class CredentialHandler {
* Returns null if credentials file is missing or malformed.
* Used for CLAUDE_CODE_OAUTH_TOKEN env var injection.
*/
readAccessToken(configDir: string): string | null {
async readAccessToken(configDir: string): Promise<string | null> {
try {
const credPath = join(configDir, '.credentials.json');
if (!existsSync(credPath)) return null;
const raw = readFileSync(credPath, 'utf-8');
const raw = await readFile(credPath, 'utf-8');
const parsed = JSON.parse(raw);
return parsed.claudeAiOauth?.accessToken ?? null;
} catch {
@@ -126,7 +127,7 @@ export class CredentialHandler {
accountConfigDir = getAccountConfigDir(this.workspaceRoot, accountId);
const account = await this.accountRepository.findById(accountId);
if (account) {
this.writeCredentialsToDisk(account, accountConfigDir);
await this.writeCredentialsToDisk(account, accountConfigDir);
}
processEnv[provider.configDirEnv] = accountConfigDir;
@@ -138,7 +139,7 @@ export class CredentialHandler {
await this.persistRefreshedCredentials(accountId, accountConfigDir);
}
const accessToken = this.readAccessToken(accountConfigDir);
const accessToken = await this.readAccessToken(accountConfigDir);
if (accessToken) {
processEnv['CLAUDE_CODE_OAUTH_TOKEN'] = accessToken;
log.debug({ accountId }, 'CLAUDE_CODE_OAUTH_TOKEN injected');
@@ -191,7 +192,7 @@ export class CredentialHandler {
// Write credentials and ensure they're fresh
const nextConfigDir = getAccountConfigDir(this.workspaceRoot, nextAccount.id);
this.writeCredentialsToDisk(nextAccount, nextConfigDir);
await this.writeCredentialsToDisk(nextAccount, nextConfigDir);
const { valid, refreshed } = await this.ensureCredentials(nextConfigDir, nextAccount.id);
if (!valid) {
log.warn({ newAccountId: nextAccount.id }, 'failed to refresh failover account credentials');