diff --git a/package-lock.json b/package-lock.json index 8c25a11..c2773da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -77,7 +77,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1735,7 +1734,6 @@ "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "playwright": "1.58.2" }, @@ -1866,7 +1864,6 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1926,7 +1923,6 @@ "integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/types": "8.55.0", @@ -2445,7 +2441,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2865,7 +2860,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3150,7 +3144,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -3645,7 +3638,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3831,7 +3823,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5094,7 +5085,6 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -5885,7 +5875,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -6044,7 +6033,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6054,7 +6042,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -6906,7 +6893,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7096,7 +7082,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7404,7 +7389,6 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/lib/agent-reservations.ts b/src/lib/agent-reservations.ts index b0b206d..6cb1ca5 100644 --- a/src/lib/agent-reservations.ts +++ b/src/lib/agent-reservations.ts @@ -169,28 +169,47 @@ async function readActiveReservations(): Promise { } } -async function lockActiveReservations(): Promise { - // Ensure the directory and file exist before trying to lock - await fs.mkdir(path.dirname(activeReservationsPath()), { recursive: true }); - try { - const fd = await fs.open(activeReservationsPath(), 'r+'); - await fs.flock(fd, 'ex'); - return fd; - } catch (error) { - if ((error as NodeJS.ErrnoException).code === 'ENOENT') { - // File doesn't exist, create it first - await fs.writeFile(activeReservationsPath(), JSON.stringify({ reservations: [] }), 'utf8'); - const fd = await fs.open(activeReservationsPath(), 'r+'); - await fs.flock(fd, 'ex'); - return fd; +// Simple mutex-based locking using a shared lock file to prevent race conditions +const LOCK_FILE_PATH = path.join(reservationsRoot(), '.lock'); + +async function lockActiveReservations(): Promise { + // Ensure the directory exists + await fs.mkdir(path.dirname(LOCK_FILE_PATH), { recursive: true }); + + // Use a simple file-based mutex - create file exclusively, fail if exists + let attempts = 0; + const maxAttempts = 100; + + while (attempts < maxAttempts) { + try { + await fs.writeFile(LOCK_FILE_PATH, String(process.pid), { flag: 'wx' }); + return; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'EEXIST') { + // Lock file exists, wait and retry + await new Promise(resolve => setTimeout(resolve, 50)); + attempts++; + continue; + } + throw error; } - throw error; } + throw new Error('Failed to acquire lock after maximum attempts'); } -async function unlockActiveReservations(fd: number): Promise { - await fs.flock(fd, 'un'); - await fs.close(fd); +async function unlockActiveReservations(): Promise { + try { + const content = await fs.readFile(LOCK_FILE_PATH, 'utf8'); + // Only release if we own the lock + if (content.trim() === String(process.pid)) { + await fs.unlink(LOCK_FILE_PATH); + } + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { + throw error; + } + // Lock file doesn't exist, ignore + } } async function atomicWriteJson(filePath: string, payload: string): Promise { @@ -294,10 +313,9 @@ export async function reserveAgentScope( return invalid(command, 'INVALID_ARGS', `TTL must be an integer between ${MIN_TTL_MINUTES} and ${MAX_TTL_MINUTES} minutes.`); } - let lockFd: number | null = null; try { // Acquire exclusive lock to prevent race conditions - lockFd = await lockActiveReservations(); + await lockActiveReservations(); const now = deps.now ? deps.now() : new Date().toISOString(); const reservations = await readActiveReservations(); @@ -349,9 +367,7 @@ export async function reserveAgentScope( } catch (error) { return invalid(command, 'INTERNAL_ERROR', error instanceof Error ? error.message : 'Failed to reserve scope.'); } finally { - if (lockFd !== null) { - await unlockActiveReservations(lockFd); - } + await unlockActiveReservations(); } } @@ -372,10 +388,9 @@ export async function releaseAgentReservation( return invalid(command, 'INVALID_ARGS', 'Scope is required.'); } - let lockFd: number | null = null; try { // Acquire exclusive lock to prevent race conditions - lockFd = await lockActiveReservations(); + await lockActiveReservations(); const now = deps.now ? deps.now() : new Date().toISOString(); const reservations = await readActiveReservations(); @@ -408,9 +423,7 @@ export async function releaseAgentReservation( } catch (error) { return invalid(command, 'INTERNAL_ERROR', error instanceof Error ? error.message : 'Failed to release reservation.'); } finally { - if (lockFd !== null) { - await unlockActiveReservations(lockFd); - } + await unlockActiveReservations(); } }