akm-cli 0.9.17-alpha.6 → 0.9.17-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +285 -0
- package/STABILITY.md +2 -2
- package/dist/akm +62 -29
- package/dist/akm-migrate +38 -19
- package/dist/assets/prompts/retrieval-relevance-judge.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +5 -3
- package/dist/commands/improve/consolidate.js +11 -0
- package/dist/commands/improve/improve-cli.js +27 -7
- package/dist/commands/improve/ledger.js +7 -3
- package/dist/commands/improve/loop-stages.js +4 -3
- package/dist/commands/improve/preparation.js +40 -10
- package/dist/commands/improve/reflect.js +46 -22
- package/dist/commands/improve/retrieval-gate.js +127 -0
- package/dist/commands/improve/retrieval-scope.js +77 -0
- package/dist/commands/read/curate.js +41 -30
- package/dist/commands/read/show.js +55 -2
- package/dist/commands/sources/info.js +3 -0
- package/dist/commands/tasks/tasks-cli.js +10 -12
- package/dist/commands/tasks/tasks.js +57 -56
- package/dist/commands/tasks/validate.js +27 -46
- package/dist/core/adapter/adapters/akm-adapter.js +2 -0
- package/dist/core/adapter/adapters/akm-metadata.js +31 -0
- package/dist/core/adapter/adapters/akm-task-adapter.js +29 -8
- package/dist/core/improve-result.js +4 -1
- package/dist/core/non-task-input.js +20 -0
- package/dist/core/paths.js +0 -4
- package/dist/indexer/db/graph-db.js +0 -32
- package/dist/indexer/graph/graph-extraction.js +3 -1
- package/dist/indexer/indexer.js +1 -3
- package/dist/indexer/links/declared-links.js +90 -0
- package/dist/indexer/scan/doc-to-entry.js +1 -0
- package/dist/indexer/usage/usage-events.js +34 -0
- package/dist/llm/graph-extract.js +26 -37
- package/dist/output/shapes/helpers.js +3 -0
- package/dist/output/text/show-format.js +16 -0
- package/dist/scripts/akm-migrate-node.js +6377 -6437
- package/dist/scripts/akm-migrate.js +6860 -6920
- package/dist/storage/repositories/index-entries-repository.js +12 -6
- package/dist/storage/repositories/index-entry-schema.js +18 -1
- package/dist/storage/repositories/index-links-repository.js +143 -0
- package/dist/storage/repositories/index-schema.js +27 -0
- package/dist/storage/repositories/proposals-repository.js +4 -0
- package/dist/tasks/backends/cron.js +80 -43
- package/dist/tasks/backends/launchd.js +28 -15
- package/dist/tasks/backends/schtasks.js +25 -10
- package/dist/tasks/run/load-task.js +1 -1
- package/dist/tasks/scheduler-binding.js +4 -2
- package/dist/tasks/scheduler-invocation.js +127 -235
- package/dist/tasks/scheduler-sync.js +13 -8
- package/dist/tasks/source/parse-task-source.js +22 -126
- package/dist/tasks/source/task-to-v4.js +463 -87
- package/docs/migration/release-notes/0.9.17.md +7 -5
- package/docs/migration/v0.9.1-to-v0.9.2.md +7 -3
- package/docs/reference/cli.md +26 -10
- package/docs/reference/tasks.md +58 -40
- package/package.json +1 -1
- package/dist/tasks/source/task-to-v3.js +0 -507
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,291 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.9.17-alpha.8] - 2026-09-28
|
|
10
|
+
|
|
11
|
+
`akm index` now records the links a bundle already declares (`xrefs`,
|
|
12
|
+
`supersededBy`, a `.derived` memory's parent, wiki sources, page links, and
|
|
13
|
+
workflow and task targets) as typed links, with no model. `akm show` lists
|
|
14
|
+
them, and curate's support refs come from them instead of the LLM entity
|
|
15
|
+
graph. The index moves to layout 26 in place on its first writable open;
|
|
16
|
+
0.9.17-alpha.4 through alpha.7 cannot open it. `index.graph.enabled: false`
|
|
17
|
+
now stops graph extraction in `akm improve`, and a partly failed extraction is
|
|
18
|
+
retried. `akm migrate` converts a v2 or v3 task file to v4 in one pass, and
|
|
19
|
+
the launchers no longer lose a signal that arrives before their child starts.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **Declared links (#935).** The relations a bundle already declares are now
|
|
24
|
+
stored as typed links: `xrefs:` (`xref`), `supersededBy:`
|
|
25
|
+
(`superseded_by`), `contradictedBy:` (`contradicted_by`),
|
|
26
|
+
`currentBeliefRefs:` (`belief_peer`), a `.derived` memory's parent
|
|
27
|
+
(`derived_from`), wiki `sources:` that name an asset (`cites`), the page
|
|
28
|
+
links an llm-wiki or OKF bundle resolves (`links_to`), and a workflow step's
|
|
29
|
+
or a task's target (`uses`). `akm index` reads them from what it already
|
|
30
|
+
parses, with no model, so an install without an LLM engine gets them. They
|
|
31
|
+
live in their own table (`asset_links`), apart from the LLM entity graph:
|
|
32
|
+
each link belongs to the entry that declares it and is written, replaced
|
|
33
|
+
and deleted with it, including on incremental and write-path (`akm
|
|
34
|
+
remember`) indexing. A target resolves when it is indexed, not when its
|
|
35
|
+
citer was, so a note that cites something added later links to it without
|
|
36
|
+
being reindexed. Retired spellings convert in memory (`memory:<name>`,
|
|
37
|
+
`wiki:<wiki>/<page>`, a `.md` suffix), and, as lint already allows (#882),
|
|
38
|
+
a memory whose own file is gone resolves to its `.derived` child.
|
|
39
|
+
`akm show` lists an asset's links grouped by kind: `outgoing`, `incoming`
|
|
40
|
+
and the `unresolved` tokens it names, at most 10 per kind with a `total`.
|
|
41
|
+
`akm info` reports links per kind with how many are unresolved. Links do
|
|
42
|
+
not change search ranking, and `related` is unchanged: on the retrieval
|
|
43
|
+
suite, against 0.9.17-alpha.7 on the same index with two runs per arm,
|
|
44
|
+
search nDCG@10 moved +0.002 [−0.005, +0.009] (a rerun of alpha.7 alone
|
|
45
|
+
moved +0.006) and curate P@5 +0.000 [−0.001, +0.001].
|
|
46
|
+
On the retrieval snapshot of the maintainer's 21 bundles (23,979 entries)
|
|
47
|
+
there are 14,917 links: 7,393 `contradicted_by`, 4,401 `xref`, 2,577
|
|
48
|
+
`derived_from`, 540 `cites` and 6 `superseded_by`. 1,801 are unresolved;
|
|
49
|
+
1,744 of those are `.derived` memories whose parent memory no longer
|
|
50
|
+
exists. (`src/indexer/links/declared-links.ts`,
|
|
51
|
+
`src/storage/repositories/index-links-repository.ts`,
|
|
52
|
+
`src/commands/read/show.ts`, `src/commands/sources/info.ts`)
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
|
|
56
|
+
- **`akm migrate` converts a v2 or v3 task file straight to v4 in one pass.**
|
|
57
|
+
The chain that read every file as v2, converted it to an intermediate v3
|
|
58
|
+
shape, then converted that to v4 (`src/tasks/source/task-to-v3.ts` ->
|
|
59
|
+
`task-to-v4.ts`, composed by `scripts/akm-migrate/migrate/task-files.ts`)
|
|
60
|
+
is now one planner: `task-to-v4.ts` reads a file once and, for v2, builds
|
|
61
|
+
the v3-shape record in memory — never written to disk or reported as its
|
|
62
|
+
own outcome — before hoisting it to v4 through the same code path a real
|
|
63
|
+
v3 file goes through. `akm migrate status`/`apply` output shapes, and
|
|
64
|
+
every blocked/changed reason code, are unchanged, checked fixture by
|
|
65
|
+
fixture against the prior two-hop chain's actual output (one exception:
|
|
66
|
+
a v3 document that fails only the typed pre-check's own "exactly one
|
|
67
|
+
scheduling source" rule — unreachable through the real chain, which
|
|
68
|
+
always ran that same pre-check first — now reports `invalid-v3-task`
|
|
69
|
+
instead of the raw hoist stage's own `ambiguous-scheduling-source`,
|
|
70
|
+
matching what `akm migrate apply` already returned end to end). The
|
|
71
|
+
`already-v3` and `pending-v2-to-v3-migration` intermediate states are
|
|
72
|
+
gone with the generation split that produced them.
|
|
73
|
+
`src/tasks/source/task-to-v3.ts` (500 lines) is deleted; its logic moved
|
|
74
|
+
into `task-to-v4.ts`, which also drops the duplicate raw-YAML reader and
|
|
75
|
+
outcome-base helpers the two files each carried their own copy of.
|
|
76
|
+
(`src/tasks/source/task-to-v4.ts`, `scripts/akm-migrate/migrate/task-files.ts`)
|
|
77
|
+
- **Curate's support refs come from declared links.** Each curated item's
|
|
78
|
+
support refs (at most two) are now assets its declared links name: what
|
|
79
|
+
it links to, then what links to it, in the order `akm show` lists them,
|
|
80
|
+
skipping assets curate already selected. They no longer come from the LLM
|
|
81
|
+
entity graph's `related` list. On the retrieval snapshot, `related` offered
|
|
82
|
+
support refs for 47 of 867 curate items (5.4%) and declared links for 257
|
|
83
|
+
(29.6%). The retrieval judge graded 157 of those items, asking whether each
|
|
84
|
+
support ref is worth opening next: 56% of declared support refs were useful
|
|
85
|
+
against 68% of `related`'s, so curate attaches about 24 useful support refs
|
|
86
|
+
per 100 items instead of 7. Curate's items are unchanged.
|
|
87
|
+
(`src/commands/read/curate.ts`)
|
|
88
|
+
- **Index layout 26.** The first writable open of an older index derives
|
|
89
|
+
every entry's links from its stored `document_json` in place, reading no
|
|
90
|
+
file: on the 23,979-entry snapshot index that takes 0.8 s and adds 3.5 MB.
|
|
91
|
+
Earlier layouts never stored a workflow's or a task's targets, so only the
|
|
92
|
+
directories holding workflows and tasks re-read on the next `akm index`.
|
|
93
|
+
The open leaves `index_meta.vacuumPending` like every layout migration.
|
|
94
|
+
0.9.17-alpha.4 through alpha.7 refuse an index at layout 26
|
|
95
|
+
(`INDEX_SCHEMA_INCOMPATIBLE`, naming the upgrade), for writing as well as
|
|
96
|
+
reading, so going back to one of them needs a new index: move `index.db`
|
|
97
|
+
aside and run `akm index` under that release (the LLM graph and enrichment
|
|
98
|
+
cache it held are not rebuilt by `akm index`).
|
|
99
|
+
(`src/storage/repositories/index-schema.ts`,
|
|
100
|
+
`src/storage/repositories/index-entry-schema.ts`)
|
|
101
|
+
|
|
102
|
+
### Fixed
|
|
103
|
+
|
|
104
|
+
- **`index.graph.enabled: false` stops graph extraction in `akm improve`.**
|
|
105
|
+
The switch was read only when improve had no strategy plan, which is never
|
|
106
|
+
the case in a real run, so the nightly and weekly graph tasks kept
|
|
107
|
+
extracting with it set. Improve now skips its graph extraction stage
|
|
108
|
+
whenever `index.graph.enabled` is `false`, whatever the strategy enables.
|
|
109
|
+
This also makes the graph ablation harness's "graph off" arm, which sets
|
|
110
|
+
exactly this key, turn extraction off. Improve still does not read
|
|
111
|
+
`index.defaults` when it picks the engine for graph extraction.
|
|
112
|
+
(`src/commands/improve/loop-stages.ts`)
|
|
113
|
+
- **Graph extraction never has more calls in flight than the run's
|
|
114
|
+
concurrency.** Batches run side by side up to the runner's concurrency, and
|
|
115
|
+
each batch also sent its per-file calls (long bodies, a non-array response)
|
|
116
|
+
up to that limit at once, so at a concurrency of 2 four calls could be in
|
|
117
|
+
flight. A batch now makes its per-file calls one at a time. At the default
|
|
118
|
+
concurrency of 1 nothing changes. (`src/llm/graph-extract.ts`)
|
|
119
|
+
- **A long document whose extraction partly failed is extracted again.** A
|
|
120
|
+
body over 1,600 characters is extracted in chunks. When some chunks failed
|
|
121
|
+
(a timeout, an error, an empty response) and others found entities, the
|
|
122
|
+
file was recorded and cached as extracted, so the failed chunks were never
|
|
123
|
+
retried. Such a file is now recorded as failed and not cached, and the next
|
|
124
|
+
run extracts it again, every chunk: partial failures are rare outside
|
|
125
|
+
provider outages, and keeping per-chunk results to skip the chunks that
|
|
126
|
+
succeeded would need a second cache. Until then the entities the other
|
|
127
|
+
chunks found are stored when the file had no graph rows yet; a file with
|
|
128
|
+
rows keeps them. (`src/llm/graph-extract.ts`)
|
|
129
|
+
- **`scripts/node-runtime/akm` could die from a raw signal instead of
|
|
130
|
+
forwarding it to its child.** The launcher registered its
|
|
131
|
+
SIGTERM/SIGINT/SIGHUP forwarding listeners only after spawning the child;
|
|
132
|
+
under load, a signal could arrive in that window and fall through to the
|
|
133
|
+
runtime's default (process-terminating) disposition, killing the launcher
|
|
134
|
+
before the child ever saw it. The listeners now go up before anything else
|
|
135
|
+
runs, with a small queue for a signal that arrives before the child exists.
|
|
136
|
+
- **`scripts/node-runtime/akm-migrate` carried the same pre-spawn signal
|
|
137
|
+
race** as `scripts/node-runtime/akm` above, for the same reason (listeners
|
|
138
|
+
registered only after `spawn()`), with no test covering it. Fixed the same
|
|
139
|
+
way, and added `tests/integration/akm-migrate-signal-forwarding.test.ts`
|
|
140
|
+
(modelled on `launcher-signal-forwarding.test.ts`) for its forwarding.
|
|
141
|
+
- **The launcher signal tests failed intermittently because of their own
|
|
142
|
+
fixture.** The fake child wrote its ready file before it registered its
|
|
143
|
+
signal handler, so a forwarded signal could reach it in between and kill it,
|
|
144
|
+
and the launcher then reported that signal. Each fixture now registers its
|
|
145
|
+
handler first. The launcher's pre-spawn window above could not cause this:
|
|
146
|
+
the tests signal only after the child is running.
|
|
147
|
+
|
|
148
|
+
## [0.9.17-alpha.7] - 2026-09-28
|
|
149
|
+
|
|
150
|
+
A scheduled task is now just a command and a schedule. Each native row
|
|
151
|
+
carries its own `AKM_BUNDLE_DIR` instead of pointing at a descriptor file,
|
|
152
|
+
and the runtime reads only v4 task files; older files convert once with
|
|
153
|
+
`akm migrate apply`. The first `akm task sync` after upgrading rewrites each
|
|
154
|
+
row once, keeping every task and schedule. `akm improve` reworks only assets
|
|
155
|
+
that retrieval returned or that are new, and reflect refuses a rewrite that
|
|
156
|
+
grades worse on the asset's own searches. `--require-engines` no longer
|
|
157
|
+
skips a run because the LLM endpoint is busy.
|
|
158
|
+
|
|
159
|
+
### Changed
|
|
160
|
+
|
|
161
|
+
- **akm reads only task source v4.** A `version: 2` or `version: 3` task
|
|
162
|
+
file, or a `version: 4` file that still carries 0.9.15's retired
|
|
163
|
+
`schedule[].enabled`, now fails on its own with a message naming
|
|
164
|
+
`akm migrate apply`, which converts it once, under a backup (`akm upgrade`
|
|
165
|
+
runs it after an install). Until now every read converted such a file in
|
|
166
|
+
memory. `akm task sync` reports each one as a failure, leaves its installed
|
|
167
|
+
row as it is, and keeps reconciling every other task; `akm task run`,
|
|
168
|
+
`akm lint` and `akm task validate` report it the same way, and
|
|
169
|
+
`akm task validate`'s `converts` outcome is gone (such a file is
|
|
170
|
+
`blocked`). `akm migrate apply` now also converts the tasks of the stash
|
|
171
|
+
`AKM_BUNDLE_DIR` selects when no configured bundle names it, since the
|
|
172
|
+
runtime reads those too, and a root whose top-level task files are all
|
|
173
|
+
v2/v3 is still detected as an `akm-task` bundle, so they are found and
|
|
174
|
+
converted. A host whose task files are all v4 (`akm migrate status`
|
|
175
|
+
reports `current`) sees no difference.
|
|
176
|
+
(`src/tasks/source/parse-task-source.ts`, `src/commands/tasks/validate.ts`,
|
|
177
|
+
`scripts/akm-migrate/task-migrate.ts`,
|
|
178
|
+
`src/core/adapter/adapters/akm-task-adapter.ts`)
|
|
179
|
+
- **A scheduled row is its command plus its schedule, and carries its own
|
|
180
|
+
context.** Rows no longer name a `--scheduler-context` descriptor file;
|
|
181
|
+
they set what it held themselves. Every row sets `AKM_BUNDLE_DIR` to the
|
|
182
|
+
working stash of the shell that ran `akm task sync`, plus any
|
|
183
|
+
`AKM_CONFIG_DIR`, `AKM_DATA_DIR`, `AKM_CACHE_DIR` or `AKM_STATE_DIR` that
|
|
184
|
+
shell set explicitly: a `VAR=value` prefix in the crontab, an
|
|
185
|
+
`EnvironmentVariables` entry in a launchd plist, a `$env:VAR='value';`
|
|
186
|
+
assignment ahead of the command in Task Scheduler (its action has no
|
|
187
|
+
environment of its own). Scheduled runs see the same environment as
|
|
188
|
+
before, and sync still tells installations sharing a crontab apart by
|
|
189
|
+
that path (#846).
|
|
190
|
+
**What hosts see:** the first `akm task sync` after upgrading rewrites
|
|
191
|
+
every akm row once. `akm task sync --dry-run` lists each one as an update,
|
|
192
|
+
never an add or a remove; each keeps its launcher and its schedule, and
|
|
193
|
+
the `--scheduler-context <file>` argument becomes an inline
|
|
194
|
+
`AKM_BUNDLE_DIR=<working stash>`. On a host whose working stash is
|
|
195
|
+
`/home/u/akm` a row changes from
|
|
196
|
+
|
|
197
|
+
```text
|
|
198
|
+
30 8 * * * /home/u/.bun/bin/bun /home/u/.bun/lib/node_modules/akm-cli/dist/akm --scheduler-context /home/u/.local/share/akm/tasks/context/e898….json task run capture --bundle akm --scheduled > /home/u/.cache/akm/tasks/logs/capture.log 2>&1
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
to
|
|
202
|
+
|
|
203
|
+
```text
|
|
204
|
+
30 8 * * * AKM_BUNDLE_DIR=/home/u/akm /home/u/.bun/bin/bun /home/u/.bun/lib/node_modules/akm-cli/dist/akm task run capture --bundle akm --scheduled > /home/u/.cache/akm/tasks/logs/capture.log 2>&1
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Rows written by 0.9.0 through 0.9.17-alpha.6 keep firing until that sync:
|
|
208
|
+
the CLI still accepts `--scheduler-context <file>` and applies the file's
|
|
209
|
+
environment (PATH included, for a 0.9.16 row). The files under
|
|
210
|
+
`$DATA/tasks/context/` are no longer written, and the uid, mode, symlink
|
|
211
|
+
and content-hash checks made on every scheduled run are gone. A sync
|
|
212
|
+
leaves some rows as they are (one whose task file failed to load, one a
|
|
213
|
+
`--bundle` sync did not cover), and those still name their file: once
|
|
214
|
+
`akm task doctor` lists no binding with a `contextPath`, nothing reads
|
|
215
|
+
them and they can be deleted. `akm task prune` now
|
|
216
|
+
finds rows whose `AKM_BUNDLE_DIR` names a directory that is gone, and older
|
|
217
|
+
rows whose descriptor cannot be read; `akm task doctor` lists `contextPath`
|
|
218
|
+
only for an older row. (`src/tasks/scheduler-invocation.ts`,
|
|
219
|
+
`src/tasks/backends/cron.ts`, `src/tasks/backends/launchd.ts`,
|
|
220
|
+
`src/tasks/backends/schtasks.ts`, `src/tasks/scheduler-sync.ts`,
|
|
221
|
+
`src/commands/tasks/tasks.ts`)
|
|
222
|
+
- **`akm improve` reworks only what gets read (#986).** An asset with fresh
|
|
223
|
+
feedback, or one you name (`akm improve skills/x`), is handled as before.
|
|
224
|
+
Every other pick must now be in the retrieval scope. That covers the
|
|
225
|
+
proactive-maintenance, high-salience and forgetting-safety lanes, and the
|
|
226
|
+
memories consolidation judges. An asset is in scope if a user `search`,
|
|
227
|
+
`curate` or `show` returned it, or user `feedback` named it, in the last 90
|
|
228
|
+
days, which is the usage log's retention. A hit on a `.derived` memory counts
|
|
229
|
+
for its parent. New material that no improve stage has processed yet is also
|
|
230
|
+
in scope. There is no new config key.
|
|
231
|
+
|
|
232
|
+
Measured with `akm improve --dry-run` on a copy of the maintainer's bundle
|
|
233
|
+
(19,870 assets), against 0.9.17-alpha.6:
|
|
234
|
+
- The fallback lanes pick from 6,450 assets instead of 15,686, and 9,236
|
|
235
|
+
refs are left out. No lane setting can reach the unread tail any more. In
|
|
236
|
+
July the proactive lane rewrote 3,069 assets, and 3,059 of them had not
|
|
237
|
+
been retrieved since the usage log began on 1 July.
|
|
238
|
+
- Consolidation judges 59 memories instead of 69.
|
|
239
|
+
- The high-salience lane no longer admits distill outputs nobody has read (2
|
|
240
|
+
today).
|
|
241
|
+
- Under the scheduled caps, today's nightly work is unchanged. The default
|
|
242
|
+
strategy selects the same 50 feedback-driven refs, and weekly proactive
|
|
243
|
+
maintenance selects the same 25, because salience ranking already puts
|
|
244
|
+
retrieved assets first.
|
|
245
|
+
|
|
246
|
+
`akm improve --dry-run` and the run result report the left-out refs as a new
|
|
247
|
+
`retrieval` gate. Health reports them under the skip reason `not_retrieved`.
|
|
248
|
+
Improve results stored by earlier releases, which have no such gate, still
|
|
249
|
+
decode. (`src/commands/improve/retrieval-scope.ts`,
|
|
250
|
+
`src/commands/improve/preparation.ts`, `src/commands/improve/consolidate.ts`)
|
|
251
|
+
|
|
252
|
+
- **Reflect refuses a rewrite that makes an asset worse for its own searches
|
|
253
|
+
(#722).** Before reflect proposes a rewrite of an existing asset, it grades
|
|
254
|
+
the old and the new content on up to five of the queries that actually
|
|
255
|
+
retrieved the asset (user `search` and `curate`). It uses the retrieval
|
|
256
|
+
eval's relevance prompt, which agrees with human grades at kappa 0.83. When
|
|
257
|
+
the new content grades lower on average, the rewrite is refused the same way
|
|
258
|
+
a quality-judge rejection is: `quality_rejected`, with the 14-day reflect
|
|
259
|
+
window. An asset without retrieval queries is not graded.
|
|
260
|
+
|
|
261
|
+
This was measured before it was built. Of 60 accepted rewrites since July,
|
|
262
|
+
judged this way, 14 graded lower (23%, 95% CI 14–35%) and 12 graded higher.
|
|
263
|
+
The gate was built because the lower bound cleared the 10% threshold set
|
|
264
|
+
before any judging. It costs two judge calls per query on the engine that
|
|
265
|
+
already runs the quality judge. On the maintainer's 2026-09-28 nightly run,
|
|
266
|
+
whose 30 rewrites had 102 usable queries, that is 204 calls, about 6 more
|
|
267
|
+
minutes on a 73-minute run. (`src/commands/improve/retrieval-gate.ts`,
|
|
268
|
+
`src/commands/improve/reflect.ts`)
|
|
269
|
+
|
|
270
|
+
### Fixed
|
|
271
|
+
|
|
272
|
+
- **A cron row too long for one line is seen by `akm task sync` again.** A
|
|
273
|
+
command over 1,000 bytes runs from a wrapper script, and sync could not
|
|
274
|
+
read which task such a row ran: every sync, and every `--dry-run`, showed
|
|
275
|
+
it as an add and wrote it again. Sync now reads the script, so the row is
|
|
276
|
+
unchanged or an update like any other. (`src/tasks/backends/cron.ts`)
|
|
277
|
+
- **A `$` or a backslash in a scheduled row's value is kept.** launchd and
|
|
278
|
+
Task Scheduler rows passed their values through a string replacement that
|
|
279
|
+
read `$'`, `$&` and `$$` as patterns, so a path such as a Windows admin
|
|
280
|
+
share (`\\nas\share\akm$`) came out corrupted; reading a crontab row
|
|
281
|
+
back dropped a backslash inside a single-quoted value.
|
|
282
|
+
(`src/tasks/backends/launchd.ts`, `src/tasks/backends/schtasks.ts`,
|
|
283
|
+
`src/tasks/backends/cron.ts`)
|
|
284
|
+
- **`akm improve --require-engines` no longer skips a run because the LLM
|
|
285
|
+
endpoint is busy.** Its reachability probe, one short completion, gave up
|
|
286
|
+
after 3 seconds, so a local server busy with another job looked
|
|
287
|
+
unreachable and the whole scheduled run failed (all four scheduled runs on
|
|
288
|
+
2026-09-27). The probe now waits up to the engine's own `timeoutMs`, at
|
|
289
|
+
most two minutes, so a busy server can answer while a hung one still fails
|
|
290
|
+
fast. The error's hint now says to check the endpoint rather than to run
|
|
291
|
+
`akm setup`.
|
|
292
|
+
(`src/commands/improve/improve-cli.ts`)
|
|
293
|
+
|
|
9
294
|
## [0.9.17-alpha.6] - 2026-09-27
|
|
10
295
|
|
|
11
296
|
Graph extraction stops losing and wasting work. A timed-out extraction is
|
package/STABILITY.md
CHANGED
|
@@ -281,8 +281,8 @@ CHANGELOG with a migration note.
|
|
|
281
281
|
print unredacted. `akm task validate <path>` (new in 0.9.11) is the same
|
|
282
282
|
kind of zero-write introspection as `explain`, but takes a bare filesystem
|
|
283
283
|
path rather than a bundle-qualified ref — it reports whether that ONE file
|
|
284
|
-
would parse cleanly (`valid`),
|
|
285
|
-
|
|
284
|
+
would parse cleanly (`valid`), need `akm migrate apply` first (`blocked`:
|
|
285
|
+
task v2/v3, or a retired `schedule[].enabled`),
|
|
286
286
|
fail schema validation (`invalid`), or isn't a task source at all
|
|
287
287
|
(`not-a-task`) — exactly the diagnostic `akm task sync` would produce for
|
|
288
288
|
it, before the file is ever wired into a bundle or the scheduler.
|
package/dist/akm
CHANGED
|
@@ -6,13 +6,55 @@
|
|
|
6
6
|
import { spawn, spawnSync } from "node:child_process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
9
|
+
// #956 round 3: install the signal-forwarding scaffolding before ANYTHING
|
|
10
|
+
// else in this process — env var setup, the bun-version probe, spawning the
|
|
11
|
+
// child — gets a chance to run. The three `process.once` listeners used to
|
|
12
|
+
// go up only after spawn() (below) returned; a scheduler preemption right
|
|
13
|
+
// after that syscall was enough for a signal to arrive with no listener yet,
|
|
14
|
+
// and the runtime's default (process-terminating) disposition killed the
|
|
15
|
+
// launcher outright, never reaching the child at all. `child` starts
|
|
16
|
+
// unset: a signal that arrives before it exists is queued and flushed the
|
|
17
|
+
// moment it does; if this process ends up never spawning one (the
|
|
18
|
+
// direct-import branch below), the queued signal is re-raised against
|
|
19
|
+
// ourselves once we hand default disposition back, so it behaves exactly
|
|
20
|
+
// like the runtime's own default rather than being silently swallowed.
|
|
21
|
+
let child;
|
|
22
|
+
let childExited = false;
|
|
23
|
+
const pendingSignals = [];
|
|
24
|
+
const forwardHandlers = new Map();
|
|
25
|
+
const forwardSignal = (signal) => {
|
|
26
|
+
if (childExited) return;
|
|
27
|
+
if (!child) {
|
|
28
|
+
pendingSignals.push(signal);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
child.kill(signal);
|
|
33
|
+
} catch {
|
|
34
|
+
// Child exited in the race between the check above and here.
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
// `.once`, not `.on`: Node/Bun suppress a signal's default
|
|
38
|
+
// (process-terminating) disposition for as long as ANY listener stays
|
|
39
|
+
// registered for it. A persistent `.on` listener would still be registered
|
|
40
|
+
// when the `process.kill(process.pid, result.signal)` re-raise below runs at
|
|
41
|
+
// shutdown, swallowing it and leaving this launcher exiting 0 instead of
|
|
42
|
+
// reflecting the child's signal. `.once` consumes only the
|
|
43
|
+
// externally-delivered signal that triggers the forward, so the re-raise
|
|
44
|
+
// correctly falls through to the OS default.
|
|
45
|
+
for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) {
|
|
46
|
+
const handler = () => forwardSignal(signal);
|
|
47
|
+
forwardHandlers.set(signal, handler);
|
|
48
|
+
process.once(signal, handler);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// A scheduled row sets its environment itself (a cron `VAR=value` prefix, a
|
|
52
|
+
// launchd plist's EnvironmentVariables), and the scheduler supplies PATH, so
|
|
53
|
+
// runtime selection below needs nothing from it. A row written by 0.9.0 –
|
|
54
|
+
// 0.9.17-alpha.6 passes `--scheduler-context <descriptor>` instead; it passes
|
|
55
|
+
// through untouched, and the CLI applies it (`consumeSchedulerContextArg` in
|
|
56
|
+
// src/cli.ts). This launcher used to re-validate the descriptor with its own
|
|
57
|
+
// copy of the schema, and that copy rejected every descriptor 0.9.17 wrote.
|
|
16
58
|
|
|
17
59
|
process.env.AKM_LAUNCHER_NODE = process.execPath;
|
|
18
60
|
process.env.AKM_LAUNCHER_PATH = fileURLToPath(import.meta.url);
|
|
@@ -42,13 +84,21 @@ const bunEntry = fileURLToPath(new URL("./cli.js", import.meta.url));
|
|
|
42
84
|
const nodeEntry = fileURLToPath(new URL("./cli-node.mjs", import.meta.url));
|
|
43
85
|
|
|
44
86
|
if (!useBun && !process.versions.bun) {
|
|
45
|
-
|
|
87
|
+
// No child is ever spawned on this path — the imported module runs
|
|
88
|
+
// in-process, so it IS the work, and the runtime's ordinary default signal
|
|
89
|
+
// disposition is exactly correct for it. Hand that back (the forwarding
|
|
90
|
+
// listeners above no longer apply here), re-raising anything that already
|
|
91
|
+
// arrived so it is not silently swallowed by a listener that now does
|
|
92
|
+
// nothing.
|
|
93
|
+
for (const [signal, handler] of forwardHandlers) process.removeListener(signal, handler);
|
|
94
|
+
if (pendingSignals.length > 0) process.kill(process.pid, pendingSignals[0]);
|
|
95
|
+
else await import("./cli-node.mjs");
|
|
46
96
|
} else {
|
|
47
97
|
const command = useBun ? (process.versions.bun ? process.execPath : "bun") : "node";
|
|
48
98
|
const entry = useBun ? bunEntry : nodeEntry;
|
|
49
99
|
const runtime = useBun ? "Bun" : "Node.js";
|
|
50
100
|
const result = await new Promise((resolve) => {
|
|
51
|
-
|
|
101
|
+
child = spawn(command, [entry, ...process.argv.slice(2)], {
|
|
52
102
|
stdio: "inherit",
|
|
53
103
|
env: process.env,
|
|
54
104
|
// #956: give the child its OWN process group on POSIX (`setsid()` —
|
|
@@ -75,29 +125,12 @@ if (!useBun && !process.versions.bun) {
|
|
|
75
125
|
// forward once the child has already exited: forwarding to a
|
|
76
126
|
// dead/replaced pid would be at best a no-op and at worst a signal to
|
|
77
127
|
// an unrelated process that reused the pid.
|
|
78
|
-
let childExited = false;
|
|
79
128
|
child.once("exit", () => {
|
|
80
129
|
childExited = true;
|
|
81
130
|
});
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
child.kill(signal);
|
|
86
|
-
} catch {
|
|
87
|
-
// Child exited in the race between the check above and here.
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
// `.once`, not `.on`: Node/Bun suppress a signal's default
|
|
91
|
-
// (process-terminating) disposition for as long as ANY listener stays
|
|
92
|
-
// registered for it. A persistent `.on` listener would still be
|
|
93
|
-
// registered when the `process.kill(process.pid, result.signal)`
|
|
94
|
-
// re-raise below runs at shutdown, swallowing it and leaving this
|
|
95
|
-
// launcher exiting 0 instead of reflecting the child's signal. `.once`
|
|
96
|
-
// consumes only the externally-delivered signal that triggers the
|
|
97
|
-
// forward, so the re-raise correctly falls through to the OS default.
|
|
98
|
-
for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) {
|
|
99
|
-
process.once(signal, () => forwardSignal(signal));
|
|
100
|
-
}
|
|
131
|
+
// Flush whatever arrived in the (now much smaller) window between the
|
|
132
|
+
// listeners going up and the child existing to receive them.
|
|
133
|
+
for (const signal of pendingSignals.splice(0)) forwardSignal(signal);
|
|
101
134
|
child.once("error", (error) => resolve({ error }));
|
|
102
135
|
child.once("exit", (code, signal) => resolve({ code, signal }));
|
|
103
136
|
});
|
package/dist/akm-migrate
CHANGED
|
@@ -6,6 +6,40 @@
|
|
|
6
6
|
import { spawn, spawnSync } from "node:child_process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
|
|
9
|
+
// #956 round 3: install the signal-forwarding scaffolding before ANYTHING
|
|
10
|
+
// else in this process — the Node-version check, the bun-version probe,
|
|
11
|
+
// spawning the child — gets a chance to run. See the matching fix and
|
|
12
|
+
// comment in scripts/node-runtime/akm: the three `process.once` listeners
|
|
13
|
+
// used to go up only after spawn() (below) returned; a scheduler preemption
|
|
14
|
+
// right after that syscall was enough for a signal to arrive with no
|
|
15
|
+
// listener yet, and the runtime's default (process-terminating) disposition
|
|
16
|
+
// killed the launcher outright, never reaching the child at all. `child`
|
|
17
|
+
// starts unset: a signal that arrives before it exists is queued and
|
|
18
|
+
// flushed the moment it does.
|
|
19
|
+
let child;
|
|
20
|
+
let childExited = false;
|
|
21
|
+
const pendingSignals = [];
|
|
22
|
+
const forwardSignal = (signal) => {
|
|
23
|
+
if (childExited) return;
|
|
24
|
+
if (!child) {
|
|
25
|
+
pendingSignals.push(signal);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
child.kill(signal);
|
|
30
|
+
} catch {
|
|
31
|
+
// Child exited in the race between the check above and here.
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
// `.once`, not `.on`: see the matching comment in scripts/node-runtime/akm —
|
|
35
|
+
// a persistent `.on` listener would still be registered when the
|
|
36
|
+
// `process.kill(process.pid, result.signal)` re-raise below runs,
|
|
37
|
+
// suppressing the OS default disposition and leaving this launcher exiting
|
|
38
|
+
// 0 instead of reflecting the child's signal.
|
|
39
|
+
for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) {
|
|
40
|
+
process.once(signal, () => forwardSignal(signal));
|
|
41
|
+
}
|
|
42
|
+
|
|
9
43
|
if (!process.versions.bun) {
|
|
10
44
|
const [major = 0, minor = 0] = process.versions.node.split(".").map(Number);
|
|
11
45
|
if (major < 22) {
|
|
@@ -34,7 +68,7 @@ const nodeEntry = fileURLToPath(new URL("./scripts/akm-migrate-node.js", import.
|
|
|
34
68
|
const command = process.versions.bun ? process.execPath : useBun ? "bun" : process.execPath;
|
|
35
69
|
const entry = process.versions.bun || useBun ? bunEntry : nodeEntry;
|
|
36
70
|
const result = await new Promise((resolve) => {
|
|
37
|
-
|
|
71
|
+
child = spawn(command, [entry, ...process.argv.slice(2)], {
|
|
38
72
|
stdio: "inherit",
|
|
39
73
|
env,
|
|
40
74
|
// #956: own process group on POSIX so the launcher's forward below is
|
|
@@ -43,27 +77,12 @@ const nodeEntry = fileURLToPath(new URL("./scripts/akm-migrate-node.js", import.
|
|
|
43
77
|
// directly — see the matching comment in scripts/node-runtime/akm.
|
|
44
78
|
detached: process.platform !== "win32",
|
|
45
79
|
});
|
|
46
|
-
let childExited = false;
|
|
47
80
|
child.once("exit", () => {
|
|
48
81
|
childExited = true;
|
|
49
82
|
});
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
child.kill(signal);
|
|
54
|
-
} catch {
|
|
55
|
-
// Child exited in the race between the check above and here.
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
// `.once`, not `.on`: see the matching comment in
|
|
59
|
-
// scripts/node-runtime/akm — a persistent `.on` listener would still be
|
|
60
|
-
// registered when the `process.kill(process.pid, result.signal)`
|
|
61
|
-
// re-raise below runs, suppressing the OS default disposition and
|
|
62
|
-
// leaving this launcher exiting 0 instead of reflecting the child's
|
|
63
|
-
// signal.
|
|
64
|
-
for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) {
|
|
65
|
-
process.once(signal, () => forwardSignal(signal));
|
|
66
|
-
}
|
|
83
|
+
// Flush whatever arrived in the (now much smaller) window between the
|
|
84
|
+
// listeners going up and the child existing to receive them.
|
|
85
|
+
for (const signal of pendingSignals.splice(0)) forwardSignal(signal);
|
|
67
86
|
child.once("error", (error) => resolve({ error }));
|
|
68
87
|
child.once("exit", (code, signal) => resolve({ code, signal }));
|
|
69
88
|
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
You are grading search-and-retrieval results for an AI coding agent's knowledge base (the akm tool). For the given query and ONE candidate asset, grade how useful loading this asset would be, on this scale:
|
|
2
|
+
3 = exactly the asset an agent should load for this query or task; it directly answers or performs the request.
|
|
3
|
+
2 = relevant and clearly useful, though not the single best asset for the query.
|
|
4
|
+
1 = same general topic as the query, but would not actually help complete this specific query or task.
|
|
5
|
+
0 = unrelated to the query.
|
|
6
|
+
Reply with ONLY a JSON object: {"grade": <integer 0-3>, "reason": "<=25 words"}.
|
|
@@ -3,7 +3,7 @@ type: fact
|
|
|
3
3
|
category: convention
|
|
4
4
|
description: How to cross-link assets so retrieval compounds — a provenance xref when derived, sparse real associative xrefs, corrections as new assets, and canonical entity naming.
|
|
5
5
|
when_to_use: Surfaced to authoring agents when they create or revise any asset that derives from, corrects, or relates to another asset.
|
|
6
|
-
updated: 2026-
|
|
6
|
+
updated: 2026-09-28
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
<!--
|
|
@@ -19,8 +19,10 @@ updated: 2026-07-28
|
|
|
19
19
|
|
|
20
20
|
Cross-references are how knowledge compounds instead of being re-derived every
|
|
21
21
|
session. In AKM they are also **indexed**: the strings in an asset's `xrefs:`
|
|
22
|
-
frontmatter fold into its search-hint text
|
|
23
|
-
|
|
22
|
+
frontmatter fold into its search-hint text and are stored as links that
|
|
23
|
+
`akm show` lists on both assets (with `supersededBy:`, `contradictedBy:` and a
|
|
24
|
+
`.derived` memory's parent), and knowledge/memory bodies feed an LLM-extracted
|
|
25
|
+
entity/relation graph that boosts ranking. So links are a retrieval
|
|
24
26
|
lever — which means both too few and too many hurt.
|
|
25
27
|
|
|
26
28
|
```yaml
|
|
@@ -26,6 +26,7 @@ import { warn, warnVerbose } from "../../core/warn.js";
|
|
|
26
26
|
import { resolveWriteTarget } from "../../core/write-source.js";
|
|
27
27
|
import { deriveInstallations } from "../../indexer/installations.js";
|
|
28
28
|
import { resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
29
|
+
import { USAGE_EVENT_RETENTION_DAYS } from "../../indexer/usage/usage-events.js";
|
|
29
30
|
import { assertRunnerCredentials } from "../../integrations/agent/runner-dispatch.js";
|
|
30
31
|
import { cosineSimilarity, embedBatch, resolveEmbeddingModelId } from "../../llm/embedder.js";
|
|
31
32
|
import { getBodyEmbeddings, upsertBodyEmbeddings } from "../../storage/repositories/embeddings-repository.js";
|
|
@@ -39,6 +40,7 @@ import { sanitizeMergedContent } from "./consolidate/sanitize.js";
|
|
|
39
40
|
import { contentHash } from "./content-hash.js";
|
|
40
41
|
import { resolveImproveStrategy, resolveProcessEnabled } from "./improve-strategies.js";
|
|
41
42
|
import { isLedgerBlocked, ledgerKey, loadLedgerSnapshot, recordLedgerAttempt } from "./ledger.js";
|
|
43
|
+
import { isInRetrievalScope, loadRetrievalScope } from "./retrieval-scope.js";
|
|
42
44
|
import { callStage, mintProposal, noticeSet, stageRunner } from "./stage.js";
|
|
43
45
|
/** A plan op worth acting on. Retired advisory ops (merge/delete/contradict) are dropped, never thrown on. */
|
|
44
46
|
export function isValidOp(op) {
|
|
@@ -428,6 +430,11 @@ export function inspectConsolidationPool(opts, stashDir, warnings, existingKnowl
|
|
|
428
430
|
});
|
|
429
431
|
}
|
|
430
432
|
const judgedUnchanged = poolSize - memories.length;
|
|
433
|
+
// Only what retrieval returned or new material improve never processed (#986).
|
|
434
|
+
const retrievalScope = loadRetrievalScope({ proposalsCtx: opts.proposalsCtx, readOnly }, stashDir);
|
|
435
|
+
const beforeScope = memories.length;
|
|
436
|
+
memories = memories.filter((memory) => isInRetrievalScope(retrievalScope, conceptIdFromTypeName("memory", memory.name), memory.filePath));
|
|
437
|
+
const outsideRetrievalScope = beforeScope - memories.length;
|
|
431
438
|
if (opts.incrementalSince && memories.length > 0) {
|
|
432
439
|
memories = narrowToIncrementalCandidates(memories, opts.incrementalSince, warnings, opts.neighborsPerChanged, readOnly);
|
|
433
440
|
}
|
|
@@ -465,6 +472,7 @@ export function inspectConsolidationPool(opts, stashDir, warnings, existingKnowl
|
|
|
465
472
|
memories,
|
|
466
473
|
prefilteredAlreadyPromoted,
|
|
467
474
|
judgedUnchanged,
|
|
475
|
+
outsideRetrievalScope,
|
|
468
476
|
};
|
|
469
477
|
}
|
|
470
478
|
const ABORT_MIN_CHUNKS = 4;
|
|
@@ -641,6 +649,9 @@ async function consolidate(opts, config, stashDir, startMs, stateDb) {
|
|
|
641
649
|
if (pool.judgedUnchanged > 0) {
|
|
642
650
|
warnings.push(`Consolidation: skipped ${pool.judgedUnchanged} ${plural(pool.judgedUnchanged)} judged within the revisit window and unchanged since.`);
|
|
643
651
|
}
|
|
652
|
+
if (pool.outsideRetrievalScope > 0) {
|
|
653
|
+
warnings.push(`Consolidation: skipped ${pool.outsideRetrievalScope} ${plural(pool.outsideRetrievalScope)} already judged that retrieval has not returned in the last ${USAGE_EVENT_RETENTION_DAYS} days.`);
|
|
654
|
+
}
|
|
644
655
|
if (prefilteredAlreadyPromoted > 0) {
|
|
645
656
|
warnings.push(`Consolidation: pre-filtered ${prefilteredAlreadyPromoted} ${plural(prefilteredAlreadyPromoted)} whose body already exists verbatim in knowledge/ before chunking.`);
|
|
646
657
|
}
|