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"); }