akm-cli 0.9.0-beta.5 → 0.9.0-beta.51

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 (221) hide show
  1. package/CHANGELOG.md +711 -0
  2. package/README.md +12 -4
  3. package/dist/akm +38 -0
  4. package/dist/akm-migrate-storage +38 -0
  5. package/dist/assets/profiles/default.json +9 -4
  6. package/dist/assets/profiles/frequent.json +1 -1
  7. package/dist/assets/profiles/memory-focus.json +1 -1
  8. package/dist/assets/profiles/quick.json +1 -1
  9. package/dist/assets/profiles/synthesize.json +15 -0
  10. package/dist/assets/profiles/thorough.json +1 -1
  11. package/dist/assets/prompts/consolidate-system.md +23 -0
  12. package/dist/assets/prompts/contradiction-judge.md +33 -0
  13. package/dist/assets/prompts/distill-knowledge-system.md +22 -0
  14. package/dist/assets/prompts/distill-lesson-system.md +36 -0
  15. package/dist/assets/prompts/extract-session.md +6 -2
  16. package/dist/assets/prompts/graph-extract-system.md +1 -0
  17. package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
  18. package/dist/assets/prompts/memory-infer-system.md +1 -0
  19. package/dist/assets/prompts/memory-infer-user.md +5 -0
  20. package/dist/assets/prompts/metadata-enhance-system.md +1 -0
  21. package/dist/assets/prompts/procedural-system.md +44 -0
  22. package/dist/assets/prompts/recombine-system.md +40 -0
  23. package/dist/assets/prompts/staleness-detect-system.md +6 -0
  24. package/dist/assets/prompts/validate-summary-judge.md +1 -0
  25. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
  26. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
  27. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
  28. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
  29. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
  34. package/dist/assets/templates/html/health.html +281 -111
  35. package/dist/assets/wiki/ingest-workflow-template.md +38 -10
  36. package/dist/cli/parse-args.js +46 -1
  37. package/dist/cli/shared.js +28 -0
  38. package/dist/cli.js +27 -11
  39. package/dist/commands/agent/agent-dispatch.js +2 -2
  40. package/dist/commands/agent/agent-support.js +0 -7
  41. package/dist/commands/agent/contribute-cli.js +17 -4
  42. package/dist/commands/config-cli.js +18 -2
  43. package/dist/commands/env/child-env.js +47 -0
  44. package/dist/commands/env/env-cli.js +33 -26
  45. package/dist/commands/env/secret-cli.js +36 -22
  46. package/dist/commands/feedback-cli.js +15 -6
  47. package/dist/commands/graph/graph-cli.js +5 -13
  48. package/dist/commands/graph/graph.js +76 -72
  49. package/dist/commands/health/checks.js +49 -1
  50. package/dist/commands/health/html-report.js +422 -80
  51. package/dist/commands/health.js +386 -9
  52. package/dist/commands/improve/calibration.js +161 -0
  53. package/dist/commands/improve/consolidate/chunking.js +141 -0
  54. package/dist/commands/improve/consolidate/eligibility.js +81 -0
  55. package/dist/commands/improve/consolidate/merge.js +145 -0
  56. package/dist/commands/improve/consolidate/sanitize.js +231 -0
  57. package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
  58. package/dist/commands/improve/consolidate.js +635 -660
  59. package/dist/commands/improve/dedup.js +482 -0
  60. package/dist/commands/improve/distill.js +159 -69
  61. package/dist/commands/improve/eligibility.js +434 -0
  62. package/dist/commands/improve/encoding-salience.js +205 -0
  63. package/dist/commands/improve/extract-cli.js +124 -2
  64. package/dist/commands/improve/extract-prompt.js +39 -2
  65. package/dist/commands/improve/extract-watch.js +140 -0
  66. package/dist/commands/improve/extract.js +389 -40
  67. package/dist/commands/improve/feedback-valence.js +54 -0
  68. package/dist/commands/improve/homeostatic.js +467 -0
  69. package/dist/commands/improve/improve-auto-accept.js +138 -7
  70. package/dist/commands/improve/improve-cli.js +36 -61
  71. package/dist/commands/improve/improve-profiles.js +14 -0
  72. package/dist/commands/improve/improve-result-file.js +14 -25
  73. package/dist/commands/improve/improve-session.js +58 -0
  74. package/dist/commands/improve/improve.js +485 -2498
  75. package/dist/commands/improve/locks.js +154 -0
  76. package/dist/commands/improve/loop-stages.js +1083 -0
  77. package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
  78. package/dist/commands/improve/outcome-loop.js +256 -0
  79. package/dist/commands/improve/preparation.js +1966 -0
  80. package/dist/commands/improve/proactive-maintenance.js +115 -0
  81. package/dist/commands/improve/procedural.js +418 -0
  82. package/dist/commands/improve/recombine.js +850 -0
  83. package/dist/commands/improve/reflect-noise.js +0 -0
  84. package/dist/commands/improve/reflect.js +183 -40
  85. package/dist/commands/improve/salience.js +438 -0
  86. package/dist/commands/improve/triage.js +93 -0
  87. package/dist/commands/lint/agent-linter.js +19 -24
  88. package/dist/commands/lint/base-linter.js +173 -60
  89. package/dist/commands/lint/command-linter.js +19 -24
  90. package/dist/commands/lint/env-key-rules.js +38 -1
  91. package/dist/commands/lint/fact-linter.js +39 -0
  92. package/dist/commands/lint/index.js +31 -13
  93. package/dist/commands/lint/memory-linter.js +1 -1
  94. package/dist/commands/lint/registry.js +7 -2
  95. package/dist/commands/lint/task-linter.js +3 -3
  96. package/dist/commands/lint/workflow-linter.js +26 -1
  97. package/dist/commands/proposal/drain-policies.js +5 -0
  98. package/dist/commands/proposal/drain.js +43 -50
  99. package/dist/commands/proposal/proposal-cli.js +21 -31
  100. package/dist/commands/proposal/proposal.js +5 -0
  101. package/dist/commands/proposal/propose.js +7 -2
  102. package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
  103. package/dist/commands/proposal/validators/proposals.js +189 -63
  104. package/dist/commands/read/curate.js +414 -94
  105. package/dist/commands/read/knowledge.js +6 -3
  106. package/dist/commands/read/search-cli.js +9 -4
  107. package/dist/commands/read/search.js +10 -6
  108. package/dist/commands/read/show.js +86 -7
  109. package/dist/commands/sources/init.js +49 -17
  110. package/dist/commands/sources/installed-stashes.js +11 -3
  111. package/dist/commands/sources/schema-repair.js +43 -45
  112. package/dist/commands/sources/self-update.js +2 -2
  113. package/dist/commands/sources/source-add.js +7 -3
  114. package/dist/commands/sources/stash-cli.js +28 -40
  115. package/dist/commands/sources/stash-skeleton.js +23 -8
  116. package/dist/commands/tasks/tasks-cli.js +19 -27
  117. package/dist/commands/tasks/tasks.js +39 -11
  118. package/dist/commands/wiki-cli.js +21 -35
  119. package/dist/core/asset/asset-registry.js +3 -1
  120. package/dist/core/asset/asset-spec.js +18 -2
  121. package/dist/core/asset/frontmatter.js +166 -167
  122. package/dist/core/asset/markdown.js +8 -0
  123. package/dist/core/authoring-rules.js +92 -0
  124. package/dist/core/common.js +0 -5
  125. package/dist/core/config/config-migration.js +12 -11
  126. package/dist/core/config/config-schema.js +340 -56
  127. package/dist/core/config/config-types.js +3 -3
  128. package/dist/core/config/config.js +28 -7
  129. package/dist/core/events.js +3 -7
  130. package/dist/core/improve-types.js +11 -8
  131. package/dist/core/logs-db.js +10 -66
  132. package/dist/core/parse.js +36 -16
  133. package/dist/core/paths.js +3 -0
  134. package/dist/core/standards/resolve-standards-context.js +87 -0
  135. package/dist/core/standards/resolve-stash-standards.js +99 -0
  136. package/dist/core/standards/resolve-type-conventions.js +66 -0
  137. package/dist/core/state/migrations.js +714 -0
  138. package/dist/core/state-db.js +525 -474
  139. package/dist/indexer/db/db.js +439 -247
  140. package/dist/indexer/db/graph-db.js +129 -86
  141. package/dist/indexer/ensure-index.js +152 -17
  142. package/dist/indexer/graph/graph-boost.js +51 -41
  143. package/dist/indexer/graph/graph-extraction.js +218 -4
  144. package/dist/indexer/index-writer-lock.js +99 -0
  145. package/dist/indexer/indexer.js +123 -221
  146. package/dist/indexer/passes/dir-staleness.js +114 -0
  147. package/dist/indexer/passes/memory-inference.js +13 -5
  148. package/dist/indexer/passes/staleness-detect.js +2 -5
  149. package/dist/indexer/search/db-search.js +19 -6
  150. package/dist/indexer/search/ranking-contributors.js +22 -0
  151. package/dist/indexer/search/ranking.js +4 -0
  152. package/dist/indexer/search/search-source.js +17 -18
  153. package/dist/indexer/search/semantic-status.js +4 -0
  154. package/dist/indexer/walk/matchers.js +9 -0
  155. package/dist/integrations/agent/config.js +6 -53
  156. package/dist/integrations/agent/index.js +2 -18
  157. package/dist/integrations/agent/prompts.js +75 -9
  158. package/dist/integrations/agent/runner-dispatch.js +59 -0
  159. package/dist/integrations/harnesses/claude/session-log.js +11 -1
  160. package/dist/integrations/harnesses/index.js +2 -3
  161. package/dist/integrations/harnesses/opencode/session-log.js +173 -3
  162. package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
  163. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +0 -2
  164. package/dist/integrations/session-logs/index.js +16 -0
  165. package/dist/llm/client.js +45 -15
  166. package/dist/llm/embedder.js +42 -3
  167. package/dist/llm/embedders/deterministic.js +66 -0
  168. package/dist/llm/embedders/local.js +66 -2
  169. package/dist/llm/feature-gate.js +8 -4
  170. package/dist/llm/graph-extract.js +67 -44
  171. package/dist/llm/memory-infer-impl.js +138 -0
  172. package/dist/llm/memory-infer.js +1 -127
  173. package/dist/llm/metadata-enhance.js +44 -31
  174. package/dist/llm/structured-call.js +49 -0
  175. package/dist/migrate-storage-node.mjs +8 -0
  176. package/dist/output/context.js +5 -5
  177. package/dist/output/renderers.js +74 -2
  178. package/dist/output/shapes/curate.js +14 -2
  179. package/dist/output/shapes/passthrough.js +0 -1
  180. package/dist/output/text/helpers.js +16 -1
  181. package/dist/registry/providers/skills-sh.js +21 -147
  182. package/dist/registry/providers/static-index.js +15 -157
  183. package/dist/registry/resolve.js +22 -9
  184. package/dist/runtime.js +25 -1
  185. package/dist/scripts/migrate-storage.js +2617 -1961
  186. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +759 -510
  187. package/dist/setup/setup.js +29 -8
  188. package/dist/sources/include.js +6 -2
  189. package/dist/sources/providers/filesystem.js +0 -1
  190. package/dist/sources/providers/git-install.js +210 -0
  191. package/dist/sources/providers/git-provider.js +234 -0
  192. package/dist/sources/providers/git-stash.js +248 -0
  193. package/dist/sources/providers/git.js +10 -661
  194. package/dist/sources/providers/npm.js +2 -6
  195. package/dist/sources/providers/provider-utils.js +13 -7
  196. package/dist/sources/providers/sync-from-ref.js +9 -1
  197. package/dist/sources/providers/tar-utils.js +16 -8
  198. package/dist/sources/providers/website.js +9 -5
  199. package/dist/sources/website-ingest.js +187 -29
  200. package/dist/sources/wiki-fetchers/registry.js +53 -0
  201. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  202. package/dist/storage/database.js +45 -10
  203. package/dist/storage/managed-db.js +82 -0
  204. package/dist/storage/repositories/registry-cache.js +92 -0
  205. package/dist/storage/sqlite-pragmas.js +146 -0
  206. package/dist/tasks/backends/cron.js +1 -1
  207. package/dist/tasks/backends/launchd.js +1 -1
  208. package/dist/tasks/backends/schtasks.js +1 -1
  209. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  210. package/dist/tasks/runner.js +5 -13
  211. package/dist/text-import-hook.mjs +0 -0
  212. package/dist/wiki/wiki.js +37 -0
  213. package/dist/workflows/db.js +3 -4
  214. package/dist/workflows/runtime/runs.js +1 -117
  215. package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
  216. package/dist/workflows/validate-summary.js +2 -7
  217. package/docs/data-and-telemetry.md +3 -2
  218. package/docs/migration/release-notes/0.9.0.md +39 -0
  219. package/package.json +13 -11
  220. package/dist/commands/db-cli.js +0 -23
  221. package/dist/indexer/db/db-backup.js +0 -376
