akm-cli 0.9.7 → 0.9.8-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +218 -0
- package/dist/commands/env/env.js +2 -21
- package/dist/commands/health/checks.js +0 -25
- package/dist/commands/health/improve-metrics.js +8 -34
- package/dist/commands/health/windows.js +0 -4
- package/dist/commands/health.js +1 -35
- package/dist/commands/improve/consolidate/eligibility.js +11 -5
- package/dist/commands/improve/extract.js +36 -32
- package/dist/commands/improve/memory/memory-belief.js +15 -5
- package/dist/commands/improve/memory/memory-contradiction-detect.js +16 -25
- package/dist/commands/improve/memory/memory-improve.js +7 -19
- package/dist/commands/improve/preparation.js +52 -36
- package/dist/commands/improve/reflect.js +13 -17
- package/dist/commands/lint/base-linter.js +42 -2
- package/dist/commands/lint/index.js +4 -5
- package/dist/commands/migrate/config-extra-params.js +61 -0
- package/dist/commands/migrate/dead-residue.js +113 -0
- package/dist/commands/migrate/stale-txn.js +49 -0
- package/dist/commands/migrate-cli.js +42 -1
- package/dist/commands/proposal/proposal.js +1 -21
- package/dist/commands/proposal/repository.js +0 -4
- package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -6
- package/dist/commands/read/curate.js +4 -18
- package/dist/commands/read/show.js +2 -1
- package/dist/commands/sources/installed-stashes.js +5 -1
- package/dist/commands/sources/self-update.js +38 -1
- package/dist/commands/sources/sources-cli.js +17 -1
- package/dist/commands/tasks/tasks.js +0 -8
- package/dist/commands/url-checker.js +20 -28
- package/dist/core/adapter/adapters/agent-skills-adapter.js +2 -6
- package/dist/core/adapter/adapters/akm-adapter.js +2 -3
- package/dist/core/adapter/adapters/akm-lint.js +2 -20
- package/dist/core/adapter/adapters/akm-task-adapter.js +2 -6
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +2 -6
- package/dist/core/adapter/adapters/dotenv-adapter.js +2 -22
- package/dist/core/adapter/adapters/generic-files-adapter.js +3 -9
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +2 -6
- package/dist/core/adapter/adapters/okf-adapter.js +7 -18
- package/dist/core/adapter/adapters/shared.js +2 -0
- package/dist/core/adapter/adapters/tool-dir-shared.js +3 -9
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +2 -6
- package/dist/core/adapter/validate-context.js +1 -3
- package/dist/core/asset/asset-placement.js +1 -3
- package/dist/core/asset/frontmatter.js +96 -0
- package/dist/core/common.js +66 -2
- package/dist/core/config/config-walker.js +6 -10
- package/dist/core/config/config.js +11 -8
- package/dist/core/extra-params.js +17 -13
- package/dist/core/improve-result.js +1 -3
- package/dist/core/json-schema.js +9 -11
- package/dist/core/state/migrations.js +52 -2
- package/dist/core/state-db.js +2 -1
- package/dist/execution/executable-identity.js +1 -3
- package/dist/execution/guarded-source.js +1 -6
- package/dist/indexer/bundle-identity-guard.js +6 -1
- package/dist/indexer/db/graph-db.js +139 -154
- package/dist/indexer/ensure-index.js +11 -19
- package/dist/indexer/graph/graph-boost.js +23 -34
- package/dist/indexer/graph/graph-extraction.js +12 -2
- package/dist/indexer/indexer.js +1 -1
- package/dist/indexer/lookup/adapter-concept-owner.js +12 -9
- package/dist/indexer/passes/memory-inference.js +7 -2
- package/dist/indexer/scan/drain-dir.js +2 -4
- package/dist/indexer/search/db-search.js +3 -3
- package/dist/indexer/search/fts-query.js +10 -15
- package/dist/indexer/search/search-source.js +0 -13
- package/dist/indexer/usage/usage-events.js +9 -1
- package/dist/indexer/walk/walker.js +11 -6
- package/dist/integrations/harnesses/claude/result-extractor.js +30 -15
- package/dist/integrations/harnesses/codex/result-extractor.js +43 -6
- package/dist/integrations/harnesses/copilot/result-extractor.js +39 -12
- package/dist/integrations/harnesses/gemini/result-extractor.js +40 -12
- package/dist/integrations/harnesses/index.js +0 -4
- package/dist/integrations/harnesses/openhands/result-extractor.js +51 -19
- package/dist/integrations/harnesses/pi/result-extractor.js +45 -12
- package/dist/integrations/lockfile.js +0 -14
- package/dist/integrations/session-logs/index.js +0 -81
- package/dist/llm/client.js +0 -12
- package/dist/llm/memory-infer.js +1 -3
- package/dist/llm/usage-telemetry.js +1 -3
- package/dist/registry/create-provider-registry.js +4 -0
- package/dist/registry/factory.js +4 -0
- package/dist/registry/resolve.js +23 -8
- package/dist/runtime.js +0 -12
- package/dist/scripts/akm-migrate-node.js +142 -244
- package/dist/scripts/akm-migrate.js +142 -244
- package/dist/setup/setup.js +6 -12
- package/dist/sources/providers/git-install.js +7 -2
- package/dist/sources/providers/tar-utils.js +1 -7
- package/dist/sources/snapshot-fetchers/content-extract.js +0 -17
- package/dist/storage/managed-db.js +20 -7
- package/dist/storage/repositories/events-repository.js +0 -81
- package/dist/storage/repositories/index-entries-repository.js +0 -17
- package/dist/storage/repositories/index-fts-repository.js +24 -30
- package/dist/storage/repositories/index-utility-repository.js +0 -57
- package/dist/storage/repositories/index-vec-repository.js +25 -27
- package/dist/storage/repositories/task-history-repository.js +9 -3
- package/dist/tasks/backends/launchd.js +8 -18
- package/dist/tasks/run/task-history.js +21 -31
- package/dist/tasks/scheduler-sync.js +1 -6
- package/dist/tasks/source/bounded-document.js +1 -14
- package/dist/workflows/exec/child-workflow.js +1 -1
- package/dist/workflows/exec/native-executor.js +2 -2
- package/dist/workflows/exec/step-work.js +5 -17
- package/dist/workflows/exec/worktree.js +40 -6
- package/dist/workflows/freeze/task-bindings.js +2 -4
- package/dist/workflows/ir/compile.js +3 -14
- package/dist/workflows/ir/schema-v4.js +4 -6
- package/dist/workflows/ir/schema.js +2 -5
- package/dist/workflows/parser.js +23 -26
- package/dist/workflows/source-files.js +8 -13
- package/dist/workflows/source-ir/schema.js +1 -19
- package/docs/migration/v0.9.0-troubleshooting.md +12 -2
- package/docs/reference/cli.md +2 -0
- package/docs/reference/workflow-schema.md +24 -0
- package/package.json +2 -5
- package/dist/workflows/source-ir/compare.js +0 -17
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,224 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
|
|
7
|
+
## [0.9.8-beta.1] - 2026-09-01
|
|
8
|
+
|
|
9
|
+
A cleanup and stabilization release: deletion of machinery that policed the
|
|
10
|
+
codebase's shape rather than its behaviour, and — because auditing for that
|
|
11
|
+
machinery meant reading the code closely — a run of real defects it had been
|
|
12
|
+
sitting on top of.
|
|
13
|
+
|
|
14
|
+
Two security holes, two search-correctness bugs, a locale-dependent hash, a
|
|
15
|
+
deletion shield that failed open, and sixteen places that answered a failure
|
|
16
|
+
with a confident wrong answer instead of an error.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **Historical state migrations are reachable where akm cannot reinstall
|
|
21
|
+
itself** (#895). A migration flagged `historical-destructive` is refused
|
|
22
|
+
during an ordinary open — it needs a verified safety copy, taken under the
|
|
23
|
+
migration writer lock, so an unattended `akm index` can never quietly drop
|
|
24
|
+
operator state. That guard is correct and unchanged. Its *remedy* was not:
|
|
25
|
+
the only code path that admitted the migration ran as a post-install step of
|
|
26
|
+
`akm upgrade`, behind an npm install. Where akm is installed globally by a
|
|
27
|
+
container image and the runtime user is unprivileged, that install fails
|
|
28
|
+
`EACCES` and throws long before the migration is reached, so `akm index
|
|
29
|
+
--full` was blocked with no supported way out — and the two obvious
|
|
30
|
+
workarounds are both wrong (upgrading as root installs a *newer* akm than the
|
|
31
|
+
image ships, which `config-version-shim` then fails closed against; deleting
|
|
32
|
+
`state.db` destroys task history, proposals, and lessons metadata, none of
|
|
33
|
+
which is derived). New `akm upgrade --state-only` applies pending state
|
|
34
|
+
migrations and installs nothing. Nothing about the migration needed the
|
|
35
|
+
network, root, or a new binary; coupling it to one was the bug. The safety
|
|
36
|
+
copy is still taken — this changes who may request the migration, not what it
|
|
37
|
+
does.
|
|
38
|
+
|
|
39
|
+
- **Two security holes closed.** A failed `git ls-remote` made
|
|
40
|
+
`verifyClonedRevision` a no-op, silently skipping the R-011 post-clone
|
|
41
|
+
revision-integrity check — the guard against a compromised mirror — so any
|
|
42
|
+
network blip disabled it without a word. And `scanExtractedFiles`, the
|
|
43
|
+
post-extraction TOCTOU path-traversal rescan, returned silently when a
|
|
44
|
+
directory could not be read: it passed clean in exactly the race it exists
|
|
45
|
+
to catch. Both now fail loudly.
|
|
46
|
+
|
|
47
|
+
- **`akm curate --type` ignored the type filter.** A set `--type` bypassed
|
|
48
|
+
`selectCuratedStashHits` entirely and did a raw slice of the hits as
|
|
49
|
+
received, which neither filtered by type nor ranked by score — so
|
|
50
|
+
`--type command` could return a skill, in arbitrary order. It now narrows
|
|
51
|
+
the candidate pool and runs the full curation pipeline over it. The test
|
|
52
|
+
that should have caught this passed by accident, because the off-type hit
|
|
53
|
+
happened to sit past the result limit.
|
|
54
|
+
|
|
55
|
+
- **Curate silently dropped relevant results.** An undocumented floor excluded
|
|
56
|
+
any hit scoring below `max(0.35, leader * 0.7)`. Measured against a real
|
|
57
|
+
3,265-memory bundle, one query went from one result to four once it was
|
|
58
|
+
removed — the suppressed hits were the ones actually matching. Ordering is
|
|
59
|
+
unchanged: results were already sorted before the floor ran, so it could
|
|
60
|
+
only ever hide the tail.
|
|
61
|
+
|
|
62
|
+
- **A bundle-audit hash depended on the machine's locale.** `canonicalJson`
|
|
63
|
+
in `installed-stashes.ts` sorted keys with `localeCompare` — ICU-dependent —
|
|
64
|
+
and fed the result to `sha256Hex`, so the same object could hash differently
|
|
65
|
+
on two machines. Now codepoint order, matching the other implementations.
|
|
66
|
+
|
|
67
|
+
- **A deletion shield failed open.** `isHotCapturedMemory` returned false when
|
|
68
|
+
it could not read or parse a memory, marking exactly the memories it failed
|
|
69
|
+
to inspect as fair game for consolidate to merge or delete. It now fails
|
|
70
|
+
closed.
|
|
71
|
+
|
|
72
|
+
- **Sixteen places returned a confident wrong answer instead of an error.**
|
|
73
|
+
The pattern (#791) recurred across search, graph, indexing, and scheduling:
|
|
74
|
+
a corrupt or locked index reported "no matches"; a failed inline rebuild
|
|
75
|
+
surfaced as "Index is empty. Run 'akm index'" at exit 0; `akm show` reported
|
|
76
|
+
"0 related files" for an unreadable index; a DB failure in
|
|
77
|
+
`listProposalsReadOnly` returned `[]`, so dedup could not tell "no prior
|
|
78
|
+
proposal" from "database unreadable" and could re-mint an already-rejected
|
|
79
|
+
one; a permission fault on the bundle directory told the user to run
|
|
80
|
+
`akm bundle create`; and `launchd`'s `list()` returned `[]` when the
|
|
81
|
+
LaunchAgents directory could not be read, so task reconciliation concluded
|
|
82
|
+
there was nothing to manage. In each case the failure now surfaces.
|
|
83
|
+
|
|
84
|
+
- **Drifted copies of shared helpers.** `asNonEmptyString` trimmed in two
|
|
85
|
+
places and not in four others, so a whitespace-only session id decoded to
|
|
86
|
+
`" "` in some subsystems and `undefined` in others. `isPlainObject` had two
|
|
87
|
+
incompatible definitions under one name; the loose form would accept a
|
|
88
|
+
`Date` or class instance as a plain record. Both consolidated, along with
|
|
89
|
+
`toPosix` (15 copies, whose "avoid an import cycle" justification was false
|
|
90
|
+
in every case), `isRecord` (11), `compareCodePoints` (8), and five more
|
|
91
|
+
families.
|
|
92
|
+
|
|
93
|
+
- **Pruning a memory no longer leaves dangling belief edges (#885).**
|
|
94
|
+
`writeContradictEdge` — the hardened, test-covered `contradictedBy` writer —
|
|
95
|
+
had no production caller; its docstring named three that did not exist. The
|
|
96
|
+
live pass used a private near-copy that had drifted on exactly the two
|
|
97
|
+
behaviours the original was hardened for: it read the key with
|
|
98
|
+
`Array.isArray` only, so a SCALAR edge (live data the indexer accepts and
|
|
99
|
+
lint never flags) read as "no edges" and was overwritten out of existence;
|
|
100
|
+
and it set `beliefState: "contradicted"` unconditionally, promoting an
|
|
101
|
+
`archived` memory back up the ranking. The tests for both behaviours were
|
|
102
|
+
guarding dead code. The copy is gone.
|
|
103
|
+
|
|
104
|
+
`persistBeliefStateTransition` is deliberately NOT routed through the shared
|
|
105
|
+
primitive: it is a state-transition writer that replaces the edge list
|
|
106
|
+
wholesale and can clear it, which an append-only, never-weaken primitive
|
|
107
|
+
cannot express.
|
|
108
|
+
|
|
109
|
+
- **`git worktree` operations no longer fail on a busy machine (#891).** The
|
|
110
|
+
module's internal `GIT_TIMEOUT_MS` was a flat 30s with no margin, so a
|
|
111
|
+
healthy `git worktree remove` on a loaded host returned
|
|
112
|
+
`{ removed: false, error: "timed out after 30000ms" }`. Anyone running a
|
|
113
|
+
workflow with `isolation: worktree` alongside other git activity could hit
|
|
114
|
+
spurious create failures, or have a clean worktree wrongly retained as
|
|
115
|
+
unremovable. Raised to 120s, matching the existing `GIT_PUSH_TIMEOUT_MS`
|
|
116
|
+
precedent. Note `scripts/test-integration.sh` had already raised *bun's*
|
|
117
|
+
per-test timeout for this reason, but that never touched the production
|
|
118
|
+
subprocess timeout, which fires first.
|
|
119
|
+
|
|
120
|
+
- **`akm health`'s dead-link check no longer reports an unearned all-clear
|
|
121
|
+
(#892).** `checkDeadUrls` capped at 20 URLs across the whole bundle, plus an
|
|
122
|
+
undocumented `slice(0, 3)` per entry, then the caller logged "URL check
|
|
123
|
+
complete (0 dead)". A bundle with thousands of links got a clean bill of
|
|
124
|
+
health after twenty were examined. Both caps removed; every URL is checked
|
|
125
|
+
and a failed request surfaces instead of being swallowed.
|
|
126
|
+
|
|
127
|
+
- **`akm lint --fix` no longer breaks on gitignored drafts (#887).** The doc
|
|
128
|
+
linter walked `docs/` with no gitignore awareness, so unpublished drafts
|
|
129
|
+
under `docs/.pending/` were held to the published-docs contract —
|
|
130
|
+
`bun run lint` failed locally while CI, which never has those files, stayed
|
|
131
|
+
green.
|
|
132
|
+
|
|
133
|
+
### Changed
|
|
134
|
+
|
|
135
|
+
- **Search no longer truncates long queries (#892).** `MAX_LEXICAL_QUERY_TOKENS
|
|
136
|
+
= 16` silently dropped every token past the sixteenth, and tokens are
|
|
137
|
+
collected in order, so the discarded half was the tail — for
|
|
138
|
+
natural-language input, usually where the discriminating words are. It also
|
|
139
|
+
fed ranking, so token-overlap scoring ran on the truncated set too. It was
|
|
140
|
+
unexplained in the code and in the commit that introduced it, and unreachable
|
|
141
|
+
from any flag, config key, or environment variable. Removed: the planner
|
|
142
|
+
handles 10,000 tokens in 9ms, so no performance cliff was being protected.
|
|
143
|
+
|
|
144
|
+
- **Content and memory bodies are no longer silently truncated.**
|
|
145
|
+
`MAX_CONTENT_CHARS` (100k, duplicated across 8 adapters) cut indexed content
|
|
146
|
+
so the tail of a long document was unsearchable; `MAX_BODY_CHARS` (4000) cut
|
|
147
|
+
the text sent for memory inference, so on a large-context engine the model
|
|
148
|
+
saw a fraction of the input while the derived memory looked complete. Both
|
|
149
|
+
removed.
|
|
150
|
+
|
|
151
|
+
- **GitHub Actions are pinned to commit SHAs (#768).** All 29 `uses:` steps
|
|
152
|
+
across every workflow, with the tag preserved in a trailing comment.
|
|
153
|
+
|
|
154
|
+
- **Gated CI runs on schedule, dispatch, and candidate tags only.** The
|
|
155
|
+
`detect-changes` job that selected suites by regex-matching a PR diff is
|
|
156
|
+
gone — its path patterns had gone stale and still named test files this
|
|
157
|
+
release moved or deleted, so it was silently under-selecting suites. Release
|
|
158
|
+
evidence is unchanged; the checklist always required an exact-SHA dispatch.
|
|
159
|
+
|
|
160
|
+
- **`akm-eval` in CI is now a determinism check only.** Its score gates are
|
|
161
|
+
removed. Measured before cutting: the baseline scored a perfect 1.0 against
|
|
162
|
+
a 0.75 gate, and seven of nine case types never ran — CI has no LLM and no
|
|
163
|
+
run history, so everything the eval exists to measure was skipped while the
|
|
164
|
+
job reported green. The harness itself is unchanged and remains a genuine
|
|
165
|
+
quality signal when run against a real bundle.
|
|
166
|
+
|
|
167
|
+
### Removed
|
|
168
|
+
|
|
169
|
+
- **`akm health --clean-dead-residue` and every other compensating shim.**
|
|
170
|
+
A special-purpose flag on `health` that deleted files existed because
|
|
171
|
+
migrations had not finished their own job. Removing a superseded layout IS
|
|
172
|
+
migration, so `akm migrate status` now reports it and `akm migrate apply`
|
|
173
|
+
removes it. Four more of the same class went the same way: the D8
|
|
174
|
+
`task_history` vocabulary is rewritten once by a state migration instead of
|
|
175
|
+
re-decided at three read sites; the legacy `extraParams` config lift happens
|
|
176
|
+
once in `migrate apply` instead of silently on every load forever (an
|
|
177
|
+
unmigrated config now fails closed, naming the command to run); retired
|
|
178
|
+
`type:slug` xrefs are rewritten by `lint --fix`; and stale transaction
|
|
179
|
+
journals are recovered by `migrate apply` rather than reported by an
|
|
180
|
+
advisory that pointed at documentation while the recovery function it needed
|
|
181
|
+
sat with zero callers.
|
|
182
|
+
|
|
183
|
+
- **Dead code with no consumers.** An empty `{}` type threaded through two
|
|
184
|
+
functions as a parameter neither read, with a `{}` placeholder at the call
|
|
185
|
+
site; unused helpers, imports, and parameters throughout.
|
|
186
|
+
|
|
187
|
+
- **14 architecture ratchets, 15 golden/snapshot suites, 12 characterization
|
|
188
|
+
suites.** Function-size and import-cycle ratchets that failed on refactors
|
|
189
|
+
harming nothing; byte-for-byte snapshots "fixed" by regenerating; tests that
|
|
190
|
+
pinned "what the code does today" by definition.
|
|
191
|
+
- **11 of 14 lint scripts (-3,988 lines).** `lint-tests-isolation` alone was
|
|
192
|
+
717 lines standing in front of `src/core/paths.ts`, which already throws
|
|
193
|
+
`TEST_ISOLATION_MISSING` at runtime. Also removed: the `gen-config-schema
|
|
194
|
+
--check` gate (`build` regenerates the schema anyway, so what ships is always
|
|
195
|
+
current), and `lint-devto-posts` (322 lines that were wired to nothing and
|
|
196
|
+
duplicated checks `devto-cli` performs itself). The three kept catch
|
|
197
|
+
user-visible problems: secrets must route through the resolver, no dead refs
|
|
198
|
+
in shipped assets, no docs teaching commands that do not exist.
|
|
199
|
+
- **Both `MIN_TESTS` floors** — arbitrary numbers whose only job was to fail
|
|
200
|
+
the suite when test count dropped.
|
|
201
|
+
- **`release-workflow-syntax.yml`** — a workflow that ran actionlint over the
|
|
202
|
+
other workflows. A broken workflow file already fails at GitHub.
|
|
203
|
+
- **Dead `$STASH/.akm` residue is now reportable (#889).** `akm health` gains a
|
|
204
|
+
read-only `stash-dead-residue` advisory naming each stale path and its size;
|
|
205
|
+
deletion is gated behind an explicit `akm health --clean-dead-residue`. On a
|
|
206
|
+
real bundle 82% of `.akm` (135 MB) was pre-0.9.0 leftovers no code reads.
|
|
207
|
+
|
|
208
|
+
### Testing
|
|
209
|
+
|
|
210
|
+
- Test isolation grandfather list drained 58 -> 2 (#785); the two retained
|
|
211
|
+
entries are the helper-definitions file and a meta-test of the guard itself.
|
|
212
|
+
- The test tree now has a stated unit/integration rule and ~250 files were
|
|
213
|
+
reclassified by evidence — real DB, network, or process spawn — rather than
|
|
214
|
+
by filename (#786).
|
|
215
|
+
- 31 status-only `not.toBe(0)` assertions replaced with exact exit codes and
|
|
216
|
+
machine-readable error codes, every value observed rather than inferred
|
|
217
|
+
(#787).
|
|
218
|
+
- Near-duplicate clusters drained (#788); real-server timing flakes removed or
|
|
219
|
+
made deterministic (#789).
|
|
220
|
+
- Kept deliberately: `registry-network-boundary.test.ts`, which enforces that
|
|
221
|
+
only `pinned-transport.ts` may import `node:http`/`node:https` — the control
|
|
222
|
+
that stops a raw request bypassing address-pinned fetching. No lint script
|
|
223
|
+
covers that boundary.
|
|
224
|
+
|
|
7
225
|
## [0.9.7] - 2026-08-31
|
|
8
226
|
|
|
9
227
|
### Added
|
package/dist/commands/env/env.js
CHANGED
|
@@ -48,28 +48,9 @@
|
|
|
48
48
|
import fs from "node:fs";
|
|
49
49
|
import path from "node:path";
|
|
50
50
|
import dotenv from "dotenv";
|
|
51
|
-
import { writeFileAtomic } from "../../core/common.js";
|
|
51
|
+
import { scanEnvKeyNames, writeFileAtomic } from "../../core/common.js";
|
|
52
52
|
import { sensitiveMarkerPath } from "./marker-path.js";
|
|
53
|
-
|
|
54
|
-
const ASSIGN_RE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/;
|
|
55
|
-
/** Scan lines and return KEY names in file order, without duplicates. */
|
|
56
|
-
export function scanEnvKeyNames(text) {
|
|
57
|
-
const keys = [];
|
|
58
|
-
const seen = new Set();
|
|
59
|
-
for (const line of text.split(/\r?\n/)) {
|
|
60
|
-
const m = line.match(ASSIGN_RE);
|
|
61
|
-
if (!m)
|
|
62
|
-
continue;
|
|
63
|
-
const key = m[1];
|
|
64
|
-
if (!key)
|
|
65
|
-
continue;
|
|
66
|
-
if (seen.has(key))
|
|
67
|
-
continue;
|
|
68
|
-
seen.add(key);
|
|
69
|
-
keys.push(key);
|
|
70
|
-
}
|
|
71
|
-
return keys;
|
|
72
|
-
}
|
|
53
|
+
export { scanEnvKeyNames };
|
|
73
54
|
/**
|
|
74
55
|
* Read and return ONLY non-secret metadata: key names.
|
|
75
56
|
*
|
|
@@ -706,29 +706,4 @@ export const HEALTH_CHECKS = [
|
|
|
706
706
|
};
|
|
707
707
|
},
|
|
708
708
|
},
|
|
709
|
-
{
|
|
710
|
-
// Item 4: stale durable-transaction journals under $DATA/txn (stranded
|
|
711
|
-
// recovery state seen twice in a real 0.9 migration) had zero health
|
|
712
|
-
// visibility. `count`/`unreadable` already exclude journals younger than
|
|
713
|
-
// the sweeper's grace period (a currently-running operation), so any
|
|
714
|
-
// nonzero count here is a real leftover.
|
|
715
|
-
name: "stale-txn-journals",
|
|
716
|
-
channel: "advisory",
|
|
717
|
-
run: (ctx) => {
|
|
718
|
-
const s = ctx.staleTxnJournals;
|
|
719
|
-
const warn = s.count > 0;
|
|
720
|
-
const unreadablePart = s.unreadable > 0 ? `, ${s.unreadable} unreadable` : "";
|
|
721
|
-
const agePart = s.oldestAgeMs !== null ? `, oldest ${Math.round(s.oldestAgeMs / 60000)}m old` : "";
|
|
722
|
-
return {
|
|
723
|
-
name: "stale-txn-journals",
|
|
724
|
-
kind: "deterministic",
|
|
725
|
-
status: warn ? "warn" : "pass",
|
|
726
|
-
confidence: "high",
|
|
727
|
-
message: warn
|
|
728
|
-
? `${s.count} stale transaction journal(s) found under ${s.dir}${unreadablePart}${agePart} — see docs/migration/v0.9.0-troubleshooting.md for journal reconciliation steps.`
|
|
729
|
-
: "No stale transaction journals found.",
|
|
730
|
-
evidence: { dir: s.dir, count: s.count, unreadable: s.unreadable, oldestAgeMs: s.oldestAgeMs },
|
|
731
|
-
};
|
|
732
|
-
},
|
|
733
|
-
},
|
|
734
709
|
];
|
|
@@ -33,42 +33,16 @@ export function taskFailureDetail(row) {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* `
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* - LEGACY rows (no marker, written before P1b's F-2 re-code) stored
|
|
44
|
-
* `target_kind: "prompt"` for the same arm — and `"command"` for the
|
|
45
|
-
* UNRELATED native shell/script arm, which must NOT be counted here.
|
|
46
|
-
* So an unmarked `"command"` row is a legacy shell/script run, not an
|
|
47
|
-
* agent/LLM one; a marked `"command"` row (or an unmarked `"prompt"` row) is.
|
|
36
|
+
* `akm health`'s `agentFailureRate` predicate: true for a `task_history` row
|
|
37
|
+
* that represents a prepared command (agent/LLM) result. `target_kind` is
|
|
38
|
+
* read in the current (post-D8) vocabulary — the
|
|
39
|
+
* `025-task-history-vocabulary-backfill` state migration rewrites every
|
|
40
|
+
* legacy-vocabulary row (which stored the agent/LLM arm as `"prompt"` and
|
|
41
|
+
* the native shell/script arm as `"command"`) before this ever runs against
|
|
42
|
+
* it, so a `"command"` row here is unambiguously the agent/LLM arm.
|
|
48
43
|
*/
|
|
49
44
|
export function isAgentTaskHistoryRow(row) {
|
|
50
|
-
|
|
51
|
-
// (e.g. improve-pipeline task_history rows, target_kind "improve") carry
|
|
52
|
-
// metadata_json that predates the metadataVersion:2 shape entirely, and
|
|
53
|
-
// decodeTaskHistoryMetadata throws on that — exactly like the pre-fix
|
|
54
|
-
// `target_kind === "prompt"` filter, which never called it for a row this
|
|
55
|
-
// function isn't going to count anyway. Only decode for the two target
|
|
56
|
-
// kinds this predicate can return true for.
|
|
57
|
-
if (row.target_kind !== "command" && row.target_kind !== "prompt")
|
|
58
|
-
return false;
|
|
59
|
-
// An undecodable metadata_json is by definition unmarked: pre-P1b rows can
|
|
60
|
-
// carry shapes decodeTaskHistoryMetadata rejects, and `akm health` must
|
|
61
|
-
// classify them as legacy rather than throw (round-3 review advisory).
|
|
62
|
-
let marked = false;
|
|
63
|
-
try {
|
|
64
|
-
marked = decodeTaskHistoryMetadata(row.metadata_json).targetVocab === 2;
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
marked = false;
|
|
68
|
-
}
|
|
69
|
-
if (row.target_kind === "command")
|
|
70
|
-
return marked;
|
|
71
|
-
return !marked;
|
|
45
|
+
return row.target_kind === "command";
|
|
72
46
|
}
|
|
73
47
|
function createUnknownImproveMetrics() {
|
|
74
48
|
return {
|
|
@@ -148,10 +148,6 @@ export function buildWindowMetrics(db, stateDbPath, since, until, now = () => Da
|
|
|
148
148
|
const failedTaskRows = taskRows.filter((row) => row.status === "failed");
|
|
149
149
|
const activeRows = taskRows.filter((row) => row.status === "active" && row.completed_at === null);
|
|
150
150
|
const stuckActiveRuns = activeRows.filter((row) => now() - new Date(row.started_at).getTime() > ACTIVE_RUN_WARN_MS).length;
|
|
151
|
-
// D8 (spec §5.3): a marked "command" row or a legacy (unmarked) "prompt"
|
|
152
|
-
// row is the agent/LLM arm; an unmarked "command" row is the legacy
|
|
153
|
-
// native shell/script arm and must not be counted here (see
|
|
154
|
-
// isAgentTaskHistoryRow's header comment for the full mapping).
|
|
155
151
|
const agentRows = taskRows.filter((row) => isAgentTaskHistoryRow(row));
|
|
156
152
|
const agentFailures = agentRows.filter((row) => {
|
|
157
153
|
const detail = taskFailureDetail(row);
|
package/dist/commands/health.js
CHANGED
|
@@ -7,10 +7,9 @@ import { resolveStashDir } from "../core/common.js";
|
|
|
7
7
|
import { loadConfig } from "../core/config/config.js";
|
|
8
8
|
import { ConfigError, UsageError } from "../core/errors.js";
|
|
9
9
|
import { readEvents } from "../core/events.js";
|
|
10
|
-
import { listTxnJournalsTolerant, TXN_SWEEP_GRACE_MS } from "../core/fs-txn.js";
|
|
11
10
|
import { openLogsDatabase } from "../core/logs-db.js";
|
|
12
11
|
import { classifyPathAccess, describeInaccessiblePath } from "../core/path-access.js";
|
|
13
|
-
import { getConfigPath,
|
|
12
|
+
import { getConfigPath, getDbPath, getStateDbPathInDataDir } from "../core/paths.js";
|
|
14
13
|
import { listExistingTableNames, openStateDatabase } from "../core/state-db.js";
|
|
15
14
|
import { DURATION_UNITS, parseDuration, parseSinceToIso } from "../core/time.js";
|
|
16
15
|
import { closeDatabase, openReadonlyExistingDatabase } from "../storage/repositories/index-connection.js";
|
|
@@ -150,37 +149,6 @@ function gatherTaskHistoryPhase(db, logsDb, since, stateDbPath, now) {
|
|
|
150
149
|
agentFailureRate,
|
|
151
150
|
};
|
|
152
151
|
}
|
|
153
|
-
/**
|
|
154
|
-
* Item 4: leftover durable-transaction journals under `$DATA/txn` (stranded
|
|
155
|
-
* recovery state seen twice in a real 0.9 migration) had zero health
|
|
156
|
-
* visibility. Uses the tolerant scan variant so one corrupt journal.json is
|
|
157
|
-
* counted rather than aborting the whole gather; a journal younger than
|
|
158
|
-
* {@link TXN_SWEEP_GRACE_MS} is presumed to belong to a currently-running
|
|
159
|
-
* operation and is not counted as stale. Best-effort: any unexpected error
|
|
160
|
-
* (e.g. a permissions issue under `$DATA/txn`) degrades to "no signal" rather
|
|
161
|
-
* than aborting the health report.
|
|
162
|
-
*/
|
|
163
|
-
function gatherStaleTxnJournalsPhase(now) {
|
|
164
|
-
const dir = path.join(getDataDir(), "txn");
|
|
165
|
-
try {
|
|
166
|
-
const { matches, unreadableMtimes } = listTxnJournalsTolerant(() => true);
|
|
167
|
-
const nowMs = now();
|
|
168
|
-
const staleAges = [
|
|
169
|
-
...matches.map((m) => nowMs - m.mtimeMs),
|
|
170
|
-
...unreadableMtimes.map((mtimeMs) => nowMs - mtimeMs),
|
|
171
|
-
].filter((ageMs) => ageMs >= TXN_SWEEP_GRACE_MS);
|
|
172
|
-
const staleUnreadableCount = unreadableMtimes.filter((mtimeMs) => nowMs - mtimeMs >= TXN_SWEEP_GRACE_MS).length;
|
|
173
|
-
return {
|
|
174
|
-
dir,
|
|
175
|
-
count: staleAges.length,
|
|
176
|
-
oldestAgeMs: staleAges.length > 0 ? Math.max(...staleAges) : null,
|
|
177
|
-
unreadable: staleUnreadableCount,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
catch {
|
|
181
|
-
return { dir, count: 0, oldestAgeMs: null, unreadable: 0 };
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
152
|
/**
|
|
185
153
|
* Config fields the surfaces advisory needs. Best-effort: an unloadable
|
|
186
154
|
* config leaves the field undefined and the caller falls back to a generic
|
|
@@ -512,7 +480,6 @@ export function akmHealth(options = {}) {
|
|
|
512
480
|
try {
|
|
513
481
|
const taskHistory = gatherTaskHistoryPhase(db, logsDb, since, stateDbPath, now);
|
|
514
482
|
const { tableNames, missingTables, probe } = taskHistory;
|
|
515
|
-
const staleTxnJournals = gatherStaleTxnJournalsPhase(now);
|
|
516
483
|
const { egressConfigView } = gatherEgressConfigPhase();
|
|
517
484
|
const { improveSummary } = gatherImproveSummaryPhase(db, stateDbPath, since, now);
|
|
518
485
|
advisories.push(...gatherAncillaryAdvisories(db, stateDbPath, since, improveSummary, options, egressConfigView));
|
|
@@ -535,7 +502,6 @@ export function akmHealth(options = {}) {
|
|
|
535
502
|
stuckActiveRuns: taskHistory.stuckActiveRuns,
|
|
536
503
|
stuckActiveTasks: taskHistory.stuckActiveTasks,
|
|
537
504
|
worstTaskFailRate: taskHistory.worstTaskFailRate,
|
|
538
|
-
staleTxnJournals,
|
|
539
505
|
sessionExtraction: improveSummary.sessionExtraction,
|
|
540
506
|
autoAccept: improveSummary.autoAccept,
|
|
541
507
|
engineProbes,
|
|
@@ -18,8 +18,8 @@ export function isConsolidationEligibleMemoryName(name) {
|
|
|
18
18
|
*
|
|
19
19
|
* Reads the file once per check; consolidate runs against ~10 memories per
|
|
20
20
|
* chunk so the IO cost is trivial. Returns false on any read/parse error
|
|
21
|
-
* (fail-safe: an unparseable file is treated as
|
|
22
|
-
*
|
|
21
|
+
* (fail-safe: an unreadable or unparseable file is treated as HOT — protected
|
|
22
|
+
* — because a deletion shield must not fail open; a missing file is not-hot).
|
|
23
23
|
*
|
|
24
24
|
* Defends against four observed defect classes (see
|
|
25
25
|
* `memories/akm-improve-critical-review-2026-05-20`):
|
|
@@ -29,14 +29,20 @@ export function isConsolidationEligibleMemoryName(name) {
|
|
|
29
29
|
* - Cascade deletes (LLM uses ref:X as `contradictedBy` for ref:Y then deletes both)
|
|
30
30
|
*/
|
|
31
31
|
export function isHotCapturedMemory(filePath) {
|
|
32
|
+
if (!fs.existsSync(filePath))
|
|
33
|
+
return false;
|
|
32
34
|
try {
|
|
33
|
-
if (!fs.existsSync(filePath))
|
|
34
|
-
return false;
|
|
35
35
|
const content = fs.readFileSync(filePath, "utf8");
|
|
36
36
|
const parsed = parseFrontmatter(content);
|
|
37
37
|
return hasHotCaptureMode(parsed.data);
|
|
38
38
|
}
|
|
39
39
|
catch {
|
|
40
|
-
|
|
40
|
+
// Fail CLOSED. This predicate is a deletion shield: "hot" memories are
|
|
41
|
+
// protected from consolidate's merge/delete. Returning false on a read or
|
|
42
|
+
// parse failure marked exactly the memories we could not inspect as fair
|
|
43
|
+
// game — the one direction a protection check must never fail. A missing
|
|
44
|
+
// file stays false (nothing to protect); an unreadable one is protected
|
|
45
|
+
// until someone can actually read it.
|
|
46
|
+
return true;
|
|
41
47
|
}
|
|
42
48
|
}
|
|
@@ -632,8 +632,40 @@ function unavailableExtractionResult(args) {
|
|
|
632
632
|
...args.notices,
|
|
633
633
|
};
|
|
634
634
|
}
|
|
635
|
+
// #561 — ADDITIVE session indexing. Generate + write the session asset
|
|
636
|
+
// (`sessions/<harness>/<id>.md`). FAIL-OPEN: any failure only returns a
|
|
637
|
+
// warning; it NEVER changes the proposal/skip outcome of extract. Returns the
|
|
638
|
+
// frontmatter fields to merge into the per-session result for state-db
|
|
639
|
+
// correlation. When disabled this makes NO LLM call and writes NOTHING.
|
|
640
|
+
async function maybeWriteSessionAsset(runCtx, session) {
|
|
641
|
+
const { stashDir, lease, sessionIndexing, dryRun } = runCtx;
|
|
642
|
+
const { data } = session.gate;
|
|
643
|
+
if (!sessionIndexing.enabled || dryRun)
|
|
644
|
+
return {};
|
|
645
|
+
if (!sessionMeetsDurationGate(data, sessionIndexing.minDurationMinutes))
|
|
646
|
+
return {};
|
|
647
|
+
try {
|
|
648
|
+
const result = await writeSessionAsset(data, stashDir, (summaryData) => sessionIndexing.generate(summaryData, lease));
|
|
649
|
+
if (result.written) {
|
|
650
|
+
// Write-path indexing (itself fail-open): standalone `akm extract`
|
|
651
|
+
// (session-end hook) has no post-loop reindex to pick this file up.
|
|
652
|
+
if (result.filePath)
|
|
653
|
+
await indexWrittenAssets(stashDir, [result.filePath]);
|
|
654
|
+
return {
|
|
655
|
+
...(result.ref ? { sessionAssetRef: result.ref } : {}),
|
|
656
|
+
...(result.logPath ? { sessionLogPath: result.logPath } : {}),
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
catch (err) {
|
|
661
|
+
if (err instanceof ConfigError)
|
|
662
|
+
throw err;
|
|
663
|
+
return { warning: `session asset write failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
664
|
+
}
|
|
665
|
+
return {};
|
|
666
|
+
}
|
|
635
667
|
async function processSession(runCtx, session) {
|
|
636
|
-
const { harness, stashDir, config, llmRunner, lease, onNotices, getNotices, chat, ctx, eventsCtx, sourceRun, dryRun, timeoutMs,
|
|
668
|
+
const { harness, stashDir, config, llmRunner, lease, onNotices, getNotices, chat, ctx, eventsCtx, sourceRun, dryRun, timeoutMs, signal, standardsContext, } = runCtx;
|
|
637
669
|
const { sessionRef, gate } = session;
|
|
638
670
|
const warnings = [];
|
|
639
671
|
const { data, filtered, contentHash } = gate;
|
|
@@ -645,36 +677,6 @@ async function processSession(runCtx, session) {
|
|
|
645
677
|
inlineRefs: data.inlineRefs,
|
|
646
678
|
...(standardsContext.trim() ? { standardsContext } : {}),
|
|
647
679
|
});
|
|
648
|
-
// #561 — ADDITIVE session indexing. Generate + write the session asset
|
|
649
|
-
// (`sessions/<harness>/<id>.md`). FAIL-OPEN: any failure only records a
|
|
650
|
-
// warning; it NEVER changes the proposal/skip outcome of extract. Returns the
|
|
651
|
-
// frontmatter fields to merge into the per-session result for state-db
|
|
652
|
-
// correlation. When disabled this closure makes NO LLM call and writes NOTHING.
|
|
653
|
-
const maybeWriteSessionAsset = async () => {
|
|
654
|
-
if (!sessionIndexing.enabled || dryRun)
|
|
655
|
-
return {};
|
|
656
|
-
if (!sessionMeetsDurationGate(data, sessionIndexing.minDurationMinutes))
|
|
657
|
-
return {};
|
|
658
|
-
try {
|
|
659
|
-
const result = await writeSessionAsset(data, stashDir, (summaryData) => sessionIndexing.generate(summaryData, lease));
|
|
660
|
-
if (result.written) {
|
|
661
|
-
// Write-path indexing (itself fail-open): standalone `akm extract`
|
|
662
|
-
// (session-end hook) has no post-loop reindex to pick this file up.
|
|
663
|
-
if (result.filePath)
|
|
664
|
-
await indexWrittenAssets(stashDir, [result.filePath]);
|
|
665
|
-
return {
|
|
666
|
-
...(result.ref ? { sessionAssetRef: result.ref } : {}),
|
|
667
|
-
...(result.logPath ? { sessionLogPath: result.logPath } : {}),
|
|
668
|
-
};
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
catch (err) {
|
|
672
|
-
if (err instanceof ConfigError)
|
|
673
|
-
throw err;
|
|
674
|
-
warnings.push(`session asset write failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
675
|
-
}
|
|
676
|
-
return {};
|
|
677
|
-
};
|
|
678
680
|
const extraction = await runSessionExtractionLlmCall({
|
|
679
681
|
config,
|
|
680
682
|
llmRunner,
|
|
@@ -708,7 +710,9 @@ async function processSession(runCtx, session) {
|
|
|
708
710
|
const { payload } = extraction;
|
|
709
711
|
const proposalIds = [];
|
|
710
712
|
// Provenance refs are added only after the cited session asset exists.
|
|
711
|
-
const sessionAsset = await maybeWriteSessionAsset();
|
|
713
|
+
const sessionAsset = await maybeWriteSessionAsset(runCtx, session);
|
|
714
|
+
if (sessionAsset.warning)
|
|
715
|
+
warnings.push(sessionAsset.warning);
|
|
712
716
|
if (payload.candidates.length === 0) {
|
|
713
717
|
appendEvent({
|
|
714
718
|
eventType: "extract_invoked",
|
|
@@ -52,10 +52,17 @@ function readEdgeList(value) {
|
|
|
52
52
|
* Write `contradictedBy` and `beliefState: contradicted` edges to a memory
|
|
53
53
|
* file's frontmatter (C-3 / #382).
|
|
54
54
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
55
|
+
* The shared primitive for APPENDING one contradiction edge. Used by
|
|
56
|
+
* `memory-contradiction-detect.ts`'s automated contradiction pass.
|
|
57
|
+
*
|
|
58
|
+
* NOT used by `persistBeliefStateTransition` in `memory-improve.ts`, and that
|
|
59
|
+
* is deliberate (#885): the SCC resolver is a state-TRANSITION writer, not an
|
|
60
|
+
* edge appender. It replaces `contradictedBy` wholesale from recomputed
|
|
61
|
+
* `currentBeliefRefs`, deletes the key when a memory transitions away from
|
|
62
|
+
* `contradicted`, and moves a memory to any target state including back to
|
|
63
|
+
* active. An append-only primitive that never weakens a demotion cannot
|
|
64
|
+
* express those, and routing it through this would break the resolver's
|
|
65
|
+
* ability to clear an edge.
|
|
59
66
|
*
|
|
60
67
|
* Idempotent: if the `contradictedByRef` is already in `contradictedBy` AND
|
|
61
68
|
* the file already carries the demotion state, the file is not rewritten. The
|
|
@@ -72,9 +79,12 @@ function readEdgeList(value) {
|
|
|
72
79
|
*
|
|
73
80
|
* @param filePath - Absolute path to the memory markdown file.
|
|
74
81
|
* @param contradictedByRef - The ref that contradicts this memory.
|
|
82
|
+
* @returns `true` when the file was rewritten, `false` when the edge and the
|
|
83
|
+
* demotion were already present (the idempotent no-op). Callers count
|
|
84
|
+
* edges written from this.
|
|
75
85
|
*/
|
|
76
86
|
export function writeContradictEdge(filePath, contradictedByRef) {
|
|
77
|
-
mutateFrontmatter(filePath, (parsed) => {
|
|
87
|
+
return mutateFrontmatter(filePath, (parsed) => {
|
|
78
88
|
const existing = readEdgeList(parsed.data.contradictedBy);
|
|
79
89
|
const currentState = parsed.data.beliefState;
|
|
80
90
|
const nextState = currentState === "archived" ? currentState : "contradicted";
|
|
@@ -17,8 +17,19 @@
|
|
|
17
17
|
* 2. For each family, enumerate candidate pairs.
|
|
18
18
|
* 3. For each pair, call the LLM to judge whether the two memories are in
|
|
19
19
|
* direct factual conflict.
|
|
20
|
-
* 4. For confirmed contradictions,
|
|
21
|
-
*
|
|
20
|
+
* 4. For confirmed contradictions, append a `contradictedBy` edge to the
|
|
21
|
+
* losing memory's frontmatter via `writeContradictEdge`
|
|
22
|
+
* (`./memory-belief.ts`).
|
|
23
|
+
*
|
|
24
|
+
* That last step used to call a private near-copy of `writeContradictEdge`
|
|
25
|
+
* living in this file. The copy had drifted (#885): it read `contradictedBy`
|
|
26
|
+
* with `Array.isArray` only, so a SCALAR edge — live data the indexer accepts
|
|
27
|
+
* and lint never flags — read as "no edges" and was overwritten out of
|
|
28
|
+
* existence; and it set `beliefState: "contradicted"` unconditionally,
|
|
29
|
+
* promoting an `archived` memory back up (archived ranks BELOW contradicted).
|
|
30
|
+
* Both behaviors had tests, but the tests exercised the shared primitive,
|
|
31
|
+
* which nothing called — so they guarded dead code while the live path
|
|
32
|
+
* carried the bugs.
|
|
22
33
|
*
|
|
23
34
|
* # LLM Feature Gate
|
|
24
35
|
*
|
|
@@ -36,12 +47,13 @@
|
|
|
36
47
|
import fs from "node:fs";
|
|
37
48
|
import path from "node:path";
|
|
38
49
|
import contradictionJudgeTemplate from "../../../assets/prompts/contradiction-judge.md" with { type: "text" };
|
|
39
|
-
import {
|
|
50
|
+
import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
|
|
40
51
|
import { parseEmbeddedJsonResponse } from "../../../core/parse.js";
|
|
41
52
|
import { disposeLoweredExecutionDispatchLease } from "../../../integrations/agent/execution-lowering.js";
|
|
42
53
|
import { callStructured, preflightStructuredLlmRunner } from "../../../llm/structured-call.js";
|
|
43
54
|
import { resolveImproveLlmExecution } from "../execution.js";
|
|
44
55
|
import { isDerivedMemory, memoryIdentityRef, resolveParentRef } from "./derived-ref.js";
|
|
56
|
+
import { writeContradictEdge } from "./memory-belief.js";
|
|
45
57
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
46
58
|
/**
|
|
47
59
|
* Minimum confidence required to write a contradiction edge. Below this
|
|
@@ -89,27 +101,6 @@ function toMemoryRef(memoriesDir, filePath) {
|
|
|
89
101
|
return memoryIdentityRef(name);
|
|
90
102
|
}
|
|
91
103
|
// ── Edge writing ─────────────────────────────────────────────────────────────
|
|
92
|
-
/**
|
|
93
|
-
* Write a `contradictedBy` edge to the losing memory's frontmatter file.
|
|
94
|
-
* Preserves all existing frontmatter keys; only adds/updates `contradictedBy`
|
|
95
|
-
* and `beliefState: contradicted`.
|
|
96
|
-
*/
|
|
97
|
-
/** Returns true if the edge was newly written, false if it already existed. */
|
|
98
|
-
function writeContradictedByEdge(filePath, contradictedByRef) {
|
|
99
|
-
return mutateFrontmatter(filePath, (parsed) => {
|
|
100
|
-
const existing = Array.isArray(parsed.data.contradictedBy)
|
|
101
|
-
? parsed.data.contradictedBy
|
|
102
|
-
: [];
|
|
103
|
-
if (existing.includes(contradictedByRef))
|
|
104
|
-
return null; // Edge already written.
|
|
105
|
-
const updatedContradictedBy = [...new Set([...existing, contradictedByRef])].sort();
|
|
106
|
-
return {
|
|
107
|
-
...parsed.data,
|
|
108
|
-
contradictedBy: updatedContradictedBy,
|
|
109
|
-
beliefState: "contradicted",
|
|
110
|
-
};
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
104
|
/**
|
|
114
105
|
* Deterministically pick, for a confirmed-contradiction pair, the LOSER memory
|
|
115
106
|
* that receives the single directed `contradictedBy` edge (SCC-resolved to
|
|
@@ -284,7 +275,7 @@ export async function detectAndWriteContradictions(stashDir, config, chat, strat
|
|
|
284
275
|
// a 2-cycle that the SCC resolver refreshes back to active, erasing the
|
|
285
276
|
// contradiction every run (see pickContradictionLoser).
|
|
286
277
|
try {
|
|
287
|
-
const wrote =
|
|
278
|
+
const wrote = writeContradictEdge(loser.filePath, winnerRef);
|
|
288
279
|
result.edgesWritten += wrote ? 1 : 0;
|
|
289
280
|
}
|
|
290
281
|
catch (err) {
|