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
74 lines (73 loc) • 3.43 kB
JavaScript
// 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/.
import path from "node:path";
import { TransientError } from "../../core/errors.js";
import { appendEvent } from "../../core/events.js";
import { releaseLock } from "../../core/file-lock.js";
import { formatLockHolderPid, tryAcquireRunLock } from "../../core/run-lock.js";
import { warn } from "../../core/warn.js";
export function improveLockPath(lockBaseDir) {
return path.join(lockBaseDir, "improve.lock");
}
export function tryAcquireImproveLock(lockPath, skipIfLocked, eventsCtx) {
let recoveryEvent;
const acquire = () => tryAcquireImproveLockUnlocked(lockPath, skipIfLocked, (event) => {
recoveryEvent = event;
});
const result = acquire();
if (recoveryEvent) {
try {
// R25: lock acquisition runs BEFORE akmImprove opens its long-lived
// handle, so the caller passes the C2 boundary-pinned dbPath ctx — the
// rare stale-recovery event lands in the RIGHT state.db (correctness,
// not the handle fast path; no handle exists yet at lock time).
appendEvent(recoveryEvent, eventsCtx);
}
catch {
/* event emission is best-effort; never block lock recovery */
}
}
return result;
}
function tryAcquireImproveLockUnlocked(lockPath, skipIfLocked, onRecovered) {
// Mechanics (PID-liveness-only acquire/probe/absent-race-retry/stale-reclaim)
// live in the shared `core/run-lock.ts` module (#956) — this wrapper only
// owns improve's own policy: what "held" means (skip vs throw) and the
// improve_lock_recovered audit event.
const result = tryAcquireRunLock(lockPath, {
label: "improve",
onReclaimed: (info) => {
onRecovered({
eventType: "improve_lock_recovered",
metadata: {
lockName: "improve",
stalePid: info.holderPid,
lockedAt: info.lockedAt,
recoveredAt: new Date().toISOString(),
lockAgeMs: info.ageMs,
reason: info.reason,
},
});
},
});
if (result.state === "acquired") {
return { state: "acquired", ownership: result.ownership };
}
const { startedAt } = result.holder;
const pid = formatLockHolderPid(result.holder);
if (skipIfLocked) {
warn(`[improve] another improve run holds the lock (PID ${pid}, started ${startedAt}); skipping (--skip-if-locked)`);
return { state: "skipped" };
}
// #948 field follow-up: two legitimate `improve` invocations colliding on
// this lock is ordinary, retryable contention — not a broken config file.
// A `ConfigError` here surfaced as exit 78 (INVALID_CONFIG_FILE) and told
// a supervisor to stop retrying a normal lock collision. Reclassified to
// `TransientError`/`IMPROVE_LOCK_HELD` (exit 75), mirroring the
// `INDEX_DB_CONTENDED` treatment #956 applied to the index rebuild lock.
throw new TransientError(`akm improve is already running (PID ${pid}, started ${startedAt}). Delete ${lockPath} to force.`, "IMPROVE_LOCK_HELD");
}
export function releaseImproveLock(ownership) {
releaseLock(ownership);
}