@@ -10,8 +10,10 @@ import { getDbPath } from "../core/paths.js";
10
10
  import { isVerbose, warn, warnVerbose } from "../core/warn.js";
11
11
  import { resolveIndexPassLLM } from "../llm/index-passes.js";
12
12
  import { takeWorkflowDocument } from "../workflows/runtime/document-cache.js";
13
- import { clearStaleCacheEntries, closeDatabase, deleteEntriesByDir, deleteEntriesByIds, deleteEntriesByStashDir, deleteIndexDirStatesByStashDir, getAllEntriesForEmbedding, getEmbeddableEntryCount, getEmbeddingCount, getEntriesByDir, getEntryCount, getIndexDirState, getMeta, isVecAvailable, openDatabase, openExistingDatabase, rebuildFts, relinkUsageEvents, setMeta, upsertEmbedding, upsertEntry, upsertIndexDirState, upsertUtilityScore, upsertWorkflowDocument, warnIfVecMissing, } from "./db/db.js";
13
+ import { clearStaleCacheEntries, closeDatabase, deleteEntriesByDir, deleteEntriesByIds, deleteEntriesByStashDir, deleteIndexDirStatesByStashDir, getAllEntriesForEmbedding, getEmbeddableEntryCount, getEmbeddingCount, getEntryCount, getMeta, isVecAvailable, openExistingDatabase, openIndexDatabase, purgeEmbeddings, rebuildFts, relinkUsageEvents, setMeta, upsertEmbedding, upsertEntry, upsertIndexDirState, upsertUtilityScore, upsertWorkflowDocument, warnIfVecMissing, } from "./db/db.js";
14
14
  import { deleteStoredGraph } from "./db/graph-db.js";
