akm-cli 0.9.17-alpha.8 → 0.9.17

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.
Files changed (111) hide show
  1. package/CHANGELOG.md +277 -1694
  2. package/STABILITY.md +9 -8
  3. package/dist/assets/hints/cli-hints-full.md +6 -7
  4. package/dist/assets/improve-strategies/catchup.json +0 -3
  5. package/dist/assets/improve-strategies/consolidate.json +0 -1
  6. package/dist/assets/improve-strategies/default.json +1 -2
  7. package/dist/assets/improve-strategies/proactive-maintenance.json +1 -2
  8. package/dist/assets/improve-strategies/quick.json +1 -2
  9. package/dist/assets/improve-strategies/reflect-distill.json +1 -2
  10. package/dist/assets/improve-strategies/thorough.json +0 -3
  11. package/dist/assets/prompts/consolidate-pair.md +20 -0
  12. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +17 -19
  13. package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -2
  14. package/dist/assets/templates/html/health.html +3 -5
  15. package/dist/cli/retired-commands.js +1 -1
  16. package/dist/cli/unknown-flags.js +24 -1
  17. package/dist/cli.js +46 -1
  18. package/dist/commands/health/archive-usage.js +92 -0
  19. package/dist/commands/health/data-dir-usage.js +25 -13
  20. package/dist/commands/health/html-report.js +1 -4
  21. package/dist/commands/health/improve-metrics.js +25 -37
  22. package/dist/commands/health/md-report.js +1 -6
  23. package/dist/commands/health/report-view-model.js +4 -14
  24. package/dist/commands/health/windows.js +0 -1
  25. package/dist/commands/health.js +13 -0
  26. package/dist/commands/improve/consolidate/continuity-check.js +137 -0
  27. package/dist/commands/improve/consolidate/pair-pass.js +791 -0
  28. package/dist/commands/improve/consolidate.js +38 -63
  29. package/dist/commands/improve/extract-prompt.js +1 -2
  30. package/dist/commands/improve/improve-cli.js +1 -1
  31. package/dist/commands/improve/improve-strategies.js +23 -5
  32. package/dist/commands/improve/improve.js +19 -30
  33. package/dist/commands/improve/ledger.js +3 -2
  34. package/dist/commands/improve/loop-stages.js +5 -85
  35. package/dist/commands/improve/memory/memory-belief.js +3 -1
  36. package/dist/commands/improve/memory/memory-improve.js +262 -11
  37. package/dist/commands/improve/planner.js +0 -5
  38. package/dist/commands/improve/preparation.js +20 -135
  39. package/dist/commands/improve/retrieval-scope.js +19 -4
  40. package/dist/commands/improve/salience.js +1 -14
  41. package/dist/commands/improve/stage.js +0 -1
  42. package/dist/commands/lint/base-linter.js +19 -11
  43. package/dist/commands/proposal/drain.js +8 -1
  44. package/dist/commands/proposal/proposal-cli.js +16 -2
  45. package/dist/commands/proposal/proposal-types.js +7 -0
  46. package/dist/commands/proposal/proposal.js +37 -6
  47. package/dist/commands/proposal/repository.js +613 -4
  48. package/dist/commands/proposal/validators/proposals.js +9 -0
  49. package/dist/commands/read/knowledge.js +3 -2
  50. package/dist/commands/read/show.js +0 -14
  51. package/dist/commands/sources/info.js +122 -18
  52. package/dist/commands/sources/stash-cli.js +23 -3
  53. package/dist/core/bundle-rename.js +1 -7
  54. package/dist/core/config/config-schema.js +8 -1
  55. package/dist/core/config/config.js +23 -48
  56. package/dist/core/config/engine-semantics.js +0 -2
  57. package/dist/core/config/schema/improve-processes.js +17 -42
  58. package/dist/core/config/schema/index-config.js +5 -25
  59. package/dist/core/file-change.js +13 -5
  60. package/dist/core/improve-result.js +22 -6
  61. package/dist/core/improve-types.js +0 -1
  62. package/dist/core/loopback.js +7 -12
  63. package/dist/core/parse.js +13 -16
  64. package/dist/core/state/migrations.js +15 -0
  65. package/dist/core/time.js +0 -20
  66. package/dist/indexer/db/llm-cache.js +2 -2
  67. package/dist/indexer/ensure-index.js +2 -2
  68. package/dist/indexer/index-written-assets.js +2 -3
  69. package/dist/indexer/indexer.js +18 -418
  70. package/dist/indexer/passes/metadata.js +0 -19
  71. package/dist/indexer/walk/walker.js +3 -4
  72. package/dist/llm/client.js +8 -10
  73. package/dist/llm/embedders/remote.js +1 -2
  74. package/dist/llm/feature-gate.js +0 -5
  75. package/dist/output/shapes/helpers.js +20 -4
  76. package/dist/output/text/command-format.js +9 -8
  77. package/dist/output/text/proposal-format.js +47 -1
  78. package/dist/output/text/show-format.js +0 -20
  79. package/dist/scripts/akm-migrate-node.js +923 -950
  80. package/dist/scripts/akm-migrate.js +923 -950
  81. package/dist/setup/steps/connection.js +5 -6
  82. package/dist/setup/steps/platforms.js +2 -2
  83. package/dist/sources/providers/git-stash.js +83 -4
  84. package/dist/storage/repositories/improve-ledger-repository.js +48 -7
  85. package/dist/storage/repositories/index-connection.js +5 -2
  86. package/dist/storage/repositories/index-entries-repository.js +4 -7
  87. package/dist/storage/repositories/index-entry-schema.js +4 -2
  88. package/dist/storage/repositories/index-llm-cache-repository.js +7 -26
  89. package/dist/storage/repositories/index-schema.js +55 -104
  90. package/dist/storage/repositories/proposals-repository.js +61 -0
  91. package/dist/storage/repositories/salience-repository.js +1 -19
  92. package/docs/migration/README.md +1 -1
  93. package/docs/migration/release-notes/0.9.17.md +130 -41
  94. package/docs/migration/release-notes/README.md +7 -0
  95. package/docs/reference/cli.md +27 -21
  96. package/docs/reference/configuration.md +21 -12
  97. package/docs/reference/data-and-telemetry.md +0 -1
  98. package/package.json +1 -1
  99. package/schemas/akm-config.json +0 -342
  100. package/dist/assets/improve-strategies/graph-refresh.json +0 -15
  101. package/dist/assets/prompts/contradiction-judge.md +0 -33
  102. package/dist/assets/prompts/graph-extract-system.md +0 -1
  103. package/dist/assets/prompts/graph-extract-user-prompt.md +0 -35
  104. package/dist/assets/prompts/metadata-enhance-system.md +0 -1
  105. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +0 -4
  106. package/dist/indexer/db/graph-db.js +0 -399
  107. package/dist/indexer/graph/graph-extraction.js +0 -809
  108. package/dist/indexer/graph/graph-related.js +0 -131
  109. package/dist/indexer/graph/graph-types.js +0 -4
  110. package/dist/llm/graph-extract.js +0 -892
  111. package/dist/llm/metadata-enhance.js +0 -95
package/CHANGELOG.md CHANGED
@@ -6,1714 +6,297 @@ 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.17-alpha.8] - 2026-09-28
10
-
11
- `akm index` now records the links a bundle already declares (`xrefs`,
12
- `supersededBy`, a `.derived` memory's parent, wiki sources, page links, and
13
- workflow and task targets) as typed links, with no model. `akm show` lists
14
- them, and curate's support refs come from them instead of the LLM entity
15
- graph. The index moves to layout 26 in place on its first writable open;
16
- 0.9.17-alpha.4 through alpha.7 cannot open it. `index.graph.enabled: false`
17
- now stops graph extraction in `akm improve`, and a partly failed extraction is
18
- retried. `akm migrate` converts a v2 or v3 task file to v4 in one pass, and
19
- the launchers no longer lose a signal that arrives before their child starts.
9
+ ## [0.9.17] - 2026-09-29
20
10
 
21
11
  ### Added
22
12
 
