UNPKG

akm-cli

Version:

akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur

216 lines (215 loc) • 7.34 kB
// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. /** * PID-stamped exclusive lock files. * * A lock is one file created with `O_EXCL` (`wx`), which the filesystem * makes atomic: exactly one creator wins, so no further mutex or barrier is * needed around acquisition. The payload records the holder's PID (and the * launcher PID under a scheduler) so a probe can tell a live holder from a * dead one; only a verifiably dead holder — or, when the caller opts in via * `staleAfterMs`, an over-age one — is ever reclaimed. Reclaim moves the * stale file aside, re-verifies it is the very file that was probed, and only * then unlinks it, so two reclaimers cannot both "win" the same stale lock. */ import { randomUUID } from "node:crypto"; import fs from "node:fs"; import { isProcessAlive, MAX_LOCK_METADATA_BYTES, readTextFileDescriptorWithLimit } from "./common.js"; function readLockSnapshot(lockPath) { let fd; try { fd = fs.openSync(lockPath, "r"); } catch (err) { if (err.code === "ENOENT") return undefined; throw err; } try { const rawContent = readTextFileDescriptorWithLimit(fd, MAX_LOCK_METADATA_BYTES, "Lock metadata", lockPath); const stat = fs.fstatSync(fd); return { rawContent, identity: { dev: stat.dev, ino: stat.ino, size: stat.size, mtimeMs: stat.mtimeMs }, }; } finally { fs.closeSync(fd); } } function sameIdentity(left, right) { return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeMs === right.mtimeMs; } function releaseLockRaw(lockPath) { try { fs.unlinkSync(lockPath); } catch { // Already gone. } } /** Create the lock file atomically; `undefined` when another holder already has it. */ export function tryAcquireLockSync(lockPath, payload) { try { fs.writeFileSync(lockPath, payload, { flag: "wx" }); } catch (err) { if (err.code === "EEXIST") return undefined; throw err; } let snapshot; try { snapshot = readLockSnapshot(lockPath); } catch (error) { releaseLockRaw(lockPath); throw error; } if (!snapshot) { releaseLockRaw(lockPath); throw new Error(`Could not read newly acquired lock at ${lockPath}.`); } return { lockPath, ...snapshot }; } export function launcherPidFromEnv() { const raw = process.env.AKM_LAUNCHER_PID; if (!raw) return undefined; const pid = Number.parseInt(raw, 10); return Number.isInteger(pid) && pid > 0 ? pid : undefined; } export function createLockPayload(metadata = {}) { const launcherPid = launcherPidFromEnv(); return JSON.stringify({ ...metadata, pid: process.pid, ...(launcherPid !== undefined ? { launcherPid } : {}), lockId: randomUUID(), }); } export function probeLock(lockPath, opts) { let snapshot; try { snapshot = readLockSnapshot(lockPath); } catch (error) { const code = error?.code; if (code === "EACCES" || code === "EPERM") return { state: "inaccessible", code }; return { state: "stale", reason: "unreadable" }; } if (!snapshot) return { state: "absent" }; const { rawContent, identity } = snapshot; const ageMs = Date.now() - identity.mtimeMs; const { holderPid, launcherPid } = extractLockIdentity(rawContent); if (holderPid === undefined) { return { state: "stale", reason: "invalid_pid", ageMs, rawContent, identity }; } if (!isProcessAlive(holderPid)) { return { state: "stale", reason: "pid_dead", holderPid, launcherPid, ageMs, rawContent, identity }; } if (opts?.staleAfterMs !== undefined && ageMs > opts.staleAfterMs) { return { state: "stale", reason: "age_exceeded", holderPid, launcherPid, ageMs, rawContent, identity }; } return { state: "held", holderPid, launcherPid, ageMs, rawContent, identity }; } /** * Remove a lock that `probeLock` reported stale. Returns `false` when the file * changed since the probe (someone else reclaimed or re-acquired it) so the * caller re-probes instead of assuming the path is free. */ export function reclaimStaleLock(lockPath, probe, options) { if (probe.rawContent === undefined || probe.identity === undefined) return false; const expectedContent = probe.rawContent; const expectedIdentity = probe.identity; let current; try { current = readLockSnapshot(lockPath); } catch { return false; } if (!current || current.rawContent !== expectedContent || !sameIdentity(current.identity, expectedIdentity)) { return false; } const quarantinePath = `${lockPath}.stale-${process.pid}-${randomUUID()}`; try { fs.renameSync(lockPath, quarantinePath); } catch (err) { if (err.code === "ENOENT") return false; throw err; } let quarantined; try { quarantined = readLockSnapshot(quarantinePath); } catch { quarantined = undefined; } if (!quarantined || quarantined.rawContent !== expectedContent || !sameIdentity(quarantined.identity, expectedIdentity)) { // The file we moved aside is not the one we probed: a fresh holder took // the path in between. Put it back and report no reclaim. try { fs.linkSync(quarantinePath, lockPath); } catch (err) { if (err.code !== "EEXIST") throw err; } releaseLockRaw(quarantinePath); return false; } options?.afterQuarantineVerified?.(); try { fs.unlinkSync(quarantinePath); return true; } catch (err) { if (err.code === "ENOENT") return false; throw err; } } /** Unlink the lock only while it is still the exact file this owner created. */ export function releaseLock(ownership) { const { lockPath } = ownership; let current; try { current = readLockSnapshot(lockPath); } catch { return; } if (current && current.rawContent === ownership.rawContent && sameIdentity(current.identity, ownership.identity)) { releaseLockRaw(lockPath); } } function extractLockIdentity(content) { const trimmed = content.trim(); if (!trimmed) return {}; if (trimmed.startsWith("{")) { try { const parsed = JSON.parse(trimmed); const pid = typeof parsed.pid === "number" ? parsed.pid : Number.NaN; const rawLauncherPid = typeof parsed.launcherPid === "number" ? parsed.launcherPid : Number.NaN; return { holderPid: Number.isInteger(pid) && pid > 0 ? pid : undefined, launcherPid: Number.isInteger(rawLauncherPid) && rawLauncherPid > 0 ? rawLauncherPid : undefined, }; } catch { return {}; } } const pid = Number.parseInt(trimmed, 10); return { holderPid: Number.isInteger(pid) && pid > 0 ? pid : undefined }; }