15
+ import { withIndexWriterLease } from "./index-writer-lock.js";
16
+ import { canUseIncrementalSkip, computeDirFingerprint, getCachedZeroRowDirState, getDirIndexState, inferZeroRowReason, } from "./passes/dir-staleness.js";
15
17
  import { applyCuratedFrontmatter, applyWikiFrontmatter, generateMetadataFlat, isEnrichmentComplete, isWorkflowSkipWarning, loadStashFile, shouldIndexStashFile, } from "./passes/metadata.js";
16
18
  import { buildSearchText } from "./search/search-fields.js";
17
19
  import { classifySemanticFailure, clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus, } from "./search/semantic-status.js";
@@ -195,8 +197,8 @@ async function runFinalizePhase(ctx) {
195
197
  }
196
198
  onProgress({ phase: "verify", message: verification.message });
197
199
  // Store verification result and totalEntries on ctx for the caller to use
198
- ctx._verification = verification;
199
- ctx._totalEntries = totalEntries;
200
+ ctx.verification = verification;
201
+ ctx.totalEntries = totalEntries;
200
202
  // suppress unused warning — sources was previously used inline
201
203
  void sources;
202
204
  }
@@ -225,119 +227,123 @@ function runCleanPass(db, dryRun) {
225
227
  }
226
228
  // ── Indexer ──────────────────────────────────────────────────────────────────
