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 324938d2..d8b89969 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 e9996240..97b3d2fc 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"); }