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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user