227
229
  export async function akmIndex(options) {
228
- const stashDir = options?.stashDir || resolveStashDir();
229
- const onProgress = options?.onProgress ?? (() => { });
230
- const signal = options?.signal;
231
- const reEnrich = options?.reEnrich === true;
232
- const full = options?.full === true;
233
- const clean = options?.clean === true;
234
- const dryRun = options?.dryRun === true;
235
- // Load config and resolve all stash sources
236
- const { loadConfig } = await import("../core/config/config.js");
237
- const config = loadConfig();
238
- // One-time, read-only guard: warn if the writable stash still holds an
239
- // un-migrated `vaults/` directory. In 0.9.0 the indexer skips `vaults/`
240
- // entirely, so an unmigrated vault's `.env` data would silently never be
241
- // indexed. Non-destructive only stats, never reads/writes/deletes.
242
- const { warnOnUnmigratedVaults } = await import("./usage/unmigrated-vaults-guard.js");
243
- warnOnUnmigratedVaults(stashDir);
244
- // Ensure git stash caches are extracted before resolving stash dirs,
245
- // so their content directories exist on disk for the walker to discover.
246
- const { ensureSourceCaches, resolveSourceEntries } = await import("./search/search-source.js");
247
- await ensureSourceCaches(config, { force: full });
248
- const allSourceEntries = resolveSourceEntries(stashDir, config);
249
- const allSourceDirs = allSourceEntries.map((s) => s.path);
250
- const t0 = Date.now();
251
- // Open database — pass embedding dimension from config if available
252
- const dbPath = getDbPath();
253
- const embeddingDim = config.embedding?.dimension;
254
- const db = openDatabase(dbPath, embeddingDim ? { embeddingDim } : undefined);
255
- try {
256
- // Determine incremental vs full mode
257
- const prevStashDir = getMeta(db, "stashDir");
258
- const prevBuiltAt = getMeta(db, "builtAt");
259
- const isIncremental = !full && prevStashDir === stashDir && !!prevBuiltAt;
260
- const builtAtMs = isIncremental && prevBuiltAt ? new Date(prevBuiltAt).getTime() : 0;
261
- // Assemble the run context
262
- const ctx = {
263
- db,
264
- config,
265
- sources: allSourceEntries,
266
- sourceDirs: allSourceDirs,
267
- full,
268
- reEnrich,
269
- stashDir,
270
- onProgress,
271
- signal,
272
- timing: {
273
- t0,
274
- tWalkStart: t0,
275
- tWalkEnd: t0,
276
- tLlmEnd: t0,
277
- tFtsEnd: t0,
278
- tEmbedEnd: t0,
279
- },
280
- isIncremental,
281
- builtAtMs,
282
- hadRemovedSources: false,
283
- scannedDirs: 0,
284
- skippedDirs: 0,
285
- generatedCount: 0,
286
- walkWarnings: [],
287
- dirsNeedingLlm: [],
288
- embeddingResult: null,
289
- graphExtractionResult: null,
290
- };
291
- onProgress({
292
- phase: "summary",
293
- message: buildIndexSummaryMessage({
230
+ return withIndexWriterLease({ purpose: "akm-index", signal: options?.signal }, async () => {
231
+ const stashDir = options?.stashDir || resolveStashDir();
232
+ const onProgress = options?.onProgress ?? (() => { });
233
+ const signal = options?.signal;
234
+ const reEnrich = options?.reEnrich === true;
235
+ const full = options?.full === true;
236
+ const clean = options?.clean === true;
237
+ const dryRun = options?.dryRun === true;
238
+ // Load config and resolve all stash sources
239
+ const { loadConfig } = await import("../core/config/config.js");
240
+ const config = loadConfig();
241
+ // One-time, read-only guard: warn if the writable stash still holds an
242
+ // un-migrated `vaults/` directory. In 0.9.0 the indexer skips `vaults/`
243
+ // entirely, so an unmigrated vault's `.env` data would silently never be
244
+ // indexed. Non-destructive only stats, never reads/writes/deletes.
245
+ const { warnOnUnmigratedVaults } = await import("./usage/unmigrated-vaults-guard.js");
246
+ warnOnUnmigratedVaults(stashDir);
247
+ // Ensure git stash caches are extracted before resolving stash dirs,
248
+ // so their content directories exist on disk for the walker to discover.
249
+ const { ensureSourceCaches, resolveSourceEntries } = await import("./search/search-source.js");
250
+ await ensureSourceCaches(config, { force: full });
251
+ const allSourceEntries = resolveSourceEntries(stashDir, config);
252
+ const allSourceDirs = allSourceEntries.map((s) => s.path);
253
+ const t0 = Date.now();
254
+ // Open database — pass embedding dimension from config if available
255
+ const dbPath = getDbPath();
256
+ const embeddingDim = config.embedding?.dimension;
257
+ const db = openIndexDatabase(dbPath, embeddingDim ? { embeddingDim } : undefined);
258
+ try {
259
+ // Determine incremental vs full mode
260
+ const prevStashDir = getMeta(db, "stashDir");
261
+ const prevBuiltAt = getMeta(db, "builtAt");
262
+ const isIncremental = !full && prevStashDir === stashDir && !!prevBuiltAt;
263
+ const builtAtMs = isIncremental && prevBuiltAt ? new Date(prevBuiltAt).getTime() : 0;
264
+ // Assemble the run context
265
+ const ctx = {
266
+ db,
267
+ config,
268
+ sources: allSourceEntries,
269
+ sourceDirs: allSourceDirs,
270
+ full,
271
+ reEnrich,
272
+ stashDir,
273
+ onProgress,
274
+ signal,
275
+ timing: {
276
+ t0,
277
+ tWalkStart: t0,
278
+ tWalkEnd: t0,
279
+ tLlmEnd: t0,
280
+ tFtsEnd: t0,
281
+ tEmbedEnd: t0,
282
+ },
283
+ isIncremental,
284
+ builtAtMs,
285
+ hadRemovedSources: false,
286
+ scannedDirs: 0,
287
+ skippedDirs: 0,
288
+ generatedCount: 0,
289
+ walkWarnings: [],
290
+ dirsNeedingLlm: [],
291
+ embeddingResult: null,
292
+ graphExtractionResult: null,
293
+ };
294
+ onProgress({
295
+ phase: "summary",
296
+ message: buildIndexSummaryMessage({
297
+ mode: isIncremental ? "incremental" : "full",
298
+ sourcesCount: allSourceDirs.length,
299
+ semanticSearchMode: config.semanticSearchMode,
300
+ embeddingProvider: getEmbeddingProvider(config.embedding),
301
+ llmEnabled: !!resolveIndexPassLLM("enrichment", config),
302
+ vecAvailable: isVecAvailable(db),
303
+ }),
304
+ });
305
+ // ── Phase sequence ───────────────────────────────────────────────────────
306
+ await runSourceCachePhase(ctx);
307
+ await runWalkPhase(ctx);
308
+ await runEmbeddingPhase(ctx);
309
+ await runFinalizePhase(ctx);
310
+ // ────────────────────────────────────────────────────────────────────────
311
+ // runFinalizePhase always populates these before returning.
312
+ const verification = ctx.verification;
313
+ const totalEntries = ctx.totalEntries;
314
+ const { timing } = ctx;
315
+ // ── Clean pass ───────────────────────────────────────────────────────────
316
+ // After the normal index completes, remove entries whose source files no
317
+ // longer exist on disk. Remote entries (empty file_path) are skipped.
318
+ let cleanResult;
319
+ if (clean) {
320
+ cleanResult = runCleanPass(db, dryRun);
321
+ }
322
+ // ────────────────────────────────────────────────────────────────────────
323
+ return {
324
+ stashDir,
325
+ totalEntries,
326
+ generatedMetadata: ctx.generatedCount,
327
+ indexPath: dbPath,
294
328
  mode: isIncremental ? "incremental" : "full",
295
- sourcesCount: allSourceDirs.length,
296
- semanticSearchMode: config.semanticSearchMode,
297
- embeddingProvider: getEmbeddingProvider(config.embedding),
298
- llmEnabled: !!resolveIndexPassLLM("enrichment", config),
299
- vecAvailable: isVecAvailable(db),
300
- }),
301
- });
302
- // ── Phase sequence ───────────────────────────────────────────────────────
303
- await runSourceCachePhase(ctx);
304
- await runWalkPhase(ctx);
305
- await runEmbeddingPhase(ctx);
306
- await runFinalizePhase(ctx);
307
- // ────────────────────────────────────────────────────────────────────────
308
- const { _verification: verification, _totalEntries: totalEntries } = ctx;
309
- const { timing } = ctx;
310
- // ── Clean pass ───────────────────────────────────────────────────────────
311
- // After the normal index completes, remove entries whose source files no
312
- // longer exist on disk. Remote entries (empty file_path) are skipped.
313
- let cleanResult;
314
- if (clean) {
315
- cleanResult = runCleanPass(db, dryRun);
329
+ directoriesScanned: ctx.scannedDirs,
330
+ directoriesSkipped: ctx.skippedDirs,
331
+ ...(ctx.walkWarnings.length > 0 ? { warnings: ctx.walkWarnings } : {}),
332
+ verification,
333
+ timing: {
334
+ totalMs: Date.now() - timing.t0,
335
+ walkMs: timing.tWalkEnd - timing.tWalkStart,
336
+ llmMs: timing.tLlmEnd - timing.tWalkEnd,
337
+ embedMs: timing.tEmbedEnd - timing.tLlmEnd,
338
+ ftsMs: timing.tFtsEnd - timing.tEmbedEnd,
339
+ },
340
+ ...(cleanResult !== undefined ? { clean: cleanResult } : {}),
341
+ };
316
342
  }
317
- // ────────────────────────────────────────────────────────────────────────
318
- return {
319
- stashDir,
320
- totalEntries,
321
- generatedMetadata: ctx.generatedCount,
322
- indexPath: dbPath,
323
- mode: isIncremental ? "incremental" : "full",
324
- directoriesScanned: ctx.scannedDirs,
325
- directoriesSkipped: ctx.skippedDirs,
326
- ...(ctx.walkWarnings.length > 0 ? { warnings: ctx.walkWarnings } : {}),
327
- verification,
328
- timing: {
329
- totalMs: Date.now() - timing.t0,
330
- walkMs: timing.tWalkEnd - timing.tWalkStart,
331
- llmMs: timing.tLlmEnd - timing.tWalkEnd,
332
- embedMs: timing.tEmbedEnd - timing.tLlmEnd,
333
- ftsMs: timing.tFtsEnd - timing.tEmbedEnd,
334
- },
335
- ...(cleanResult !== undefined ? { clean: cleanResult } : {}),
336
- };
337
- }
338
- finally {
339
- closeDatabase(db);
340
- }
343
+ finally {
344
+ closeDatabase(db);
345
+ }
346
+ });
341
347
  }
342
348
  // ── Extracted helpers for indexing ────────────────────────────────────────────
343
349
  async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, doFullDelete = false, onProgress) {
@@ -634,98 +640,6 @@ async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadR
634
640
  insertTransaction();
635
641
  return { scannedDirs, skippedDirs, generatedCount, warnings, dirsNeedingLlm };
636
642
  }
