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

74 lines (73 loc) • 3.43 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/. 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); }