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();
|
package/dist/core/file-lock.js
CHANGED
|
@@ -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.
|
|
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": [
|