akm-cli 0.7.4 → 0.8.0-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/CHANGELOG.md +224 -1
  2. package/README.md +22 -6
  3. package/SECURITY.md +93 -0
  4. package/dist/cli/config-migrate.js +144 -0
  5. package/dist/cli/config-validate.js +39 -0
  6. package/dist/cli/confirm.js +73 -0
  7. package/dist/cli/parse-args.js +133 -0
  8. package/dist/cli/shared.js +129 -0
  9. package/dist/cli.js +2631 -1440
  10. package/dist/commands/add-cli.js +279 -0
  11. package/dist/commands/agent-dispatch.js +110 -0
  12. package/dist/commands/agent-support.js +68 -0
  13. package/dist/commands/completions.js +3 -0
  14. package/dist/commands/config-cli.js +130 -534
  15. package/dist/commands/consolidate.js +2122 -0
  16. package/dist/commands/curate.js +45 -3
  17. package/dist/commands/db-cli.js +23 -0
  18. package/dist/commands/distill-promotion-policy.js +660 -0
  19. package/dist/commands/distill.js +1081 -73
  20. package/dist/commands/env.js +213 -0
  21. package/dist/commands/eval-cases.js +43 -0
  22. package/dist/commands/events.js +15 -24
  23. package/dist/commands/extract-cli.js +127 -0
  24. package/dist/commands/extract-prompt.js +204 -0
  25. package/dist/commands/extract.js +477 -0
  26. package/dist/commands/feedback-cli.js +331 -0
  27. package/dist/commands/graph.js +477 -0
  28. package/dist/commands/health.js +1302 -0
  29. package/dist/commands/help/help-accept.md +12 -0
  30. package/dist/commands/help/help-improve.md +69 -0
  31. package/dist/commands/help/help-proposals.md +18 -0
  32. package/dist/commands/help/help-propose.md +17 -0
  33. package/dist/commands/help/help-reject.md +11 -0
  34. package/dist/commands/history.js +54 -46
  35. package/dist/commands/improve-auto-accept.js +97 -0
  36. package/dist/commands/improve-cli.js +217 -0
  37. package/dist/commands/improve-profiles.js +166 -0
  38. package/dist/commands/improve-result-file.js +167 -0
  39. package/dist/commands/improve.js +2373 -0
  40. package/dist/commands/info.js +5 -2
  41. package/dist/commands/init.js +50 -2
  42. package/dist/commands/installed-stashes.js +102 -139
  43. package/dist/commands/knowledge.js +136 -0
  44. package/dist/commands/lint/agent-linter.js +49 -0
  45. package/dist/commands/lint/base-linter.js +479 -0
  46. package/dist/commands/lint/command-linter.js +49 -0
  47. package/dist/commands/lint/default-linter.js +16 -0
  48. package/dist/commands/lint/env-key-rules.js +154 -0
  49. package/dist/commands/lint/index.js +196 -0
  50. package/dist/commands/lint/knowledge-linter.js +16 -0
  51. package/dist/commands/lint/markdown-insertion.js +343 -0
  52. package/dist/commands/lint/memory-linter.js +61 -0
  53. package/dist/commands/lint/registry.js +36 -0
  54. package/dist/commands/lint/skill-linter.js +45 -0
  55. package/dist/commands/lint/task-linter.js +50 -0
  56. package/dist/commands/lint/types.js +4 -0
  57. package/dist/commands/lint/workflow-linter.js +56 -0
  58. package/dist/commands/lint.js +4 -0
  59. package/dist/commands/migration-help.js +3 -0
  60. package/dist/commands/proposal.js +67 -12
  61. package/dist/commands/propose.js +120 -45
  62. package/dist/commands/reflect.js +1104 -60
  63. package/dist/commands/registry-cli.js +150 -0
  64. package/dist/commands/registry-search.js +5 -2
  65. package/dist/commands/remember-cli.js +257 -0
  66. package/dist/commands/remember.js +70 -7
  67. package/dist/commands/schema-repair.js +203 -0
  68. package/dist/commands/search.js +115 -14
  69. package/dist/commands/secret.js +173 -0
  70. package/dist/commands/self-update.js +3 -0
  71. package/dist/commands/show.js +158 -60
  72. package/dist/commands/source-add.js +17 -45
  73. package/dist/commands/source-clone.js +3 -0
  74. package/dist/commands/source-manage.js +14 -19
  75. package/dist/commands/tasks.js +437 -0
  76. package/dist/commands/url-checker.js +42 -0
  77. package/dist/core/action-contributors.js +28 -0
  78. package/dist/core/asset-ref.js +17 -2
  79. package/dist/core/asset-registry.js +12 -17
  80. package/dist/core/asset-serialize.js +88 -0
  81. package/dist/core/asset-spec.js +67 -1
  82. package/dist/core/common.js +182 -0
  83. package/dist/core/concurrent.js +25 -0
  84. package/dist/core/config-io.js +347 -0
  85. package/dist/core/config-migration.js +622 -0
  86. package/dist/core/config-schema.js +534 -0
  87. package/dist/core/config-sources.js +108 -0
  88. package/dist/core/config-types.js +4 -0
  89. package/dist/core/config-walker.js +337 -0
  90. package/dist/core/config.js +364 -968
  91. package/dist/core/errors.js +42 -20
  92. package/dist/core/events.js +105 -135
  93. package/dist/core/file-lock.js +104 -0
  94. package/dist/core/frontmatter.js +75 -8
  95. package/dist/core/lesson-lint.js +3 -0
  96. package/dist/core/markdown.js +20 -0
  97. package/dist/core/memory-belief.js +62 -0
  98. package/dist/core/memory-contradiction-detect.js +274 -0
  99. package/dist/core/memory-improve.js +806 -0
  100. package/dist/core/parse.js +158 -0
  101. package/dist/core/paths.js +280 -14
  102. package/dist/core/proposal-quality-validators.js +380 -0
  103. package/dist/core/proposal-validators.js +69 -0
  104. package/dist/core/proposals.js +512 -42
  105. package/dist/core/state-db.js +1068 -0
  106. package/dist/core/text-truncation.js +107 -0
  107. package/dist/core/time.js +54 -0
  108. package/dist/core/tty.js +59 -0
  109. package/dist/core/warn.js +64 -1
  110. package/dist/core/write-source.js +3 -0
  111. package/dist/indexer/db-backup.js +391 -0
  112. package/dist/indexer/db-search.js +198 -489
  113. package/dist/indexer/db.js +990 -108
  114. package/dist/indexer/ensure-index.js +136 -0
  115. package/dist/indexer/file-context.js +3 -0
  116. package/dist/indexer/graph-boost.js +376 -101
  117. package/dist/indexer/graph-db.js +391 -0
  118. package/dist/indexer/graph-dedup.js +95 -0
  119. package/dist/indexer/graph-extraction.js +550 -114
  120. package/dist/indexer/index-context.js +4 -0
  121. package/dist/indexer/indexer.js +547 -309
  122. package/dist/indexer/llm-cache.js +52 -0
  123. package/dist/indexer/manifest.js +3 -0
  124. package/dist/indexer/matchers.js +167 -160
  125. package/dist/indexer/memory-inference.js +152 -74
  126. package/dist/indexer/metadata-contributors.js +29 -0
  127. package/dist/indexer/metadata.js +275 -196
  128. package/dist/indexer/path-resolver.js +92 -0
  129. package/dist/indexer/project-context.js +192 -0
  130. package/dist/indexer/ranking-contributors.js +331 -0
  131. package/dist/indexer/ranking.js +81 -0
  132. package/dist/indexer/search-fields.js +5 -9
  133. package/dist/indexer/search-hit-enrichers.js +111 -0
  134. package/dist/indexer/search-source.js +44 -10
  135. package/dist/indexer/semantic-status.js +6 -17
  136. package/dist/indexer/staleness-detect.js +447 -0
  137. package/dist/indexer/usage-events.js +12 -9
  138. package/dist/indexer/walker.js +28 -0
  139. package/dist/integrations/agent/builders.js +135 -0
  140. package/dist/integrations/agent/config.js +122 -230
  141. package/dist/integrations/agent/detect.js +3 -0
  142. package/dist/integrations/agent/index.js +7 -13
  143. package/dist/integrations/agent/model-aliases.js +55 -0
  144. package/dist/integrations/agent/profiles.js +70 -5
  145. package/dist/integrations/agent/prompts.js +250 -36
  146. package/dist/integrations/agent/runner.js +151 -0
  147. package/dist/integrations/agent/sdk-runner.js +126 -0
  148. package/dist/integrations/agent/spawn.js +183 -35
  149. package/dist/integrations/github.js +3 -0
  150. package/dist/integrations/lockfile.js +32 -69
  151. package/dist/integrations/session-logs/index.js +69 -0
  152. package/dist/integrations/session-logs/inline-refs.js +35 -0
  153. package/dist/integrations/session-logs/pre-filter.js +152 -0
  154. package/dist/integrations/session-logs/providers/claude-code.js +282 -0
  155. package/dist/integrations/session-logs/providers/opencode.js +258 -0
  156. package/dist/integrations/session-logs/types.js +4 -0
  157. package/dist/llm/call-ai.js +62 -0
  158. package/dist/llm/client.js +79 -88
  159. package/dist/llm/embedder.js +20 -29
  160. package/dist/llm/embedders/cache.js +3 -7
  161. package/dist/llm/embedders/local.js +42 -1
  162. package/dist/llm/embedders/remote.js +20 -8
  163. package/dist/llm/embedders/types.js +3 -7
  164. package/dist/llm/feature-gate.js +95 -48
  165. package/dist/llm/graph-extract.js +676 -72
  166. package/dist/llm/index-passes.js +44 -29
  167. package/dist/llm/memory-infer.js +80 -71
  168. package/dist/llm/metadata-enhance.js +42 -29
  169. package/dist/llm/prompts/extract-session.md +80 -0
  170. package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
  171. package/dist/output/cli-hints-full.md +292 -0
  172. package/dist/output/cli-hints-short.md +66 -0
  173. package/dist/output/cli-hints.js +7 -311
  174. package/dist/output/context.js +60 -8
  175. package/dist/output/renderers.js +306 -258
  176. package/dist/output/shapes/curate.js +56 -0
  177. package/dist/output/shapes/distill.js +10 -0
  178. package/dist/output/shapes/env-list.js +19 -0
  179. package/dist/output/shapes/events.js +11 -0
  180. package/dist/output/shapes/helpers.js +424 -0
  181. package/dist/output/shapes/history.js +7 -0
  182. package/dist/output/shapes/passthrough.js +102 -0
  183. package/dist/output/shapes/proposal-accept.js +7 -0
  184. package/dist/output/shapes/proposal-diff.js +7 -0
  185. package/dist/output/shapes/proposal-list.js +7 -0
  186. package/dist/output/shapes/proposal-producer.js +11 -0
  187. package/dist/output/shapes/proposal-reject.js +7 -0
  188. package/dist/output/shapes/proposal-show.js +7 -0
  189. package/dist/output/shapes/registry-search.js +6 -0
  190. package/dist/output/shapes/registry.js +30 -0
  191. package/dist/output/shapes/search.js +6 -0
  192. package/dist/output/shapes/secret-list.js +19 -0
  193. package/dist/output/shapes/show.js +6 -0
  194. package/dist/output/shapes/vault-list.js +19 -0
  195. package/dist/output/shapes.js +51 -511
  196. package/dist/output/text/add.js +6 -0
  197. package/dist/output/text/clone.js +6 -0
  198. package/dist/output/text/config.js +6 -0
  199. package/dist/output/text/curate.js +6 -0
  200. package/dist/output/text/distill.js +7 -0
  201. package/dist/output/text/enable-disable.js +7 -0
  202. package/dist/output/text/events.js +10 -0
  203. package/dist/output/text/feedback.js +6 -0
  204. package/dist/output/text/helpers.js +1039 -0
  205. package/dist/output/text/history.js +7 -0
  206. package/dist/output/text/import.js +6 -0
  207. package/dist/output/text/index.js +6 -0
  208. package/dist/output/text/info.js +6 -0
  209. package/dist/output/text/init.js +6 -0
  210. package/dist/output/text/list.js +6 -0
  211. package/dist/output/text/proposal-producer.js +8 -0
  212. package/dist/output/text/proposal.js +11 -0
  213. package/dist/output/text/registry-commands.js +11 -0
  214. package/dist/output/text/registry.js +30 -0
  215. package/dist/output/text/remember.js +6 -0
  216. package/dist/output/text/remove.js +6 -0
  217. package/dist/output/text/save.js +6 -0
  218. package/dist/output/text/search.js +6 -0
  219. package/dist/output/text/show.js +6 -0
  220. package/dist/output/text/update.js +6 -0
  221. package/dist/output/text/upgrade.js +6 -0
  222. package/dist/output/text/vault.js +16 -0
  223. package/dist/output/text/wiki.js +15 -0
  224. package/dist/output/text/workflow.js +14 -0
  225. package/dist/output/text.js +44 -1093
  226. package/dist/registry/build-index.js +3 -0
  227. package/dist/registry/create-provider-registry.js +3 -0
  228. package/dist/registry/factory.js +4 -1
  229. package/dist/registry/origin-resolve.js +3 -0
  230. package/dist/registry/providers/index.js +3 -0
  231. package/dist/registry/providers/skills-sh.js +71 -50
  232. package/dist/registry/providers/static-index.js +53 -48
  233. package/dist/registry/providers/types.js +3 -24
  234. package/dist/registry/resolve.js +11 -16
  235. package/dist/registry/types.js +3 -0
  236. package/dist/scripts/migrate-storage.js +17750 -0
  237. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
  238. package/dist/scripts/migrations/v16-to-v17.js +141 -0
  239. package/dist/setup/detect.js +3 -0
  240. package/dist/setup/ripgrep-install.js +3 -0
  241. package/dist/setup/ripgrep-resolve.js +3 -0
  242. package/dist/setup/setup.js +775 -37
  243. package/dist/setup/steps.js +3 -15
  244. package/dist/sources/include.js +3 -0
  245. package/dist/sources/provider-factory.js +5 -12
  246. package/dist/sources/provider.js +3 -20
  247. package/dist/sources/providers/filesystem.js +19 -23
  248. package/dist/sources/providers/git.js +179 -20
  249. package/dist/sources/providers/index.js +3 -0
  250. package/dist/sources/providers/install-types.js +3 -13
  251. package/dist/sources/providers/npm.js +3 -4
  252. package/dist/sources/providers/provider-utils.js +3 -0
  253. package/dist/sources/providers/sync-from-ref.js +3 -11
  254. package/dist/sources/providers/tar-utils.js +3 -0
  255. package/dist/sources/providers/website.js +18 -22
  256. package/dist/sources/resolve.js +3 -0
  257. package/dist/sources/types.js +3 -0
  258. package/dist/sources/website-ingest.js +7 -0
  259. package/dist/tasks/backends/cron.js +203 -0
  260. package/dist/tasks/backends/exec-utils.js +28 -0
  261. package/dist/tasks/backends/index.js +24 -0
  262. package/dist/tasks/backends/launchd-template.xml +19 -0
  263. package/dist/tasks/backends/launchd.js +187 -0
  264. package/dist/tasks/backends/schtasks-template.xml +29 -0
  265. package/dist/tasks/backends/schtasks.js +215 -0
  266. package/dist/tasks/parser.js +211 -0
  267. package/dist/tasks/resolveAkmBin.js +87 -0
  268. package/dist/tasks/runner.js +458 -0
  269. package/dist/tasks/schedule.js +227 -0
  270. package/dist/tasks/schema.js +15 -0
  271. package/dist/tasks/validator.js +62 -0
  272. package/dist/version.js +3 -0
  273. package/dist/wiki/index-template.md +12 -0
  274. package/dist/wiki/ingest-workflow-template.md +54 -0
  275. package/dist/wiki/log-template.md +8 -0
  276. package/dist/wiki/schema-template.md +61 -0
  277. package/dist/wiki/wiki-templates.js +15 -0
  278. package/dist/wiki/wiki.js +13 -61
  279. package/dist/workflows/authoring.js +8 -25
  280. package/dist/workflows/cli.js +3 -0
  281. package/dist/workflows/db.js +141 -2
  282. package/dist/workflows/document-cache.js +3 -10
  283. package/dist/workflows/parser.js +3 -0
  284. package/dist/workflows/renderer.js +11 -3
  285. package/dist/workflows/runs.js +91 -89
  286. package/dist/workflows/schema.js +3 -0
  287. package/dist/workflows/scope-key.js +79 -0
  288. package/dist/workflows/validator.js +4 -8
  289. package/dist/workflows/workflow-template.md +24 -0
  290. package/docs/README.md +10 -2
  291. package/docs/data-and-telemetry.md +225 -0
  292. package/docs/migration/release-notes/0.7.0.md +1 -1
  293. package/docs/migration/release-notes/0.7.4.md +1 -1
  294. package/docs/migration/release-notes/0.7.5.md +20 -0
  295. package/docs/migration/release-notes/0.8.0.md +48 -0
  296. package/docs/migration/v0.7-to-v0.8.md +1307 -0
  297. package/package.json +29 -11
  298. package/dist/commands/install-audit.js +0 -381
  299. package/dist/commands/vault.js +0 -333
  300. package/dist/templates/wiki-templates.js +0 -100