23
- - **Declared links (#935).** The relations a bundle already declares are now
24
- stored as typed links: `xrefs:` (`xref`), `supersededBy:`
25
- (`superseded_by`), `contradictedBy:` (`contradicted_by`),
26
- `currentBeliefRefs:` (`belief_peer`), a `.derived` memory's parent
27
- (`derived_from`), wiki `sources:` that name an asset (`cites`), the page
28
- links an llm-wiki or OKF bundle resolves (`links_to`), and a workflow step's
29
- or a task's target (`uses`). `akm index` reads them from what it already
30
- parses, with no model, so an install without an LLM engine gets them. They
31
- live in their own table (`asset_links`), apart from the LLM entity graph:
32
- each link belongs to the entry that declares it and is written, replaced
33
- and deleted with it, including on incremental and write-path (`akm
34
- remember`) indexing. A target resolves when it is indexed, not when its
35
- citer was, so a note that cites something added later links to it without
36
- being reindexed. Retired spellings convert in memory (`memory:<name>`,
37
- `wiki:<wiki>/<page>`, a `.md` suffix), and, as lint already allows (#882),
38
- a memory whose own file is gone resolves to its `.derived` child.
39
- `akm show` lists an asset's links grouped by kind: `outgoing`, `incoming`
40
- and the `unresolved` tokens it names, at most 10 per kind with a `total`.
41
- `akm info` reports links per kind with how many are unresolved. Links do
42
- not change search ranking, and `related` is unchanged: on the retrieval
43
- suite, against 0.9.17-alpha.7 on the same index with two runs per arm,
44
- search nDCG@10 moved +0.002 [−0.005, +0.009] (a rerun of alpha.7 alone
45
- moved +0.006) and curate P@5 +0.000 [−0.001, +0.001].
46
- On the retrieval snapshot of the maintainer's 21 bundles (23,979 entries)
47
- there are 14,917 links: 7,393 `contradicted_by`, 4,401 `xref`, 2,577
48
- `derived_from`, 540 `cites` and 6 `superseded_by`. 1,801 are unresolved;
49
- 1,744 of those are `.derived` memories whose parent memory no longer
50
- exists. (`src/indexer/links/declared-links.ts`,
51
- `src/storage/repositories/index-links-repository.ts`,
52
- `src/commands/read/show.ts`, `src/commands/sources/info.ts`)
13
+ - **Declared links (#935).** `akm index` now records the relations a bundle
14
+ already declares — `xrefs:`, `supersededBy:`, `contradictedBy:`,
15
+ `currentBeliefRefs:`, a `.derived` memory's parent, a wiki's `sources:`,
16
+ resolved wiki/OKF page links, and a workflow step's or task's target — as
17
+ typed links, read from what it already parses, with no model call. `akm
18
+ show` lists an asset's links grouped `outgoing`/`incoming`/`unresolved` (up
19
+ to 10 per kind, with a total), and `akm info` reports link counts and how
20
+ many are unresolved. Curate's support refs (at most two per item) now come
21
+ from declared links instead of the LLM entity graph's `related` list (see
22
+ Removed): on a 23,979-entry snapshot, `related` supplied support refs for
23
+ 5.4% of curate items against 29.6% for declared links, and judged
24
+ usefulness rose from about 7 to about 24 useful support refs per 100 items.
25
+ - **Consolidate pair pass and retire proposals.** A second consolidation pass
26
+ compares each new-or-changed memory, flat `knowledge/` file, or lesson
27
+ against its nearest neighbours (the best 5 of 20 candidates, same bundle,
28
+ memory tier only) and has an LLM judge label each pair `duplicate`,
29
+ `subsumed`, `supersedes`, `contradicts`, `overlap`, or `unrelated` (judged
30
+ at cosine ≥ 0.93, or ≥ 0.95 for an asset the pass has never attempted —
31
+ unless git added it within the last 7 days, which still judges at 0.93).
32
+ The first three labels mint a reviewed `retire` proposal for the losing side
33
+ (owner-calibrated precision 20/22 = 0.91 [0.72, 0.97]); `contradicts` is
34
+ counted but left to a human; at most 300 pairs are judged a night. Retire
35
+ proposals mint under their own generator, `consolidate-pair` — kept apart
36
+ from the promote pass's `consolidate` proposals in bulk accept/reject — and
37
+ are reviewed like any other proposal (`akm proposal list --generator
38
+ consolidate-pair`, `show`, `diff`, `accept`, `reject`). Accepting one
39
+ archives the losing asset (and its `.derived` twin) instead of deleting it;
40
+ `akm proposal revert` restores the archived bytes exactly, including a file
41
+ with no trailing newline. Triage never auto-accepts a retire proposal,
42
+ whatever `applyMode` says.
43
+ - **Retirement continuity check (rule R3).** Before the pair pass mints a
44
+ retire proposal, it replays up to five of the retired asset's own past
45
+ `search`/`curate` queries and requires the successor to rank in the top 10
46
+ everywhere the retired asset did. A failing or unverifiable query
47
+ (including one where the embedding endpoint is unreachable) flags the
48
+ proposal's `retirement.continuityRisk`, which excludes it from every bulk
49
+ accept path — but not bulk reject, and a person can still accept it by id.
50
+ `akm proposal list`/`show` surface the flag and the failing queries;
51
+ `akm proposal accept --generator ... --dry-run` reports how many
52
+ otherwise-matching proposals were skipped for this reason. `akm proposal
53
+ list` also gained a `--generator <name>` filter.
54
+ - **A promotion now retires its source memory (O1).** When `akm proposal
55
+ accept` promotes a consolidate `promote` proposal — by a person or by
56
+ triage auto-promotion — it archives the source memory (and its `.derived`
57
+ twin) through the same retire-archive path, provided the source is
58
+ unchanged since the promotion was minted. A promotion no longer leaves a
59
+ memory/knowledge duplicate behind. Best-effort: a failure to archive only
60
+ warns.
61
+ - **Archive purge sweep.** At the very start of every `akm improve` run, a
62
+ deterministic pass with no LLM call deletes an archived retirement's files
63
+ (never its `cleanup.md` tombstone) once it is more than 30 days old and
64
+ every file under its archive directory is git-tracked, clean, and
65
+ verifiable; anything less than that — or a bundle with no git history at
66
+ all — is left in place for a later sweep. A new `akm health` advisory,
67
+ `memory-cleanup-archive`, reports how many archived files and bytes are
68
+ not yet purgeable — untracked, modified, or unverifiable for a git-backed
69
+ bundle; everything, forever, for one with no git history at all.
70
+ - **`akm bundle rename <old> <new>`.** Properly re-keys a bundle instead of
71
+ requiring a hand-edit of `config.json`: it rewrites `bundles`,
72
+ `defaultBundle`/`defaultWriteTarget`, and every scheduler ref that name the
73
+ old id, re-keys the indexed entries and enrichment cache in `index.db`,
74
+ rewrites this tool's own state rows that name the old bundle, and re-syncs
75
+ native scheduler rows under the new name. Refs inside asset content
76
+ (cross-references, a task's `uses:`) are reported, never silently
77
+ rewritten. `--dry-run` shows the full plan without writing anything.
53
78
 
54
79
  ### Changed
55
80
 
56
- - **`akm migrate` converts a v2 or v3 task file straight to v4 in one pass.**
57
- The chain that read every file as v2, converted it to an intermediate v3
58
- shape, then converted that to v4 (`src/tasks/source/task-to-v3.ts` ->
59
- `task-to-v4.ts`, composed by `scripts/akm-migrate/migrate/task-files.ts`)
60
- is now one planner: `task-to-v4.ts` reads a file once and, for v2, builds
61
- the v3-shape record in memory — never written to disk or reported as its
62
- own outcome — before hoisting it to v4 through the same code path a real
63
- v3 file goes through. `akm migrate status`/`apply` output shapes, and
64
- every blocked/changed reason code, are unchanged, checked fixture by
65
- fixture against the prior two-hop chain's actual output (one exception:
66
- a v3 document that fails only the typed pre-check's own "exactly one
67
- scheduling source" rule — unreachable through the real chain, which
68
- always ran that same pre-check first — now reports `invalid-v3-task`
69
- instead of the raw hoist stage's own `ambiguous-scheduling-source`,
70
- matching what `akm migrate apply` already returned end to end). The
71
- `already-v3` and `pending-v2-to-v3-migration` intermediate states are
72
- gone with the generation split that produced them.
73
- `src/tasks/source/task-to-v3.ts` (500 lines) is deleted; its logic moved
74
- into `task-to-v4.ts`, which also drops the duplicate raw-YAML reader and
75
- outcome-base helpers the two files each carried their own copy of.
76
- (`src/tasks/source/task-to-v4.ts`, `scripts/akm-migrate/migrate/task-files.ts`)
77
- - **Curate's support refs come from declared links.** Each curated item's
78
- support refs (at most two) are now assets its declared links name: what
79
- it links to, then what links to it, in the order `akm show` lists them,
80
- skipping assets curate already selected. They no longer come from the LLM
81
- entity graph's `related` list. On the retrieval snapshot, `related` offered
82
- support refs for 47 of 867 curate items (5.4%) and declared links for 257
83
- (29.6%). The retrieval judge graded 157 of those items, asking whether each
84
- support ref is worth opening next: 56% of declared support refs were useful
85
- against 68% of `related`'s, so curate attaches about 24 useful support refs
86
- per 100 items instead of 7. Curate's items are unchanged.
87
- (`src/commands/read/curate.ts`)
88
- - **Index layout 26.** The first writable open of an older index derives
89
- every entry's links from its stored `document_json` in place, reading no
90
- file: on the 23,979-entry snapshot index that takes 0.8 s and adds 3.5 MB.
91
- Earlier layouts never stored a workflow's or a task's targets, so only the
92
- directories holding workflows and tasks re-read on the next `akm index`.
93
- The open leaves `index_meta.vacuumPending` like every layout migration.
94
- 0.9.17-alpha.4 through alpha.7 refuse an index at layout 26
95
- (`INDEX_SCHEMA_INCOMPATIBLE`, naming the upgrade), for writing as well as
96
- reading, so going back to one of them needs a new index: move `index.db`
97
- aside and run `akm index` under that release (the LLM graph and enrichment
98
- cache it held are not rebuilt by `akm index`).
99
- (`src/storage/repositories/index-schema.ts`,
100
- `src/storage/repositories/index-entry-schema.ts`)
101
-
102
- ### Fixed
103
-
104
- - **`index.graph.enabled: false` stops graph extraction in `akm improve`.**
105
- The switch was read only when improve had no strategy plan, which is never
106
- the case in a real run, so the nightly and weekly graph tasks kept
107
- extracting with it set. Improve now skips its graph extraction stage
108
- whenever `index.graph.enabled` is `false`, whatever the strategy enables.
109
- This also makes the graph ablation harness's "graph off" arm, which sets
110
- exactly this key, turn extraction off. Improve still does not read
111
- `index.defaults` when it picks the engine for graph extraction.
112
- (`src/commands/improve/loop-stages.ts`)
113
- - **Graph extraction never has more calls in flight than the run's
114
- concurrency.** Batches run side by side up to the runner's concurrency, and
115
- each batch also sent its per-file calls (long bodies, a non-array response)
116
- up to that limit at once, so at a concurrency of 2 four calls could be in
117
- flight. A batch now makes its per-file calls one at a time. At the default
118
- concurrency of 1 nothing changes. (`src/llm/graph-extract.ts`)
119
- - **A long document whose extraction partly failed is extracted again.** A
120
- body over 1,600 characters is extracted in chunks. When some chunks failed
121
- (a timeout, an error, an empty response) and others found entities, the
122
- file was recorded and cached as extracted, so the failed chunks were never
123
- retried. Such a file is now recorded as failed and not cached, and the next
124
- run extracts it again, every chunk: partial failures are rare outside
125
- provider outages, and keeping per-chunk results to skip the chunks that
126
- succeeded would need a second cache. Until then the entities the other
127
- chunks found are stored when the file had no graph rows yet; a file with
128
- rows keeps them. (`src/llm/graph-extract.ts`)
129
- - **`scripts/node-runtime/akm` could die from a raw signal instead of
130
- forwarding it to its child.** The launcher registered its
131
- SIGTERM/SIGINT/SIGHUP forwarding listeners only after spawning the child;
132
- under load, a signal could arrive in that window and fall through to the
133
- runtime's default (process-terminating) disposition, killing the launcher
134
- before the child ever saw it. The listeners now go up before anything else
135
- runs, with a small queue for a signal that arrives before the child exists.
136
- - **`scripts/node-runtime/akm-migrate` carried the same pre-spawn signal
137
- race** as `scripts/node-runtime/akm` above, for the same reason (listeners
138
- registered only after `spawn()`), with no test covering it. Fixed the same
139
- way, and added `tests/integration/akm-migrate-signal-forwarding.test.ts`
140
- (modelled on `launcher-signal-forwarding.test.ts`) for its forwarding.
141
- - **The launcher signal tests failed intermittently because of their own
142
- fixture.** The fake child wrote its ready file before it registered its
143
- signal handler, so a forwarded signal could reach it in between and kill it,
144
- and the launcher then reported that signal. Each fixture now registers its
145
- handler first. The launcher's pre-spawn window above could not cause this:
146
- the tests signal only after the child is running.
147
-
148
- ## [0.9.17-alpha.7] - 2026-09-28
149
-
150
- A scheduled task is now just a command and a schedule. Each native row
151
- carries its own `AKM_BUNDLE_DIR` instead of pointing at a descriptor file,
152
- and the runtime reads only v4 task files; older files convert once with
153
- `akm migrate apply`. The first `akm task sync` after upgrading rewrites each
154
- row once, keeping every task and schedule. `akm improve` reworks only assets
155
- that retrieval returned or that are new, and reflect refuses a rewrite that
156
- grades worse on the asset's own searches. `--require-engines` no longer
157
- skips a run because the LLM endpoint is busy.
158
-
159
- ### Changed
160
-
161
- - **akm reads only task source v4.** A `version: 2` or `version: 3` task
162
- file, or a `version: 4` file that still carries 0.9.15's retired
163
- `schedule[].enabled`, now fails on its own with a message naming
164
- `akm migrate apply`, which converts it once, under a backup (`akm upgrade`
165
- runs it after an install). Until now every read converted such a file in
166
- memory. `akm task sync` reports each one as a failure, leaves its installed
167
- row as it is, and keeps reconciling every other task; `akm task run`,
168
- `akm lint` and `akm task validate` report it the same way, and
169
- `akm task validate`'s `converts` outcome is gone (such a file is
170
- `blocked`). `akm migrate apply` now also converts the tasks of the stash
171
- `AKM_BUNDLE_DIR` selects when no configured bundle names it, since the
172
- runtime reads those too, and a root whose top-level task files are all
173
- v2/v3 is still detected as an `akm-task` bundle, so they are found and
174
- converted. A host whose task files are all v4 (`akm migrate status`
175
- reports `current`) sees no difference.
176
- (`src/tasks/source/parse-task-source.ts`, `src/commands/tasks/validate.ts`,
177
- `scripts/akm-migrate/task-migrate.ts`,
178
- `src/core/adapter/adapters/akm-task-adapter.ts`)
179
- - **A scheduled row is its command plus its schedule, and carries its own
180
- context.** Rows no longer name a `--scheduler-context` descriptor file;
181
- they set what it held themselves. Every row sets `AKM_BUNDLE_DIR` to the
182
- working stash of the shell that ran `akm task sync`, plus any
183
- `AKM_CONFIG_DIR`, `AKM_DATA_DIR`, `AKM_CACHE_DIR` or `AKM_STATE_DIR` that
184
- shell set explicitly: a `VAR=value` prefix in the crontab, an
185
- `EnvironmentVariables` entry in a launchd plist, a `$env:VAR='value';`
186
- assignment ahead of the command in Task Scheduler (its action has no
187
- environment of its own). Scheduled runs see the same environment as
188
- before, and sync still tells installations sharing a crontab apart by
189
- that path (#846).
190
- **What hosts see:** the first `akm task sync` after upgrading rewrites
191
- every akm row once. `akm task sync --dry-run` lists each one as an update,
192
- never an add or a remove; each keeps its launcher and its schedule, and
193
- the `--scheduler-context <file>` argument becomes an inline
194
- `AKM_BUNDLE_DIR=<working stash>`. On a host whose working stash is
195
- `/home/u/akm` a row changes from
196
-
197
- ```text
198
- 30 8 * * * /home/u/.bun/bin/bun /home/u/.bun/lib/node_modules/akm-cli/dist/akm --scheduler-context /home/u/.local/share/akm/tasks/context/e898….json task run capture --bundle akm --scheduled > /home/u/.cache/akm/tasks/logs/capture.log 2>&1
199
- ```
200
-
201
- to
202
-
203
- ```text
204
- 30 8 * * * AKM_BUNDLE_DIR=/home/u/akm /home/u/.bun/bin/bun /home/u/.bun/lib/node_modules/akm-cli/dist/akm task run capture --bundle akm --scheduled > /home/u/.cache/akm/tasks/logs/capture.log 2>&1
205
- ```
206
-
207
- Rows written by 0.9.0 through 0.9.17-alpha.6 keep firing until that sync:
208
- the CLI still accepts `--scheduler-context <file>` and applies the file's
209
- environment (PATH included, for a 0.9.16 row). The files under
210
- `$DATA/tasks/context/` are no longer written, and the uid, mode, symlink
211
- and content-hash checks made on every scheduled run are gone. A sync
212
- leaves some rows as they are (one whose task file failed to load, one a
213
- `--bundle` sync did not cover), and those still name their file: once
214
- `akm task doctor` lists no binding with a `contextPath`, nothing reads
215
- them and they can be deleted. `akm task prune` now
216
- finds rows whose `AKM_BUNDLE_DIR` names a directory that is gone, and older
217
- rows whose descriptor cannot be read; `akm task doctor` lists `contextPath`
218
- only for an older row. (`src/tasks/scheduler-invocation.ts`,
219
- `src/tasks/backends/cron.ts`, `src/tasks/backends/launchd.ts`,
220
- `src/tasks/backends/schtasks.ts`, `src/tasks/scheduler-sync.ts`,
221
- `src/commands/tasks/tasks.ts`)
222
- - **`akm improve` reworks only what gets read (#986).** An asset with fresh
223
- feedback, or one you name (`akm improve skills/x`), is handled as before.
224
- Every other pick must now be in the retrieval scope. That covers the
225
- proactive-maintenance, high-salience and forgetting-safety lanes, and the
226
- memories consolidation judges. An asset is in scope if a user `search`,
227
- `curate` or `show` returned it, or user `feedback` named it, in the last 90
228
- days, which is the usage log's retention. A hit on a `.derived` memory counts
229
- for its parent. New material that no improve stage has processed yet is also
230
- in scope. There is no new config key.
231
-
232
- Measured with `akm improve --dry-run` on a copy of the maintainer's bundle
233
- (19,870 assets), against 0.9.17-alpha.6:
234
- - The fallback lanes pick from 6,450 assets instead of 15,686, and 9,236
235
- refs are left out. No lane setting can reach the unread tail any more. In
236
- July the proactive lane rewrote 3,069 assets, and 3,059 of them had not
237
- been retrieved since the usage log began on 1 July.
238
- - Consolidation judges 59 memories instead of 69.
239
- - The high-salience lane no longer admits distill outputs nobody has read (2
240
- today).
241
- - Under the scheduled caps, today's nightly work is unchanged. The default
242
- strategy selects the same 50 feedback-driven refs, and weekly proactive
243
- maintenance selects the same 25, because salience ranking already puts
244
- retrieved assets first.
245
-
246
- `akm improve --dry-run` and the run result report the left-out refs as a new
247
- `retrieval` gate. Health reports them under the skip reason `not_retrieved`.
248
- Improve results stored by earlier releases, which have no such gate, still
249
- decode. (`src/commands/improve/retrieval-scope.ts`,
250
- `src/commands/improve/preparation.ts`, `src/commands/improve/consolidate.ts`)
251
-
252
- - **Reflect refuses a rewrite that makes an asset worse for its own searches
253
- (#722).** Before reflect proposes a rewrite of an existing asset, it grades
254
- the old and the new content on up to five of the queries that actually
255
- retrieved the asset (user `search` and `curate`). It uses the retrieval
256
- eval's relevance prompt, which agrees with human grades at kappa 0.83. When
257
- the new content grades lower on average, the rewrite is refused the same way
258
- a quality-judge rejection is: `quality_rejected`, with the 14-day reflect
259
- window. An asset without retrieval queries is not graded.
260
-
261
- This was measured before it was built. Of 60 accepted rewrites since July,
262
- judged this way, 14 graded lower (23%, 95% CI 14–35%) and 12 graded higher.
263
- The gate was built because the lower bound cleared the 10% threshold set
264
- before any judging. It costs two judge calls per query on the engine that
265
- already runs the quality judge. On the maintainer's 2026-09-28 nightly run,
266
- whose 30 rewrites had 102 usable queries, that is 204 calls, about 6 more
267
- minutes on a 73-minute run. (`src/commands/improve/retrieval-gate.ts`,
268
- `src/commands/improve/reflect.ts`)
269
-
270
- ### Fixed
271
-
272
- - **A cron row too long for one line is seen by `akm task sync` again.** A
273
- command over 1,000 bytes runs from a wrapper script, and sync could not
274
- read which task such a row ran: every sync, and every `--dry-run`, showed
275
- it as an add and wrote it again. Sync now reads the script, so the row is
276
- unchanged or an update like any other. (`src/tasks/backends/cron.ts`)
277
- - **A `$` or a backslash in a scheduled row's value is kept.** launchd and
278
- Task Scheduler rows passed their values through a string replacement that
279
- read `$'`, `$&` and `$$` as patterns, so a path such as a Windows admin
280
- share (`\\nas\share\akm$`) came out corrupted; reading a crontab row
281
- back dropped a backslash inside a single-quoted value.
282
- (`src/tasks/backends/launchd.ts`, `src/tasks/backends/schtasks.ts`,
283
- `src/tasks/backends/cron.ts`)
284
- - **`akm improve --require-engines` no longer skips a run because the LLM
285
- endpoint is busy.** Its reachability probe, one short completion, gave up
286
- after 3 seconds, so a local server busy with another job looked
287
- unreachable and the whole scheduled run failed (all four scheduled runs on
288
- 2026-09-27). The probe now waits up to the engine's own `timeoutMs`, at
289
- most two minutes, so a busy server can answer while a hung one still fails
290
- fast. The error's hint now says to check the endpoint rather than to run
291
- `akm setup`.
292
- (`src/commands/improve/improve-cli.ts`)
293
-
294
- ## [0.9.17-alpha.6] - 2026-09-27
295
-
296
- Graph extraction stops losing and wasting work. A timed-out extraction is
297
- retried instead of cached as empty. Long documents are extracted once, and
298
- per-file calls respect the run's concurrency. `akm improve` honors
299
- `index.graph`. `akm curate` returns nothing for harness and tool envelopes,
300
- and search and curate show identical content once. Lazy graph extraction,
301
- which never ran under Bun, is removed.
302
-
303
- ### Changed
304
-
305
- - **`akm curate` returns nothing, on purpose, for input that is not a task.**
306
- A harness or tool envelope (input that starts with an XML-style tag and
307
- contains a closing tag, such as `<task-notification>…</task-notification>`,
308
- `<system-reminder>…` or `<cross-session-message …>…`) and the stash README
309
- line each used to get `--limit` unrelated assets. Every caller of
310
- `akm curate` (the CLI, the OpenCode plugin, other harnesses) now gets an
311
- empty `items` list with a `summary` that starts with `Curate abstained` and
312
- names the reason, and a `tip`. On the retrieval suite curate abstains on 57
313
- of 60 recorded non-task inputs and on none of the 221 real queries (nor on
314
- any of 5,725 mined task queries). Length is not a reason to abstain: the
315
- other 3 are task prompts of 2,431–5,531 characters, and in a judged sample
316
- of 30 inputs over 2,000 characters the top 5 held a relevant asset for 24
317
- of them (P@5 0.42, against 0.46 for prompts of 400–2,000 characters).
318
- (`src/commands/read/curate.ts`)
319
- - **Search and curate return identical content once.** Of entries whose
320
- indexed content is identical (the same body saved under another name, as
321
- both a memory and a knowledge doc, or in another bundle), only the
322
- highest-ranked is kept, and the next candidate takes the freed slot. On the
323
- retrieval suite such copies filled 7.5% of curate's top 5. Unique
324
- precision@5, where a copy of a higher-ranked result earns nothing, rises
325
- from 0.467 to 0.514 (+0.046, 95% CI [+0.028, +0.067]), and the share of
326
- top-5 slots that repeat a higher-ranked result falls from 0.131 to 0.055.
327
- Plain P@5 (0.553 → 0.551) and nDCG@10 stay within noise: they counted each
328
- copy as another relevant result. Latency is unchanged.
329
- (`src/indexer/search/db-search.ts`)
330
-
331
- ### Removed
332
-
333
- - **The unused `utility_scores_scoped` index table is gone.** It shipped in
334
- 0.9.17-alpha.5 for per-project scoped utility scores, but no code ever read
335
- or wrote a row. An index database drops it on its next writable open, the
336
- same way other retired derived tables are dropped, with no layout-version
337
- change. (`src/storage/repositories/index-schema.ts`)
338
- - **Lazy graph extraction in `akm show` and `akm curate`.** With
339
- `index.graph.lazyGraphExtraction: true`, `show` extracted an asset's graph
340
- after building its response, so only the next `show` saw it. `curate`
341
- queued assets for a later pass, which drained only the working bundle's
342
- queue, and extractions made this way wrote no cache entry. Under Bun neither
343
- path ever ran: the "already has a graph" check read a missing row as
344
- present. Graph extraction now runs only in `akm improve`. The
345
- `graph_extraction_queue` table is dropped the next time the index is opened
346
- for writing. A config that still sets the key loads, and the key is named
347
- once as unknown. (`src/commands/read/show.ts`,
348
- `src/commands/read/curate.ts`, `src/indexer/graph/graph-extraction.ts`,
349
- `src/storage/repositories/index-schema.ts`)
350
-
351
- ### Fixed
352
-
353
- - **`index.metadataEnhance`'s default is no longer contradicted by dead
354
- code.** Metadata enhancement has always defaulted to off
355
- (`isLlmFeatureEnabled`); a second, unreachable code path in
356
- `isProcessEnabled` claimed the opposite default and had no caller. Removed,
357
- so one default remains. (`src/llm/feature-gate.ts`)
358
- - **Eval tooling and docs catch up to the current config and index shape.**
359
- `scripts/akm-eval/src/curate-bench.ts` wrote the retired `sources` config
360
- key and called a nonexistent `akm index --dir`; it now seeds its sandbox
361
- the same way the other akm-eval scripts and integration tests do, and
362
- drops `--dir`. The graph A/B ablation harness
363
- (`scripts/akm-eval/src/graph-ablation.ts`) planted its "graph off" config
364
- where the sandboxed `akm` never read it, with config keys that didn't gate
365
- anything (one of them a type error); it now writes
366
- `index.graph.enabled: false` to the sandbox's actual `AKM_CONFIG_DIR`.
367
- Updated `scripts/akm-eval/README.md` and `docs/maintainers/eval.md` to
368
- match, and corrected stale `docs/architecture/architecture.md` references
369
- to `db-backup`, `staleness-detect`, and `src/commands/graph/`.
370
- - **Scheduled graph extraction reads `index.graph`.** `akm improve` passed
371
- graph extraction a batch size of 4 and the `memory` and `knowledge` types
372
- whenever the strategy's `processes.graphExtraction` did not set them, so
373
- `index.graph.graphExtractionBatchSize` and `graphExtractionIncludeTypes`
374
- never applied. It did not read `index.graph`'s `engine`, `model`,
375
- `timeoutMs` or `llm` either, so a setting such as
376
- `index.graph.llm.enableThinking: false` had no effect on improve runs. A
377
- value in the strategy's `processes.graphExtraction` still wins. A setting it
378
- leaves unset now comes from `index.graph`, then from the built-in default.
379
- Where `index.graph` asks for something improve did not use before, the
380
- extractor changes and cached extractions stop applying, so those files are
381
- extracted again. (`src/commands/improve/loop-stages.ts`,
382
- `src/commands/improve/execution.ts`,
383
- `src/commands/improve/improve-strategies.ts`)
384
- - **A graph extraction that times out is retried, not cached as empty.** A
385
- call that ran past the engine's `timeoutMs` was recorded as "no entities"
386
- and cached, so the file was never extracted again. It is now recorded as
387
- failed, and the next run retries it; timeouts also count toward the run's
388
- failure-rate abort. A batch that times out fails its files without then
389
- calling the model once per file. An empty response is likewise recorded as
390
- failed. (`src/llm/graph-extract.ts`)
391
- - **Long bodies are extracted once after batching turns itself off.** Two
392
- non-array batch responses turn batching off for the rest of a run. From
393
- then on, a body over 1,600 characters was extracted on its own and then
394
- again with the rest of its batch. Each body is now extracted once.
395
- (`src/llm/graph-extract.ts`)
396
- - **A batch's per-file calls respect the run's concurrency.** When a batch
397
- fell back to one call per file (long bodies, a non-array response, batching
398
- turned off), those calls all went out at once, up to the batch size. Local
399
- endpoints serve one or two requests at a time. The calls now run within the
400
- limit the run applies to its batches, one at a time by default.
401
- (`src/llm/graph-extract.ts`)
402
- - **`related` counts a shared entity once.** `akm show`'s `related` list,
403
- and curate's support refs taken from it, ranked files by the number of
404
- matching entity rows. A file holding two case forms of one entity, as rows
405
- from older extractors can, counted it twice and could outrank a file that
406
- shared two entities. `related` now counts distinct entities. Extraction also
407
- keeps one form of each entity before writing. The stored key `related`
408
- matches on is now the one extraction deduplicates on, which also drops
409
- surrounding quotes and backticks.
410
- (`src/indexer/graph/graph-related.ts`,
411
- `src/indexer/graph/graph-extraction.ts`, `src/indexer/db/graph-db.ts`)
412
- - **A config change that re-extracts the graph says so.** Cached graph
413
- extractions are keyed by extractor: model, batch size, included asset types
414
- and prompt version. Changing any of them made every cached file extract
415
- again without a word. The first run after such a change now warns once,
416
- naming the change and the number of cached files it will extract again, and
417
- records the warning in the run's result.
418
- (`src/indexer/graph/graph-extraction.ts`)
419
- - **Graph extraction reports what its parser filtered.** A run's graph
420
- telemetry, part of `akm improve`'s result, now carries
421
- `filteredGenericEntities`, `filteredInvalidRelations`,
422
- `filteredLowConfidenceRelations` and `contextBatchRetries`. The pass
423
- computed them and dropped them, and did not count batch responses at all.
424
- (`src/indexer/graph/graph-extraction.ts`, `src/llm/graph-extract.ts`)
425
- - **An unknown key under `index.<pass>` is kept and named once.** It was
426
- dropped from the loaded config and named twice. It is now handled like an
427
- unknown key anywhere else in config. (`src/core/config/schema/index-config.ts`)
428
-
429
- ## [0.9.17-alpha.5] - 2026-09-27
430
-
431
- `akm show` works again for a memory that has a `.derived.md` child (835 of them
432
- in one real bundle), and `akm bundle add --provider … --name` holds to the same
433
- `--name` contract as every other add.
434
-
435
- ### Fixed
436
-
437
- - **`akm show` works for a memory that has a `.derived.md` child.** When
438
- `memories/X.md` and `memories/X.derived.md` both existed, `akm show
439
- memories/X`, with or without a `#fragment`, failed with
440
- `RESOURCE_ALREADY_EXISTS` ("multiple physical owners"); `akm curate`
441
- previewed such a memory from its description alone, and `akm curate --pack`
442
- left it out. The index gives the derived child its own ref,
443
- `memories/X.derived`, but the ref lookup also counted `X.derived.md` as a
444
- file for `memories/X`. The lookup now follows the index: `memories/X` is
445
- `X.md` and `memories/X.derived` is `X.derived.md`. A derived child whose
446
- parent file is gone no longer answers for the parent's ref either, so it
447
- cannot hide a real `X.md` in a lower-priority bundle. `akm lint` and
448
- `--xref` / `--supersedes` validation still accept a ref to `memories/X`
449
- when only `X.derived.md` remains. Broken since 0.9.7.
450
- (`src/core/asset/asset-placement.ts`, `src/commands/lint/base-linter.ts`)
451
- - **`akm bundle add --provider … --name` keeps the `--name` contract too.**
452
- Since 0.9.17-alpha.4 an explicit `--name` that is not a legal bundle slug,
453
- or is taken by another bundle, fails with exit 2, and re-adding a source
454
- under a different name points at `akm bundle rename`. A declarative add
455
- (`akm bundle add <target> --provider npm|git|website`) still replaced such
456
- a name with a derived one and exited 0. It now fails the same way, before
457
- any write. (`src/commands/sources/source-manage.ts`)
458
-
459
- ## [0.9.17-alpha.4] - 2026-09-27
460
-
461
- Search and curate are rebuilt on measured evidence. On a 221-query suite of real
462
- akm queries judged for relevance, search nDCG@10 goes from 0.346 to 0.556 and
463
- curate precision@5 from 0.350 to 0.551, with search p50 falling from 787 ms to
464
- about 400 ms and a fresh index shrinking from 560 MB to 340 MB.
465
-
466
- Upgrades stop breaking because the machinery that broke them is gone, not
467
- because more was added: this release removes the scheduler-grant layer, the
468
- filesystem transaction journals, the maintenance barrier and lock mutex, the
469
- strict config schemas and their retired-key registry, and every per-key
470
- config migration, and it lands with fewer lines in `src/` than 0.9.17-alpha.3.
471
-
472
- ### Changed
473
-
474
- - **Search ranks by reciprocal rank fusion of BM25 and document vectors.**
475
- Two candidate lists, 100 each, are fused with equal weights (k = 60): BM25
476
- over whole documents (`entries_fts`) matching any of the query's
477
- non-stopword words (every word when the query has nothing else), and the
478
- document vectors nearest to the query embedding. Equal scores are ordered by
479
- ref, so the ranking depends only on the index and the query's embedding
480
- (keyword-only runs of the suite reproduce exactly). Filters (`--type`,
481
- `--from`, `--filter`, `--belief`, the default session exclusion, proposed
482
- quality) and one-hit-per-file deduplication narrow the fused list without
483
- reordering it. A hit's `score` is its fused score (at most 2/61 ≈ 0.033),
484
- and `--detail full`'s `whyMatched` lists its rank in each list
485
- (`lexical rank 3`, `vector rank 12`). On the retrieval suite (221 real
486
- queries over a 23k-document snapshot, LLM-judged) nDCG@10 rises from 0.347
487
- to 0.566 and P@5 from 0.347 to 0.558, level with the lab reference design;
488
- every query class improves, questions (0.20 → 0.44) and long prompts
489
- (0.24 → 0.52) most. End to end, process start included, search p50/p95
490
- fell from 787/4130 ms to 341/830 ms. BM25 weighs the five columns
491
- equally: the previous 10/5/3/2/1 weights measured 0.011 lower P@5.
492
- (`src/indexer/search/db-search.ts`,
493
- `src/indexer/search/ranking.ts`, `src/indexer/search/fts-query.ts`,
494
- `src/storage/repositories/index-fts-repository.ts`.)
495
- - **Queries are embedded the way the embedding model expects.** An embedding
496
- profile picks query and document templates by model name: Qwen3-Embedding
497
- gets its retrieval instruction on queries, nomic-embed `search_query: ` /
498
- `search_document: `, the BGE English, mxbai and arctic models the
499
- "Represent this sentence for searching relevant passages: " query prefix,
500
- E5 `query: ` / `passage: `, and other models none; `embedding.queryTemplate`
501
- and `embedding.documentTemplate` override the preset (`""` turns it off).
502
- The document template is part of the embedding fingerprint, so a nomic or
503
- E5 index re-embeds on the next `akm index`; Qwen3, BGE and the default local
504
- model keep their vectors. Text reaches the embedder with its case: the query
505
- is no longer lowercased, and an entry's embedded text keeps its case once
506
- the entry is next re-indexed (`akm index --reembed` refreshes every vector
507
- at once). The query embedding is requested before the keyword query runs,
508
- and a search waits for it at most `embedding.queryTimeoutMs` (default 3000)
509
- before serving keyword ranking alone with one warning — a hung endpoint
510
- used to hold a search for up to 120 s. (`src/llm/embedders/profile.ts`,
511
- `src/indexer/materialize-embeddings.ts`.)
512
- - **Curate is one search.** `akm curate` takes the top `--limit` hits of the
513
- fused search in order and enriches each with its preview, run details and
514
- up to two graph-related support refs, so curate's items are search's top
515
- hits (P@5 0.350 → 0.556 on the retrieval suite; p50/p95 1082/4148 ms →
516
- 433/888 ms). The optional reranker (`search.curateRerank`, still off by
517
- default) now reorders the top 30 fused candidates (`topN`, previously 8 but
518
- applied only to the final `limit` items) and sends each as its name,
519
- description and the start of its indexed content (2,000 characters in all)
520
- instead of name and description. (`src/commands/read/curate.ts`.)
521
- - **Vectors are stored once (index layout 25).** Each entry's vector lives
522
- only in `embeddings`, and search scores every current-model row by cosine
523
- similarity in JavaScript. The sqlite-vec mirror `entries_vec` is gone with
524
- its repair pass, readiness flag and width bookkeeping: sqlite-vec cannot
525
- load in the standalone binaries (`bun build --compile` does not bundle the
526
- optional package), under Bun on macOS (the system SQLite refuses
527
- extensions) or wherever the optional dependency is missing, so those
528
- installs always searched the BLOB rows anyway, and a mirror that fell out
529
- of step returned wrong neighbours without an error. The scan now reads
530
- float32 views of the rows instead of copying each into an array: 85 ms per
531
- query for 24k 1,024-dimension vectors in a fresh process, against 460 ms for
532
- the old fallback and 41 ms for sqlite-vec. The first writable open drops
533
- `entries_vec` (100 MB on that index) and the `embeddingDim` and
534
- `vecFastPathReady` meta keys; dropping a vec0 table needs the extension, so
535
- sqlite-vec stays an optional dependency for that alone, and an install
536
- without it leaves the unread table in place. `semanticStatus` is `ready-js`
537
- whenever every entry has a vector (`ready-vec` is gone), the `vecAvailable`
538
- field leaves `akm index` and `akm info` output, setup no longer probes for
539
- sqlite-vec, and `embedding.dimension` loses its 4,096 cap, which only the
540
- vec0 column needed. (`src/storage/repositories/index-vec-repository.ts`,
541
- `src/storage/repositories/index-schema.ts`,
542
- `src/indexer/materialize-embeddings.ts`.)
543
- - **The fragment full-text table is gone (index layout 25).** Nothing has
544
- read `entry_fragments_fts` since fragments stopped competing as search
545
- candidates, so an upsert no longer splits the body into fragment rows and
546
- the first writable open drops the table (39 MB on a 24k-entry index).
547
- `entry_fragments` stays: `akm show <ref>#akm-fragment-…` (#937) resolves the
548
- selector from its stored safe Markdown.
549
- (`src/storage/repositories/index-fts-repository.ts`,
550
- `src/storage/repositories/index-schema.ts`.)
551
- - **The embedding input is derived, not stored (index layout 25).**
552
- `entries.search_text` held a third copy of every body (74 MB on a
553
- 24k-entry index) only to feed the embedder and to notice when an entry's
554
- vector went stale. The embedding pass now derives the text from
555
- `document_json` when it embeds an entry, and `entries.embed_hash` keeps its
556
- SHA-256: an upsert whose hash differs deletes the vector, exactly as a
557
- changed `search_text` did. The first writable open hashes each stored
558
- `search_text` before dropping the column, so every vector stays attached
559
- until its entry's text really changes, and nothing is re-embedded by the
560
- upgrade. (`src/storage/repositories/index-entries-repository.ts`,
561
- `src/storage/repositories/index-vec-repository.ts`,
562
- `src/storage/repositories/index-schema.ts`.)
563
- - **`akm index` reclaims index.db's free pages.** Nothing ever VACUUMed
564
- `index.db`, so every table an upgrade rebuilt or dropped stayed on disk as
565
- free pages: a 24k-entry index measured 979 MB, 427 MB of it free, against
566
- 560 MB for a fresh build of the same content. The run now ends with a
567
- VACUUM when the writable open migrated the layout (it leaves
568
- `index_meta.vacuumPending` for the next `akm index`, since the open may sit
569
- inside a caller's transaction) and whenever more than half the pages are
570
- free, the threshold and pass improve already apply to `state.db`
571
- (`vacuumIfReclaimable`, formerly `vacuumStateDbIfReclaimable`). A busy
572
- database skips the VACUUM instead of failing the run; each VACUUM prints
573
- its page counts and appends an `index_db_vacuumed` event. With the three
574
- layout-25 removals above, a fresh build of the 24k-entry retrieval snapshot
575
- is 340 MB instead of 560 MB, and a copy of the 601 MB layout-24 build
576
- migrates in 0.8 s with all 23,979 vectors kept byte for byte, then VACUUMs
577
- to 377 MB. Retrieval is unchanged on the suite (search nDCG@10 0.5562 →
578
- 0.5560, Δ −0.0002 [−0.0014, +0.0009]; P@5 0.5507 → 0.5517; curate P@5
579
- identical), and search p50/p95 moved from 374/912 ms to 401/870 ms.
580
- (`src/indexer/indexer.ts`, `src/storage/state-db-integrity.ts`.)
581
- - **An index a newer akm wrote is refused, naming the upgrade.** Readers
582
- used to serve a newer layout "as far as they could" and the writable open
583
- continued at its own layout, setting the marker back so the two releases
584
- alternated. A newer layout can lack a column an older reader selects —
585
- layout 25 drops `entries.search_text` — so every opener now refuses it with
586
- `INDEX_SCHEMA_INCOMPATIBLE` ("Upgrade akm to use this index.") and leaves
587
- the file untouched; an older layout is still served as-is and migrated by
588
- the next writable open, and `akm improve --dry-run` reports the refusal as
589
- an incompatible snapshot. (`src/storage/repositories/index-connection.ts`,
590
- `src/storage/repositories/index-schema.ts`.)
591
- Downgrading to 0.9.17-alpha.3 or earlier is not supported for the index:
592
- those releases select columns layout 25 dropped, so rebuild it with
593
- `akm index --full` under the older release.
594
- - **Scheduled rows no longer freeze the syncing shell's directories or PATH.**
595
- A `--scheduler-context` descriptor now carries the resolved bundle path
596
- (sync's ownership signal, #846) plus only the `AKM_CONFIG_DIR`,
597
- `AKM_DATA_DIR`, `AKM_CACHE_DIR` and `AKM_STATE_DIR` values the process that
598
- ran `task sync` had set explicitly; resolved defaults are left to resolve at
599
- fire time, exactly as they do for an interactive command. It used to capture
600
- every resolved directory and the whole PATH: one host's `task sync`, run from
601
- inside a desktop app whose environment pointed `$STATE` at the app's own
602
- config directory, froze that directory into eight cron rows on 2026-08-06,
603
- every later sync preserved it, and the nightly improve run then held its
604
- locks where no interactive command could see them. PATH moves into the
605
- native artifact, where it is visible and editable: a `PATH=` line inside a
606
- `# akm:env BEGIN`/`END` section written directly above the first akm task
607
- block (cron applies it to the rows that follow it; akm rewrites the line on
608
- every crontab write and removes it with the last task block), and an
609
- `EnvironmentVariables` entry in each launchd plist. Task Scheduler runs a
610
- task with the account's own environment and carries no PATH. Plain
611
- `akm task sync` recomputes the descriptor on every run — an installed row
612
- whose descriptor no longer matches is updated while its launcher is kept as
613
- before (only `--rebind` moves that) — so one sync after upgrading rewrites
614
- every row written under the old policy. Descriptors an older release wrote
615
- still load, PATH included, until that sync. (`src/tasks/scheduler-invocation.ts`,
616
- `src/tasks/backends/cron.ts`, `src/tasks/backends/launchd.ts`,
617
- `src/tasks/scheduler-sync.ts`.)
618
- - **The write path keeps only what its callers use** (`src/core/write-source.ts`,
619
- 1,485 → 602 lines). The git transaction chain — publication identity capture,
620
- path, worktree and commit snapshot validation, base-HEAD assertions,
621
- transaction-commit discovery, a per-repo pending-mutation registry and a
622
- plan/begin/publish API — lost its last callers when the proposal
623
- transaction journals were removed and survived only because one test
624
- imported it; its 15 exports and their private helpers are gone. A write to a
625
- git-backed bundle now writes the file atomically inside the bundle root,
626
- records the exact path, and the boundary commits exactly those paths and
627
- pushes with `--force-with-lease`. A dirty or gitignored destination is no
628
- longer refused: an ignored path stays local with a warning instead of the
629
- command throwing after the file had already landed, and an upstream that
630
- cannot be inspected during preparation warns instead of aborting. Path
631
- containment, the symlink-escape refusal and the detached-HEAD refusal stay.
632
- - **Readers tolerate everything older releases wrote.** No config object is
633
- strict any more: a key this release does not know — retired, misspelled,
634
- or written by a newer release — is kept in memory and named once
635
- (`unknownConfigKeyPaths`, `src/core/config/config.ts`, found by walking
636
- the schema), round-trips through ordinary writes so a newer release's
637
- settings survive a downgrade, and is dropped only by `akm migrate apply`.
638
- The retired-keys registry, its read shim, and the schema-compatibility
639
- lint are removed; nothing needs registering for a key to be tolerated.
640
- - **`akm migrate apply` has one config step.** `configFile`
641
- (`normalizeConfigFile`) reads config.json through the same pipeline every
642
- load runs (`configVersion` read, legacy source shape, `extraParams` lift)
643
- and writes the current shape back under a backup, dropping unknown keys.
644
- It replaces the per-key `configLegacySourceShape`, `configExtraParams`,
645
- `configRetiredKeys` and `configSchedulerSourceIds` steps, the
646
- `schedulerActivation` and `staleTxns` steps, and the `--host-local` mode. A
647
- pending config lift is now `ready`, never a blocker for the other steps.
648
- The `deadResidue` step also removes the transaction-journal,
649
- maintenance-barrier, lock-mutex and version-stamp files older releases left
650
- under `$DATA`, `$STATE` and `$CONFIG`, and runs whether or not a bundle is
651
- configured.
652
- - **Scheduling is one list.** `scheduler.enabled` holds the fully-qualified
653
- refs this host schedules (`bundle//tasks/x`). It is still written in the
654
- `{kind, ref, sourceId}` shape 0.9.16 reads, so that release keeps working
655
- against a config this one wrote; either shape is read as the ref.
656
- A config with no list at all (every release before 0.9.17) means "keep
657
- what is installed": the first `akm task sync` (or `setup`, `task enable`,
658
- `task disable`, `task add`) takes the akm-written rows already in the
659
- native scheduler as the host's choice, writes the list, and says so;
660
- `task sync --dry-run` reports it without writing. An explicit list, empty
661
- or not, is never second-guessed. Grants, source identities, the
662
- carry-forward, the scheduler-activation and source-id migrations and the
663
- fire-time re-check are gone (`src/tasks/activation-config.ts`).
664
- - **Proposal accept and revert write directly.** The asset file is written
665
- (temp file + rename), committed through the ordinary write-target
666
- boundary, then the proposal row and its event are recorded in one
667
- state.db transaction and the file is indexed best-effort. A crash in
668
- between leaves a re-acceptable pending proposal, nothing corrupt. The
669
- filesystem transaction journals (`src/core/fs-txn.ts`) with their
670
- recovery, quarantine, deferral and fencing are removed, along with the
671
- `txn-quarantine`/`txn-awaiting-recovery` health advisories.
672
- - **`akm bundle update` publishes, records the lock entry, then reindexes —
673
- with no rollback transaction.** An update still fetches into a staging
674
- directory beside the cache and audits the staged bytes for dangerous env
675
- keys before anything goes live; a blocked or failed audit changes nothing.
676
- It then publishes with one rename (a fast-forward for a writable Git
677
- checkout), writes the lock entry, and reindexes. If the reindex fails, the
678
- new content and lock entry stay for the next `akm index`, and the previous
679
- install directory is kept. The config, staged-content, lockfile-byte and
680
- checkout-HEAD fences and the lockfile compare-and-swap restore are gone, so
681
- an update no longer fails with "changed concurrently" or "changed after its
682
- staged bytes were audited": it already runs under the asset-mutation lease,
683
- and Git refuses a fast-forward that would overwrite local work. A website
684
- source refreshes through its mirror's own snapshot staging, so a killed
685
- refresh still keeps the previous mirror.
686
- - **A lock file is one `O_EXCL` create** (`src/core/file-lock.ts`). The
687
- SQLite lock-operation mutex, the maintenance barrier (a lock guarding lock
688
- registration) and its per-open activity registry — the source of the
689
- lock-sidecar leak that grew `$STATE` by hundreds of megabytes — are
690
- removed. `MAINTENANCE_BARRIER_BUSY` no longer exists; contention is
691
- reported as `INDEX_DB_CONTENDED`, `STATE_DB_CONTENDED` or
692
- `IMPROVE_LOCK_HELD`, as before.
693
- - **Removed from 0.9.17-alpha:** startup version reconciliation
694
- (`version-reconcile.json`), the akm-install enumerator and `akm upgrade
695
- --version`/`--tag` with its other-install mover, the `version-reconcile`,
696
- `scheduler-grants`, `scheduled-startup-failures` and `akm-installs` health
697
- advisories, and the `akm info` `compat` manifest with its
698
- `PLUGIN_PROTOCOL_VERSION`. `akm upgrade` is what it was in 0.9.16.
699
- - **Documented the persisted-data compatibility contract.** Added
700
- `docs/architecture/persisted-data-compat.md`: the four-sentence contract a
701
- reader owes data an earlier release wrote, plus a per-format table (config,
702
- `state.db`, `index.db`, task source, workflow IR, native scheduler rows,
703
- proposal and task-history metadata, lock payloads, `.akm` residue) naming
704
- where each is written, its version marker, its older/newer-data behavior,
705
- and which gate covers it — with explicit `Gap:` notes where the code does
706
- not meet the contract yet. Registered in `docs/architecture/README.md`.
707
- `AGENTS.md`'s "Reading persisted data" section now points at this doc
708
- instead of a deleted file.
709
-
710
- - **Scheduler writes hold one lock and apply row by row.** `akm task sync`,
711
- `add`, `enable`, `disable` and `prune --yes` hold one `O_EXCL` lock,
712
- `$STATE/locks/scheduler.lock` (`src/tasks/scheduler-lock.ts`), for the
713
- whole read–plan–write; a second scheduler command exits 75
714
- (`SCHEDULER_LOCK_HELD`), and a lock left by a dead process is reclaimed.
715
- Under it, sync reads the installed rows once and diffs by native id: a
716
- missing row is installed, a changed row rewritten, a row whose source is
717
- gone or no longer enabled removed. A row that fails to install or remove,
718
- or two sources claiming one native id, is reported in `failures` (exit 1)
719
- while every other row applies; the per-row compare-and-swap expectations
720
- and whole-set rollback are gone. A task whose source stops parsing keeps
721
- its installed row instead of being unscheduled by a YAML typo.
722
- (`src/tasks/scheduler-sync.ts`, `src/commands/tasks/tasks.ts`)
723
- - **`akm task add` is "write, enable, sync".** It validates the task and
724
- refuses an id already scheduled from another bundle before writing
725
- anything, then writes the source, adds the ref to `scheduler.enabled`
726
- (unless `--disabled`) and syncs the bundle. When the row cannot be
727
- installed, add fails naming the cause and the task stays written and
728
- enabled for the next `akm task sync` to retry; it no longer restores the
729
- prior source and rows byte-for-byte. `--force` with fewer schedules removes
730
- the dropped schedules' rows through the same sync, and `--rebind` means
731
- what it means for `task sync`.
732
- - **Improve records what it tried in one ledger** (`improve_ledger`,
733
- `src/storage/repositories/improve-ledger-repository.ts`). One row per
734
- stash, ref and stage holds the last attempt, its outcome and when the ref
735
- is next eligible, from one cadence table:
736
-
737
- | Outcome | Next eligible | Lifted early by newer feedback? |
738
- | --- | --- | --- |
739
- | rejected, quality_rejected | 14 d reflect, 30 d distill, 7 d other stages | no |
740
- | expired | 1 d | no |
741
- | proposed, review_needed, unchanged, judged_no_action | 7 d | yes |
742
- | accepted, failed | immediately | — |
743
-
744
- Every stage reads it before any LLM call. It replaces proposal
745
- fingerprints, the per-stage cooldowns, the distill reject files and the
746
- event-timestamp cursors, which disagreed with one another (quality
747
- rejections never reached the fingerprints; consolidate re-judged promoted
748
- memories). Distill and consolidate now key by their input refs, so each
749
- such input may be attempted once more after upgrading. Schema repair paces
750
- itself with the ledger too, replacing its private 7-day cooldown and
751
- 3-attempts-per-30-days cap. Every stage — reflect, distill, consolidate,
752
- extract, triage, memory inference, graph extraction — runs through one
753
- shared path (`src/commands/improve/stage.ts`): pick the runner, call the
754
- model, judge the output, mint the proposal, record the usage.
755
- - **`akm proposal drain` has one rule.** A proposal the quality judge passed
756
- (a `staged` gate decision whose content hash still matches) is accepted, an
757
- empty diff is rejected, and everything else goes to the judgment tier
758
- (`processes.triage.judgment`) or waits for review. Extract and consolidate
759
- proposals, which the `personal-stash` policy auto-accepted on size alone,
760
- carry no judge stamp, so they now go to the judgment tier — or wait for
761
- review when none is configured — instead of being accepted. The policies
762
- and their flags are retired (see Removed). `--dry-run` now predicts what a
763
- real drain does: a proposal whose target already holds its content (an
764
- accept that wrote the file but was interrupted before recording it) is
765
- reported as promoted, as the real drain finishes it, instead of as a
766
- stale-target rejection.
767
- - **State migration `028-improve-ledger` creates the ledger and drops six
768
- tables.** It backfills the ledger from each ref's latest proposal and drops
769
- `proposal_fingerprints`, `improve_gate_thresholds`, `proposal_fs_imports`,
770
- `consolidation_judged`, `improve_cycle_metrics` and `canary_queries`.
771
- Because it drops schema, the first open after upgrading copies the
772
- database to `state.db.pre-028-improve-ledger.bak` before it runs.
773
- - **Upgrading no longer rebuilds or re-embeds the search index (index layout
774
- 24).** The first writable open applies a layout change in place — added
775
- columns, and a one-time rebuild of the two full-text tables from the stored
776
- entries (about 2–3 s for 24k entries); embeddings, utility scores, the
777
- enrichment cache and the graph are never dropped, and only a corrupt file
778
- is rebuilt from scratch (#865). Both FTS5 tables are contentless, so
779
- indexed text is stored once (153 MB of a 980 MB index on a 23.9k-entry
780
- stash; a SQLite older than 3.43 keeps the previous layout). Each vector
781
- records its model (`embeddings.model`): a model change re-embeds only the
782
- entries missing a vector for the configured model, per batch and
783
- resumably, replacing the purge, the #955 re-embed canary and
784
- `embedding_salvage`. `akm index --full` keeps unchanged entries' vectors,
785
- and a one-file change in a large directory re-persists only that file.
786
- Readers serve an older layout as-is and say so once on stderr. An akm
787
- older than this release refuses a layout-24 index and asks to be upgraded.
788
- - **`--verbose` embedding output lists each document's size without a
789
- predicted batch number.** The per-batch lines already report every
790
- provider request's document and token counts, and skipped documents are
791
- listed at the end of the pass.
792
- - **Workflow runs are never refused for their plan's version or hash.**
793
- Markdown and the GitHub-shaped YAML subset compile straight to one plan
794
- type, and new runs record plan `irVersion` 6. A stored plan that decodes
795
- runs whatever release froze it — irVersion 4 and 5 plans are read
796
- tolerantly, and a key this release does not know is ignored instead of
797
- abandoning the run; one that does not decode is marked abandoned and `akm
798
- workflow run <ref>` starts afresh; only a plan a newer akm froze is
799
- refused, with "Upgrade akm" (`WORKFLOW_IR_VERSION_UNSUPPORTED` is gone).
800
- One driver per run is a lock file,
801
- `<data dir>/workflow-run-locks/<run id>.lock`: a second `akm workflow run`
802
- exits 75 (`RUN_LEASE_HELD`) naming the holder's pid, and a dead pid's lock
803
- is reclaimed at once — the database run lease, its heartbeat and the
804
- check-ins are gone. Resume reuses every completed unit whatever its
805
- recorded input hash, and warns once when the workflow file's sha256
806
- differs from the one recorded at freeze, then continues on the frozen
807
- plan. Executable identity (realpath, inode and hash captured at freeze,
808
- checked at dispatch) is gone, so upgrading `claude` mid-run no longer
809
- strands a run.
810
- - **Every execution goes through three plain functions:** `resolveExecution`
811
- → `buildExecution` → `runExecution` (`src/integrations/agent/execution.ts`,
812
- `runner-dispatch.ts`), replacing a 12-hop pipeline across 14 modules — the
813
- cascade planner, authorized-plan and provenance checks, lowerer registry
814
- and dispatch lease. Two behaviour changes: credentials are read at each
815
- dispatch, so a key rotated mid-run is used on the next call instead of a
816
- snapshot taken at the start; and an explicit `engine: null` in a task,
817
- workflow or command layer means "no preference here" and falls through to
818
- `defaults.engine` instead of forcing the `opencode-sdk` fallback.
819
- - **`state.db` opens on one connection.** The open creates the parent
820
- directory, opens the file, applies the pragmas, reads the migration ledger
821
- and runs every pending migration in one `BEGIN IMMEDIATE`; the read-only
822
- preflight connection, the `/proc/self/fd` alias and the refusal of an empty
823
- "unversioned" file are gone. Before a migration that drops schema runs on
824
- an existing database, it is copied to `state.db.pre-<id>.bak`. Since any
825
- open applies pending migrations, `akm health`'s `state-db-migrations` check
826
- now reports what its own open applied (`evidence.applied`,
827
- `evidence.backupPath`) and fails only when a migration could not be
828
- applied.
829
- - **`akm health` drops checks nothing acted on.** Removed: the
830
- `task-log-backing` hard check, the `pool-saturation` advisory, the six
81
+ - **Search and curate are rebuilt on measured evidence.** Search now ranks by
82
+ reciprocal rank fusion (RRF) of two 100-candidate lists — whole-document
83
+ BM25 and nearest document vectors — replacing the previous require-every-
84
+ word keyword ladder plus a dozen additional ranking-signal boosts (exact-
85
+ name, type, belief-state, tag, graph, utility, and more): on a 221-query
86
+ LLM-judged retrieval suite, plain whole-document BM25 alone beat that whole
87
+ boosted pipeline by 0.156 nDCG@10. A hit's `score` is now its fused RRF
88
+ value (at most 2/61 ≈ 0.033, not comparable to an old score), keyword
89
+ matching no longer does prefix matching (a search for `dock` no longer
90
+ matches `docker`), and a slow embedding endpoint falls back to keyword-only
91
+ ranking after `embedding.queryTimeoutMs` (default 3000 ms) instead of
92
+ blocking the search. `akm curate` is now the top hits of that same fused
93
+ search, enriched with a preview and up to two support refs, rather than its
94
+ own layer of second-guessing fallback searches and nudges. Query embedding
95
+ now uses the template a model actually expects (Qwen3's retrieval
96
+ instruction, `search_query:`/`search_document:` for nomic,
97
+ `query:`/`passage:` for E5, and others — overridable with
98
+ `embedding.queryTemplate`/`documentTemplate`) instead of one generic
99
+ prefix; a nomic-embed or E5 configuration re-embeds every entry on the next
100
+ `akm index` because the new document template changes what gets embedded
101
+ (set `embedding.documentTemplate: ""` to keep the old vectors instead).
102
+ Measured on the suite: search nDCG@10 rises from 0.346 to 0.556 and curate
103
+ precision@5 from 0.350 to 0.551; search p50 latency falls from about 787 ms
104
+ to about 400 ms, and a freshly built index shrinks from 560 MB to 340 MB.
105
+ Content indexed under two names (a memory also promoted verbatim to
106
+ knowledge, say) now returns only the higher-ranked copy instead of both,
107
+ and `akm curate` returns an empty, explained result for input that is not
108
+ a real query (a harness/tool envelope, a bare stash README) instead of
109
+ `--limit` unrelated items.
110
+ - **Index layout 26.** Vectors are stored once — the sqlite-vec mirror, the
111
+ fragment full-text table, and the stored embedding-input text are dropped
112
+ in favor of a derived hash — and every relation `akm index` already parses
113
+ is stored as a typed link (see Added). An index an older 0.9.17 prerelease
114
+ or 0.9.16 wrote migrates to layout 26 in place on the first writable open —
115
+ a 0.9.16 index also gets a one-time full-text rebuild along the way, a few
116
+ seconds for a 24k-entry index — and the run ends with a VACUUM that
117
+ reclaims the space the migration frees (in one measurement, a 601 MB index
118
+ built one layer short of layout 26 dropped to 377 MB; actual savings vary
119
+ with how much of an existing index was already free space). An index this
120
+ release cannot read (written by a newer akm) is refused, naming the
121
+ upgrade, instead of being silently reinterpreted.
122
+ - **A scheduled task is just a command and a schedule.** Each native
123
+ crontab/launchd/Task Scheduler row now carries its own `AKM_BUNDLE_DIR`
124
+ (plus any `AKM_CONFIG_DIR`/`AKM_DATA_DIR`/`AKM_CACHE_DIR`/`AKM_STATE_DIR`
125
+ the syncing shell set explicitly) inline, instead of pointing at a
126
+ `--scheduler-context <file>` descriptor; a crontab now carries its PATH the
127
+ same way, in a `# akm:env` block akm writes and rewrites next to its task
128
+ rows, instead of inside each row's own descriptor file. The first `akm task
129
+ sync` after upgrading rewrites every akm-managed row once, in place — same
130
+ launcher, same schedule, nothing added or removed — and once `akm task
131
+ doctor` lists no binding still pointing at a descriptor, the old
132
+ `$DATA/tasks/context/` files it leaves behind can be deleted. Rows written
133
+ by 0.9.0 through 0.9.16 keep firing until that sync.
134
+ - **`akm improve` reworks only what retrieval actually returned, or what's
135
+ new.** The proactive-maintenance and high-salience lanes, and the memory
136
+ consolidation judge, are now scoped to assets a real `search`, `curate`,
137
+ `show`, or `feedback` touched in the last 90 days, plus material no
138
+ improve stage has processed yet — not the whole stash.
139
+ Measured on a 19,870-asset copy of the maintainer's bundle: the fallback
140
+ lanes' candidate pool drops from 15,686 to 6,450 assets, and in July the
141
+ proactive lane had rewritten 3,069 assets, 3,059 of which had never been
142
+ retrieved since usage logging began. Left-out assets are reported under a
143
+ new `retrieval` gate (`akm improve --dry-run`, and health's `not_retrieved`
144
+ skip reason).
145
+ - **Reflect refuses a rewrite that grades worse on the asset's own searches
146
+ (#722).** Before proposing a rewrite of existing content, reflect grades
147
+ the old and new versions on up to five queries that actually retrieved the
148
+ asset, using the same relevance judge the retrieval eval uses (kappa 0.83
149
+ against human grades); a rewrite that grades lower on average is refused as
150
+ `quality_rejected`. Of 60 accepted rewrites reviewed this way after the
151
+ fact, 23% [14-35%] graded lower than the content they replaced.
152
+ - **Config is fully tolerant of what an older or newer release wrote.** No
153
+ config object is strict any more: an unknown key at any depth — retired,
154
+ misspelled, or written by a newer release — is kept in memory, named once,
155
+ round-trips through ordinary writes, and is dropped only by `akm migrate
156
+ apply`. `akm migrate apply` itself collapses to one config step that reads
157
+ `config.json` through the normal load pipeline and writes the current
158
+ shape back under a backup.
159
+ - **Locking and writes are simpler.** A lock file is now one `O_EXCL`
160
+ create. A write to a git-backed bundle writes the file directly and
161
+ commits exactly that path. `akm proposal accept`/`revert` write the asset
162
+ file, then record the proposal and its event in one `state.db`
163
+ transaction, so a crash in between leaves a re-acceptable pending proposal
164
+ rather than something corrupt.
165
+ - **Scheduling is one list.** `scheduler.enabled` in `config.json` holds the
166
+ fully-qualified refs a host schedules; it is still written and read in the
167
+ `{kind, ref, sourceId}` shape 0.9.16 used. A config with no list at all —
168
+ 0.9.15 and earlier — is read on the first sync after upgrading as "keep
169
+ what's already installed," and the list is written from there.
170
+ - **A frozen workflow plan carries `irVersion` 6.** Every workflow — Markdown
171
+ and the GitHub-shaped YAML subset alike — compiles to the one plan type
172
+ that was previously irVersion 4/5's target; a stored irVersion 4 or 5 plan
173
+ is still read and run tolerantly (an unrecognized key in it is ignored
174
+ instead of abandoning the run), and only a plan a *newer* akm froze is
175
+ refused. An explicit `engine: null` on a task, workflow, or command layer
176
+ now means "no preference here" and falls through to `defaults.engine`,
177
+ instead of forcing the `opencode-sdk` fallback.
178
+ - **The quality judge that gates reflect and distill scores each criterion
179
+ separately** instead of one blended float, no longer scores an
180
+ ACTIONABILITY criterion that measured no better than chance (AUC 0.46), and
181
+ now runs at a pinned temperature of 0 — at the previous effective default
182
+ of 0.3, 10 of 16 identical inputs had flipped verdict. Consolidate's own
183
+ prompt and schema now ask only for `promote`; `merge`/`delete`/`contradict`
184
+ were advisory-only and had not actually executed since July regardless.
185
+ - **A pasted credential in a search or curate query is redacted before it's
186
+ stored.** A password, bearer token, JWT, `ghp_…` token, PEM key, and
187
+ similar patterns are replaced with `[REDACTED]` in `state.db`'s usage and
188
+ event logs; a scan of mined queries had found 22 stored verbatim. Existing
189
+ rows are not rewritten.
190
+ - **One bad item no longer aborts a whole `akm task sync` or `akm migrate`
191
+ run.** A binding or bundle that fails to reconcile, or a migration step
192
+ that throws, is now reported individually (`failures`/`failedSteps`) while
193
+ every other task, bundle, or step still completes.
194
+ - **A one-file change in a large directory no longer costs `akm index` tens
195
+ of minutes.** Both full-text tables' per-entry deletes were unindexed table
196
+ scans; on a 23.9k-entry index, one touched file in a 13.7k-entry directory
197
+ took 26-31 minutes before this release and well under a minute after.
198
+ - **Output shapes changed along with the features above.** A search hit
199
+ drops `selectedRef`, `parentRef`, `fragmentOrdinal`, `fragmentCount`, its
200
+ fragment line/size fields, `matchStage`, and `graph` (fragments no longer
201
+ compete as search candidates; `akm show <ref>#<fragment>` still resolves a
202
+ section). `akm index`/`akm info` drop `vecAvailable`, and
203
+ `semanticStatus` no longer reports `ready-vec`. `akm migrate status`'s
204
+ separate `taskV3Migration`/`taskV4Migration` sections are now one
205
+ `taskFiles` section. `akm workflow plan` drops its `sourceReadSet` block.
206
+ - **`akm health` gets a new hard `state-db-integrity` check** (a read-only
207
+ SQLite `PRAGMA quick_check` against `state.db`, plus a freelist-ratio
208
+ warning above 50%), and drops eight checks nothing acted on: the
209
+ `task-log-backing` hard check, the `pool-saturation` advisory, and six
831
210
  research advisories (`outcome-proxy-adequacy`, `outcome-proxy-dead`,
832
211
  `salience-uniformity-collapse`, `enrichment-lane-minting`,
833
- `improve-churn-ratio`, `collapse-churn-detector`) and the report's
834
- coverage, degradation and minting rollups. The HTML report's embedded
835
- `RUNS` data drops 11 per-run counters no chart or table read (scope mode,
836
- consolidation `processed`/`failedChunks`/`totalChunks`, memory-inference
837
- `considered`/`yieldRate`, graph-extraction `failures`, distill
838
- `skipped`/`queued`/`llmFailed`, `orphansPurged`); `--group-by run` and
839
- `--format md` are unchanged.
840
- - **`configVersion` is read, never gated on.** A missing field or `"0.9.0"`
841
- loads silently; any other value is named once and read as `0.9.0`.
842
- `UNSUPPORTED_CONFIG_VERSION` and `src/core/config/config-version-shim.ts`
843
- are gone.
844
- - **`akm migrate` converts a task file in one step, whatever its version.**
845
- One planner (`scripts/akm-migrate/migrate/task-files.ts`) takes a v2, v3 or
846
- v4 file still carrying `schedule[].enabled` to v4 in one pass, with one
847
- backup directory per run (`$DATA/backups/tasks/<ts>-<uuid>`); `akm migrate
848
- status` reports one `taskFiles` section instead of
849
- `taskV3Migration`/`taskV4Migration`. The per-generation steps, their
850
- convergence checks and backup pruning, and the writer-relocation step are
851
- gone.
852
- - **Registry requests use plain `fetch()`.** DNS pinning — a Node child
853
- process per request that resolved each registry host, rejected private
854
- addresses and pinned the connection — is removed: a registry URL is the
855
- built-in one or one an operator configured. `src/registry/network.ts`
856
- retries network failures, timeouts, 429 and 5xx with backoff, caps the
857
- body, and reports every failure as a classified error, never exit 70:
858
- `REGISTRY_NOT_FOUND` and `REGISTRY_RESPONSE_INVALID` exit 1,
859
- `REGISTRY_UNREACHABLE` exits 75, `REGISTRY_URL_INVALID` exits 78. A static
860
- index whose `version` is not 2 or 3 is read with one warning instead of
861
- refused.
862
-
863
- ### Added
864
-
865
- - **Upgrade rehearsal gate** (`tests/integration/upgrade-rehearsal/`,
866
- `AKM_UPGRADE_REHEARSAL=1`): installs the previous published `akm-cli`
867
- release as a real global npm package, drives it to build a realistic home
868
- (a filesystem, git, website, and npm bundle; scheduled and manual tasks; a
869
- synced fake crontab), then installs the candidate build OVER it in place —
870
- the same prefix a real `npm i -g`/`bun add -g` upgrade replaces — and runs
871
- the candidate against that home — `migrate status`/`apply`, `bundle list`
872
- with every bundle confirmed enabled, `search`, `show`, plain `task sync`
873
- (dry-run and real, no `--rebind`, as an upgrading user actually runs it),
874
- executing the generated cron command and confirming it ran the candidate,
875
- `health`, `improve --plan` — and finally installs a separate untouched copy
876
- of the previous release and runs it back against the candidate-written
877
- home. Wired into CI (`.github/workflows/ci.yml`'s new `upgrade-rehearsal`
878
- job) and `tests/release-check.sh` (right after packing the release
879
- candidate). `.github/workflows/ci.yml` also now runs on pushes to
880
- `release/*` branches, which previously had no CI coverage at all.
881
- It also proves the fix for the defect above (Fixed, below) two
882
- ways: a new first assertion in the "previous"-origin suite runs
883
- scheduled-a's generated cron command BEFORE any `migrate` call and
884
- confirms `akm-migrate status --host-local` then reports `current` with no
885
- manual step in between; and a second, dedicated origin,
886
- `KNOWN_UPGRADE_ORIGINS`' fixed `"0.9.15"` (the last release before
887
- source-bound scheduler grants), builds a minimal home whose crontab row
888
- carries no host-local grant at all — the exact 2026-09-24 shape — and
889
- confirms the candidate carries the grant forward and a plain `task sync`
890
- afterward does not remove it.
891
- - **`akm bundle rename <old> <new>`.** Renaming a bundle used to mean
892
- hand-editing the `bundles` key in `config.json`, which stranded every
893
- durable ref the tool had minted under the old id — the index and state
894
- databases kept the old `<old>//` prefix while config named the new one
895
- (the exact hand-rename signature `warnOnBundleRenameDrift` already
896
- detected and warned about, with "there is no rekey command in 0.9.0").
897
- `akm bundle rename` is that command: under the config lock it rewrites the
898
- `bundles` key, `defaultBundle`/`defaultWriteTarget` when they name the old
899
- id, and every `scheduler.enabled[].ref` with the old `//` prefix; then it
900
- renames the lockfile entry, re-keys every indexed entry's
901
- `bundle_id`/`item_ref` and the metadata-enrichment LLM cache's
902
- `asset_ref` (in the same `index.db` write, so a rename can't land between
903
- the two and strand the cache — the next `akm index` would otherwise treat
904
- every renamed asset as stale and re-enrich it through the LLM from
905
- scratch), and rewrites this tool's own state rows that name the old bundle
906
- (`proposals.ref`, a pending proposal's `proposedTarget.source`, and
907
- workflow `task_history.target_ref`). It then re-syncs native scheduler
908
- rows under the new name (`akmTasksSync`, run from the command handler and
909
- reported in the result's `taskSync` field, never thrown, since
910
- config/index/state are already renamed by then), so a scheduled task or
911
- workflow stops invoking `<old>//…` the moment the rename applies instead of
912
- waiting on a manual `akm task sync`. `taskSync.ok` is `false` both when
913
- the sync call itself fails and when it comes back with one or more
914
- `taskSync.result.failures` — a binding that failed to prepare has already
915
- lost its old native row and is not scheduled again until a retry, so
916
- `akm bundle rename` never reports a partial re-sync as a clean one. Refs
917
- inside the bundle's own CONTENT
918
- (cross-references, a task's `uses:`, `supersededBy`) are reported, never
919
- rewritten — the result's `contentRefs` lists the indexed files that still
920
- spell the old prefix. `--dry-run` shows the full plan (row counts,
921
- scheduler refs, content files, and the installed native scheduler rows a
922
- real run's sync would replace) without writing anything.
923
-
924
- ### Removed
925
-
926
- - **Every ranking signal besides the two fused lists.** Search no longer
927
- applies exact-name tiers, type, belief-state, tag, search-hint, alias,
928
- description, metadata, graph, capture-mode, lesson-strength, pinned-fact or
929
- project-context boosts, the utility multiplier, the relaxed-query score
930
- ceiling, or the cosine floor on vector-only hits, and it no longer loads
931
- the graph snapshot. On the retrieval suite plain whole-document BM25 alone
932
- beat the boosted pipeline by 0.156 nDCG@10, and applying the belief-state
933
- weights to the fused score lowered nDCG@10 by 0.010 [−0.020, −0.001], so
934
- `--belief current` is the way to leave out contradicted or superseded
935
- entries. Usage events and utility scores are still recorded (improve's
936
- salience and graph extraction read them), and the graph still backs
937
- `akm show`'s `related` list and curate's support refs.
938
- - **The require-every-word keyword ladder and prefix matching.** The strict
939
- AND query, its prefix-AND retry and the OR recovery behind them are gone
940
- (OR matching measured 0.108 nDCG@10 better), so a word fragment such as
941
- `dock` no longer matches `docker`.
942
- - **Fragment hits in search.** Markdown fragments no longer compete as search
943
- candidates (whole documents measured 0.059 nDCG@10 better), so search
944
- returns whole-document refs and its hits drop `selectedRef`, `parentRef`,
945
- `fragmentOrdinal`, `fragmentCount`, the fragment line and size fields and
946
- `matchStage`; `akm show <ref>#<fragment>` still selects a section.
947
- - **Curate's second-guessing of search:** the per-keyword fallback searches
948
- and their max-score merge, the intent and type nudges, skill-family
949
- collapse (and the family support refs it produced), and the close-score
950
- comparator.
951
- - **Retired options.** `akm search --no-project-context` now fails as an
952
- unknown flag (exit 2). The config keys `search.minScore`,
953
- `search.graphBoost.*` and `improve.utilityDecay.*` have no effect and are
954
- kept as unknown keys. Search hits no longer carry the `graph` field, and
955
- usage events no longer record `graphExtraction` attribution.
956
- - **Guarded source reads around workflow runs.** `akm workflow run` no
957
- longer records a read set of every source it touched or re-checks those
958
- sources before publishing the run, so editing a command, task, script or
959
- env file while a run is being created no longer fails creation; a source
960
- that resolves outside its bundle is still refused. `akm workflow plan` no
961
- longer prints a `read set:` block, and its JSON drops `sourceReadSet`. At
962
- dispatch an env file is re-read from its recorded path (a changed key set
963
- is still refused), so replacing or re-cloning the bundle directory no
964
- longer fails a unit with "environment owner root physical identity
965
- changed". The resume check that refused a run whose stored params row had
966
- been edited is gone.
967
- - **Drain policies.** `processes.triage.policy` and
968
- `processes.triage.maxDiffLines` (config) and `akm proposal drain --policy`
969
- / `--max-diff-lines` are retired with `drain-policies.ts`; the flags now
970
- fail as unknown (exit 2) and the keys are kept as unknown config keys.
971
- - **Improve machinery with no remaining reader:** the collapse detector with
972
- its canary set (`scripts/refresh-canary-set.ts`) and cycle metrics, replay
973
- selection, the outcome-proxy events, and the never-called anti-collapse
974
- merge guards. Retired config keys (kept as unknown keys):
975
- `processes.consolidate.antiCollapse.{maxGeneration, lexicalDiversityCheck,
976
- mergeInformationFloor, minSpecificityRetention}`,
977
- `processes.consolidate.contradictionDetection`,
978
- `improve.salience.replayBudget` and `improve.collapseDetector`. Retired
979
- events: `improve_salience_first_run`, `improve_replay_selected`,
980
- `collapse_detector_alert`, `improve_cycle_metrics_purged`,
981
- `outcome_proxy_dead` and `outcome_proxy_inverted`.
982
-
983
- ### Fixed
984
-
985
- - **Re-extracting an unchanged note now replaces its stored graph rows.**
986
- `replaceStoredGraph` refreshed only a file's status, reason and run id when
987
- its body hash was unchanged, so an extraction of the same body — after a
988
- model or prompt change, or after a failed first attempt — never reached
989
- `graph_file_entities` or `graph_file_relations`. One install had 1,389 files
990
- marked `extracted` with no entity rows while `llm_enrichment_cache` held
991
- their extractions. A file's rows are now rewritten whenever its entities or
992
- relations differ from the stored ones, so the next graph pass refills such
993
- files from the cache without a model call. (`src/indexer/db/graph-db.ts`)
994
- - **A graph pass that stops early no longer shrinks the stored graph.** A
995
- full scan wrote back only the files it reached, so a budget abort, a
996
- failure-rate abort or `processes.graphExtraction.topN` deleted the stored
997
- rows of every other file: the 2026-09-26 backfill hit its 4 h budget after
998
- 3,358 of 15,165 eligible files, and that prefix became the whole graph. The
999
- pass now keeps the rows of every eligible file it did not reach, and of a
1000
- file whose extraction attempt failed. It drops rows only for a file that
1001
- left the eligible set — deleted, emptied, now `inferred: true`, or of a type
1002
- no longer included — which candidate-scoped runs never did; a scan that
1003
- could not read part of the stash drops nothing. Because kept rows can come
1004
- from an older extractor, the sweep no longer reuses a stored graph node as a
1005
- cache hit: only `llm_enrichment_cache`, keyed by extractor, answers for the
1006
- current one. (`src/indexer/graph/graph-extraction.ts`)
1007
- - **`graph_meta` counts describe the stored rows.** The extraction pass
1008
- wrote counts from its in-memory graph (22,304 entities reported against
1009
- 15,833 stored on one install), and deleting entries overwrote them with raw
1010
- row counts. Each write now derives them from the stored rows, one meaning
1011
- each: stored files, files with entity rows, distinct case-folded entities and
1012
- distinct case-folded relations. The pass result, and with it the
1013
- `akm improve` summary, reports the same counts. The entries-delete recompute
1014
- and the in-memory graph deduplicator (`src/indexer/graph/graph-dedup.ts`)
1015
- are gone. (`src/indexer/db/graph-db.ts`,
1016
- `src/indexer/graph/graph-extraction.ts`,
1017
- `src/storage/repositories/index-entries-repository.ts`)
1018
- - **`akm health` counts graph-extracted files per run.** Its
1019
- `graphExtraction.extractedFiles` added the whole stored graph's file count
1020
- once per improve run in the window; it now adds the files each run
1021
- extracted, as `entities` and `relations` already did.
1022
- (`src/commands/health/improve-metrics.ts`)
1023
- - **`akm show`'s `related` refs no longer depend on index row order.** When
1024
- two entries index the same file, the ref shown for it was whichever row
1025
- SQLite returned last; the lowest concept id now wins, and shared entity
1026
- names are read in a fixed order. The ranking itself (most shared entities,
1027
- then path) was already deterministic. (`src/indexer/graph/graph-related.ts`)
1028
- - **`akm search`/`akm curate` no longer store a pasted credential verbatim in
1029
- `state.db`.** The Claude Code hook curates every user prompt, so a
1030
- credential pasted into a prompt (`PASSWORD=…`, `TOKEN=…`, `SECRET=…`, a
1031
- `Bearer` header, a JWT, a `ghp_…`/`xox…`/`AKIA…` token, a PEM private key, a
1032
- `user:pass@` URL, …) flowed straight into the query text and was persisted
1033
- as-is in both `usage_events.query` and the `events` table's
1034
- `metadata_json` — a scan of mined queries found 22 credential-like values
1035
- stored this way. `logSearchEvent`/`logCurateEvent` now redact the query
1036
- with `redactCredentialPatterns` (extended with the shapes above, plus a
1037
- `NAME=value`/`NAME: value` pass for names containing password, passwd,
1038
- secret, token, auth, credential(s), or an api/private key — the value is
1039
- replaced with `[REDACTED]`, the name is kept so queries stay useful for
1040
- evaluation) before either write, so a `show`/`select` event tracing back to
1041
- the search — which copies the search event's already-persisted `query`
1042
- metadata — inherits the same redacted text. Existing rows already written
1043
- are not rewritten. (`src/core/redaction.ts`, `src/commands/read/search.ts`,
1044
- `src/commands/read/curate.ts`)
1045
- - **`engines.<name>.supportsJsonSchema` on a `kind: "llm"` engine is a known
1046
- key again.** `LlmConnectionConfigSchema` declares it and `llm/client.ts`
1047
- reads it, but the named-engine object (`LlmEngineSchema`) never listed it,
1048
- so this release's unknown-key walk named it on every load and `akm migrate
1049
- apply` would have deleted a live setting from config.json.
1050
- (`src/core/config/schema/engines.ts`)
1051
- - **`akm migrate` finds the leaked activity registry where earlier releases
1052
- actually wrote it.** The `deadResidue` step looked for
1053
- `maintenance-activities/` under `$STATE`; the maintenance barrier created it
1054
- next to its own lock under `$DATA`, so the directory that had grown to
1055
- 229,943 four-kilobyte sidecars (927 MB) on one host was never reported or
1056
- removed. Both roots are checked, the registry is reported as one entry rather
1057
- than once per sidecar, and a directory already listed whole is not descended
1058
- into by the sidecar scan. (`scripts/akm-migrate/migrate/dead-residue.ts`)
1059
- - **`akm bundle add`'s `--name` is now a contract on every add path (local,
1060
- website, registry), not a hint.** An explicit `--name` that is not a legal
1061
- bundle slug, or that is already taken by a different bundle, used to fall
1062
- back silently — `deriveBundleId` minted a derived name, or a `-<hash>`
1063
- suffix — so `akm bundle add ... --name my.bundle` installed under a name
1064
- the caller never asked for, without saying so. It now fails with a
1065
- `UsageError` (exit 2) naming the rule, before any write (config, lock, or
1066
- network sync). Re-adding an already-installed ref under a *different*
1067
- `--name` than it already carries used to keep the existing key and say
1068
- nothing; it now fails the same way, naming the existing key and
1069
- `akm bundle rename <old> <new>`. A DERIVED name (no `--name` given) is
1070
- unaffected and keeps `deriveBundleId`'s forgiving `-<hash>` uniqueness
1071
- fallback. Every `akm bundle add` result (local, website, and registry) now
1072
- also carries `bundleId` (the resolved bundle key), and a registry add's
1073
- result always carries `registryId` (the registry install id) rather than
1074
- only when it happens to differ from `bundleId`, so a caller no longer has
1075
- to reconstruct the key from `sourceAdded`/`installed`.
1076
- - **`akm bundle add <registry ref> --name <name>` now keys the bundle by
1077
- `<name>`.** For npm, `github:` and Git refs, `--name` was accepted and then
1078
- dropped before the bundle key was derived, so the bundle was keyed by the
1079
- basename of its materialized cache directory instead — `extracted`, or
1080
- `extracted-<hash>` once that was taken — and its assets were only
1081
- addressable as `extracted//…`. The name now goes through the same
1082
- slug-legality and uniqueness rules as a local or website add. The install's
1083
- registry id is still recorded as `registryId`, so `akm bundle update` and
1084
- `akm bundle remove` keep resolving the original ref. Re-adding a ref that is
1085
- already installed keeps its existing key, as local and website re-adds do.
1086
- - **A registry bundle added without `--name` is keyed by its package or repo
1087
- name instead of `extracted`.** `akm bundle add npm:<pkg>` now creates bundle
1088
- `<pkg>` (`npm:@scope/pkg` → `pkg`), and `github:owner/repo` or a Git URL
1089
- ending in `/repo` creates `repo` — the mapping the bundle schema already
1090
- documented for `registryId`. The key used to come from the basename of the
1091
- cache directory the package was unpacked into, which is always `extracted`,
1092
- so every registry bundle after the first was `extracted-<hash>`. A dotted
1093
- or mixed-case name is slugged like a directory name (`Foo.js` → `foo-js`).
1094
- Bundles that are already installed keep their current key, including
1095
- `extracted`, because every recorded `extracted//…` ref depends on it.
1096
- - **A one-file change in a large directory no longer costs `akm index` half
1097
- an hour.** Both full-text tables keyed their per-entry deletes on
1098
- `entry_id`, an unindexed FTS5 column, so every upsert scanned the whole
1099
- full-text index, twice per entry per run. On a 23.9k-entry index, one
1100
- touched file in a flat `knowledge/` directory of 13.7k entries took 26
1101
- minutes (task run `2026-09-24T20-30-01-663Z`); on backup copies of that
1102
- index the same rescan took 31 minutes before this change and takes 38 s
1103
- after it. FTS rows are keyed by rowid, and the
1104
- first writable open after upgrading realigns an existing index in place,
1105
- about 10 s and ~1.1 GB peak memory at that size, with no index-generation
1106
- bump, so an older binary keeps reading it. A writable open realigns again
1107
- if an older binary sharing the generation has written rows since.
1108
- - **One bad scheduler-sync item, or one bad migration step, no longer fails
1109
- the whole operation.** `akm task sync` used to throw and abort the entire
1110
- reconciliation over one binding it could not reconcile or one bundle whose
1111
- sources failed to read; that binding or bundle is now reported in the sync
1112
- result's `failures: [{path, ref?, reason}]` (documented in
1113
- `docs/reference/cli.md`) while every other one still syncs (see Changed).
1114
- `akm-migrate`'s
1115
- `runMigration` (`scripts/akm-migrate/run-migrate.ts`) now runs every step
1116
- under its own catch too: a step's own throw (or, under `apply`, its
1117
- read-only fallback failing as well) is recorded in the plan's new
1118
- `failedSteps: [{step, error}]` and forces `status: "blocked"` instead of
1119
- ending the run with no plan at all — the remaining steps still run in
1120
- order. `akm migrate status|apply` (`scripts/akm-migrate/main.ts`) already
1121
- exits 1 for any blocked plan, so a poisoned step no longer exits the
1122
- internal-error code 70 with nothing printed.
1123
- - **The legacy `stashDir`/`sources[]`/`installed` config shape is persisted
1124
- by `akm migrate apply`, and an empty one no longer fails every command**
1125
- (#863). `migrateLegacySourceShape`
1126
- (`src/core/config/legacy-source-shape-shim.ts`) has always converted a
1127
- usable `stashDir`/`sources[]`/`installed` in memory on every load and told
1128
- the user to run `akm migrate apply` to make that stick, but nothing on disk
1129
- ever did; the migrator's `configFile` step now writes that current shape
1130
- back once, under a backup. Separately, through 0.9.16 and 0.9.17-alpha.3 a
1131
- config whose `sources` was `[]` (what 0.8.9's `akm source remove` writes
1132
- after the last source is removed) or whose `stashDir` was empty or
1133
- unusable failed every command with exit 78; it now loads, with the shim's
1134
- one-time warning.
1135
- - **A `version: 2` or `version: 3` task source reads and runs again instead
1136
- of failing closed on upgrade.** `e413af024` deleted the in-memory
1137
- v2/v3 -> v4 read shim on the argument that "untrusted source cannot carry
1138
- obsolete activation semantics" — but activation had already moved to
1139
- host-local `scheduler.enabled` in that same commit, so the shim never
1140
- carried activation in the first place, and deleting it just reintroduced
1141
- the exact upgrade break 0.9.4 originally shipped the shim to fix ("would
1142
- have broken every pre-0.9.4 scheduled task headlessly on upgrade").
1143
- `parseTaskSource` (`src/tasks/source/parse-task-source.ts`) once again
1144
- routes `version: 2`/`version: 3` through the SAME pure planners
1145
- `akm migrate apply` uses, entirely in memory, with a one-line stderr
1146
- deprecation warning (once per file per process) and no disk write; the
1147
- parsed document never carries a source-owned `enabled` field, since the
1148
- v3->v4 planner already never hoists `akm.enabled` or a schedule entry's
1149
- `enabled` key. Only a v2/v3 document the deterministic conversion itself
1150
- cannot resolve still fails with `TASK_SCHEMA_VERSION_UNSUPPORTED`, naming
1151
- the specific blocked reason. The same in-memory shim now also tolerates a
1152
- declared `version: 4` document whose `schedule[]` still carries a
1153
- per-entry `enabled` key — 0.9.15's v4 grammar accepted it (`akm task add
1154
- --disabled` wrote it), this release's does not, and without this the
1155
- upgrade break above recurs for every 0.9.15-authored scheduled task. The
1156
- key is stripped without ever being read — `enabled: false` cannot
1157
- suppress a granted task and `enabled: true` cannot schedule an ungranted
1158
- one, since activation stays host-local `scheduler.enabled`. `akm task
1159
- validate` reports such a file `converts` (`sourceVersion` still `4`)
1160
- instead of `valid`, since it read through the shim rather than the direct
1161
- v4 path.
1162
- - **Lock contention exits 75, like `state.db` contention.** Another process
1163
- holding `akm.lock`'s write sentinel (`LOCKFILE_CONTENDED`, was a config
1164
- error, exit 78) or the asset-mutation writer lease past its wait
1165
- (`ASSET_MUTATION_LEASE_HELD`, was an unclassified error, exit 70) is now a
1166
- retry-shortly `TransientError`, exit 75.
1167
- - **`akm health`'s `state.db` repair steps no longer corrupt the rebuilt
1168
- file.** `state-db-integrity` used to suggest `.dump` into a new file with
1169
- no writer stop; it now says to back up `state.db`, `.recover` it into
1170
- `state.new.db`, confirm that passes `quick_check`, stop every akm process,
1171
- delete `state.db-wal` and `state.db-shm`, then swap the new file in — a
1172
- leftover WAL replays onto the new database and corrupts it.
1173
- - **The package launcher (`dist/akm`) passes `--scheduler-context` through to
1174
- the CLI** instead of re-validating the descriptor with a stale copy of its
1175
- schema, which rejected every descriptor 0.9.17 writes.
1176
- (`scripts/node-runtime/akm`)
1177
- - **A `task_history` row with a malformed `engine` value decodes.** The
1178
- decoder used to reject the whole row when `engine` was present but not a
1179
- string or `null`; it now drops the bad value and decodes the rest, the
1180
- tolerance it already applied to every other unrecognized field.
1181
- (`src/storage/repositories/task-history-repository.ts`)
1182
- - **The LLM enrichment budget warning prints for every index run.** When the
1183
- metadata-enrichment pass ran out of its wall-clock budget during an index
1184
- another command started (`akm bundle update`, `akm setup`, `akm bundle
1185
- add`, improve's preflight, a read command's auto-index), it stopped
1186
- silently; it now prints the same "LLM enrichment budget exceeded" warning
1187
- `akm index` does.
1188
-
1189
- ## [0.9.17-alpha.3] - 2026-09-24
1190
-
1191
- ### Fixed
1192
-
1193
- - **Unscoped `akm task sync` no longer aborts the whole host on the first
1194
- bundle root that happens to contain any symlink.** `captureGuardedDirectoryManifest`
1195
- threw for every symbolic directory entry it listed, even one the scheduler
1196
- never reads (e.g. a third-party skill repo's `CLAUDE.md -> AGENTS.md`) —
1197
- `SchedulerSourceCollector` manifests every scanned bundle's root, so one
1198
- such bundle among many enabled ones failed sync entirely, dry-run included.
1199
- A symlink that stays inside its bundle root is now recorded in the guarded
1200
- directory manifest as its own `"symlink"` kind, identified without
1201
- following it (its `readlink` text plus its no-follow `lstat` identity), so
1202
- change detection still works; it is never read or descended into. A symlink
1203
- sitting exactly where a task or workflow source lives (a `.yml` under
1204
- `tasks/`, any `.yml` under an `akm-task` bundle, or a workflow-named file
1205
- under `workflows/`) is reported as its own per-source failure — "is a
1206
- symbolic source; guarded reads require a regular no-follow owner" — and its
1207
- ref is not scheduled, even when a real sibling file shares that ref, while
1208
- every other task and workflow still reconciles. A
1209
- symlink that resolves outside the bundle root, or one that is broken and
1210
- cannot be identified safely, is still refused, and a bundle root whose
1211
- `tasks` or `workflows` entry is itself a symlink still refuses loudly,
1212
- since that is a schedulable source location.
1213
-
1214
- ## [0.9.17-alpha.2] - 2026-09-24
1215
-
1216
- ### Fixed
1217
-
1218
- - **A config carrying the retired `experimental.workflowEngine` key no longer
1219
- fails to load.** `ExperimentalConfigSchema` moved from `.passthrough()` to
1220
- `.strict()` in 0.9.16 (`cc6152e02`), after `workflowEngine` had already been
1221
- removed from it in `e0655d13c`; a real config a 0.9.15 install wrote (whose
1222
- passthrough still accepted the key) then failed every command with
1223
- `Invalid config: experimental: Unrecognized key(s) in object: 'workflowEngine'`.
1224
- The config loader now strips known-retired `experimental.*` keys in memory
1225
- before validation, warning once and naming `akm migrate apply`; a genuinely
1226
- unknown/misspelled key (e.g. `improveAutonomyy`) still fails closed.
1227
- `akm migrate apply` removes the retired key from `config.json` on disk
1228
- (with the usual backup), and `--dry-run` reports the pending removal.
1229
- - **Unscoped `akm task sync` no longer crashes when an enabled website or npm
1230
- bundle is configured.** The sync plan loop resolved every active source
1231
- through the write-target resolver, which rejects any kind other than
1232
- `filesystem`/`git` outright (writes, and therefore scheduler state, are
1233
- undefined for those kinds — the same rejection `akm task enable` already
1234
- hit). Unscoped sync now skips non-filesystem/git bundles when building
1235
- install operations — they never carried schedulable tasks — while
1236
- inactive-bundle removal/revocation still sees them. A scoped
1237
- `akm task sync --bundle <website-or-npm-bundle>` now fails with a clear
1238
- usage error instead of the write-target `ConfigError`.
1239
-
1240
- ## [0.9.17-alpha.1] - 2026-09-24
1241
-
1242
- ### Added
1243
-
1244
- - **`akm improve --require-engines` now records its reachability probe on the
1245
- run result (R17).** `assertRequiredEnginesReachable` only ever reported a
1246
- failure (abort, exit 78); a probe that passed — including a slow or
1247
- flapping gateway that still answered in time — left no trace once the run
1248
- proceeded. It now returns one outcome per probed target (`process`,
1249
- `engine`, `endpoint`, `reachable`, `latencyMs`), threaded through a new
1250
- `AkmImproveOptions.engineProbe` and copied onto the persisted result as
1251
- `AkmImproveResult.engineProbe`. Omitted entirely when `--require-engines`
1252
- was not passed; a result persisted without it (every run before this
1253
- change) still decodes. `--require-engines --dry-run` results carry it too.
1254
- - **Reflect had no way to exclude raw wiki-ingest snapshots, which are the
1255
- longest generations in the ledger (89.5s/161.8s observed).** `wikis/articles/raw/*.md`
1256
- website snapshots index as `knowledge/wikis/articles/raw/<slug>`, and
1257
- reflect's `allowedTypes` filter is type-only, so it can't exclude a subset
1258
- of the `knowledge` type. `processes.reflect` now accepts an optional
1259
- `excludeRefPrefixes: string[]` — conceptId prefixes, matched after
1260
- stripping an optional `bundle//` from both the ref and each prefix.
1261
- `shouldSkipRef` skips a matching ref with reason `exclude-filter`, for
1262
- reflect only (distill and consolidate are memory-only and reject the key).
1263
- A trailing `/` on a prefix is ignored, so
1264
- `"knowledge/wikis/articles/raw/"` excludes the same refs as
1265
- `"knowledge/wikis/articles/raw"`.
1266
-
1267
- - **`akm health` now checks state.db's own SQLite integrity.** A new hard
1268
- `state-db-integrity` check runs a read-only `PRAGMA quick_check` against
1269
- `state.db` and fails, naming the returned diagnostic lines and the repair
1270
- steps (back up, dump/restore via `sqlite3`, verify, swap in), when it
1271
- reports anything other than `ok`. The same check reports state.db's
1272
- freelist ratio (the fraction of pages `VACUUM` could reclaim) and warns
1273
- above 50%. Previously nothing in `akm health` looked past a successful
1274
- append/read round trip, which stays true on a database that is corrupt at
1275
- the SQLite level.
1276
- - **The retention purge (`akm improve`) now VACUUMs state.db when more than
1277
- half its pages are free**, immediately after the events/improve_runs/
1278
- cycle-metrics purge, recording a `state_db_vacuumed` event with pages
1279
- before/after. Opportunistic: a locked/busy database is skipped, not
1280
- raised, so it never fails the purge pass it follows.
1281
-
1282
- ### Changed
1283
-
1284
- - **The orphan-state GC pass no longer probes index.db once per pending
1285
- row.** `runOrphanStateGcPass` used to call `getEntryByRef` (up to two
1286
- statements each, via its bare-ref fallback) for every pending
1287
- `asset_salience` / `asset_outcome` row — 2,101 pending rows cost 83–100s
1288
- per run. It now builds one snapshot of every live `item_ref` in index.db up
1289
- front and matches every pending row against it in memory: O(1) index.db
1290
- queries per run instead of one probe per row, with the same live/orphan
1291
- resolution (including the bundle-qualified-exact and bare-conceptId-suffix
1292
- fallback) as before.
1293
- - **Memory inference no longer forces a full reindex for the file(s) it
1294
- writes.** The post-inference maintenance step used to call the full
1295
- `reindexFn` (42–220s per run, typically for one written derived fact)
1296
- whenever memory inference split a parent. `runMemoryInferencePass` now
1297
- reports the exact paths it wrote or rewrote (`writtenPaths`, sourced from
1298
- the run's write-provenance journal), and the maintenance pass indexes just
1299
- those files with `indexWrittenAssets` instead — closing and reopening the
1300
- shared index.db handle around the call with the same discipline the full
1301
- reindex used (#584). The separate post-consolidation full reindex is
1302
- removed outright rather than re-gated: it used to fire whenever
1303
- `consolidation.processed > 0` (memories the LLM judged), but
1304
- merge/delete/contradict ops are advisory and never auto-applied, and the
1305
- one op that does execute — promote — writes a proposal to state.db, not to
1306
- the stash. Consolidation therefore cannot change a file the index reads,
1307
- so the reindex had no precondition it could ever satisfy.
1308
- - **The improve loop's reflect dispatch now checks the proposal
1309
- fingerprint/rejection-backoff guard *before* calling reflect, not just
1310
- after.** `fingerprint_match` and `rejection_backoff` were evaluated only
1311
- inside `createProposal`, which runs after reflect's full generation and
1312
- quality-judge call — so a ref already guaranteed to be skipped still paid
1313
- the LLM cost (measured: 2–16% of reflect LLM seconds spent on refs the
1314
- guard then discarded). The guard's fingerprint is an input fingerprint
1315
- (target ref, source, before-hash, model id), computable before dispatch, so
1316
- `checkProposalGuard` (`src/commands/proposal/repository.ts`) exposes the
1317
- identical check `createProposal` runs post-generation — the two share one
1318
- implementation and can never disagree. `runLoopReflectPass`
1319
- (`src/commands/improve/loop-stages.ts`) now calls it first; a hit skips
1320
- `reflectFn` entirely and lands in the existing `reflect-cooldown` bucket
1321
- with the same `reflect_invoked` event the signal-delta cursor
1322
- (`buildLatestProposalTsMap`) reads, so cursor advancement and run-result
1323
- classification are unchanged. `createProposal`'s post-generation check
1324
- remains the authoritative gate.
1325
- - **Consolidate's plan schema and prompt are promote-only.** The apply loop
1326
- only ever executed `promote` — `merge`/`delete`/`contradict` were advisory
1327
- by design and never applied — but the schema still asked for all four ops
1328
- plus a free-text `warnings` array, and completion tokens rose from 7–8k to
1329
- 21–30k per run after the 35B-A3B model switch with no change in
1330
- promotions. `CONSOLIDATE_PLAN_JSON_SCHEMA` and `consolidate-system.md` now
1331
- request only `promote` (with `reason` capped at 200 chars), and `isValidOp`
1332
- rejects any other op shape — e.g. from a model that ignores the schema —
1333
- with the existing "skipping invalid operation" warning instead of treating
1334
- it as an actionable plan entry. `ConsolidateResult.merged` / `deleted` /
1335
- `contradicted` and the `planned` op breakdown are unchanged in shape and
1336
- stay zero.
1337
- - **`improve-maintenance-passes.test.ts` moved under `tests/integration/`.**
1338
- The suite opens a real `state.db` via `openStateDatabase`, which AGENTS.md's
1339
- ORG-03..06 rule places under `tests/integration/`, not `tests/`; no content
1340
- change. Also corrected
1341
- `docs/architecture/specs/improve-collapse-churn-detector-design.md` §2.5,
1342
- which described the post-loop collapse-detector gate as `consolidationRan
1343
- OR recombination.processed > 0` — no `recombination` value is plumbed into
1344
- `runImprovePostLoopStage` and no recombine pass exists in the codebase, so
1345
- the spec now matches the shipped `consolidationRan`-only gate and notes
1346
- that the recombine-triggered pass is not implemented.
1347
- - **Graph-extraction relations are now compact `[from, type, to]` triples
1348
- instead of `{"from","to","type"}` objects, and the batch graph-extraction
1349
- call now sends a `responseSchema`.** The object-keyed form cost 10+ tokens
1350
- per relation for no signal, and completion tokens cost far more than
1351
- prompt tokens; a compact triple form measured −51% / −18% completion
1352
- tokens on two chunks. `graph-extract.ts`'s single-asset and batch prompts
1353
- and JSON schemas now ask for `["from", "type", "to"]` (`type` may be `""`);
1354
- `parseGraphExtraction` accepts both the triple form and the legacy object
1355
- form (a relation-level `confidence` is still read from a legacy object,
1356
- though the schema no longer offers it — the prompt never asked for one).
1357
- Separately, production runs graph extraction batched
1358
- (`processes.graphExtraction.batchSize`), and `extractGraphFromBodies` sent
1359
- no `responseSchema` at all, so the R12b output-bounding schema only ever
1360
- reached the single-asset path. The batch call now sends the same
1361
- `maxItems`-bounded schema (scoped to the batch's asset count) through the
1362
- same `supportsJsonSchema`-gated `responseSchema` field the single-asset
1363
- call uses. `GRAPH_EXTRACT_PROMPT_VERSION` bumps `v2` → `v3`, so every file
1364
- re-extracts once on the next graph pass — entity semantics, caps, chunking
1365
- and batch sizing are unchanged.
212
+ `improve-churn-ratio`, `collapse-churn-detector`). `processes.reflect` also
213
+ gains `excludeRefPrefixes: string[]` to skip a ref prefix (a raw
214
+ wiki-ingest snapshot tree, say) that a type-only `allowedTypes` filter
215
+ can't carve out on its own.
1366
216
 
1367
217
  ### Removed
1368
218
 
1369
- - **The write-only distill/proposal eval-cases path.** `writeEvalCase`
1370
- (`src/commands/improve/eval-cases.ts`) wrote a Markdown file per rejection
1371
- under `$STATE/improve/eval-cases/<stash>/` that nothing ever read back, and
1372
- `countEvalCases` reported a cumulative on-disk file count as if it were a
1373
- per-run number (surfaced as `evalCasesWritten` on the improve result and in
1374
- `akm health`'s improve metrics). A rejected proposal row (see above) now
1375
- carries the same information through a path something actually reads.
1376
- Deleted `eval-cases.ts` and its two `loop-stages.ts` call sites, the
1377
- `evalCasesWritten` field from `AkmImproveResult` and every health-metrics
1378
- reader/aggregator, and the `improve_completed` event's `evalCasesWritten`
1379
- field. `decodeImproveResult` still accepts (and ignores) `evalCasesWritten`
1380
- on an envelope an older release wrote, and existing eval-case files on disk
1381
- are untouched — `getEvalCasesDir` (`core/paths.ts`) stays, since
1382
- `scripts/akm-migrate/migrate/writer-relocation.ts` still uses it to
1383
- relocate them from the legacy `$STASH/.akm/eval-cases/` path.
219
+ - **The LLM entity-graph extraction pass.** `akm improve`'s per-file
220
+ entity/relation extraction, its tables, and `akm show`'s `related` list are
221
+ gone: on the navigation eval, vector kNN beat `related` by 0.157 P@5
222
+ [0.051, 0.260], and its only other consumer, curate's support refs, moved
223
+ to declared links (see Added). The tables are dropped unconditionally on
224
+ an index's next writable open. The `graph-refresh` improve strategy and its
225
+ `akm-graph-refresh-weekly` task template are retired with it — naming
226
+ `graph-refresh` (via `--strategy` or a task) now fails outright, naming the
227
+ retirement. `akm health` drops every graph metric (KPI card, summary rows,
228
+ per-run duration/entity/relation columns).
229
+ - **LLM metadata enrichment (`index.metadataEnhance`).** On a 49-query,
230
+ 1,968-entry measurement it moved search nDCG@10 by -0.0092 [-0.0324,
231
+ +0.0165] and long-prompt curate P@5 by -0.054 [-0.093, -0.012] — no
232
+ measurable benefit for a full pass costing about 27 B70-hours. It was
233
+ already off by default.
234
+ - **The per-run forgetting-safety lane.** A one-time cutover guard from a
235
+ June 2026 ranking-formula change that had kept running on every improve run
236
+ since. Thirty days of events showed no marginal pick over the signal-delta
237
+ lane once the new retrieval scope (above) applied: 4 of its last 5 flagged
238
+ refs were also picked by signal-delta, and the 5th was independently
239
+ planned under signal-delta the same run.
240
+ - **Removed flags and drain policies.** `akm search --no-project-context` and
241
+ `akm proposal drain --policy`/`--max-diff-lines` now fail as unknown flags
242
+ (exit 2), and the `personal-stash`/`conservative`/`manual` drain policies
243
+ are gone. Drain, and improve's triage pre-pass, now accept only a proposal
244
+ the quality judge passed on its exact content and reject an empty diff;
245
+ everything else goes to `processes.triage.judgment` or waits for review.
246
+ Extract and consolidate proposals, which `personal-stash` auto-accepted on
247
+ size alone, are no longer auto-accepted.
248
+ - **Retired config keys** — kept and tolerated, dropped only by `akm migrate
249
+ apply`: `index.graph.*`, `index.metadataEnhance`, `search.minScore`,
250
+ `search.graphBoost.*`, `improve.utilityDecay.*`, `improve.collapseDetector`,
251
+ `improve.salience.replayBudget`, `processes.triage.policy`,
252
+ `processes.triage.maxDiffLines`, `processes.consolidate.contradictionDetection`,
253
+ the retired `processes.consolidate.antiCollapse` merge guards
254
+ (`maxGeneration`, `lexicalDiversityCheck`, `mergeInformationFloor`,
255
+ `minSpecificityRetention` — `antiCollapse` itself, and its
256
+ `randomClusterFraction` mixing, are unaffected), and every improve
257
+ strategy's `processes.graphExtraction.*` and
258
+ `processes.consolidate.incrementalSince`/`.neighborsPerChanged` (the pair
259
+ pass replaces incremental-window candidate selection with the improve
260
+ ledger). A leftover `improve.strategies["graph-refresh"]` override block is
261
+ also dropped this way; `defaults.improveStrategy: "graph-refresh"` is not —
262
+ change that one by hand.
263
+ - **Also removed, superseded by the simpler mechanisms above:** the
264
+ scheduler source-grant layer and its fire-time re-check, the filesystem
265
+ transaction journals used by proposal accept/revert, the maintenance
266
+ barrier and its per-process activity registry, the SQLite lock-operation
267
+ mutex, the strict per-key config schemas and the retired-key registry, and
268
+ the health advisories tied to all of the above. `akm migrate apply` deletes
269
+ the files they left behind: `$DATA/txn/`, `$DATA/txn-quarantine/`,
270
+ `maintenance.barrier.lock`, and the `maintenance-activities/` registry,
271
+ which leaked an entry per process (one host had 229,943 entries, 927 MB).
1384
272
 
1385
273
  ### Fixed
1386
274
 
1387
- - **The lesson quality judge's ACTIONABILITY criterion carried no signal, and
1388
- the judge's request/parser let a differently-spelled or extra key change
1389
- the verdict (R16).** Splinter measured ACTIONABILITY at AUC 0.46 against
1390
- accept/reject outcomes — no better than chance — and averaging it into the
1391
- score pulled every verdict toward its 3.0 mode, i.e. the review band.
1392
- `buildJudgePrompt` no longer asks for it;
1393
- `LESSON_JUDGE_CRITERIA_KEYS` is now `novelty`/`nonRedundancy` only.
1394
- Separately, `runQualityJudge`'s request sent no `responseSchema` while the
1395
- prompt text spelled criteria as NON-REDUNDANCY / FEEDBACK ALIGNMENT, so a
1396
- model that echoed a differently-cased or -spelled key turned the verdict
1397
- into a parse failure routed to review; and `parseJudgeResponse` averaged
1398
- over every key present in `scores`, so an unexpected extra key changed the
1399
- score. `runQualityJudge` now sends a strict `responseSchema` — built from
1400
- the judge's own expected criteria keys, `additionalProperties: false` at
1401
- both levels — through the same `supportsJsonSchema`-gated
1402
- `request.responseSchema` path `src/llm/graph-extract.ts` uses, a no-op for
1403
- providers that don't opt in; and `parseJudgeResponse` now reads, validates,
1404
- and averages only the expected keys, silently ignoring any other key
1405
- instead of averaging or validating it. A missing expected key is still a
1406
- parse failure, unchanged.
1407
- - **The reflect quality-gate's "no judge configured" warning named a config
1408
- key nothing reads.** It told users to set
1409
- `improve.strategies.<name>.processes.reflect.qualityGate.engine`, but
1410
- `qualityGate` is `{ enabled }` passthrough — `resolveReflectQualityJudgeRunner`
1411
- always uses the generation runner when it is an LLM, or falls back to
1412
- `defaults.llmEngine` via `resolveImproveLlmExecution` with no profile/process
1413
- layer, so that key was never read. The warning now names only
1414
- `defaults.llmEngine`.
1415
- - **The distill/reflect LLM-as-judge quality gate inherited the generation
1416
- runner's temperature, and its averaged score hid which criterion actually
1417
- failed.** `runQualityJudge`'s request only pinned `enableThinking: false`,
1418
- so the judge ran at whatever temperature generation used — measured at 0.3,
1419
- the verdict flipped on 10/16 identical inputs, vs. 0/16 at temperature 0.
1420
- The request now also pins `temperature: 0`, for both the distill and
1421
- reflect judges that share this function, independent of the runner's
1422
- configured temperature. Separately, both judge prompts asked for one
1423
- averaged float, so a criterion carrying no signal was invisible in
1424
- production. They now ask for per-criterion integer scores
1425
- (`buildJudgePrompt`: novelty/actionability/nonRedundancy;
1426
- `buildReflectJudgePrompt`: feedbackAlignment/preservation/quality), averaged
1427
- in code to the same `score` the unchanged 3.5/2.5 thresholds gate on. The
1428
- parser accepts this new `{"scores": {...}, "reason"}` shape and still
1429
- accepts the old `{"score": <float>, "reason"}` shape a model may return;
1430
- each criterion (or the bare score) must be a finite number in 1..5 or the
1431
- response routes to review exactly as a parse failure does today. The
1432
- per-criterion scores, when present, are now carried through
1433
- `QualityJudgeResult.criteria` into the `distill_invoked` event metadata and
1434
- rejection-envelope frontmatter `writeQualityRejection` writes, and into
1435
- reflect's `reflect_completed` rejection event as `qualityCriteria`.
1436
- - **The judge parser accepted a partial `scores` object and auto-passed it.**
1437
- `parseJudgeResponse` validated only that whatever criterion keys arrived
1438
- held finite 1-5 values, then averaged over those keys alone — so a
1439
- truncated judge response like `{"scores": {"novelty": 5}, "reason": "…"}`
1440
- parsed to `score: 5.0` and `pass: true`, promoting content the judge never
1441
- finished evaluating on its other criteria. `runQualityJudge` now passes the
1442
- criterion key set its prompt asked for (`buildJudgePrompt`:
1443
- novelty/actionability/nonRedundancy; `buildReflectJudgePrompt`:
1444
- feedbackAlignment/preservation/quality) down to `parseJudgeResponse`, which
1445
- returns a parse failure — routed to review, exactly as a malformed response
1446
- is today — when any expected key is missing from `scores`.
1447
- - **The reflect pre-generation proposal-guard skip (R9) emitted `reflect_invoked` with no paired `reflect_completed`.** `runLoopReflectPass`'s guard-skip branch in `loop-stages.ts` appended a synthetic `reflect_invoked` event to advance the signal-delta cursor, but never called `reflectFn`, so `reflect.ts`'s own `reflect_completed` emission never ran either — a new, permanent source of unpaired `reflect_invoked` rows for every fingerprint/backoff hit, violating the invoke/complete pairing invariant `buildReflectEventEmitters` documents. The branch now also appends a matching `reflect_completed` (`ok:false`, `reason:"cooldown"`, `subreason:"pre_generation_guard"`), mirroring `emitFailed`'s shape.
1448
- - **R9's pre-generation proposal guard covered reflect only — distill paid for a full generation + judge call before the same fingerprint/backoff guard could reject it.** `runLoopDistillPass` had no equivalent of `runLoopReflectPass`'s pre-check, even though `createProposal`'s post-generation guard (and every rejected row R10 now mints under `source: "distill"`) applies to distill just as much. `runLoopDistillPass` now calls `checkProposalGuard` against the derived lesson/knowledge ref (distill's real `createProposal` call never targets the input ref) before dispatching `distillFn`; a hit routes to the pass's existing `distill-skipped` bucket and emits `distill_invoked` with a `skipped` outcome so `buildLatestProposalTsMap`'s signal cursor still advances.
1449
- - **The distill pre-generation proposal guard could suppress a legitimate dispatch by checking a ref distill would never target.** For a memory input, distill's real `createProposal` call targets one of two refs decided at dispatch time inside `planMemoryKnowledgePromotion` — the derived knowledge ref when the deterministic promotion heuristic fires, the derived lesson ref otherwise — but `runLoopDistillPass`'s pre-check checked both candidate refs and skipped on the FIRST guard hit, so a stale fingerprint/backoff hit on the ref distill would NOT have targeted silently suppressed dispatch until that ref's fingerprint happened to change. The pre-check now resolves the SAME target `planMemoryKnowledgePromotion` would via `wouldPromoteMemoryToKnowledge` (`distill/promote-memory.ts`) — a thin wrapper that delegates to `planMemoryKnowledgePromotion` itself so the classification can never drift from the real dispatch decision, with no LLM call — and checks only that ref; content and the classification's `durableInputRef` are read via `planned.ref` alone, matching `akmDistill`'s real dispatch, while `planned.itemRef ?? planned.ref` feeds only the feedback-events query.
1450
- - **The `akm improve` triage pre-pass drain's judgment LLM calls were unattributed in the usage report.** `runTriagePrePass`'s `drainProposalsFn` call dispatched judgment calls with no `withLlmStage` wrapper, unlike the standalone `akm proposal drain` CLI path, so they landed in `byProcessEngineModel` as unattributed (5 calls, 24s per run) instead of under a `triage` stage. The pre-pass drain is now wrapped in `withLlmStage("triage", …, { engine, process: "triage.judgment" })`, mirroring the CLI path.
1451
- - **The batch graph-extraction provider-storm guard only recognized one error
1452
- code.** After a failed batch call, `extractGraphFromBodies` skipped the
1453
- per-asset fallback retry only for `LlmCallError`s coded `provider_error` —
1454
- but a dead endpoint more often raises `network_error` (a dropped
1455
- connection) or `provider_html_error` (a provider serving an HTML error
1456
- page), both of which still paid the full per-asset fallback storm the
1457
- guard exists to prevent. The predicate is now `isTransportFailure`
1458
- (`src/llm/client.ts`), shared with `chatCompletion`'s retry classifier so
1459
- the two cannot drift apart, and covers `provider_error`, `network_error`,
1460
- and `provider_html_error`.
1461
- - **`akm health`'s `state-db-integrity` check no longer crashes when the freelist/page-count read fails.** `getStateDbFreelistInfo` had a `finally` but no `catch` around its read-only open and pragma reads, unlike its sibling `runStateDbQuickCheck` — a throw there (e.g. an unopenable state.db) escaped `akm health` as an unclassified exit 70 on exactly the damaged database the check exists to report. It now returns a zeroed `StateDbFreelistInfo` with an `error` field, and the check renders that as a failed check instead of throwing.
1462
- - **The post-purge VACUUM's `state_db_vacuumed` event now honors the caller's `EventsContext`.** `vacuumStateDbIfReclaimable` appended its event with a direct `insertEvent` call, bypassing `EventsContext.readOnly` and the injectable clock its sibling purge events (`events_purged`, `improve_runs_purged`, `improve_cycle_metrics_purged`) use in the same `runRetentionPurgePass` callback. It now appends the event via `appendEvent` with the caller's `EventsContext` plumbed through.
1463
- - **Consolidate's per-chunk prompt excerpt truncated the raw file (frontmatter
1464
- + body) instead of the body.** `buildChunkPrompt` sliced `body.slice(0,
1465
- bodyTruncation)` off the unstripped file; a memory whose frontmatter alone
1466
- exceeded the excerpt length was judged on metadata only and never showed
1467
- its own body text. The excerpt now truncates `stripFrontmatterBody(body)`;
1468
- hot/queued detection is unchanged and still reads the raw body.
1469
- - **Consolidate's chunk prompt carried an unused ~14k-char standards block and
1470
- a header the model sometimes echoed back as a bogus `ref`.** Every chunk
1471
- prompt resolved and injected a "Standards to follow" section
1472
- (`resolveStandardsContext("memories/_consolidated", ...)`), but the chunk
1473
- output is a promote-only op list that never reads it. Separately, the
1474
- chunk header (`Chunk N of M, memories <first>–<last>:`) named the chunk's
1475
- boundary memories with an en dash between two `memories/<name>` refs; on
1476
- 2026-09-24 the judge model returned promote ops whose `ref` was exactly
1477
- that `memories/<first>–memories/<last>` range, naming a memory that does
1478
- not exist and losing the promotion. `buildChunkPrompt` no longer takes a
1479
- `standardsContext` and the header is now
1480
- `Chunk N of M (<count> memories):` — no refs in it.
1481
- - **Consolidate re-judged memories that were already promoted verbatim into
1482
- `knowledge/`.** That duplication was previously discovered only after the
1483
- LLM (`shouldSkipPromotionBodyDuplicate`), so a pool where the large
1484
- majority of memories were already-promoted duplicates still paid the full
1485
- chunk/LLM cost on all of them before being skipped.
1486
- `inspectConsolidationPool` now drops those memories before any chunking or
1487
- LLM work, sharing one `loadExistingKnowledgeBodyHashes` call and the same
1488
- `cacheHash` domain with the post-LLM check so the two cannot disagree. The
1489
- dropped count is reported as `prefilteredAlreadyPromoted` on the
1490
- consolidate result and in a warning line. The pre-filter also now runs
1491
- *before* the `consolidate.limit` cap (previously after), so a run with a
1492
- limit set selects its oldest-modified window from the pre-filtered pool
1493
- instead of re-selecting and re-dropping the same permanently-undeletable
1494
- duplicates every run while fresh memories past the cap went unreached; the
1495
- preview/eligibility path (`preparation.ts`) computes and passes the same
1496
- hash set so the reported candidate pool agrees with what the run will act
1497
- on. A live (non-preview) `akm improve` run reuses that same hash set for
1498
- the actual `akmConsolidate` call instead of recomputing it, so a run still
1499
- walks `knowledge/` only once.
1500
- - **`improve`'s start-of-run index rescan ran after triage dirtied the stash,
1501
- not before it.** Proposal triage promotes accepted proposals straight into
1502
- the flat `knowledge/` root, and the blocking `ensureIndex` call that is
1503
- supposed to give the run a current index ran only afterward (inside
1504
- `collectEligibleRefs`'s setup), so every triage promotion guaranteed the
1505
- very full rescan it should have preceded — up to ~27 minutes, holding the
1506
- index lock against co-scheduled writers. `ensureIndex` now runs before the
1507
- triage pre-pass, and triage's own writes are indexed incrementally
1508
- (`indexWrittenAssets`) so `collectEligibleRefs` still sees them without a
1509
- second full walk. Because `indexWrittenAssets` upserts a file's
1510
- `content_hash` without bumping `builtAt`, index staleness detection
1511
- (`ensure-index.ts`) is now per-file: a file newer than the last build is
1512
- only treated as stale when its current content actually differs from what
1513
- is indexed, so incrementally-reindexed content stops re-triggering the
1514
- same full rescan on every subsequent run. The implicit reindex's timing
1515
- breakdown (walk/llm/embed/finalize), previously discarded, is now logged
1516
- at verbose level and surfaced on the improve result as `ensureIndexDurationMs`.
1517
- - **Distill quality rejections vanished instead of persisting, so backoff and
1518
- Reflexion never saw them and the same ref was re-selected and re-rejected
1519
- on every run** (two refs were rejected 11× and 10×). `writeQualityRejection`
1520
- wrote only a `$STATE`-side file and an event, never a `proposals` row, so
1521
- `rejection_backoff`/`fingerprint_match` (proposal/repository.ts) and the
1522
- Reflexion "previously rejected" context had nothing to find; the distill
1523
- signal-delta cursor (`buildLatestProposalTsMap`) also only advanced for
1524
- `queued`/`skipped`/`validation_failed` outcomes, so a rejected ref stayed
1525
- eligible forever. `writeQualityRejection` now mints a real proposal through
1526
- the same `createProposal`/`archiveProposal` path every other proposal
1527
- source uses: a `quality_rejected` outcome is minted pending then archived
1528
- to `rejected` carrying the judge's reason; a `review_needed` outcome stays
1529
- `pending` in the normal queue, where triage — a human, or the drain's
1530
- judgment tier when one is configured — decides, the same path every other
1531
- pending distill proposal (including quality-gate passes) already takes.
1532
- The cursor now also advances on both outcomes (still excluding
1533
- `llm_failed`, where no real attempt produced anything). A retry for the
1534
- same target, source, and model is skipped by `fingerprint_match` (the
1535
- input fingerprint recorded at mint, retained `archiveRetentionDays`,
1536
- default 90 days); the 30-day `rejection_backoff` window only applies once
1537
- the target's before-hash or the model differs. Because these machine
1538
- rejections are now real `rejected` rows under `source: "distill"`, `akm
1539
- health`'s distill accept rate (`computeAcceptRateBySource`,
1540
- src/commands/health/accept-rate.ts) drops relative to earlier releases and
1541
- no longer measures reviewer acceptance alone. Nothing gates on that
1542
- metric.
1543
- - **`writeQualityRejection` could throw instead of returning a rejection
1544
- result.** Minting the proposal row above runs the mint-time canonical
1545
- validator (`createProposal` → `rejectProposal`,
1546
- `src/commands/proposal/repository.ts`), which throws `UsageError` for
1547
- structurally-invalid content — e.g. a `lessons/` ref whose body lacks
1548
- `description`/`when_to_use`. `writeQualityRejection` is the terminal,
1549
- non-throwing rejection path and none of its callers handled a throw. The
1550
- proposal row is bookkeeping for backoff/Reflexion, never the authoritative
1551
- record of the rejection, so a validator throw now degrades to "no row
1552
- minted" — the envelope file and `distill_invoked` event are still written,
1553
- matching the existing fingerprint/backoff skip behavior.
1554
- - **A `review_needed` quality-gate rejection could be auto-promoted by the
1555
- triage drain's judgment tier with no human ever seeing it.**
1556
- `writeQualityRejection` minted a `review_needed` outcome as an ordinary
1557
- pending proposal under `source: "distill"` (knowledge promotions from
1558
- `promote-memory.ts` take the same path); the `personal-stash` drain policy
1559
- defers `distill` proposals to the judgment tier, which can auto-accept
1560
- under `applyMode: promote` + `experimental.improveAutonomy` — so content
1561
- the quality judge explicitly refused to auto-queue (the 2.5–3.5
1562
- review-needed band) could be promoted without a human in the loop.
1563
- `writeQualityRejection` now stamps a `review_needed` mint with a
1564
- `{ outcome: "deferred", reason: "quality-review", gate: "quality-gate" }`
1565
- gate decision (best-effort: a stamp failure warns and continues, like the
1566
- existing mint/archive tolerance), and `classifyPendingProposals`
1567
- (`proposal/drain.ts`) skips any pending row carrying it — leaving it
1568
- pending and untouched, before the drain's own policy-deferred re-stamp
1569
- loop would otherwise overwrite the stamp.
1570
- - **Consolidate's post-LLM promote-dedup hash double-stripped frontmatter.**
1571
- `shouldSkipPromotionBodyDuplicate`'s `bodyHash` was computed as
1572
- `cacheHash(parseFrontmatter(memoryContent).content.trim())` — the body was
1573
- already frontmatter-stripped before being handed to `cacheHash`, which
1574
- strips it again internally — diverging from the single-strip
1575
- `cacheHash(raw)` domain `loadExistingKnowledgeBodyHashes` and the pre-filter
1576
- use for a source memory body that begins with its own `---` block. The
1577
- check now hashes `cacheHash(memoryContent)` directly, so the two sides of
1578
- the dedup comparison agree.
1579
- - **Consolidate's per-chunk prompt still warned against proposing `delete`
1580
- for `(captureMode: hot)` memories.** The consolidate op schema and system
1581
- prompt dropped `delete` (along with `merge`/`contradict`), leaving
1582
- `buildChunkPrompt`'s top-of-prompt hot-ref block as the only remaining
1583
- mention of `delete` anywhere in the prompt — a retired op name that
1584
- `isValidOp` now rejects if the model echoes it back, wasting tokens on
1585
- "skipping invalid operation" warnings. The block and the `hotRefs`
1586
- collection that fed it are removed; the inline `(captureMode: hot)`
1587
- annotation on each memory line is unchanged.
1588
- - **Graph extraction sent no `json_schema` and no per-asset chunk cap, so a
1589
- long file could pay for dozens of LLM calls whose output was then sliced
1590
- down to the same 32-entity/32-relation limit anyway** (one file spent 21 of
1591
- 27 calls and 12.9k completion tokens this way). The single-asset extraction
1592
- call (`extractGraphFromBody`) now sends a `responseSchema` (entities/
1593
- relations capped at 32 each, `additionalProperties: false` otherwise), via
1594
- the same `supportsJsonSchema`-gated request path memory-infer.ts uses — no
1595
- `maxTokens` is sent; cost is bounded by the schema's `maxItems` caps alone,
1596
- per AGENTS.md's "LLM Defaults" (a hardcoded cap risked silent truncation
1597
- with zero headroom for JSON punctuation or reasoning tokens). A body
1598
- chunked beyond the new
1599
- `processes.graphExtraction.maxChunksPerAsset` (default 8) now stops after
1600
- the first N chunks instead of processing every one; the skipped chunks are
1601
- reported as `truncatedChunks` in the run's graph-extraction telemetry so
1602
- the coverage loss is visible rather than silently absorbed. The `improve`
1603
- loop's dispatch (`loop-stages.ts`) now also forwards a configured
1604
- `maxChunksPerAsset` to the extraction call, mirroring the existing
1605
- `topN`/`batchSize` wiring — without this the config key had no effect in a
1606
- real `akm improve` run and the default of 8 always applied.
1607
- - **The graph-extraction `responseSchema` forbade the `confidence` field the
1608
- parser itself reads.** `additionalProperties: false` on both the root
1609
- object and each relation item made `confidence` impossible on a
1610
- `supportsJsonSchema` provider, even though `parseGraphExtraction` uses
1611
- `rel.confidence` to drop relations below `MIN_RELATION_CONFIDENCE` and
1612
- `item.confidence` to feed the merged extraction confidence — silently
1613
- turning the confidence filter into dead code on exactly the providers the
1614
- schema targets. `confidence: {"type": "number"}` is now allowed at both
1615
- levels; `additionalProperties: false` still forbids anything else.
1616
- - **A pending proposal went stale the moment akm's own bookkeeping touched
1617
- its target, and promote refused it forever (R20).** `resolveProposalTargetInfo`
1618
- captured the target's raw `beforeHash` at mint; the SAME nightly run's
1619
- `writeSalienceToFrontmatter` (distill) and memory inference's
1620
- `inferenceProcessed` stamp then rewrote the target's frontmatter before
1621
- promote ran, so `promoteProposalWithLease`'s guard (`repository.ts` ~L2406)
1622
- and `drain.ts`'s dry-run mirror (`assertProposalTargetFresh`) refused every
1623
- affected proposal with "target changed after proposal was created" — the
1624
- same 11+ reflect proposals, every day, on splinter. `resolveProposalTargetInfo`
1625
- now also captures `beforeHashNormalized` (`core/asset/frontmatter.ts`'s new
1626
- `computeNormalizedContentHash`, over the target with
1627
- `BOOKKEEPING_FRONTMATTER_KEYS` — `salience`/`salienceInputs`/`inferenceProcessed`
1628
- — stripped and the remaining frontmatter canonically re-serialized); the
1629
- promote guard and its dry-run mirror both prefer it over the raw
1630
- `beforeHash` when present, so a bookkeeping-only rewrite no longer stales a
1631
- proposal out while a real content change still refuses. Promotion also now
1632
- carries the live target's bookkeeping keys forward
1633
- (`carryForwardBookkeepingFrontmatter`) when the proposal's own frontmatter
1634
- doesn't set them, so accepting never drops `inferenceProcessed` and forces
1635
- memory inference to reprocess the memory. A legacy proposal minted before
1636
- this field existed keeps its exact original raw-hash check.
1637
- `computeNormalizedContentHash` also normalizes the body boundary the same
1638
- way `assembleAssetFromString` does (leading newlines stripped, exactly one
1639
- trailing newline) before hashing, and treats an empty frontmatter block as
1640
- `{}` instead of falling back to the raw hash — both `writeSalienceToFrontmatter`
1641
- and the memory-inference `assembleAsset` rewrite shift where the body starts,
1642
- which without this normalization still staled a proposal out unless the
1643
- target's frontmatter was already in that exact on-disk shape.
1644
- - **A stale-target promote failure was retried, and refused, identically
1645
- every drain run forever (R20).** The drain already categorized a
1646
- "target changed/was created after proposal" failure as `stale-target`
1647
- (`categorizeDrainFailure`), but left the row pending either way — so the
1648
- same proposals failed the same way on every subsequent `akm proposal
1649
- drain` / triage pass. Both promote-failure sites (`drainProposals`'s
1650
- deterministic loop and `runJudgmentTier`) now auto-reject a stale-target
1651
- failure once, stamping `gateDecision: { outcome: "auto-rejected", reason:
1652
- "stale-target" }` instead of leaving it to retry. This is not a merit
1653
- rejection, so `checkFingerprintAndBackoff`'s rejection-backoff window
1654
- (`repository.ts`) now excludes stale-target rows — the ref stays
1655
- re-proposable against its current content — and the Reflexion
1656
- "previously rejected" context (`reflect.ts`'s `readRejectedProposals`,
1657
- `distill.ts`'s `buildDistillMessages`) and the accept-rate health metric
1658
- (`health/accept-rate.ts`) now exclude stale-target rejections too, so a
1659
- procedural refusal doesn't misrepresent content quality. `--dry-run` now
1660
- predicts the same outcome: a stale-target promote failure it detects is
1661
- reported under `rejected`, matching what a real run does, instead of under
1662
- `failed`.
1663
-
1664
- - **Reflect quality-gate rejections were mislabelled `parse_error` and fed
1665
- back into later prompts as learned "avoid" patterns.** When the reflect
1666
- quality judge rejected an otherwise well-parsed proposal, the result
1667
- carried `reason: "parse_error"` — a real parse failure and a judge
1668
- rejection were indistinguishable. The improve loop injects non-excluded
1669
- reflect failures into the next reflect prompt's "Avoid These Patterns"
1670
- block, so a single gate rejection could poison every subsequent candidate
1671
- in the run. Judge rejections now carry a distinct `quality_rejected`
1672
- reason, stay in the `reflect-failed` metrics bucket, and are excluded from
1673
- that avoid-patterns injection like the existing deterministic skips.
1674
- - **Legacy rejected proposals no longer throw before reflect/distill prompt
1675
- dispatch.** `readRejectedProposals` (reflect.ts) and the equivalent mapper
1676
- in distill.ts built their "previously rejected" context via
1677
- `proposalContent(p)`, which throws when a proposal's `changes[0]?.after` is
1678
- undefined — the shape `storedToChanges` deliberately returns for rows
1679
- archived before the `changes` field existed (the large majority of
1680
- real-world rejected-proposal history). The throw happened before the
1681
- signal cursor advanced, so a ref with any such legacy rejection errored on
1682
- every run instead of ever completing. Both call sites now read the preview
1683
- from `payload.content`, which is populated for every row regardless of
1684
- its `changes` shape.
1685
- - **Failed graph extractions are no longer cached as permanent hits.** A
1686
- provider outage upserted thousands of `{"entities":[],"status":"failed"}`
1687
- results into `llm_enrichment_cache` and the persisted graph, and both
1688
- cache-hit paths (the DB lookup and reuse from the previous graph) treated
1689
- them as valid hits forever after — the affected files never retried.
1690
- `status: "failed"` results are now treated as a miss and are never written
1691
- to the cache; existing rows are left on disk and are overwritten naturally
1692
- on the next successful extraction. `src/llm/graph-extract.ts` also no
1693
- longer falls back to a per-asset retry for every body in a batch after a
1694
- `provider_error` — the provider has already demonstrated it is failing, so
1695
- each asset in that batch is recorded as failed directly. Graph extraction
1696
- now aborts the rest of the run (returning the partial results already
1697
- extracted) once the failure rate crosses 50% over at least 4 attempted
1698
- extraction dispatches, mirroring consolidate's existing failure-rate guard.
1699
- The abort counts one attempt per `extractGraphFromBodies` dispatch, not per
1700
- file inside its batch — per-file counting let a single batched
1701
- `provider_error` trip the guard after one HTTP failure whenever
1702
- `graphExtractionBatchSize` was at its default of 4.
1703
- - **Memory consolidation's cooldown could never engage.** `consolidate_completed`
1704
- was only emitted when a run planned zero merge/delete/contradict operations —
1705
- advisory ops the model plans daily and that are never auto-applied — so the
1706
- event had, in practice, never fired and the pool-delta gate stayed
1707
- permanently in its bootstrap "run every time" state. The event now fires
1708
- whenever the LLM pass itself completes, recording the unapplied advisory op
1709
- count (`advisoryOpsUnapplied`) instead of withholding the event. Separately,
1710
- the memory-volume override (`memoryVolumeConsolidationThreshold`, forcing a
1711
- run when the eligible pool exceeds the threshold) is now bootstrap-only: once
1712
- a `consolidate_completed` event exists for the source, the pool-delta gate
1713
- governs on its own, even when the pool is large. `akm improve --plan`'s
1714
- `consolidation.gates.delta.reason` no longer reports "memory pool has work"
1715
- for both a real pool delta and the bootstrap case (no `consolidate_completed`
1716
- event yet, so no delta was evaluated) — bootstrap now reports its own reason.
275
+ - **`akm info` now always reports and exits 0, like a help command.** An
276
+ invalid `config.json`, an unreadable or missing bundle directory, and a
277
+ locked, newer, or corrupt `index.db` are each named in the report instead
278
+ of failing the command or showing unexplained zeros.
279
+ - **`akm health` no longer silently drops every improve run recorded before
280
+ #947** (2026-09-09): a `plan` object with no `processes` key failed to
281
+ decode entirely, excluding those runs from `--window-compare`, `--group-by
282
+ run`, and the HTML/MD reports. On a real 30-day window this had dropped 11
283
+ of 55 runs.
284
+ - **`akm show` works again for a memory with a `.derived.md` child** (835 of
285
+ them on one real bundle) — broken since 0.9.7.
286
+ - **`akm bundle add --name` is a contract on every add path, not a hint.**
287
+ An explicit `--name` that is not a legal bundle slug, or is already taken,
288
+ now fails before any write instead of silently falling back to a derived
289
+ name (`akm bundle add --provider ... --name` had its own gap in this same
290
+ check, now closed). A registry add with no `--name` is keyed by its
291
+ package or repo name instead of the fixed name `extracted` every registry
292
+ bundle after the first used to collide on.
293
+ - **`akm improve --require-engines` no longer aborts a scheduled run because
294
+ a local LLM endpoint was merely busy.** Its reachability probe now waits up
295
+ to the engine's own timeout (at most two minutes) instead of a flat 3
296
+ seconds.
297
+ - **A scheduled row's value survives a `$` or a backslash**, and a cron
298
+ command too long for one line is recognized by `akm task sync` again
299
+ instead of being rewritten on every sync.
1717
300
 
1718
301
  ## [0.9.16] - 2026-09-22
1719
302