From 58f8fcd8f1ffa874b1cbe796928df25273524684 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 6 Mar 2026 16:03:50 +0100 Subject: [PATCH] fix(coding-agent): retry sync lockfile acquisition to prevent false auth errors during parallel startup Add manual retry loop (10 attempts, 20ms delay) around lockfile.lockSync() in FileAuthStorageBackend and FileSettingsStorage. Previously, concurrent pi processes would fail immediately on ELOCKED, causing auth.json to load as empty and surfacing misleading 'No API key found' errors. fixes #1871 --- .../coding-agent/src/core/auth-storage.ts | 29 ++++++++++++++++- .../coding-agent/src/core/settings-manager.ts | 31 +++++++++++++++++-- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/src/core/auth-storage.ts b/packages/coding-agent/src/core/auth-storage.ts index 324938d2f..d8b899692 100644 --- a/packages/coding-agent/src/core/auth-storage.ts +++ b/packages/coding-agent/src/core/auth-storage.ts @@ -59,13 +59,40 @@ export class FileAuthStorageBackend implements AuthStorageBackend { } } + private acquireLockSyncWithRetry(path: string): () => void { + const maxAttempts = 10; + const delayMs = 20; + let lastError: unknown; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return lockfile.lockSync(path, { realpath: false }); + } catch (error) { + const code = + typeof error === "object" && error !== null && "code" in error + ? String((error as { code?: unknown }).code) + : undefined; + if (code !== "ELOCKED" || attempt === maxAttempts) { + throw error; + } + lastError = error; + const start = Date.now(); + while (Date.now() - start < delayMs) { + // Sleep synchronously to avoid changing callers to async. + } + } + } + + throw (lastError as Error) ?? new Error("Failed to acquire auth storage lock"); + } + withLock(fn: (current: string | undefined) => LockResult): T { this.ensureParentDir(); this.ensureFileExists(); let release: (() => void) | undefined; try { - release = lockfile.lockSync(this.authPath, { realpath: false }); + release = this.acquireLockSyncWithRetry(this.authPath); const current = existsSync(this.authPath) ? readFileSync(this.authPath, "utf-8") : undefined; const { result, next } = fn(current); if (next !== undefined) { diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts index e99962405..97b3d2fce 100644 --- a/packages/coding-agent/src/core/settings-manager.ts +++ b/packages/coding-agent/src/core/settings-manager.ts @@ -146,6 +146,33 @@ export class FileSettingsStorage implements SettingsStorage { this.projectSettingsPath = join(cwd, CONFIG_DIR_NAME, "settings.json"); } + private acquireLockSyncWithRetry(path: string): () => void { + const maxAttempts = 10; + const delayMs = 20; + let lastError: unknown; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return lockfile.lockSync(path, { realpath: false }); + } catch (error) { + const code = + typeof error === "object" && error !== null && "code" in error + ? String((error as { code?: unknown }).code) + : undefined; + if (code !== "ELOCKED" || attempt === maxAttempts) { + throw error; + } + lastError = error; + const start = Date.now(); + while (Date.now() - start < delayMs) { + // Sleep synchronously to avoid changing callers to async. + } + } + } + + throw (lastError as Error) ?? new Error("Failed to acquire settings lock"); + } + withLock(scope: SettingsScope, fn: (current: string | undefined) => string | undefined): void { const path = scope === "global" ? this.globalSettingsPath : this.projectSettingsPath; const dir = dirname(path); @@ -155,7 +182,7 @@ export class FileSettingsStorage implements SettingsStorage { // Only create directory and lock if file exists or we need to write const fileExists = existsSync(path); if (fileExists) { - release = lockfile.lockSync(path, { realpath: false }); + release = this.acquireLockSyncWithRetry(path); } const current = fileExists ? readFileSync(path, "utf-8") : undefined; const next = fn(current); @@ -165,7 +192,7 @@ export class FileSettingsStorage implements SettingsStorage { mkdirSync(dir, { recursive: true }); } if (!release) { - release = lockfile.lockSync(path, { realpath: false }); + release = this.acquireLockSyncWithRetry(path); } writeFileSync(path, next, "utf-8"); }