637
- function getDirIndexState(db, dirPath, files, builtAtMs) {
638
- const prevEntries = getEntriesByDir(db, dirPath);
639
- const fingerprint = computeDirFingerprint(dirPath, files);
640
- if (prevEntries.length > 0) {
641
- const staleReason = getDirStaleReason(dirPath, files, prevEntries, builtAtMs);
642
- if (!staleReason) {
643
- return { stale: false, reason: { kind: "unchanged" }, persistedRowCount: prevEntries.length };
644
- }
645
- return { stale: true, reason: staleReason, persistedRowCount: prevEntries.length };
646
- }
647
- const cachedState = getIndexDirState(db, dirPath);
648
- if (cachedState &&
649
- cachedState.fileSetHash === fingerprint.fileSetHash &&
650
- cachedState.fileMtimeMaxMs === fingerprint.fileMtimeMaxMs) {
651
- return {
652
- stale: false,
653
- reason: { kind: "cached-zero-row-state", detail: cachedState.reason },
654
- persistedRowCount: 0,
655
- };
656
- }
657
- return {
658
- stale: true,
659
- reason: { kind: "no-previous-rows", detail: cachedState ? `cached=${cachedState.reason}` : undefined },
660
- persistedRowCount: 0,
661
- };
662
- }
663
- function getCachedZeroRowDirState(db, dirPath, files, builtAtMs, priorDirsChanged) {
664
- const state = getDirIndexState(db, dirPath, files, builtAtMs);
665
- if (state.stale || state.reason.kind !== "cached-zero-row-state")
666
- return undefined;
667
- if (!canUseIncrementalSkip(state, priorDirsChanged))
668
- return undefined;
669
- return state;
670
- }
671
- function canUseIncrementalSkip(state, priorDirsChanged) {
672
- return !(priorDirsChanged &&
673
- state.reason.kind === "cached-zero-row-state" &&
674
- state.reason.detail === "deduped-zero-row");
675
- }
676
- function computeDirFingerprint(_dirPath, files) {
677
- const normalizedFiles = [...new Set(files.map((file) => path.basename(file)))].sort();
678
- let fileMtimeMaxMs = 0;
679
- for (const file of files) {
680
- try {
681
- fileMtimeMaxMs = Math.max(fileMtimeMaxMs, fs.statSync(file).mtimeMs);
682
- }
683
- catch {
684
- fileMtimeMaxMs = Number.POSITIVE_INFINITY;
685
- break;
686
- }
687
- }
688
- return {
689
- fileSetHash: normalizedFiles.join("\0"),
690
- fileMtimeMaxMs,
691
- };
692
- }
693
- function getDirStaleReason(_dirPath, currentFiles, previousEntries, builtAtMs) {
694
- const prevFileNames = new Set(previousEntries
695
- .map((ie) => {
696
- const fromPath = path.basename(ie.filePath);
697
- return fromPath || ie.entry.filename;
698
- })
699
- .filter((e) => !!e));
700
- const currFileNames = new Set(currentFiles.map((f) => path.basename(f)));
701
- if (prevFileNames.size !== currFileNames.size) {
702
- return { kind: "file-set-changed", detail: `${prevFileNames.size} -> ${currFileNames.size} files` };
703
- }
704
- for (const name of currFileNames) {
705
- if (!prevFileNames.has(name))
706
- return { kind: "file-set-changed", detail: name };
707
- }
708
- for (const file of currentFiles) {
709
- try {
710
- if (fs.statSync(file).mtimeMs > builtAtMs)
711
- return { kind: "mtime-changed", detail: path.basename(file) };
712
- }
713
- catch {
714
- return { kind: "missing-file", detail: path.basename(file) };
715
- }
716
- }
717
- return undefined;
718
- }
719
- function inferZeroRowReason(stash, priorReason, warnings, dirPath, dedupedRows) {
720
- if (dedupedRows > 0)
721
- return "deduped-zero-row";
722
- const workflowNoise = warnings.some((warning) => warning.startsWith("Skipped workflow ") && warning.includes(dirPath));
723
- if (workflowNoise)
724
- return "workflow-noise";
725
- if (!stash || stash.entries.length === 0)
726
- return "empty-generated-set";
727
- return `zero-row:${priorReason?.kind ?? "unknown"}`;
728
- }
729
643
  async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal, _enrich = false, reEnrich = false) {
730
644
  // Resolve per-pass LLM config via the unified shim. Returns undefined when
731
645
  // either no `akm.llm` is configured or the user opted this pass out via
@@ -896,21 +810,9 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal) {
896
810
  const currentFingerprint = deriveSemanticProviderFingerprint(config.embedding);
897
811
  const storedFingerprint = getMeta(db, "embeddingFingerprint");
898
812
  if (storedFingerprint && storedFingerprint !== currentFingerprint) {
899
- try {
900
- db.exec("DELETE FROM embeddings");
901
- }
902
- catch {
903
- /* ignore */
904
- }
905
- if (isVecAvailable(db)) {
906
- try {
907
- db.exec("DELETE FROM entries_vec");
908
- }
909
- catch {
910
- /* ignore */
911
- }
912
- }
913
- setMeta(db, "hasEmbeddings", "0");
813
+ // Model/provider changed → stored vectors are incompatible. Clear them
814
+ // (same dimension, so keep the vec table); re-embedded by this index run.
815
+ purgeEmbeddings(db);
914
816
  }
915
817
  try {
916
818
  const { embedBatch } = await import("../llm/embedder.js");
@@ -0,0 +1,114 @@
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
+ /**
5
+ * Incremental dir-staleness engine.
6
+ *
7
+ * Decides, per stash directory, whether the directory's indexed rows are still
8
+ * fresh relative to what is on disk — so an incremental `akm index` run can
9
+ * skip unchanged directories instead of regenerating their metadata.
10
+ *
11
+ * Two persisted signals back the decision:
12
+ * 1. The `entries` rows already indexed for the directory (`getEntriesByDir`).
13
+ * 2. The `index_dir_state` fingerprint row (`getIndexDirState`), which caches
14
+ * the file-set hash + max mtime for directories that legitimately produced
15
+ * zero rows, so they are not rescanned every run.
16
+ *
17
+ * `computeDirFingerprint` derives the fingerprint (basename set + max mtime)
18
+ * that both the freshness check and the persisted `index_dir_state` row use.
19
+ */
20
+ import fs from "node:fs";
21
+ import path from "node:path";
22
+ import { getEntriesByDir, getIndexDirState } from "../db/db.js";
23
+ export function getDirIndexState(db, dirPath, files, builtAtMs) {
24
+ const prevEntries = getEntriesByDir(db, dirPath);
25
+ const fingerprint = computeDirFingerprint(dirPath, files);
26
+ if (prevEntries.length > 0) {
27
+ const staleReason = getDirStaleReason(dirPath, files, prevEntries, builtAtMs);
28
+ if (!staleReason) {
29
+ return { stale: false, reason: { kind: "unchanged" }, persistedRowCount: prevEntries.length };
30
+ }
31
+ return { stale: true, reason: staleReason, persistedRowCount: prevEntries.length };
32
+ }
33
+ const cachedState = getIndexDirState(db, dirPath);
34
+ if (cachedState &&
35
+ cachedState.fileSetHash === fingerprint.fileSetHash &&
36
+ cachedState.fileMtimeMaxMs === fingerprint.fileMtimeMaxMs) {
37
+ return {
38
+ stale: false,
39
+ reason: { kind: "cached-zero-row-state", detail: cachedState.reason },
40
+ persistedRowCount: 0,
41
+ };
42
+ }
43
+ return {
44
+ stale: true,
45
+ reason: { kind: "no-previous-rows", detail: cachedState ? `cached=${cachedState.reason}` : undefined },
46
+ persistedRowCount: 0,
47
+ };
48
+ }
49
+ export function getCachedZeroRowDirState(db, dirPath, files, builtAtMs, priorDirsChanged) {
50
+ const state = getDirIndexState(db, dirPath, files, builtAtMs);
51
+ if (state.stale || state.reason.kind !== "cached-zero-row-state")
52
+ return undefined;
53
+ if (!canUseIncrementalSkip(state, priorDirsChanged))
54
+ return undefined;
55
+ return state;
56
+ }
57
+ export function canUseIncrementalSkip(state, priorDirsChanged) {
58
+ return !(priorDirsChanged &&
59
+ state.reason.kind === "cached-zero-row-state" &&
60
+ state.reason.detail === "deduped-zero-row");
61
+ }
62
+ export function computeDirFingerprint(_dirPath, files) {
63
+ const normalizedFiles = [...new Set(files.map((file) => path.basename(file)))].sort();
64
+ let fileMtimeMaxMs = 0;
65
+ for (const file of files) {
66
+ try {
67
+ fileMtimeMaxMs = Math.max(fileMtimeMaxMs, fs.statSync(file).mtimeMs);
68
+ }
69
+ catch {
70
+ fileMtimeMaxMs = Number.POSITIVE_INFINITY;
71
+ break;
72
+ }
73
+ }
74
+ return {
75
+ fileSetHash: normalizedFiles.join("\0"),
76
+ fileMtimeMaxMs,
77
+ };
78
+ }
79
+ function getDirStaleReason(_dirPath, currentFiles, previousEntries, builtAtMs) {
80
+ const prevFileNames = new Set(previousEntries
81
+ .map((ie) => {
82
+ const fromPath = path.basename(ie.filePath);
83
+ return fromPath || ie.entry.filename;
84
+ })
85
+ .filter((e) => !!e));
86
+ const currFileNames = new Set(currentFiles.map((f) => path.basename(f)));
87
+ if (prevFileNames.size !== currFileNames.size) {
88
+ return { kind: "file-set-changed", detail: `${prevFileNames.size} -> ${currFileNames.size} files` };
89
+ }
90
+ for (const name of currFileNames) {
91
+ if (!prevFileNames.has(name))
92
+ return { kind: "file-set-changed", detail: name };
93
+ }
94
+ for (const file of currentFiles) {
95
+ try {
96
+ if (fs.statSync(file).mtimeMs > builtAtMs)
97
+ return { kind: "mtime-changed", detail: path.basename(file) };
98
+ }
99
+ catch {
100
+ return { kind: "missing-file", detail: path.basename(file) };
101
+ }
102
+ }
103
+ return undefined;
104
+ }
105
+ export function inferZeroRowReason(stash, priorReason, warnings, dirPath, dedupedRows) {
106
+ if (dedupedRows > 0)
107
+ return "deduped-zero-row";
108
+ const workflowNoise = warnings.some((warning) => warning.startsWith("Skipped workflow ") && warning.includes(dirPath));
109
+ if (workflowNoise)
110
+ return "workflow-noise";
111
+ if (!stash || stash.entries.length === 0)
112
+ return "empty-generated-set";
113
+ return `zero-row:${priorReason?.kind ?? "unknown"}`;
114
+ }
@@ -72,6 +72,7 @@ const FM_CAPTURE_MODE = "captureMode";
72
72
  */
73
73
  export async function runMemoryInferencePass(ctx) {
74
74
  const { config, sources, signal, db, reEnrich, onProgress, options = {} } = ctx;
75
+ const compressMemoryToDerivedMemory = options.compressMemoryToDerivedMemory ?? memoryInfer.compressMemoryToDerivedMemory;
75
76
  const result = {
76
77
  considered: 0,
77
78
  cacheHits: 0,
@@ -172,14 +173,14 @@ export async function runMemoryInferencePass(ctx) {
172
173
  retryAttempts += 1;
173
174
  };
174
175
  const derived = db
175
- ? await withLlmCache(db, record.filePath, record.body, reEnrich ?? false, () => memoryInfer.compressMemoryToDerivedMemory(llmConfig, record.body, signal, config, (evt) => {
176
+ ? await withLlmCache(db, record.filePath, record.body, reEnrich ?? false, () => compressMemoryToDerivedMemory(llmConfig, record.body, signal, config, (evt) => {
176
177
  warn(`[akm] LLM fallback for ${evt.feature}: ${evt.reason}`);
177
178
  }, inferTelemetry, onRetryAttempt), validate, undefined, "", {
178
179
  onCacheHit: () => {
179
180
  fromCache = true;
180
181
  },
181
182
  })
182
- : await memoryInfer.compressMemoryToDerivedMemory(llmConfig, record.body, signal, config, (evt) => {
183
+ : await compressMemoryToDerivedMemory(llmConfig, record.body, signal, config, (evt) => {
183
184
  warn(`[akm] LLM fallback for ${evt.feature}: ${evt.reason}`);
184
185
  }, inferTelemetry, onRetryAttempt);
185
186
  if (!derived) {
@@ -425,10 +426,17 @@ function markParentProcessed(parent) {
425
426
  warn(`memory inference: failed to re-read parent ${parent.filePath}: ${err instanceof Error ? err.message : String(err)}`);
426
427
  return;
427
428
  }
428
- const updatedFm = { ...parent.data, [FM_INFERENCE_PROCESSED]: true };
429
429
  const block = parseFrontmatterBlock(raw);
430
- const body = block?.content ?? raw;
431
- const next = assembleAsset(updatedFm, body);
430
+ if (!block) {
431
+ // Cannot safely rewrite malformed frontmatter — skip marking so the memory
432
+ // is retried on the next run once the frontmatter is repaired. Writing with
433
+ // `body = raw` would wrap the entire file (including the bad frontmatter)
434
+ // in a new block, producing a duplicate-frontmatter corruption.
435
+ warn(`memory inference: skipping markParentProcessed for ${parent.filePath} — could not parse frontmatter block`);
436
+ return;
437
+ }
438
+ const updatedFm = { ...parent.data, [FM_INFERENCE_PROCESSED]: true };
439
+ const next = assembleAsset(updatedFm, block.content);
432
440
  try {
433
441
  fs.writeFileSync(parent.filePath, next, "utf8");
434
442
  }
@@ -44,6 +44,7 @@
44
44
  import { createHash } from "node:crypto";
45
45
  import fs from "node:fs";
46
46
  import path from "node:path";
47
+ import stalenessDetectSystemPrompt from "../../assets/prompts/staleness-detect-system.md" with { type: "text" };
47
48
  import { assembleAsset } from "../../core/asset/asset-serialize.js";
48
49
  import { parseFrontmatter, parseFrontmatterBlock } from "../../core/asset/frontmatter.js";
49
50
  import { concurrentMap } from "../../core/concurrent.js";
@@ -319,11 +320,7 @@ function pickSimilar(candidate, all) {
319
320
  return scored.slice(0, TOP_K_SIMILAR).map((s) => s.snap);
320
321
  }
321
322
  // ── LLM dispatch ────────────────────────────────────────────────────────────
322
- const SYSTEM_PROMPT = "You are a belief-state classifier for a memory store. Given a candidate memory and a list of more-recent similar memories from the same store, decide whether the candidate is still current or has been superseded.\n\n" +
323
- "Respond on the first line with exactly YES or NO.\n" +
324
- "If YES, the second line MUST be of the form `SUPERSEDED_BY: <ref>` where <ref> is the exact ref of the superseding memory from the list provided. Do NOT invent refs.\n" +
325
- "If NO, do not include any additional lines.\n" +
326
- "No prose, no preamble, no markdown.";
323
+ const SYSTEM_PROMPT = stalenessDetectSystemPrompt;
327
324
  async function askValidator(connection, candidate, allMemories, signal, timeoutMs) {
328
325
  const similar = pickSimilar(candidate, allMemories);
329
326
  if (similar.length === 0) {
@@ -65,6 +65,9 @@ export async function searchLocal(input) {
65
65
  const includeProposed = input.includeProposed === true;
66
66
  const beliefFilter = input.beliefFilter ?? "all";
67
67
  const restrictToSources = input.restrictToSources === true;
68
+ const includeExcludedTypes = input.includeExcludedTypes === true;
69
+ const disableProjectContext = input.disableProjectContext === true;
70
+ const disableScopedUtility = input.disableScopedUtility === true;
68
71
  const rendererRegistry = input.rendererRegistry ?? defaultRendererRegistry;
69
72
  const allSourceDirs = sources.map((s) => s.path);
70
73
  const rawStatus = readSemanticStatus();
@@ -114,7 +117,7 @@ export async function searchLocal(input) {
114
117
  mode: "keyword",
115
118
  };
116
119
  }
117
- const { hits, embedMs, rankMs } = await searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry, filters, includeProposed, beliefFilter, restrictToSources);
120
+ const { hits, embedMs, rankMs } = await searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry, filters, includeProposed, beliefFilter, restrictToSources, includeExcludedTypes, disableProjectContext, disableScopedUtility);
118
121
  return {
119
122
  hits,
120
123
  tip: hits.length === 0
@@ -131,14 +134,19 @@ export async function searchLocal(input) {
131
134
  }
132
135
  }
133
136
  // ── Database search ─────────────────────────────────────────────────────────
134
- async function searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry = defaultRendererRegistry, filters, includeProposed = false, beliefFilter = "all", restrictToSources = false) {
137
+ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry = defaultRendererRegistry, filters, includeProposed = false, beliefFilter = "all", restrictToSources = false, includeExcludedTypes = false, disableProjectContext = false, disableScopedUtility = false) {
135
138
  const hasSearchableTokens = query.length > 0 && sanitizeFtsQuery(query).length > 0;
139
+ // #627 — resolve the default type-exclusion policy. It applies ONLY on the
140
+ // untyped ('any') path and only when the caller did not opt back in via
141
+ // `includeExcludedTypes`. When the config key is ABSENT a built-in default of
142
+ // ['session'] is applied; an explicit empty list disables exclusion.
143
+ const defaultExcludes = searchType === "any" && !includeExcludedTypes ? (config.search?.defaultExcludeTypes ?? ["session"]) : [];
136
144
  // Empty queries — including ones that sanitize down to no searchable FTS
137
145
  // tokens such as "." — should enumerate matching entries instead of
138
146
  // returning an empty result set from FTS.
139
147
  if (!hasSearchableTokens) {
140
148
  const typeFilter = searchType === "any" ? undefined : searchType;
141
- const allEntries = getAllEntries(db, typeFilter);
149
+ const allEntries = getAllEntries(db, typeFilter, defaultExcludes);
142
150
  // Deduplicate by file path — multiple entries can share the same file
143
151
  const seenFilePaths = new Set();
144
152
  const uniqueEntries = allEntries.filter((ie) => {
@@ -187,7 +195,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
187
195
  const typeFilter = searchType === "any" ? undefined : searchType;
188
196
  const tEmbed0 = Date.now();
189
197
  const embeddingPromise = tryVecScores(db, query, limit * 3, config);
190
- const ftsResults = searchFts(db, query, limit * 3, typeFilter);
198
+ const ftsResults = searchFts(db, query, limit * 3, typeFilter, defaultExcludes);
191
199
  const embeddingScores = await embeddingPromise;
192
200
  const embedMs = Date.now() - tEmbed0;
193
201
  const tRank0 = Date.now();
@@ -208,6 +216,11 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
208
216
  embedScoreMap,
209
217
  getEntryById: (id) => getEntryById(db, id) ?? undefined,
210
218
  typeFilter,
219
+ // #627 — also exclude default-hidden types from the vector-only branch so a
220
+ // session asset that is a top-k vector neighbor (but not an FTS match) does
221
+ // not leak into default ('any') results. defaultExcludes is already []
222
+ // unless this is the untyped path without includeExcludedTypes.
223
+ excludeTypes: defaultExcludes,
211
224
  });
212
225
  // ── Scoring Phase ──────────────────────────────────────────────────────
213
226
  // Apply boosts as multiplicative factors (all boosts in a single phase
@@ -236,7 +249,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
236
249
  // Resolve project-context tokens from the current working directory once
237
250
  // per search invocation. Returns null when running from home dir / /tmp,
238
251
  // or when the caller has set AKM_DISABLE_PROJECT_CONTEXT=1.
239
- const projectContext = process.env.AKM_DISABLE_PROJECT_CONTEXT === "1" ? null : resolveProjectContext(process.cwd());
252
+ const projectContext = disableProjectContext ? null : resolveProjectContext(process.cwd());
240
253
  // Phase 2A / Rec 5: resolve forgetting-curve config and skip the feedback
241
254
  // count query when the boost cannot make a difference (default ≤ 1.0 means
242
255
  // boost^count == 1 — zero overhead for the common case).
@@ -256,7 +269,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
256
269
  // AKM_DISABLE_SCOPED_UTILITY=1 opts out (e.g. for registry searches or tests).
257
270
  let scopeKey;
258
271
  try {
259
- scopeKey = process.env.AKM_DISABLE_SCOPED_UTILITY === "1" ? undefined : getCurrentWorkflowScopeKey();
272
+ scopeKey = disableScopedUtility ? undefined : getCurrentWorkflowScopeKey();
260
273
  }
261
274
  catch {
262
275
  // Non-fatal — ranking proceeds without scoped utility on any error.