@@ -1,43 +1,19 @@
1
- /**
2
- * Memory inference pass for `akm index` (#201).
3
- *
4
- * Detects memories pending inference, asks the configured LLM to compress each
5
- * into one higher-signal derived memory, and writes the result back as a new
6
- * memory file with frontmatter `inferred: true` + a `source:` backref to the
7
- * parent memory.
8
- *
9
- * Pending predicate (see {@link isPendingMemory}):
10
- * - File lives under `<stashRoot>/memories/` and ends in `.md`.
11
- * - Frontmatter does NOT have `inferenceProcessed: true` (parent already split).
12
- * - Frontmatter does NOT have `inferred: true` (this is itself a child fact).
13
- *
14
- * Idempotency: after a successful split the parent's frontmatter is rewritten
15
- * with `inferenceProcessed: true`. A subsequent `akm index` therefore skips
16
- * the parent without re-running the LLM.
17
- *
18
- * Disabling — two orthogonal gates per v1 spec §14:
19
- * 1. `llm.features.memory_inference = false` blocks the pass at the
20
- * locked feature-flag layer (no network call may ever issue).
21
- * 2. `index.memory.llm = false` (or no `akm.llm` block at all) opts the
22
- * pass out at the per-pass layer (#208).
23
- * A pass runs iff both layers allow it. Existing inferred children are
24
- * NEVER deleted — the user keeps what was already produced.
25
- *
26
- * Locked v1 contract:
27
- * - LLM access is exclusively via `resolveIndexPassLLM("memory", config)`.
28
- * - All child memory writes go through `writeAssetToSource` in
29
- * `src/core/write-source.ts`. The parent's frontmatter rewrite is an
30
- * explicit narrow exception — see {@link markParentProcessed}.
31
- */
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
32
4
  import fs from "node:fs";
