akm-cli 0.8.2 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
+ ## [0.8.3] - 2026-06-08
8
+
9
+ ### Fixed
10
+
11
+ - **`improve.lock` leaked on signal death (cron timeout).** The improve
12
+ SIGTERM/SIGINT/SIGHUP handler calls `process.exit()`, which skips `finally`
13
+ blocks — so the `finally` that releases `improve.lock` never ran, and every
14
+ timed-out cron run leaked the lock sentinel. (It wasn't a permanent deadlock
15
+ only because the next run reclaims a dead-PID lock, a path that PID reuse can
16
+ defeat.) The lock is now released from a `process.on("exit", …)` handler
17
+ registered at acquire time (exit handlers DO run on `process.exit()`), via a
18
+ new ownership-checked `releaseLockIfOwned(path, pid)` so a backstop release can
19
+ never delete a different run's lock. This generalizes to the budget watchdog
20
+ and any future exit path.
21
+ - **`quick` profile was not quick.** It was documented "Reflect-only" but did
22
+ not disable the session-`extract` process (which is default-ON), so a `quick`
23
+ run processed the entire unindexed-session backlog (~40 min) — guaranteeing a
24
+ 5-minute cron timeout → SIGTERM → the lock leak above, every run. `quick` now
25
+ explicitly sets `processes.extract.enabled: false`.
26
+
7
27
  ## [0.8.2] - 2026-06-05
8
28
 
9
29
  ### Added
@@ -1,10 +1,11 @@
1
1
  {
2
- "description": "Reflect-only pass — no distill, consolidate, memoryInference, or graphExtraction.",
2
+ "description": "Reflect-only pass — no extract, distill, consolidate, memoryInference, or graphExtraction.",
3
3
  "processes": {
4
4
  "reflect": {
5
5
  "enabled": true,
6
6
  "allowedTypes": ["agent", "command", "knowledge", "lesson", "memory", "skill", "wiki", "workflow"]
7
7
  },
8
+ "extract": { "enabled": false },
8
9
  "distill": { "enabled": false },
9
10
  "consolidate": { "enabled": false },
10
11
  "memoryInference": { "enabled": false },
@@ -8,7 +8,7 @@ import { daysToMs, isAssetType } from "../core/common";
8
8
  import { getDefaultLlmConfig, loadConfig } from "../core/config";
9
9
  import { ConfigError, NotFoundError, rethrowIfTestIsolationError, UsageError } from "../core/errors";
10
10
  import { appendEvent, readEvents } from "../core/events";
11
- import { probeLock, releaseLock, tryAcquireLockSync } from "../core/file-lock";
11
+ import { probeLock, releaseLock, releaseLockIfOwned, tryAcquireLockSync } from "../core/file-lock";
12
12
  import { parseFrontmatter } from "../core/frontmatter";
13
13
  import { detectAndWriteContradictions } from "../core/memory-contradiction-detect";
14
14
  import { analyzeMemoryCleanup, applyMemoryCleanup, } from "../core/memory-improve";
@@ -497,6 +497,17 @@ export async function akmImprove(options = {}) {
497
497
  }
498
498
  lockAcquired = false;
499
499
  };
500
+ // Signal-safe lock release (0.8.3 hotfix). The SIGTERM/SIGINT/SIGHUP handler
501
+ // in improve-cli.ts calls `process.exit()`, which does NOT run the `finally`
502
+ // below that owns lock release — so a cron-timeout SIGTERM leaked
503
+ // `improve.lock` every run. `process.exit()` DOES fire `'exit'` listeners,
504
+ // so we release the lock from one. `releaseLockIfOwned` only unlinks a lock
505
+ // still owned by this PID, so it is safe even if a later run re-acquired it.
506
+ // The listener is removed in the `finally` so the normal path stays single-release
507
+ // and repeated in-process `akmImprove` calls (tests) do not accumulate listeners.
508
+ const releaseLockOnExit = () => {
509
+ releaseLockIfOwned(resolvedLockPath, process.pid);
510
+ };
500
511
  const preEnsureCleanupWarnings = [];
501
512
  let plannedRefs;
502
513
  let memorySummary;
@@ -510,6 +521,9 @@ export async function akmImprove(options = {}) {
510
521
  if (!options.dryRun) {
511
522
  acquireLock();
512
523
  lockAcquired = true;
524
+ // Backstop release on process.exit() (signal handler / budget watchdog),
525
+ // which skips the finally below. Removed in that finally on the normal path.
526
+ process.on("exit", releaseLockOnExit);
513
527
  // Phase 4 triage pre-pass (§7, §13): drain the standing pending backlog
514
528
  // BEFORE ensureIndex so improve generates fresh proposals against a cleared
515
529
  // queue (no `duplicate_pending` collisions) and ensureIndex absorbs triage's
@@ -922,6 +936,9 @@ export async function akmImprove(options = {}) {
922
936
  catch {
923
937
  // ignore
924
938
  }
939
+ // The normal path released the lock above; drop the process.exit backstop so
940
+ // it does not fire later (or accumulate across repeated in-process calls).
941
+ process.removeListener("exit", releaseLockOnExit);
925
942
  // I1: close the long-lived state.db connection opened at the top of the run.
926
943
  try {
927
944
  eventsDb?.close();
@@ -79,6 +79,28 @@ export function releaseLock(lockPath) {
79
79
  // Sentinel already gone — fine.
80
80
  }
81
81
  }
82
+ /**
83
+ * Release a lock ONLY if it is still owned by `ownerPid`. Safe to call from a
84
+ * `process.exit()` / `'exit'` handler as a backstop: `process.exit()` skips
85
+ * `finally` blocks — so the normal lock-release never runs on signal death
86
+ * (SIGTERM/SIGINT) — but it DOES fire `'exit'` listeners synchronously. Checking
87
+ * ownership first means that if the lock was already released and re-acquired by
88
+ * a different process, this leaves that process's lock intact (no cross-run
89
+ * deletion / PID-reuse footgun). Synchronous so it is valid inside an exit handler.
90
+ */
91
+ export function releaseLockIfOwned(lockPath, ownerPid) {
92
+ let rawContent;
93
+ try {
94
+ rawContent = fs.readFileSync(lockPath, "utf8");
95
+ }
96
+ catch {
97
+ // Absent or unreadable — nothing of ours to release.
98
+ return;
99
+ }
100
+ if (extractHolderPid(rawContent) === ownerPid) {
101
+ releaseLock(lockPath);
102
+ }
103
+ }
82
104
  /**
83
105
  * Extract a PID from a sentinel body. Accepts the two shapes used across
84
106
  * the codebase: a bare numeric string (config-io, vault, lockfile) and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-cli",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "type": "module",
5
5
  "description": "akm (Agent Knowledge Management) — A package manager for AI agent skills, commands, tools, and knowledge. Works with Claude Code, OpenCode, Cursor, and any AI coding assistant.",
6
6
  "keywords": [