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

@@ -1,15 +1,15 @@
import { mkdirSync, writeFileSync } from 'node:fs';
import { mkdir, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
export function setupAccountConfigDir(
export async function setupAccountConfigDir(
configDir: string,
extracted: { configJson: object; credentials: string },
): void {
mkdirSync(configDir, { recursive: true });
): Promise<void> {
await mkdir(configDir, { recursive: true });
// Write .claude.json
writeFileSync(join(configDir, '.claude.json'), JSON.stringify(extracted.configJson, null, 2));
await writeFile(join(configDir, '.claude.json'), JSON.stringify(extracted.configJson, null, 2));
// Write .credentials.json
writeFileSync(join(configDir, '.credentials.json'), extracted.credentials);
await writeFile(join(configDir, '.credentials.json'), extracted.credentials);
}

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');

View File

@@ -212,7 +212,7 @@ export class MultiProviderAgentManager implements AgentManager {
accountId = accountResult.accountId;
accountConfigDir = accountResult.configDir;
this.credentialHandler.writeCredentialsToDisk(accountResult.account, accountConfigDir);
await this.credentialHandler.writeCredentialsToDisk(accountResult.account, accountConfigDir);
const { valid, refreshed } = await this.credentialHandler.ensureCredentials(accountConfigDir, accountId);
if (!valid) {
log.warn({ alias, accountId }, 'failed to refresh account credentials, proceeding anyway');