33
5
  import path from "node:path";
34
- import { stringify as yamlStringify } from "yaml";
35
6
  import { parseAssetRef } from "../core/asset-ref";
7
+ import { assembleAsset } from "../core/asset-serialize";
8
+ import { concurrentMap } from "../core/concurrent";
36
9
  import { parseFrontmatter, parseFrontmatterBlock } from "../core/frontmatter";
37
10
  import { warn } from "../core/warn";
38
11
  import { writeAssetToSource } from "../core/write-source";
12
+ import { isProcessEnabled } from "../llm/feature-gate";
39
13
  import { resolveIndexPassLLM } from "../llm/index-passes";
40
- import { compressMemoryToDerivedMemory } from "../llm/memory-infer";
14
+ import * as memoryInfer from "../llm/memory-infer";
15
+ import { withLlmCache } from "./llm-cache";
16
+ import { walkMarkdownFiles } from "./walker";
41
17
  /**
42
18
  * Frontmatter keys this pass cares about. Constants so a future rename only
43
19
  * needs to touch one site.
@@ -45,14 +21,15 @@ import { compressMemoryToDerivedMemory } from "../llm/memory-infer";
45
21
  const FM_INFERRED = "inferred";
46
22
  const FM_INFERENCE_PROCESSED = "inferenceProcessed";
47
23
  const FM_SOURCE = "source";
24
+ const FM_CAPTURE_MODE = "captureMode";
48
25
  /**
49
26
  * Top-level entry point. Returns a no-op result when the pass is disabled.
50
27
  *
51
- * Two orthogonal gates per v1 spec §14:
28
+ * Two orthogonal gates:
52
29
  *
53
- * 1. **Feature gate** — `llm.features.memory_inference` (defaults to
54
- * `true`). When `false`, no network call may issue regardless of
55
- * per-pass settings. This is the locked spec-§14 gate.
30
+ * 1. **Feature gate** — `profiles.improve.default.processes.memoryInference.enabled`
31
+ * (defaults to `true`). When `false`, no network call may issue regardless
32
+ * of per-pass settings.
56
33
  * 2. **Per-pass gate** — `resolveIndexPassLLM("memory", config)` (which
57
34
  * reads `index.memory.llm`). When `false`, the indexer simply skips
58
35
  * this pass for the current run.
@@ -60,16 +37,21 @@ const FM_SOURCE = "source";
60
37
  * Both must allow the call for the pass to run. Either set to `false`
61
38
  * short-circuits to a no-op result.
62
39
  */
