akm-cli 0.9.0-beta.0 → 0.9.0-beta.1

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
@@ -6,6 +6,28 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.0-beta.1] - 2026-06-08
10
+
11
+ ### Fixed
12
+
13
+ - **`improve.lock` leaked on signal death (cron timeout)** — forward-ported from
14
+ 0.8.3. The improve SIGTERM/SIGINT/SIGHUP handler calls `process.exit()`, which
15
+ skips `finally` blocks, so the `finally` releasing `improve.lock` never ran and
16
+ every timed-out cron run leaked the lock. It is now released from a
17
+ `process.on("exit", …)` handler registered at acquire time, via a new
18
+ ownership-checked `releaseLockIfOwned(path, pid)`.
19
+ - **`quick` profile was not quick** — forward-ported from 0.8.3. It did not
20
+ disable the default-ON session-`extract` process, so a `quick` run processed
21
+ the entire session backlog (~40 min). `quick` now sets
22
+ `processes.extract.enabled: false`.
23
+ - **`akm-eval` smoke suite adapted to the 0.9.0 CLI** (CI/tooling only). The
24
+ eval harness called `akm search --detail agent`, but 0.9.0 moved the
25
+ agent/summary projections to `--shape`; it now uses `--shape agent`.
26
+ Additionally, the improve-run history readers (`listRecentImproveRunIds` /
27
+ `resolveImproveRunId`) treated a missing `state.db` as an error rather than
28
+ "no runs", which broke the read-only smoke + replay-determinism gates on a
29
+ fresh checkout; a missing `state.db` is now handled as an empty history.
30
+
9
31
  ## [0.9.0-beta.0] - 2026-06-08
10
32
 
11
33
  ### Added
@@ -191,6 +213,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
191
213
  `migrate-storage` change is pinned by a sha256 + file-mode fixture-stash
192
214
  differential test.
193
215
 
216
+ ## [0.8.3] - 2026-06-08
217
+
218
+ ### Fixed
219
+
220
+ - **`improve.lock` leaked on signal death (cron timeout).** The improve
221
+ SIGTERM/SIGINT/SIGHUP handler calls `process.exit()`, which skips `finally`
222
+ blocks — so the `finally` that releases `improve.lock` never ran, and every
223
+ timed-out cron run leaked the lock sentinel. (It wasn't a permanent deadlock
224
+ only because the next run reclaims a dead-PID lock, a path that PID reuse can
225
+ defeat.) The lock is now released from a `process.on("exit", …)` handler
226
+ registered at acquire time (exit handlers DO run on `process.exit()`), via a
227
+ new ownership-checked `releaseLockIfOwned(path, pid)` so a backstop release can
228
+ never delete a different run's lock. This generalizes to the budget watchdog
229
+ and any future exit path.
230
+ - **`quick` profile was not quick.** It was documented "Reflect-only" but did
231
+ not disable the session-`extract` process (which is default-ON), so a `quick`
232
+ run processed the entire unindexed-session backlog (~40 min) — guaranteeing a
233
+ 5-minute cron timeout → SIGTERM → the lock leak above, every run. `quick` now
234
+ explicitly sets `processes.extract.enabled: false`.
235
+
194
236
  ## [0.8.2] - 2026-06-05
195
237
 
196
238
  ### 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 },
@@ -10,7 +10,7 @@ import { daysToMs, isAssetType } from "../../core/common.js";
10
10
  import { getDefaultLlmConfig, loadConfig } from "../../core/config/config.js";
11
11
  import { ConfigError, NotFoundError, rethrowIfTestIsolationError, UsageError } from "../../core/errors.js";
12
12
  import { appendEvent, readEvents } from "../../core/events.js";
13
- import { probeLock, releaseLock, tryAcquireLockSync } from "../../core/file-lock.js";
13
+ import { probeLock, releaseLock, releaseLockIfOwned, tryAcquireLockSync } from "../../core/file-lock.js";
14
14
  import { classifyImproveAction } from "../../core/improve-types.js";
15
15
  import { getDbPath, getStateDbPathInDataDir } from "../../core/paths.js";
16
16
  import { openStateDatabase, purgeOldEvents, purgeOldImproveRuns } from "../../core/state-db.js";
@@ -560,6 +560,17 @@ export async function akmImprove(options = {}) {
560
560
  }
561
561
  lockAcquired = false;
562
562
  };
563
+ // Signal-safe lock release. The SIGTERM/SIGINT/SIGHUP handler in improve-cli.ts
564
+ // calls `process.exit()`, which does NOT run the `finally` below that owns lock
565
+ // release — so a cron-timeout SIGTERM leaked `improve.lock` every run.
566
+ // `process.exit()` DOES fire `'exit'` listeners, so we release the lock from
567
+ // one. `releaseLockIfOwned` only unlinks a lock still owned by this PID, so it
568
+ // is safe even if a later run re-acquired it. The listener is removed in the
569
+ // `finally` so the normal path stays single-release and repeated in-process
570
+ // `akmImprove` calls (tests) do not accumulate listeners.
571
+ const releaseLockOnExit = () => {
572
+ releaseLockIfOwned(resolvedLockPath, process.pid);
573
+ };
563
574
  const preEnsureCleanupWarnings = [];
564
575
  let plannedRefs;
565
576
  let memorySummary;
@@ -574,6 +585,9 @@ export async function akmImprove(options = {}) {
574
585
  if (!options.dryRun) {
575
586
  acquireLock();
576
587
  lockAcquired = true;
588
+ // Backstop release on process.exit() (signal handler / budget watchdog),
589
+ // which skips the finally below. Removed in that finally on the normal path.
590
+ process.on("exit", releaseLockOnExit);
577
591
  // Phase 4 triage pre-pass (§7, §13): drain the standing pending backlog
578
592
  // BEFORE ensureIndex so improve generates fresh proposals against a cleared
579
593
  // queue (no `duplicate_pending` collisions) and ensureIndex absorbs triage's
@@ -1002,6 +1016,9 @@ export async function akmImprove(options = {}) {
1002
1016
  catch {
1003
1017
  // ignore
1004
1018
  }
1019
+ // The normal path released the lock above; drop the process.exit backstop so
1020
+ // it does not fire later (or accumulate across repeated in-process calls).
1021
+ process.removeListener("exit", releaseLockOnExit);
1005
1022
  // I1: close the long-lived state.db connection opened at the top of the run.
1006
1023
  try {
1007
1024
  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.9.0-beta.0",
3
+ "version": "0.9.0-beta.1",
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": [