akm-cli 0.9.17-alpha.7 → 0.9.17-alpha.9
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 +473 -0
- package/STABILITY.md +9 -8
- package/dist/akm +55 -22
- package/dist/akm-migrate +38 -19
- package/dist/assets/hints/cli-hints-full.md +6 -7
- package/dist/assets/improve-strategies/catchup.json +0 -3
- package/dist/assets/improve-strategies/consolidate.json +0 -1
- package/dist/assets/improve-strategies/default.json +1 -2
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -2
- package/dist/assets/improve-strategies/quick.json +1 -2
- package/dist/assets/improve-strategies/reflect-distill.json +1 -2
- package/dist/assets/improve-strategies/thorough.json +0 -3
- package/dist/assets/prompts/consolidate-pair.md +20 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +20 -20
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -2
- package/dist/assets/templates/html/health.html +3 -5
- package/dist/cli/retired-commands.js +1 -1
- package/dist/commands/health/archive-usage.js +98 -0
- package/dist/commands/health/data-dir-usage.js +25 -13
- package/dist/commands/health/html-report.js +1 -4
- package/dist/commands/health/improve-metrics.js +0 -25
- package/dist/commands/health/md-report.js +1 -6
- package/dist/commands/health/report-view-model.js +4 -14
- package/dist/commands/health/windows.js +0 -1
- package/dist/commands/health.js +13 -0
- package/dist/commands/improve/consolidate/continuity-check.js +137 -0
- package/dist/commands/improve/consolidate/pair-pass.js +791 -0
- package/dist/commands/improve/consolidate.js +38 -63
- package/dist/commands/improve/extract-prompt.js +1 -2
- package/dist/commands/improve/improve-cli.js +1 -1
- package/dist/commands/improve/improve-strategies.js +23 -5
- package/dist/commands/improve/improve.js +19 -30
- package/dist/commands/improve/ledger.js +3 -2
- package/dist/commands/improve/loop-stages.js +5 -84
- package/dist/commands/improve/memory/memory-belief.js +3 -1
- package/dist/commands/improve/memory/memory-improve.js +269 -11
- package/dist/commands/improve/planner.js +0 -5
- package/dist/commands/improve/preparation.js +20 -135
- package/dist/commands/improve/retrieval-scope.js +19 -4
- package/dist/commands/improve/salience.js +1 -14
- package/dist/commands/improve/stage.js +0 -1
- package/dist/commands/lint/base-linter.js +19 -11
- package/dist/commands/proposal/drain.js +8 -1
- package/dist/commands/proposal/proposal-cli.js +16 -2
- package/dist/commands/proposal/proposal-types.js +7 -0
- package/dist/commands/proposal/proposal.js +37 -6
- package/dist/commands/proposal/repository.js +613 -4
- package/dist/commands/proposal/validators/proposals.js +9 -0
- package/dist/commands/read/curate.js +40 -13
- package/dist/commands/read/knowledge.js +3 -2
- package/dist/commands/read/show.js +55 -16
- package/dist/commands/sources/info.js +3 -0
- package/dist/commands/sources/stash-cli.js +2 -2
- package/dist/core/adapter/adapters/akm-adapter.js +2 -0
- package/dist/core/adapter/adapters/akm-metadata.js +31 -0
- package/dist/core/bundle-rename.js +1 -7
- package/dist/core/config/config-schema.js +8 -1
- package/dist/core/config/config.js +23 -48
- package/dist/core/config/engine-semantics.js +0 -2
- package/dist/core/config/schema/improve-processes.js +17 -42
- package/dist/core/config/schema/index-config.js +5 -25
- package/dist/core/file-change.js +13 -5
- package/dist/core/improve-result.js +16 -5
- package/dist/core/improve-types.js +0 -1
- package/dist/core/loopback.js +7 -12
- package/dist/core/parse.js +13 -16
- package/dist/core/state/migrations.js +15 -0
- package/dist/core/time.js +0 -20
- package/dist/indexer/db/llm-cache.js +2 -2
- package/dist/indexer/ensure-index.js +2 -2
- package/dist/indexer/index-written-assets.js +2 -3
- package/dist/indexer/indexer.js +18 -418
- package/dist/indexer/links/declared-links.js +90 -0
- package/dist/indexer/passes/metadata.js +0 -19
- package/dist/indexer/scan/doc-to-entry.js +1 -0
- package/dist/indexer/walk/walker.js +3 -4
- package/dist/llm/client.js +8 -10
- package/dist/llm/embedders/remote.js +1 -2
- package/dist/llm/feature-gate.js +0 -5
- package/dist/output/shapes/helpers.js +23 -4
- package/dist/output/text/command-format.js +0 -8
- package/dist/output/text/proposal-format.js +47 -1
- package/dist/output/text/show-format.js +13 -17
- package/dist/scripts/akm-migrate-node.js +2754 -2836
- package/dist/scripts/akm-migrate.js +2754 -2836
- package/dist/setup/steps/connection.js +5 -6
- package/dist/setup/steps/platforms.js +2 -2
- package/dist/sources/providers/git-stash.js +55 -4
- package/dist/storage/repositories/improve-ledger-repository.js +48 -7
- package/dist/storage/repositories/index-entries-repository.js +16 -13
- package/dist/storage/repositories/index-entry-schema.js +22 -3
- package/dist/storage/repositories/index-links-repository.js +143 -0
- package/dist/storage/repositories/index-llm-cache-repository.js +7 -26
- package/dist/storage/repositories/index-schema.js +82 -104
- package/dist/storage/repositories/proposals-repository.js +61 -0
- package/dist/storage/repositories/salience-repository.js +1 -19
- package/dist/tasks/source/task-to-v4.js +462 -74
- package/docs/migration/release-notes/0.9.17.md +7 -5
- package/docs/reference/cli.md +33 -21
- package/docs/reference/configuration.md +21 -12
- package/docs/reference/data-and-telemetry.md +0 -1
- package/package.json +1 -1
- package/schemas/akm-config.json +0 -342
- package/dist/assets/improve-strategies/graph-refresh.json +0 -15
- package/dist/assets/prompts/contradiction-judge.md +0 -33
- package/dist/assets/prompts/graph-extract-system.md +0 -1
- package/dist/assets/prompts/graph-extract-user-prompt.md +0 -35
- package/dist/assets/prompts/metadata-enhance-system.md +0 -1
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +0 -4
- package/dist/indexer/db/graph-db.js +0 -431
- package/dist/indexer/graph/graph-extraction.js +0 -807
- package/dist/indexer/graph/graph-related.js +0 -131
- package/dist/indexer/graph/graph-types.js +0 -4
- package/dist/llm/graph-extract.js +0 -903
- package/dist/llm/metadata-enhance.js +0 -95
- package/dist/tasks/source/task-to-v3.js +0 -453
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,479 @@ 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`)
|
|
323
|
+
|
|
324
|
+
### Fixed
|
|
325
|
+
|
|
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.
|
|
354
|
+
|
|
355
|
+
### Added
|
|
356
|
+
|
|
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
|
+
|
|
9
482
|
## [0.9.17-alpha.7] - 2026-09-28
|
|
10
483
|
|
|
11
484
|
A scheduled task is now just a command and a schedule. Each native row
|
package/STABILITY.md
CHANGED
|
@@ -366,8 +366,8 @@ for scripted use.
|
|
|
366
366
|
### `akm improve` autonomy — opt-in in 0.9.0
|
|
367
367
|
|
|
368
368
|
**`akm improve` is review-first by default in 0.9.0.** The command itself is ON
|
|
369
|
-
— its schedules
|
|
370
|
-
|
|
369
|
+
— its schedules and reflect/distill proposals all run — but the lanes that
|
|
370
|
+
mutate assets *without* review require an explicit opt-in:
|
|
371
371
|
|
|
372
372
|
```sh
|
|
373
373
|
akm config set experimental.improveAutonomy true
|
|
@@ -388,9 +388,10 @@ review-first config correctly shows `queue`.
|
|
|
388
388
|
| memory cleanup | Belief-state frontmatter rewrites, archive moves | analyzed but not applied |
|
|
389
389
|
| `triage` `applyMode: "promote"` | Auto-accepts queued proposals into the bundle | downgraded to `queue` — triage still runs, it just does not auto-accept |
|
|
390
390
|
|
|
391
|
-
Consolidation remains enabled with autonomy off
|
|
392
|
-
|
|
393
|
-
proposal
|
|
391
|
+
Consolidation remains enabled with autonomy off: both its passes (promotion,
|
|
392
|
+
and the pair pass's duplicate/subsumed/supersedes judging) only ever emit a
|
|
393
|
+
reviewable proposal, and a pair-pass `retire` proposal is never auto-accepted
|
|
394
|
+
by `triage` `applyMode: "promote"` regardless of this gate.
|
|
394
395
|
|
|
395
396
|
Because the gate is applied before the LLM preflight, a review-first workspace
|
|
396
397
|
also needs fewer engines configured: a strategy whose only model-backed process
|
|
@@ -409,9 +410,9 @@ Autonomy is never inferred: an absent `experimental` section, an absent key, and
|
|
|
409
410
|
an explicit `false` all read as off, so a partially-written or older config is
|
|
410
411
|
review-first rather than accidentally permissive.
|
|
411
412
|
|
|
412
|
-
Reflect, distill, extract candidates, validation, proactive-maintenance
|
|
413
|
-
selection
|
|
414
|
-
|
|
413
|
+
Reflect, distill, extract candidates, validation, and proactive-maintenance
|
|
414
|
+
selection are proposal-only and never write assets directly. Two further
|
|
415
|
+
direct writes are ungated by design: `extract`'s session
|
|
415
416
|
indexing (additive `sessions/**` writes,
|
|
416
417
|
`processes.extract.indexSessions`, default on) and distill's
|
|
417
418
|
encoding-salience frontmatter stamp (metadata only).
|
package/dist/akm
CHANGED
|
@@ -6,6 +6,48 @@
|
|
|
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 — 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
|
+
|
|
9
51
|
// A scheduled row sets its environment itself (a cron `VAR=value` prefix, a
|
|
10
52
|
// launchd plist's EnvironmentVariables), and the scheduler supplies PATH, so
|
|
11
53
|
// runtime selection below needs nothing from it. A row written by 0.9.0 –
|
|
@@ -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
|
});
|