akm-cli 0.9.8-beta.3 → 0.9.9-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 +186 -132
- package/dist/commands/health/checks.js +40 -0
- package/dist/commands/health.js +57 -31
- package/dist/commands/migrate-cli.js +29 -189
- package/dist/commands/sources/add-cli.js +7 -0
- package/dist/commands/sources/installed-stashes.js +36 -8
- package/dist/commands/sources/self-update.js +104 -62
- package/dist/commands/sources/source-add.js +6 -5
- package/dist/commands/sources/sources-cli.js +7 -18
- package/dist/commands/tasks/tasks-cli.js +4 -3
- package/dist/commands/tasks/tasks.js +13 -6
- package/dist/core/adapter/adapter-ids.js +35 -0
- package/dist/core/adapter/adapters/index.js +29 -0
- package/dist/core/adapter/detect-adapter.js +91 -3
- package/dist/core/config/config.js +1 -1
- package/dist/core/config/schema/sources-bundles.js +23 -0
- package/dist/core/extra-params.js +1 -1
- package/dist/core/state/migrations.js +2 -4
- package/dist/core/state-db.js +31 -8
- package/dist/indexer/indexer.js +64 -1
- package/dist/scripts/akm-migrate-node.js +86534 -20434
- package/dist/scripts/akm-migrate.js +86415 -20280
- package/dist/tasks/backends/cron.js +21 -6
- package/dist/tasks/resolve-akm-bin.js +1 -1
- package/docs/README.md +1 -0
- package/docs/integration/bundling-akm.md +276 -0
- package/docs/migration/v0.9.0-troubleshooting.md +10 -14
- package/docs/migration/v0.9.1-to-v0.9.2.md +12 -16
- package/docs/reference/cli.md +59 -31
- package/docs/reference/tasks.md +4 -10
- package/package.json +2 -1
- package/dist/commands/migrate/config-extra-params.js +0 -61
- package/dist/commands/migrate/dead-residue.js +0 -113
- package/dist/commands/migrate/stale-txn.js +0 -49
package/CHANGELOG.md
CHANGED
|
@@ -4,115 +4,82 @@ 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.9.
|
|
7
|
+
## [0.9.9-beta.1] - 2026-09-02
|
|
8
8
|
|
|
9
|
-
###
|
|
10
|
-
|
|
11
|
-
- **The incremental index no longer misses an edit whose timestamp did not move
|
|
12
|
-
forward.** The per-directory freshness check summarised a directory as its
|
|
13
|
-
file-name set plus the single newest mtime, which lost two kinds of change.
|
|
14
|
-
An edit to any file other than the newest one landed below that maximum and
|
|
15
|
-
was invisible even though its own mtime changed — so a restore, checkout, or
|
|
16
|
-
archive extraction that stamped a plausible older date left stale content in
|
|
17
|
-
the index. And because mtime is writable by ordinary tooling (`touch -r`,
|
|
18
|
-
`rsync --times`, `cp -p`), an edit with a restored timestamp was invisible
|
|
19
|
-
outright. The directory is now digested per file over
|
|
20
|
-
`(basename, size, mtime, ctime)` at nanosecond resolution. It is the same one
|
|
21
|
-
`stat` call per file, so the incremental fast path costs what it did before.
|
|
22
|
-
Both gaps predate 0.9.8 and applied to every earlier release.
|
|
23
|
-
|
|
24
|
-
Trade-off worth knowing: `ctime` also moves on metadata-only changes such as
|
|
25
|
-
`chmod`, and after copying a tree, so those now cost one extra rescan. That
|
|
26
|
-
direction is deliberate — extra work, never stale content. Existing indexes
|
|
27
|
-
rescan once as the digest changes shape, then return to the fast path.
|
|
28
|
-
|
|
29
|
-
- **`akm migrate apply` can now clear a legacy `extraParams` config.** A config
|
|
30
|
-
still carrying a liftable key such as `extraParams.temperature` fails config
|
|
31
|
-
load closed, and that error names `akm migrate apply` as the fix — but the
|
|
32
|
-
migrate command resolved the stash directory and ran the task migrator, both
|
|
33
|
-
of which load config, so it died on the very error it exists to clear. An
|
|
34
|
-
operator hitting this had no reachable way forward. The config lift now runs
|
|
35
|
-
before anything that loads config, and `akm migrate status` reports the
|
|
36
|
-
pending lift as its blocker instead of re-raising the same error. A genuine
|
|
37
|
-
conflict, where an `extraParams` key and its first-class field disagree, still
|
|
38
|
-
hard-rejects and names both values rather than guessing.
|
|
39
|
-
|
|
40
|
-
- **`akm health` no longer warns about disk usage on a fresh install.** The
|
|
41
|
-
`data-dir-usage` advisory added earlier in 0.9.8 counted SQLite's `-wal` and
|
|
42
|
-
`-shm` sidecars toward the data directory's total but not toward the live
|
|
43
|
-
databases they belong to. On an untouched install the write-ahead log is most
|
|
44
|
-
of the directory, so the very first `akm health` reported a ~126x ratio and
|
|
45
|
-
exited `warn` with no user data present. Sidecars now count as part of their
|
|
46
|
-
database, and the advisory stays quiet below 1 GB, where a ratio says nothing
|
|
47
|
-
useful about disk pressure.
|
|
48
|
-
|
|
49
|
-
## [0.9.8-beta.2] - 2026-09-02
|
|
9
|
+
### Changed
|
|
50
10
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
11
|
+
- **`akm-migrate` is the one migration tool, and `akm upgrade` runs it**
|
|
12
|
+
(#895 follow-up, #901). The standalone `akm-migrate` executable now runs
|
|
13
|
+
every migration step in one plan — the legacy config `extraParams` lift,
|
|
14
|
+
pending `state.db` migrations (historical-destructive ones included, with
|
|
15
|
+
the verified safety copy), task v2 → v3, task v3 → task source v4, and the
|
|
16
|
+
residue sweeps — as `akm-migrate status` / `apply [--dry-run]`; its
|
|
17
|
+
per-generation verbs (`task-v4-status`, `task-v4-apply`) are gone.
|
|
18
|
+
`akm migrate status|apply` is now a thin wrapper over that executable, and
|
|
19
|
+
every historical shape (`scripts/akm-migrate/`) lives outside the CLI
|
|
20
|
+
proper. `akm upgrade` runs `akm-migrate apply` **after** its install step —
|
|
21
|
+
the migrator that shipped with whatever is now installed — on every run,
|
|
22
|
+
install or no install, and reports the plan under `migration`; a blocked or
|
|
23
|
+
failed migration exits 1. A package-manager install that fails (EACCES on a
|
|
24
|
+
root-owned global directory) still runs the migrator and says so. An akm
|
|
25
|
+
installed as a dependency of another package is now detected
|
|
26
|
+
(`installMethod: "package-local"`) and never reinstalled — an
|
|
27
|
+
`npm install -g` there "succeeded" while the parent kept executing its own
|
|
28
|
+
copy — but its migrations still run. `akm upgrade --state-only` and the
|
|
29
|
+
`stateUpgrade` response field are removed; `akm migrate apply` is the
|
|
30
|
+
offline path. An ordinary managed open still refuses a historical-
|
|
31
|
+
destructive migration, and its message names these two commands.
|
|
32
|
+
A container that ships akm can put `akm upgrade` (or `akm migrate apply`)
|
|
33
|
+
in its entrypoint: on a current installation it is a no-op.
|
|
65
34
|
|
|
66
35
|
### Fixed
|
|
67
36
|
|
|
68
|
-
- **`akm task sync`
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
-
|
|
86
|
-
|
|
87
|
-
`
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
after upgrading, then takes the fast path.
|
|
97
|
-
|
|
98
|
-
- **Task-migration snapshots are capped at the five most recent** (#897).
|
|
99
|
-
`akm migrate apply` writes one snapshot directory per run under
|
|
100
|
-
`backups/task-v3` and `backups/task-v4` and never pruned them; each apply
|
|
101
|
-
now keeps the five newest and removes the rest, the same policy config
|
|
102
|
-
backups already use. Nothing in the current code writes the legacy
|
|
103
|
-
`backups/migrations`, `manual`, `releases`, or `operations` directories,
|
|
104
|
-
so they are left alone; the new health advisory is what surfaces them.
|
|
105
|
-
|
|
106
|
-
## [0.9.8-beta.1] - 2026-09-01
|
|
37
|
+
- **`akm task sync` reports failed sources under `failures` on both the live
|
|
38
|
+
and `--dry-run` shapes** (#906). Live sync used to report them under
|
|
39
|
+
`failed` while `--dry-run` already used `failures`. No alias; both paths
|
|
40
|
+
now agree.
|
|
41
|
+
- **`akm task sync` no longer misdiagnoses a supercronic-managed container as
|
|
42
|
+
missing the `crontab` binary** (#910). `crontab -l` exiting non-zero with
|
|
43
|
+
no output, or with stderr saying "no crontab", is cron's own way of saying
|
|
44
|
+
"empty crontab" (BSD's `no crontab for <user>`, or a PATH shim like
|
|
45
|
+
OpenPalm's before any spool exists). Only a spawn that cannot find the
|
|
46
|
+
binary (ENOENT) is reported as missing; any other failure is reported as
|
|
47
|
+
what it said.
|
|
48
|
+
- **`akm task sync --rebind` no longer warns on every run of an image-baked
|
|
49
|
+
install** (#868 residue). The warning about binding scheduled tasks to a
|
|
50
|
+
mutable, unproven binary fires only when a rebind actually changes an
|
|
51
|
+
entry's bound invocation.
|
|
52
|
+
- **A mixed-layout bundle no longer auto-detects as a narrow tool-dir adapter
|
|
53
|
+
and silently drops the rest** (#908). A root that a tool-dir-shaped adapter
|
|
54
|
+
(`agent-skills`, `claude`, `opencode`) claims but that also carries ordinary
|
|
55
|
+
akm content detects as `akm`, the superset. `akm bundle list` now reports
|
|
56
|
+
each component's effective `adapter` and whether it was `detected`, and
|
|
57
|
+
indexing under an explicitly narrow adapter warns once with the count of
|
|
58
|
+
files and directories it skipped.
|
|
59
|
+
- **An unrecognised `components.*.adapter` is rejected** (#909) with
|
|
60
|
+
`INVALID_CONFIG_FILE` listing the accepted names instead of silently
|
|
61
|
+
falling back to `akm`. `akm bundle add --adapter <name>` overrides
|
|
62
|
+
auto-detection for a local directory; its `--help` lists the registry.
|
|
63
|
+
|
|
64
|
+
## [0.9.8] - 2026-09-02
|
|
107
65
|
|
|
108
66
|
A cleanup and stabilization release: deletion of machinery that policed the
|
|
109
67
|
codebase's shape rather than its behaviour, and — because auditing for that
|
|
110
68
|
machinery meant reading the code closely — a run of real defects it had been
|
|
111
|
-
sitting on top of.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
69
|
+
sitting on top of. Two security holes, two search-correctness bugs, a
|
|
70
|
+
locale-dependent hash, a deletion shield that failed open, and sixteen places
|
|
71
|
+
that answered a failure with a confident wrong answer instead of an error.
|
|
72
|
+
|
|
73
|
+
Then a second round, from verifying the release against a real 23,865-entry
|
|
74
|
+
environment: a no-op incremental index costing ~21 CPU-minutes, legacy proposal
|
|
75
|
+
rows that could not be repaired, an npm probe spawning on every scheduler tick,
|
|
76
|
+
a blocked task migration that named no remedy, and a data directory that could
|
|
77
|
+
reach 74 GB with nothing reporting it.
|
|
78
|
+
|
|
79
|
+
> **Upgrading is one-way for `state.db`.** This release adds two migrations,
|
|
80
|
+
> `025-task-history-vocabulary-backfill` and
|
|
81
|
+
> `026-proposals-strip-legacy-fragment-refs`. Once any 0.9.8 command opens
|
|
82
|
+
> `state.db`, its ledger contains IDs that 0.9.7 does not know, and 0.9.7
|
|
116
83
|
> refuses to open it: `Refusing to open a database with a newer migration
|
|
117
84
|
> ledger: unknown migration ID 025-task-history-vocabulary-backfill`.
|
|
118
85
|
>
|
|
@@ -127,12 +94,22 @@ sitting on top of.
|
|
|
127
94
|
>
|
|
128
95
|
> ```sh
|
|
129
96
|
> akm info --format json # confirm your data dir
|
|
130
|
-
> sqlite3 "$DATA_DIR/state.db" "VACUUM INTO '
|
|
97
|
+
> sqlite3 "$DATA_DIR/state.db" "VACUUM INTO 'state.db.pre-0.9.8.bak'"
|
|
131
98
|
> ```
|
|
132
99
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
100
|
+
> **`index.db` rescans once.** The per-directory freshness fingerprint changed
|
|
101
|
+
> shape, so the first `akm index` after upgrading re-reads every directory and
|
|
102
|
+
> then returns to the fast path. Nothing is lost; the index is derived.
|
|
103
|
+
|
|
104
|
+
### Added
|
|
105
|
+
|
|
106
|
+
- **`akm health` reports data-dir disk usage** (#896). A `data-dir-usage`
|
|
107
|
+
advisory sums the data directory with a stat-only walk and warns when it is
|
|
108
|
+
more than 3× the three live databases (state.db, index.db, logs.db) or when
|
|
109
|
+
one top-level subdirectory holds more than half of it, naming that
|
|
110
|
+
subdirectory with its size and share (for example `backups/ is 70G (94% of
|
|
111
|
+
data dir)`). The walk stops after 100,000 entries and says so. Silent when
|
|
112
|
+
nothing looks wrong.
|
|
136
113
|
|
|
137
114
|
### Changed
|
|
138
115
|
|
|
@@ -149,6 +126,39 @@ with a confident wrong answer instead of an error.
|
|
|
149
126
|
~60 lines of comment justifying it. `--format text` still renders the same
|
|
150
127
|
summary through the same formatter; it is simply no longer the default.
|
|
151
128
|
|
|
129
|
+
|
|
130
|
+
- **Search no longer truncates long queries (#892).** `MAX_LEXICAL_QUERY_TOKENS
|
|
131
|
+
= 16` silently dropped every token past the sixteenth, and tokens are
|
|
132
|
+
collected in order, so the discarded half was the tail — for
|
|
133
|
+
natural-language input, usually where the discriminating words are. It also
|
|
134
|
+
fed ranking, so token-overlap scoring ran on the truncated set too. It was
|
|
135
|
+
unexplained in the code and in the commit that introduced it, and unreachable
|
|
136
|
+
from any flag, config key, or environment variable. Removed: the planner
|
|
137
|
+
handles 10,000 tokens in 9ms, so no performance cliff was being protected.
|
|
138
|
+
|
|
139
|
+
- **Content and memory bodies are no longer silently truncated.**
|
|
140
|
+
`MAX_CONTENT_CHARS` (100k, duplicated across 8 adapters) cut indexed content
|
|
141
|
+
so the tail of a long document was unsearchable; `MAX_BODY_CHARS` (4000) cut
|
|
142
|
+
the text sent for memory inference, so on a large-context engine the model
|
|
143
|
+
saw a fraction of the input while the derived memory looked complete. Both
|
|
144
|
+
removed.
|
|
145
|
+
|
|
146
|
+
- **GitHub Actions are pinned to commit SHAs (#768).** All 29 `uses:` steps
|
|
147
|
+
across every workflow, with the tag preserved in a trailing comment.
|
|
148
|
+
|
|
149
|
+
- **Gated CI runs on schedule, dispatch, and candidate tags only.** The
|
|
150
|
+
`detect-changes` job that selected suites by regex-matching a PR diff is
|
|
151
|
+
gone — its path patterns had gone stale and still named test files this
|
|
152
|
+
release moved or deleted, so it was silently under-selecting suites. Release
|
|
153
|
+
evidence is unchanged; the checklist always required an exact-SHA dispatch.
|
|
154
|
+
|
|
155
|
+
- **`akm-eval` in CI is now a determinism check only.** Its score gates are
|
|
156
|
+
removed. Measured before cutting: the baseline scored a perfect 1.0 against
|
|
157
|
+
a 0.75 gate, and seven of nine case types never ran — CI has no LLM and no
|
|
158
|
+
run history, so everything the eval exists to measure was skipped while the
|
|
159
|
+
job reported green. The harness itself is unchanged and remains a genuine
|
|
160
|
+
quality signal when run against a real bundle.
|
|
161
|
+
|
|
152
162
|
### Fixed
|
|
153
163
|
|
|
154
164
|
- **Historical state migrations are reachable where akm cannot reinstall
|
|
@@ -264,39 +274,83 @@ with a confident wrong answer instead of an error.
|
|
|
264
274
|
`bun run lint` failed locally while CI, which never has those files, stayed
|
|
265
275
|
green.
|
|
266
276
|
|
|
267
|
-
### Changed
|
|
268
277
|
|
|
269
|
-
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
278
|
+
- **`akm task sync` no longer spawns `npm root --global` on every call** (#901).
|
|
279
|
+
The npm-global-root probe behind `resolveAkmInvocation` is memoized for the
|
|
280
|
+
process, so a `task sync --rebind` cycle spawns npm at most once instead of
|
|
281
|
+
twice, and an installation that loops it every minute stops accumulating an
|
|
282
|
+
npm debug log per spawn.
|
|
283
|
+
- **A blocked v2 task now says how to convert it** (#902, #899). The
|
|
284
|
+
`argv-array-has-no-portable-shell-string` blocker printed by `akm migrate`
|
|
285
|
+
and the `TASK_SCHEMA_VERSION_UNSUPPORTED` read error now state that manual
|
|
286
|
+
conversion is required and name the rewrite (`command:` argv array →
|
|
287
|
+
`run:` string plus `shell:`). The full v2 → v4 field mapping is documented in
|
|
288
|
+
`docs/migration/v0.9.1-to-v0.9.2.md`.
|
|
289
|
+
- **Legacy `#fragment` proposal rows are repaired instead of warned about
|
|
290
|
+
forever** (#898). State migration 026 strips the retired export-fragment
|
|
291
|
+
selector from `proposals.ref` in place so the rows parse again, and an
|
|
292
|
+
unparseable proposal row now warns once per process instead of once per
|
|
293
|
+
read (`akm health --report` read the table seven times).
|
|
277
294
|
|
|
278
|
-
- **
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
295
|
+
- **A no-op incremental `akm index` no longer costs minutes of CPU** (#900).
|
|
296
|
+
Two causes: the per-directory freshness check ran two full scans of the
|
|
297
|
+
`entries` table for every directory (O(directories × entries)), and every
|
|
298
|
+
file was read, hashed, and parsed before the freshness check decided the
|
|
299
|
+
directory was unchanged. The directory lookup now uses the existing
|
|
300
|
+
`file_path` index, and a stat-based gate over each directory's walked file
|
|
301
|
+
set skips unchanged directories before any file is read. On a synthetic
|
|
302
|
+
800-directory, 4,000-entry corpus a no-op pass fell from ~37 s to under 1 s
|
|
303
|
+
of CPU with identical entries and search results. The persisted directory
|
|
304
|
+
fingerprint now covers every walked file and `index_dir_state` gains a
|
|
305
|
+
`row_count` column; an existing index.db drains each directory once more
|
|
306
|
+
after upgrading, then takes the fast path.
|
|
284
307
|
|
|
285
|
-
- **
|
|
286
|
-
|
|
308
|
+
- **Task-migration snapshots are capped at the five most recent** (#897).
|
|
309
|
+
`akm migrate apply` writes one snapshot directory per run under
|
|
310
|
+
`backups/task-v3` and `backups/task-v4` and never pruned them; each apply
|
|
311
|
+
now keeps the five newest and removes the rest, the same policy config
|
|
312
|
+
backups already use. Nothing in the current code writes the legacy
|
|
313
|
+
`backups/migrations`, `manual`, `releases`, or `operations` directories,
|
|
314
|
+
so they are left alone; the new health advisory is what surfaces them.
|
|
287
315
|
|
|
288
|
-
- **Gated CI runs on schedule, dispatch, and candidate tags only.** The
|
|
289
|
-
`detect-changes` job that selected suites by regex-matching a PR diff is
|
|
290
|
-
gone — its path patterns had gone stale and still named test files this
|
|
291
|
-
release moved or deleted, so it was silently under-selecting suites. Release
|
|
292
|
-
evidence is unchanged; the checklist always required an exact-SHA dispatch.
|
|
293
316
|
|
|
294
|
-
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
317
|
+
- **The incremental index no longer misses an edit whose timestamp did not move
|
|
318
|
+
forward.** The per-directory freshness check summarised a directory as its
|
|
319
|
+
file-name set plus the single newest mtime, which lost two kinds of change.
|
|
320
|
+
An edit to any file other than the newest one landed below that maximum and
|
|
321
|
+
was invisible even though its own mtime changed — so a restore, checkout, or
|
|
322
|
+
archive extraction that stamped a plausible older date left stale content in
|
|
323
|
+
the index. And because mtime is writable by ordinary tooling (`touch -r`,
|
|
324
|
+
`rsync --times`, `cp -p`), an edit with a restored timestamp was invisible
|
|
325
|
+
outright. The directory is now digested per file over
|
|
326
|
+
`(basename, size, mtime, ctime)` at nanosecond resolution. It is the same one
|
|
327
|
+
`stat` call per file, so the incremental fast path costs what it did before.
|
|
328
|
+
Both gaps predate 0.9.8 and applied to every earlier release.
|
|
329
|
+
|
|
330
|
+
Trade-off worth knowing: `ctime` also moves on metadata-only changes such as
|
|
331
|
+
`chmod`, and after copying a tree, so those now cost one extra rescan. That
|
|
332
|
+
direction is deliberate — extra work, never stale content. Existing indexes
|
|
333
|
+
rescan once as the digest changes shape, then return to the fast path.
|
|
334
|
+
|
|
335
|
+
- **`akm migrate apply` can now clear a legacy `extraParams` config.** A config
|
|
336
|
+
still carrying a liftable key such as `extraParams.temperature` fails config
|
|
337
|
+
load closed, and that error names `akm migrate apply` as the fix — but the
|
|
338
|
+
migrate command resolved the stash directory and ran the task migrator, both
|
|
339
|
+
of which load config, so it died on the very error it exists to clear. An
|
|
340
|
+
operator hitting this had no reachable way forward. The config lift now runs
|
|
341
|
+
before anything that loads config, and `akm migrate status` reports the
|
|
342
|
+
pending lift as its blocker instead of re-raising the same error. A genuine
|
|
343
|
+
conflict, where an `extraParams` key and its first-class field disagree, still
|
|
344
|
+
hard-rejects and names both values rather than guessing.
|
|
345
|
+
|
|
346
|
+
- **`akm health` no longer warns about disk usage on a fresh install.** The
|
|
347
|
+
`data-dir-usage` advisory added earlier in 0.9.8 counted SQLite's `-wal` and
|
|
348
|
+
`-shm` sidecars toward the data directory's total but not toward the live
|
|
349
|
+
databases they belong to. On an untouched install the write-ahead log is most
|
|
350
|
+
of the directory, so the very first `akm health` reported a ~126x ratio and
|
|
351
|
+
exited `warn` with no user data present. Sidecars now count as part of their
|
|
352
|
+
database, and the advisory stays quiet below 1 GB, where a ratio says nothing
|
|
353
|
+
useful about disk pressure.
|
|
300
354
|
|
|
301
355
|
### Removed
|
|
302
356
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { loadConfig } from "../../core/config/config.js";
|
|
6
6
|
import { ConfigError } from "../../core/errors.js";
|
|
7
|
+
import { listPendingStateMigrations } from "../../core/state-db.js";
|
|
7
8
|
import { withEngineFallback } from "../../integrations/agent/engine-fallback.js";
|
|
8
9
|
import { resolveEngine } from "../../integrations/agent/engine-resolution.js";
|
|
9
10
|
import { executionEngineDefinitionsFromConfig } from "../../integrations/agent/execution-definitions.js";
|
|
@@ -441,6 +442,40 @@ export function runSelectedModelAliasesProbe(deps = {}) {
|
|
|
441
442
|
evidence: { checked, missing },
|
|
442
443
|
};
|
|
443
444
|
}
|
|
445
|
+
/**
|
|
446
|
+
* Hard check: state.db's migration ledger has no pending entries.
|
|
447
|
+
*
|
|
448
|
+
* Read-only via `listPendingStateMigrations` (a preflight open, never a
|
|
449
|
+
* managed one), so running this check is always safe — even when the
|
|
450
|
+
* corresponding managed open would refuse outright because a pending
|
|
451
|
+
* migration is historical-destructive (see `beforeMigrationLocked` in
|
|
452
|
+
* `src/core/state/migrations.ts`). This is what lets `akm health` report that
|
|
453
|
+
* refusal as an ordinary `fail` check instead of crashing the whole command —
|
|
454
|
+
* and what replaces a bundler grepping akm's refusal error text
|
|
455
|
+
* to detect the same case.
|
|
456
|
+
*/
|
|
457
|
+
export function runPendingStateMigrationsCheck(stateDbPath, deps = {}) {
|
|
458
|
+
const pending = (deps.listPendingStateMigrations ?? listPendingStateMigrations)(stateDbPath);
|
|
459
|
+
if (pending.length === 0) {
|
|
460
|
+
return {
|
|
461
|
+
name: "state-db-migrations",
|
|
462
|
+
kind: "deterministic",
|
|
463
|
+
status: "pass",
|
|
464
|
+
confidence: "high",
|
|
465
|
+
message: "state.db has no pending migrations.",
|
|
466
|
+
evidence: { path: stateDbPath, pending: [] },
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
const range = pending.length > 1 ? `${pending[0]} … ${pending[pending.length - 1]}` : pending[0];
|
|
470
|
+
return {
|
|
471
|
+
name: "state-db-migrations",
|
|
472
|
+
kind: "deterministic",
|
|
473
|
+
status: "fail",
|
|
474
|
+
confidence: "high",
|
|
475
|
+
message: `${pending.length} pending state.db migration(s) (${range}); run \`akm migrate apply\`.`,
|
|
476
|
+
evidence: { path: stateDbPath, pending },
|
|
477
|
+
};
|
|
478
|
+
}
|
|
444
479
|
/**
|
|
445
480
|
* The ordered health-check registry. ORDER IS LOAD-BEARING: `akmHealth`
|
|
446
481
|
* iterates this array and appends to hardChecks/advisories in sequence, so the
|
|
@@ -477,6 +512,11 @@ export const HEALTH_CHECKS = [
|
|
|
477
512
|
evidence: { path: ctx.stateDbPath, durationMs: ctx.probe.durationMs },
|
|
478
513
|
}),
|
|
479
514
|
},
|
|
515
|
+
{
|
|
516
|
+
name: "state-db-migrations",
|
|
517
|
+
channel: "hard",
|
|
518
|
+
run: (ctx) => runPendingStateMigrationsCheck(ctx.stateDbPath),
|
|
519
|
+
},
|
|
480
520
|
{
|
|
481
521
|
name: "task-history-read",
|
|
482
522
|
channel: "hard",
|
package/dist/commands/health.js
CHANGED
|
@@ -10,14 +10,14 @@ import { readEvents } from "../core/events.js";
|
|
|
10
10
|
import { openLogsDatabase } from "../core/logs-db.js";
|
|
11
11
|
import { classifyPathAccess, describeInaccessiblePath } from "../core/path-access.js";
|
|
12
12
|
import { getConfigPath, getDataDir, getDbPath, getStateDbPathInDataDir } from "../core/paths.js";
|
|
13
|
-
import { listExistingTableNames, openStateDatabase } from "../core/state-db.js";
|
|
13
|
+
import { listExistingTableNames, listPendingStateMigrations, openStateDatabase } from "../core/state-db.js";
|
|
14
14
|
import { DURATION_UNITS, parseDuration, parseSinceToIso } from "../core/time.js";
|
|
15
15
|
import { closeDatabase, openReadonlyExistingDatabase } from "../storage/repositories/index-connection.js";
|
|
16
16
|
import { getAllEntries } from "../storage/repositories/index-entries-repository.js";
|
|
17
17
|
import { queryTaskHistory } from "../storage/repositories/task-history-repository.js";
|
|
18
18
|
import { pkgVersion } from "../version.js";
|
|
19
19
|
import { collectImproveAdvisories } from "./health/advisories.js";
|
|
20
|
-
import { HEALTH_CHECKS, runHealthEngineProbes } from "./health/checks.js";
|
|
20
|
+
import { HEALTH_CHECKS, runHealthEngineProbes, runPendingStateMigrationsCheck, } from "./health/checks.js";
|
|
21
21
|
import { collectDataDirUsageAdvisory } from "./health/data-dir-usage.js";
|
|
22
22
|
import { buildImproveSkipSummary, computeWallTimeStats, isAgentTaskHistoryRow, roundRate, summarizeImproveCompleted, summarizeImproveRuns, taskFailureDetail, } from "./health/improve-metrics.js";
|
|
23
23
|
import { emptyLlmUsageAggregate, readLlmUsageAggregate } from "./health/llm-usage.js";
|
|
@@ -414,37 +414,32 @@ function resolveWindowComparePhase(options, db, stateDbPath, now, logsDb) {
|
|
|
414
414
|
return { windowResults, deltas };
|
|
415
415
|
}
|
|
416
416
|
/**
|
|
417
|
-
* The health report for a state.db
|
|
417
|
+
* The health report for a state.db a managed open cannot reach at all: either
|
|
418
|
+
* the file is not readable (#791), or — the same shape, a different cause —
|
|
419
|
+
* it holds a pending migration the managed open refuses to apply without
|
|
420
|
+
* deliberate consent (`akm upgrade` / `akm migrate apply` are the
|
|
421
|
+
* only two callers allowed to admit a historical-destructive migration; see
|
|
422
|
+
* `beforeMigrationLocked` in `src/core/state/migrations.ts`).
|
|
418
423
|
*
|
|
419
|
-
* `akm health` is what an operator
|
|
420
|
-
* it must survive
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
+
* `akm health` is what an operator (or a bundler's boot check) runs when
|
|
425
|
+
* something else is misbehaving, so it must survive either problem long
|
|
426
|
+
* enough to NAME it. Previously an unreadable file was already handled this
|
|
427
|
+
* way, but a pending migration was not: the managed open's refusal escaped as
|
|
428
|
+
* a thrown `ConfigError` (exit 78) and crashed the whole command before any
|
|
429
|
+
* check — including this one — could report anything.
|
|
424
430
|
*
|
|
425
|
-
* Reported as a hard-channel `fail` — the run genuinely could
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
431
|
+
* Reported as a single hard-channel `fail` check — the run genuinely could
|
|
432
|
+
* not open state.db, so every check that depends on it is skipped rather than
|
|
433
|
+
* attempted — and it still exits non-zero, just through health's normal
|
|
434
|
+
* `fail` path instead of a thrown config-error exit.
|
|
429
435
|
*/
|
|
430
|
-
function
|
|
436
|
+
function degradedStateDbReport(hardCheck, options) {
|
|
431
437
|
return {
|
|
432
438
|
schemaVersion: 3,
|
|
433
439
|
ok: false,
|
|
434
440
|
status: "fail",
|
|
435
441
|
since: parseHealthSince(options.since),
|
|
436
|
-
hardChecks: [
|
|
437
|
-
{
|
|
438
|
-
name: "state-db-readable",
|
|
439
|
-
kind: "deterministic",
|
|
440
|
-
status: "fail",
|
|
441
|
-
confidence: "high",
|
|
442
|
-
message: `state.db exists but is not readable: ${detail}. Every other health check is skipped because ` +
|
|
443
|
-
"none of them can read it. Check the owner and mode of the data directory, or point " +
|
|
444
|
-
"AKM_DATA_DIR / XDG_DATA_HOME at a location this user owns.",
|
|
445
|
-
evidence: { detail },
|
|
446
|
-
},
|
|
447
|
-
],
|
|
442
|
+
hardChecks: [hardCheck],
|
|
448
443
|
advisories: [],
|
|
449
444
|
metrics: {
|
|
450
445
|
taskFailRate: 0,
|
|
@@ -459,6 +454,18 @@ function unreadableStateDbReport(detail, options) {
|
|
|
459
454
|
improve: summarizeImproveCompleted([]),
|
|
460
455
|
};
|
|
461
456
|
}
|
|
457
|
+
function unreadableStateDbCheck(detail) {
|
|
458
|
+
return {
|
|
459
|
+
name: "state-db-readable",
|
|
460
|
+
kind: "deterministic",
|
|
461
|
+
status: "fail",
|
|
462
|
+
confidence: "high",
|
|
463
|
+
message: `state.db exists but is not readable: ${detail}. Every other health check is skipped because ` +
|
|
464
|
+
"none of them can read it. Check the owner and mode of the data directory, or point " +
|
|
465
|
+
"AKM_DATA_DIR / XDG_DATA_HOME at a location this user owns.",
|
|
466
|
+
evidence: { detail },
|
|
467
|
+
};
|
|
468
|
+
}
|
|
462
469
|
export function akmHealth(options = {}) {
|
|
463
470
|
validateAkmHealthOptions(options);
|
|
464
471
|
const now = options.now ?? (() => Date.now());
|
|
@@ -466,11 +473,13 @@ export function akmHealth(options = {}) {
|
|
|
466
473
|
const stateDbPath = options.stateDbPath ?? getStateDbPathInDataDir();
|
|
467
474
|
const hardChecks = [];
|
|
468
475
|
const advisories = [];
|
|
469
|
-
// #791: an UNREADABLE state.db
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
//
|
|
473
|
-
//
|
|
476
|
+
// #791: an UNREADABLE state.db, or one with a pending migration the
|
|
477
|
+
// managed open refuses to apply, are the two failures `akm health` most
|
|
478
|
+
// needs to be able to report, because this is the command an operator (or a
|
|
479
|
+
// bundler's boot check) runs to find out why everything else is behaving
|
|
480
|
+
// oddly. Dying here meant health could not diagnose that state at all — not
|
|
481
|
+
// even the checks that never touch state.db got to run. Report it as a
|
|
482
|
+
// finding instead.
|
|
474
483
|
let db;
|
|
475
484
|
try {
|
|
476
485
|
db = openStateDatabase(stateDbPath);
|
|
@@ -478,7 +487,24 @@ export function akmHealth(options = {}) {
|
|
|
478
487
|
catch (error) {
|
|
479
488
|
const { access, code } = classifyPathAccess(stateDbPath);
|
|
480
489
|
if (access === "inaccessible") {
|
|
481
|
-
return
|
|
490
|
+
return degradedStateDbReport(unreadableStateDbCheck(describeInaccessiblePath(stateDbPath, code)), options);
|
|
491
|
+
}
|
|
492
|
+
// The managed open's refusal of a pending historical-destructive
|
|
493
|
+
// migration is a plain `Error`, not a distinguishable error class — so
|
|
494
|
+
// confirm the cause via the read-only preflight (`listPendingStateMigrations`,
|
|
495
|
+
// which never applies anything) rather than pattern-matching the message.
|
|
496
|
+
// A ledger too broken to enumerate at all throws here too; in that case
|
|
497
|
+
// fall through to the generic config-error report below, since it is a
|
|
498
|
+
// genuinely different, rarer failure this check cannot explain.
|
|
499
|
+
let pendingMigrationsCheck;
|
|
500
|
+
try {
|
|
501
|
+
pendingMigrationsCheck = runPendingStateMigrationsCheck(stateDbPath, { listPendingStateMigrations });
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
pendingMigrationsCheck = undefined;
|
|
505
|
+
}
|
|
506
|
+
if (pendingMigrationsCheck?.status === "fail") {
|
|
507
|
+
return degradedStateDbReport(pendingMigrationsCheck, options);
|
|
482
508
|
}
|
|
483
509
|
throw new ConfigError(`Unable to open state.db: ${error instanceof Error ? error.message : String(error)}`, "INVALID_CONFIG_FILE");
|
|
484
510
|
}
|