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

105 lines (104 loc) • 5.15 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-liveness-only run lock — the shared mechanics behind `akm improve`'s * whole-run lock (`commands/improve/locks.ts`) and the index rebuild lock * (`indexer/index-rebuild-lock.ts`, #956). * * No `staleAfterMs`: only a verifiably dead holder is ever reclaimed. This is * the #872 lesson encoded at the mechanics layer so every future caller gets * it for free — an age-based stale window let a live-but-wedged holder's * lease survive forever from the holder's own point of view while stranding * every OTHER invocation once the clock passed, which cost one real install * a half-day indexing outage. SQLite's own WAL + busy_timeout + BEGIN * IMMEDIATE already serialize concurrent writers at the correctness layer — * this lock only ever avoids duplicate LOGICAL work or lets a caller choose * to skip instead of contend. * * This module performs exactly one create-or-probe attempt (with the same * absent-race and stale-reclaim retries `improve` established) and reports * whether the caller now owns the lock or, if not, who currently holds it. * It does not decide what "held" means — skip, throw, or warn-and-proceed is * entirely up to the caller. Creating the sentinel is an `O_EXCL` write, so * two racing processes never both create it. */ import fs from "node:fs"; import path from "node:path"; import { ConfigError } from "./errors.js"; import { createLockPayload, probeLock, reclaimStaleLock, tryAcquireLockSync } from "./file-lock.js"; import { describeInaccessiblePath } from "./path-access.js"; /** * Render a lock holder's pid for a message: `"4242"`, or `"4242 (launcher * 4240)"` when the holder's launcher pid is known (#956) — every process * listing and task log shows the launcher pid, not the bun/node child's, so * naming only the holder pid left an operator unable to connect the two. */ export function formatLockHolderPid(holder) { if (holder.pid === null) return "unknown"; return holder.launcherPid !== null ? `${holder.pid} (launcher ${holder.launcherPid})` : String(holder.pid); } function parseLockPayload(rawContent) { if (!rawContent) return null; try { return JSON.parse(rawContent); } catch { return null; } } function holderOf(lock) { return { pid: lock?.pid ?? null, startedAt: lock?.startedAt ?? null, launcherPid: lock?.launcherPid ?? null }; } /** * Attempt to acquire `lockPath`. Returns `{ state: "acquired" }` with an * ownership handle for {@link releaseLock}, or `{ state: "held", holder }` * naming the current holder (best-effort; `pid`/`startedAt` are `null` when * the holder identity could not be determined, e.g. a release-then-reacquire * race). Throws {@link ConfigError} if the sentinel exists but cannot be read * (#791: a lock we cannot see may be genuinely held — never a reclaim * candidate). */ export function tryAcquireRunLock(lockPath, options) { fs.mkdirSync(path.dirname(lockPath), { recursive: true }); const lockPayload = () => createLockPayload({ startedAt: new Date().toISOString(), ...options.payloadMetadata }); let ownership = tryAcquireLockSync(lockPath, lockPayload()); if (ownership) return { state: "acquired", ownership }; const probe = probeLock(lockPath); // Race: the holder released between the failed acquire above and this // probe. Retry once rather than falling through with a null-PID "held" // report for a lock nobody actually holds. if (probe.state === "absent") { ownership = tryAcquireLockSync(lockPath, lockPayload()); if (ownership) return { state: "acquired", ownership }; // Re-grabbed by another racer in this exact window — no holder detail // available without re-probing (which could itself race again). return { state: "held", holder: { pid: null, startedAt: null, launcherPid: null } }; } if (probe.state === "inaccessible") { throw new ConfigError(`${options.label} lock exists but is not readable: ${describeInaccessiblePath(lockPath, probe.code)}.`, "DATA_DIR_UNREADABLE"); } const lock = parseLockPayload(probe.rawContent); if (probe.state === "stale") { if (!reclaimStaleLock(lockPath, probe)) { return { state: "held", holder: holderOf(lock) }; } options.onReclaimed?.({ holderPid: lock?.pid ?? probe.holderPid ?? null, lockedAt: lock?.startedAt ?? null, ageMs: probe.ageMs ?? null, reason: probe.reason === "pid_dead" ? "pid_not_alive" : probe.reason, }); ownership = tryAcquireLockSync(lockPath, lockPayload()); if (ownership) return { state: "acquired", ownership }; // Acquired by another racer during stale recovery. return { state: "held", holder: holderOf(lock) }; } // probe.state === "held" return { state: "held", holder: holderOf(lock) }; }