akm-cli 0.9.17-alpha.9 → 0.9.18

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