63
- export async function runMemoryInferencePass(config, sources, signal) {
40
+ export async function runMemoryInferencePass(config, sources, signal, db, reEnrich, onProgress, options = {}) {
64
41
  const result = {
65
42
  considered: 0,
43
+ cacheHits: 0,
66
44
  splitParents: 0,
67
45
  writtenFacts: 0,
68
46
  skippedNoFacts: 0,
47
+ skippedChildExists: 0,
48
+ skippedAborted: 0,
49
+ unaccounted: 0,
69
50
  };
70
- // Gate 1 — locked feature flag (§14). Defaults to enabled; only an
71
- // explicit `false` disables the pass entirely.
72
- if (config.llm?.features?.memory_inference === false)
51
+ // Gate 1 — feature gate via isProcessEnabled, which reads the 0.8.0 path
52
+ // (profiles.improve.default.processes.memoryInference.enabled). Defaults to
53
+ // enabled when the key is absent.
54
+ if (!isProcessEnabled("index", "memory_inference", config))
73
55
  return result;
74
56
  // Gate 2 — per-pass opt-out (#208). Returns the resolved llm config or
75
57
  // `undefined` when the pass should not run.
@@ -82,26 +64,124 @@ export async function runMemoryInferencePass(config, sources, signal) {
82
64
  const primary = sources[0];
83
65
  if (!primary)
84
66
  return result;
85
- const pending = collectPendingMemories(primary.path);
67
+ const pending = collectPendingMemories(primary.path).filter((record) => !options.candidateRefs || options.candidateRefs.has(record.ref));
86
68
  result.considered = pending.length;
87
69
  if (pending.length === 0)
88
70
  return result;
89
- for (const record of pending) {
71
+ let processed = 0;
72
+ const total = pending.length;
73
+ onProgress?.({ processed, total, writtenFacts: 0, skippedNoFacts: 0 });
74
+ const perRecordResults = await concurrentMap(pending, async (record) => {
75
+ // Aborted BEFORE a fresh LLM call. Returned as a typed outcome so the
76
+ // for-loop below increments `skippedAborted` instead of silently
77
+ // dropping the record (which historically inflated freshAttempts and
78
+ // dragged the health-reported yield rate down — see investigation
79
+ // 2026-05-26).
90
80
  if (signal?.aborted)
91
- return result;
92
- const derived = await compressMemoryToDerivedMemory(llmConfig, record.body, signal);
81
+ return { aborted: true };
82
+ // Incremental cache: skip LLM call when body hash is unchanged and
83
+ // --re-enrich was not requested. The cache ref is the absolute file path.
84
+ const validate = (raw) => {
85
+ if (!raw || typeof raw !== "object")
86
+ return undefined;
87
+ const parsed = raw;
88
+ const title = typeof parsed.title === "string" ? parsed.title : "";
89
+ const description = typeof parsed.description === "string" ? parsed.description : "";
90
+ const content = typeof parsed.content === "string" ? parsed.content : "";
91
+ const tags = Array.isArray(parsed.tags) ? parsed.tags.filter((t) => typeof t === "string") : [];
92
+ const searchHints = Array.isArray(parsed.searchHints)
93
+ ? parsed.searchHints.filter((h) => typeof h === "string")
94
+ : [];
95
+ if (title && description && content && tags.length > 0 && searchHints.length > 0) {
96
+ return { title, description, tags, searchHints, content };
97
+ }
98
+ return undefined;
99
+ };
100
+ // Track whether THIS candidate's result came from the body-hash
101
+ // cache vs. a fresh LLM call. The cache short-circuits when the
102
+ // parent body has not changed since a prior derived write — surfacing
103
+ // the hit count separately so the operational yield rate
104
+ // (writtenFacts / freshAttempts) is interpretable as the cache warms.
105
+ let fromCache = false;
106
+ const derived = db
107
+ ? await withLlmCache(db, record.filePath, record.body, reEnrich ?? false, () => memoryInfer.compressMemoryToDerivedMemory(llmConfig, record.body, signal, config, (evt) => {
108
+ warn(`[akm] LLM fallback for ${evt.feature}: ${evt.reason}`);
109
+ }), validate, undefined, "", {
110
+ onCacheHit: () => {
111
+ fromCache = true;
112
+ },
113
+ })
114
+ : await memoryInfer.compressMemoryToDerivedMemory(llmConfig, record.body, signal, config, (evt) => {
115
+ warn(`[akm] LLM fallback for ${evt.feature}: ${evt.reason}`);
116
+ });
93
117
  if (!derived) {
94
- result.skippedNoFacts += 1;
95
- // Intentionally NOT marked processed — a transient LLM failure should
96
- // be retried on the next index run.
97
- continue;
118
+ return { skipped: true, fromCache };
98
119
  }
99
120
  const written = await writeDerivedMemory(record, derived);
100
121
  if (written > 0) {
101
122
  markParentProcessed(record);
123
+ return { skipped: false, splitParent: true, written, fromCache };
124
+ }
125
+ // LLM produced a valid derived draft but no file was written — either
126
+ // because `<parent>.derived.md` already exists on disk or
127
+ // `writeAssetToSource` threw. Categorise as `childExists` so the
128
+ // attempt is accounted for in health metrics rather than vanishing
129
+ // into the freshAttempts denominator.
130
+ return { skipped: false, splitParent: false, written: 0, fromCache, childExists: true };
131
+ },
132
+ // Default concurrency of 4 for cloud APIs. Set `llm.concurrency: 1`
133
+ // in config.json for local model servers (LM Studio, Ollama).
134
+ llmConfig.concurrency ?? 1);
135
+ for (let i = 0; i < perRecordResults.length; i++) {
136
+ const res = perRecordResults[i];
137
+ if (!res)
138
+ continue;
139
+ if ("aborted" in res && res.aborted) {
140
+ result.skippedAborted += 1;
141
+ processed++;
142
+ onProgress?.({
143
+ processed,
144
+ total,
145
+ writtenFacts: result.writtenFacts,
146
+ skippedNoFacts: result.skippedNoFacts,
147
+ currentRef: pending[i]?.ref,
148
+ });
149
+ continue;
150
+ }
151
+ if (res.fromCache) {
152
+ result.cacheHits += 1;
153
+ }
154
+ if (res.skipped) {
155
+ result.skippedNoFacts += 1;
156
+ // Intentionally NOT marked processed — a transient LLM failure should
157
+ // be retried on the next index run.
158
+ }
159
+ else if (res.splitParent) {
102
160
  result.splitParents += 1;
103
- result.writtenFacts += written;
161
+ result.writtenFacts += res.written;
162
+ }
163
+ else if ("childExists" in res && res.childExists) {
164
+ // LLM call was consumed but the derived file already existed (or the
165
+ // write threw). Track separately so this category is observable in
166
+ // health output and stops bleeding into the freshAttempts denominator.
167
+ result.skippedChildExists += 1;
168
+ warn(`memory inference: derived child for ${pending[i]?.ref ?? "<unknown>"} already existed or write failed; counted as skippedChildExists`);
169
+ }
170
+ else {
171
+ // The per-record state machine should cover every outcome. A hit here
172
+ // means a new code path slipped past the categorisation — surface it
173
+ // loudly so health metrics stay honest and we get a signal to fix.
174
+ result.unaccounted += 1;
175
+ warn(`memory inference: unaccounted per-record outcome for ${pending[i]?.ref ?? "<unknown>"}`);
104
176
  }
177
+ processed++;
178
+ onProgress?.({
179
+ processed,
180
+ total,
181
+ writtenFacts: result.writtenFacts,
182
+ skippedNoFacts: result.skippedNoFacts,
183
+ currentRef: pending[i]?.ref,
184
+ });
105
185
  }
106
186
  return result;
107
187
  }
@@ -125,7 +205,7 @@ export function collectPendingMemories(stashRoot) {
125
205
  continue;
126
206
  }
127
207
  const parsed = parseFrontmatter(raw);
128
- if (!isPendingMemory(parsed.data))
208
+ if (!isPendingMemory(parsed.data, filePath))
129
209
  continue;
130
210
  const relName = toMemoryName(memoriesDir, filePath);
131
211
  if (!relName)
@@ -145,34 +225,33 @@ export function collectPendingMemories(stashRoot) {
145
225
  * Predicate: true when the parsed frontmatter indicates the memory has not
146
226
  * yet been split AND is not itself an inferred child.
147
227
  *
228
+ * Also guards against `.derived` files whose `inferred:` frontmatter key has
229
+ * been dropped by a manual edit or schema-repair rewrite. The file name suffix
230
+ * is structural and immutable; frontmatter flags are mutable. A file whose
231
+ * path contains `.derived` is always treated as a derived child regardless of
232
+ * its frontmatter state — this prevents `<name>.derived.derived.md` chains.
233
+ *
234
+ * @param frontmatter - Parsed YAML frontmatter from the memory file.
235
+ * @param filePath - Optional absolute path to the memory file. When
236
+ * supplied, the name-based guard is applied.
237
+ *
148
238
  * Exported for direct unit testing — keeping the predicate in one place
149
239
  * avoids drift between the walker, tests, and any future consumers.
150
240
  */
151
- export function isPendingMemory(frontmatter) {
241
+ export function isPendingMemory(frontmatter, filePath) {
242
+ // Name-based guard: a `.derived` suffix in the path means this file is a
243
+ // derived child regardless of what its frontmatter currently says.
244
+ if (filePath !== undefined) {
245
+ const base = path.basename(filePath, ".md");
246
+ if (base.endsWith(".derived"))
247
+ return false;
248
+ }
152
249
  if (frontmatter[FM_INFERRED] === true)
153
250
  return false;
154
251
  if (frontmatter[FM_INFERENCE_PROCESSED] === true)
155
252
  return false;
156
253
  return true;
157
254
  }
158
- function* walkMarkdownFiles(root) {
159
- let entries;
160
- try {
161
- entries = fs.readdirSync(root, { withFileTypes: true });
162
- }
163
- catch {
164
- return;
165
- }
166
- for (const entry of entries) {
167
- const full = path.join(root, entry.name);
168
- if (entry.isDirectory()) {
169
- yield* walkMarkdownFiles(full);
170
- }
171
- else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
172
- yield full;
173
- }
174
- }
175
- }
176
255
  function toMemoryName(memoriesDir, filePath) {
177
256
  const rel = path.relative(memoriesDir, filePath);
178
257
  if (!rel || rel.startsWith(".."))
@@ -214,6 +293,7 @@ async function writeDerivedMemory(parent, derived) {
214
293
  function renderDerivedMemory(parent, derived) {
215
294
  const fm = {
216
295
  [FM_INFERRED]: true,
296
+ [FM_CAPTURE_MODE]: "background",
217
297
  [FM_SOURCE]: parent.ref,
218
298
  description: derived.description,
219
299
  tags: derived.tags,
@@ -221,8 +301,7 @@ function renderDerivedMemory(parent, derived) {
221
301
  title: derived.title,
222
302
  derivedFrom: parent.name,
223
303
  };
224
- const yaml = yamlStringify(fm).trimEnd();
225
- return `---\n${yaml}\n---\n\n# ${derived.title.trim()}\n\n${derived.content.trim()}\n`;
304
+ return assembleAsset(fm, `# ${derived.title.trim()}\n\n${derived.content.trim()}\n`);
226
305
  }
227
306
  function markParentProcessed(parent) {
228
307
  // Frontmatter-only rewrite of an existing asset: not a new asset write,
@@ -239,10 +318,9 @@ function markParentProcessed(parent) {
239
318
  return;
240
319
  }
241
320
  const updatedFm = { ...parent.data, [FM_INFERENCE_PROCESSED]: true };
242
- const yaml = yamlStringify(updatedFm).trimEnd();
243
321
  const block = parseFrontmatterBlock(raw);
244
322
  const body = block?.content ?? raw;
245
- const next = `---\n${yaml}\n---\n${body.startsWith("\n") ? "" : "\n"}${body}`;
323
+ const next = assembleAsset(updatedFm, body);
246
324
  try {
247
325
  fs.writeFileSync(parent.filePath, next, "utf8");
248
326
  }
@@ -0,0 +1,29 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ const contributors = [];
5
+ let builtinsPromise;
6
+ async function ensureBuiltinMetadataContributorsRegistered() {
7
+ if (!builtinsPromise) {
8
+ builtinsPromise = (async () => {
9
+ await import("../output/renderers.js");
10
+ await import("../workflows/renderer.js");
11
+ })();
12
+ }
13
+ return builtinsPromise;
14
+ }
15
+ export function registerMetadataContributor(contributor) {
16
+ contributors.push(contributor);
17
+ }
18
+ export async function getMetadataContributors() {
19
+ await ensureBuiltinMetadataContributorsRegistered();
20
+ return [...contributors];
21
+ }
22
+ export async function applyMetadataContributors(entry, ctx) {
23
+ const activeContributors = await getMetadataContributors();
24
+ for (const contributor of activeContributors) {
25
+ if (!contributor.appliesTo(ctx))
26
+ continue;
27
+ contributor.contribute(entry, ctx);
28
+ }
29
+ }