akm-cli 0.9.0-beta.6 → 0.9.0-rc.0

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 (326) hide show
  1. package/CHANGELOG.md +663 -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/help/help-improve.md +9 -6
  6. package/dist/assets/hints/cli-hints-full.md +6 -5
  7. package/dist/assets/profiles/default.json +9 -4
  8. package/dist/assets/profiles/frequent.json +1 -1
  9. package/dist/assets/profiles/memory-focus.json +1 -1
  10. package/dist/assets/profiles/proactive-maintenance.json +25 -0
  11. package/dist/assets/profiles/quick.json +1 -1
  12. package/dist/assets/profiles/recombine-only.json +21 -0
  13. package/dist/assets/profiles/reflect-distill.json +30 -0
  14. package/dist/assets/profiles/synthesize.json +15 -0
  15. package/dist/assets/profiles/thorough.json +1 -1
  16. package/dist/assets/prompts/consolidate-system.md +23 -0
  17. package/dist/assets/prompts/contradiction-judge.md +33 -0
  18. package/dist/assets/prompts/distill-knowledge-system.md +22 -0
  19. package/dist/assets/prompts/distill-lesson-system.md +36 -0
  20. package/dist/assets/prompts/extract-session.md +11 -3
  21. package/dist/assets/prompts/graph-extract-system.md +1 -0
  22. package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
  23. package/dist/assets/prompts/memory-infer-system.md +1 -0
  24. package/dist/assets/prompts/memory-infer-user.md +5 -0
  25. package/dist/assets/prompts/metadata-enhance-system.md +1 -0
  26. package/dist/assets/prompts/procedural-system.md +44 -0
  27. package/dist/assets/prompts/recombine-system.md +40 -0
  28. package/dist/assets/prompts/staleness-detect-system.md +6 -0
  29. package/dist/assets/prompts/validate-summary-judge.md +1 -0
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
  38. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
  39. package/dist/assets/templates/html/health.html +281 -111
  40. package/dist/assets/wiki/ingest-workflow-template.md +45 -16
  41. package/dist/assets/wiki/schema-template.md +4 -4
  42. package/dist/cli/clack.js +56 -0
  43. package/dist/cli/config-migrate.js +7 -1
  44. package/dist/cli/confirm.js +1 -1
  45. package/dist/cli/parse-args.js +46 -1
  46. package/dist/cli/shared.js +28 -0
  47. package/dist/cli.js +25 -14
  48. package/dist/commands/agent/agent-dispatch.js +3 -2
  49. package/dist/commands/agent/agent-support.js +0 -7
  50. package/dist/commands/agent/contribute-cli.js +26 -7
  51. package/dist/commands/config-cli.js +26 -13
  52. package/dist/commands/env/child-env.js +47 -0
  53. package/dist/commands/env/env-cli.js +220 -227
  54. package/dist/commands/env/env.js +14 -67
  55. package/dist/commands/env/secret-cli.js +140 -138
  56. package/dist/commands/feedback-cli.js +153 -147
  57. package/dist/commands/graph/graph-cli.js +5 -13
  58. package/dist/commands/graph/graph.js +76 -72
  59. package/dist/commands/health/advisories.js +151 -0
  60. package/dist/commands/health/checks.js +103 -16
  61. package/dist/commands/health/html-report.js +447 -81
  62. package/dist/commands/health/improve-metrics.js +771 -0
  63. package/dist/commands/health/llm-usage.js +65 -0
  64. package/dist/commands/health/md-report.js +103 -0
  65. package/dist/commands/health/metrics.js +278 -0
  66. package/dist/commands/health/stash-exposure.js +46 -0
  67. package/dist/commands/health/surfaces.js +216 -0
  68. package/dist/commands/health/task-runs.js +135 -0
  69. package/dist/commands/health/types.js +26 -0
  70. package/dist/commands/health/windows.js +195 -0
  71. package/dist/commands/health.js +91 -1083
  72. package/dist/commands/improve/anti-collapse.js +170 -0
  73. package/dist/commands/improve/calibration.js +161 -0
  74. package/dist/commands/improve/collapse-detector.js +421 -0
  75. package/dist/commands/improve/consolidate/chunking.js +141 -0
  76. package/dist/commands/improve/consolidate/eligibility.js +64 -0
  77. package/dist/commands/improve/consolidate/merge.js +145 -0
  78. package/dist/commands/improve/consolidate/sanitize.js +231 -0
  79. package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
  80. package/dist/commands/improve/consolidate.js +1313 -1278
  81. package/dist/commands/improve/dedup.js +482 -0
  82. package/dist/commands/improve/distill/content-repair.js +202 -0
  83. package/dist/commands/improve/distill/promote-memory.js +229 -0
  84. package/dist/commands/improve/distill/quality-gate.js +236 -0
  85. package/dist/commands/improve/distill-guards.js +127 -0
  86. package/dist/commands/improve/distill-promotion-policy.js +826 -167
  87. package/dist/commands/improve/distill.js +243 -599
  88. package/dist/commands/improve/eligibility.js +434 -0
  89. package/dist/commands/improve/encoding-salience.js +205 -0
  90. package/dist/commands/improve/extract-cli.js +179 -59
  91. package/dist/commands/improve/extract-prompt.js +55 -4
  92. package/dist/commands/improve/extract-watch.js +140 -0
  93. package/dist/commands/improve/extract.js +409 -43
  94. package/dist/commands/improve/feedback-valence.js +54 -0
  95. package/dist/commands/improve/hot-probation.js +45 -0
  96. package/dist/commands/improve/improve-auto-accept.js +160 -7
  97. package/dist/commands/improve/improve-cli.js +115 -73
  98. package/dist/commands/improve/improve-profiles.js +32 -8
  99. package/dist/commands/improve/improve-result-file.js +15 -25
  100. package/dist/commands/improve/improve-session.js +58 -0
  101. package/dist/commands/improve/improve.js +510 -2537
  102. package/dist/commands/improve/locks.js +154 -0
  103. package/dist/commands/improve/loop-stages.js +1100 -0
  104. package/dist/commands/improve/memory/memory-belief.js +14 -15
  105. package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
  106. package/dist/commands/improve/memory/memory-improve.js +27 -27
  107. package/dist/commands/improve/outcome-loop.js +270 -0
  108. package/dist/commands/improve/preparation.js +2002 -0
  109. package/dist/commands/improve/proactive-maintenance.js +115 -0
  110. package/dist/commands/improve/procedural.js +398 -0
  111. package/dist/commands/improve/recombine.js +818 -0
  112. package/dist/commands/improve/reflect-noise.js +0 -0
  113. package/dist/commands/improve/reflect.js +212 -45
  114. package/dist/commands/improve/salience.js +455 -0
  115. package/dist/commands/improve/schema-similarity-gate.js +168 -0
  116. package/dist/commands/improve/shared.js +51 -0
  117. package/dist/commands/improve/triage.js +93 -0
  118. package/dist/commands/lint/agent-linter.js +19 -24
  119. package/dist/commands/lint/base-linter.js +173 -60
  120. package/dist/commands/lint/command-linter.js +19 -24
  121. package/dist/commands/lint/env-key-rules.js +38 -1
  122. package/dist/commands/lint/fact-linter.js +39 -0
  123. package/dist/commands/lint/index.js +31 -13
  124. package/dist/commands/lint/memory-linter.js +1 -1
  125. package/dist/commands/lint/registry.js +7 -2
  126. package/dist/commands/lint/task-linter.js +3 -3
  127. package/dist/commands/lint/workflow-linter.js +26 -1
  128. package/dist/commands/observability-cli.js +4 -4
  129. package/dist/commands/proposal/drain-policies.js +13 -4
  130. package/dist/commands/proposal/drain.js +45 -51
  131. package/dist/commands/proposal/legacy-import.js +115 -0
  132. package/dist/commands/proposal/proposal-cli.js +24 -34
  133. package/dist/commands/proposal/proposal.js +7 -1
  134. package/dist/commands/proposal/propose.js +8 -3
  135. package/dist/commands/proposal/repository.js +829 -0
  136. package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
  137. package/dist/commands/proposal/validators/proposals.js +93 -882
  138. package/dist/commands/read/curate.js +419 -103
  139. package/dist/commands/read/knowledge.js +10 -3
  140. package/dist/commands/read/remember-cli.js +133 -138
  141. package/dist/commands/read/search-cli.js +15 -8
  142. package/dist/commands/read/search.js +22 -11
  143. package/dist/commands/read/show.js +106 -14
  144. package/dist/commands/registry-cli.js +76 -87
  145. package/dist/commands/remember.js +11 -12
  146. package/dist/commands/sources/add-cli.js +91 -95
  147. package/dist/commands/sources/history.js +1 -1
  148. package/dist/commands/sources/init.js +66 -18
  149. package/dist/commands/sources/installed-stashes.js +11 -3
  150. package/dist/commands/sources/schema-repair.js +44 -46
  151. package/dist/commands/sources/self-update.js +2 -2
  152. package/dist/commands/sources/source-add.js +7 -3
  153. package/dist/commands/sources/sources-cli.js +3 -3
  154. package/dist/commands/sources/stash-cli.js +29 -41
  155. package/dist/commands/sources/stash-skeleton.js +57 -8
  156. package/dist/commands/tasks/default-tasks.js +15 -2
  157. package/dist/commands/tasks/tasks-cli.js +20 -29
  158. package/dist/commands/tasks/tasks.js +39 -11
  159. package/dist/commands/wiki-cli.js +23 -38
  160. package/dist/commands/workflow-cli.js +15 -1
  161. package/dist/core/asset/asset-registry.js +3 -1
  162. package/dist/core/asset/asset-spec.js +21 -4
  163. package/dist/core/asset/frontmatter.js +188 -167
  164. package/dist/core/asset/markdown.js +8 -0
  165. package/dist/core/authoring-rules.js +92 -0
  166. package/dist/core/common.js +4 -23
  167. package/dist/core/concurrent.js +10 -1
  168. package/dist/core/config/config-io.js +10 -1
  169. package/dist/core/config/config-migration.js +18 -40
  170. package/dist/core/config/config-schema.js +389 -58
  171. package/dist/core/config/config-types.js +3 -3
  172. package/dist/core/config/config.js +67 -22
  173. package/dist/core/deep-merge.js +38 -0
  174. package/dist/core/errors.js +1 -0
  175. package/dist/core/eval/rank-metrics.js +113 -0
  176. package/dist/core/events.js +4 -7
  177. package/dist/core/improve-types.js +47 -8
  178. package/dist/core/logs-db.js +14 -75
  179. package/dist/core/parse.js +36 -16
  180. package/dist/core/paths.js +21 -18
  181. package/dist/core/standards/resolve-standards-context.js +87 -0
  182. package/dist/core/standards/resolve-stash-standards.js +99 -0
  183. package/dist/core/standards/resolve-type-conventions.js +66 -0
  184. package/dist/core/state/migrations.js +770 -0
  185. package/dist/core/state-db.js +142 -1091
  186. package/dist/core/structured.js +69 -0
  187. package/dist/core/time.js +53 -0
  188. package/dist/core/warn.js +21 -0
  189. package/dist/core/write-source.js +37 -0
  190. package/dist/indexer/db/db.js +356 -780
  191. package/dist/indexer/db/entry-mapper.js +41 -0
  192. package/dist/indexer/db/graph-db.js +129 -86
  193. package/dist/indexer/db/llm-cache.js +2 -2
  194. package/dist/indexer/db/schema.js +516 -0
  195. package/dist/indexer/ensure-index.js +103 -24
  196. package/dist/indexer/feedback/utility-policy.js +75 -0
  197. package/dist/indexer/graph/graph-boost.js +51 -41
  198. package/dist/indexer/graph/graph-extraction.js +207 -4
  199. package/dist/indexer/index-writer-lock.js +106 -0
  200. package/dist/indexer/index-written-assets.js +105 -0
  201. package/dist/indexer/indexer.js +291 -310
  202. package/dist/indexer/passes/dir-staleness.js +114 -0
  203. package/dist/indexer/passes/memory-inference.js +13 -5
  204. package/dist/indexer/passes/metadata.js +20 -0
  205. package/dist/indexer/read-preflight.js +23 -0
  206. package/dist/indexer/search/db-search.js +89 -13
  207. package/dist/indexer/search/fts-query.js +51 -0
  208. package/dist/indexer/search/ranking-contributors.js +95 -9
  209. package/dist/indexer/search/ranking.js +79 -3
  210. package/dist/indexer/search/search-fields.js +6 -0
  211. package/dist/indexer/search/search-source.js +32 -21
  212. package/dist/indexer/search/semantic-status.js +4 -0
  213. package/dist/indexer/walk/matchers.js +9 -0
  214. package/dist/indexer/walk/walker.js +21 -13
  215. package/dist/integrations/agent/builders.js +39 -13
  216. package/dist/integrations/agent/config.js +20 -59
  217. package/dist/integrations/agent/detect.js +9 -0
  218. package/dist/integrations/agent/index.js +3 -19
  219. package/dist/integrations/agent/model-aliases.js +7 -2
  220. package/dist/integrations/agent/profiles.js +7 -1
  221. package/dist/integrations/agent/prompts.js +75 -9
  222. package/dist/integrations/agent/runner-dispatch.js +59 -0
  223. package/dist/integrations/agent/runner.js +13 -9
  224. package/dist/integrations/agent/spawn.js +69 -67
  225. package/dist/integrations/harnesses/claude/agent-builder.js +1 -1
  226. package/dist/integrations/harnesses/claude/index.js +2 -0
  227. package/dist/integrations/harnesses/claude/session-log.js +11 -1
  228. package/dist/integrations/harnesses/index.js +2 -3
  229. package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
  230. package/dist/integrations/harnesses/opencode/index.js +2 -0
  231. package/dist/integrations/harnesses/opencode/session-log.js +173 -3
  232. package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
  233. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +98 -17
  234. package/dist/integrations/harnesses/types.js +1 -0
  235. package/dist/integrations/session-logs/index.js +16 -0
  236. package/dist/llm/call-ai.js +2 -2
  237. package/dist/llm/client.js +57 -15
  238. package/dist/llm/embedder.js +67 -4
  239. package/dist/llm/embedders/cache.js +3 -1
  240. package/dist/llm/embedders/deterministic.js +66 -0
  241. package/dist/llm/embedders/local.js +73 -3
  242. package/dist/llm/feature-gate.js +16 -15
  243. package/dist/llm/graph-extract.js +67 -44
  244. package/dist/llm/memory-infer-impl.js +138 -0
  245. package/dist/llm/memory-infer.js +1 -127
  246. package/dist/llm/metadata-enhance.js +44 -31
  247. package/dist/llm/structured-call.js +49 -0
  248. package/dist/migrate-storage-node.mjs +8 -0
  249. package/dist/output/context.js +5 -5
  250. package/dist/output/renderers.js +85 -14
  251. package/dist/output/shapes/curate.js +14 -2
  252. package/dist/output/shapes/helpers.js +0 -3
  253. package/dist/output/shapes/passthrough.js +2 -1
  254. package/dist/output/text/helpers.js +29 -1
  255. package/dist/output/text/workflow.js +1 -0
  256. package/dist/registry/providers/skills-sh.js +21 -147
  257. package/dist/registry/providers/static-index.js +15 -157
  258. package/dist/registry/resolve.js +27 -9
  259. package/dist/runtime.js +25 -1
  260. package/dist/scripts/migrate-storage.js +2718 -2354
  261. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +891 -597
  262. package/dist/setup/detect.js +9 -0
  263. package/dist/setup/legacy-config.js +106 -0
  264. package/dist/setup/prompt.js +57 -0
  265. package/dist/setup/providers.js +14 -0
  266. package/dist/setup/registry-stash-loader.js +12 -0
  267. package/dist/setup/semantic-assets.js +124 -0
  268. package/dist/setup/setup.js +52 -1614
  269. package/dist/setup/steps/connection.js +734 -0
  270. package/dist/setup/steps/output.js +31 -0
  271. package/dist/setup/steps/platforms.js +124 -0
  272. package/dist/setup/steps/semantic.js +27 -0
  273. package/dist/setup/steps/sources.js +222 -0
  274. package/dist/setup/steps/stashdir.js +42 -0
  275. package/dist/setup/steps/tasks.js +152 -0
  276. package/dist/sources/include.js +6 -2
  277. package/dist/sources/providers/filesystem.js +0 -1
  278. package/dist/sources/providers/git-install.js +210 -0
  279. package/dist/sources/providers/git-provider.js +234 -0
  280. package/dist/sources/providers/git-stash.js +248 -0
  281. package/dist/sources/providers/git.js +10 -661
  282. package/dist/sources/providers/npm.js +2 -6
  283. package/dist/sources/providers/provider-utils.js +13 -7
  284. package/dist/sources/providers/sync-from-ref.js +9 -1
  285. package/dist/sources/providers/tar-utils.js +16 -8
  286. package/dist/sources/providers/website.js +9 -5
  287. package/dist/sources/website-ingest.js +187 -29
  288. package/dist/sources/wiki-fetchers/registry.js +53 -0
  289. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  290. package/dist/storage/database.js +45 -10
  291. package/dist/storage/managed-db.js +82 -0
  292. package/dist/storage/repositories/canaries-repository.js +107 -0
  293. package/dist/storage/repositories/consolidation-repository.js +38 -0
  294. package/dist/storage/repositories/embeddings-repository.js +72 -0
  295. package/dist/storage/repositories/events-repository.js +187 -0
  296. package/dist/storage/repositories/extract-sessions-repository.js +96 -0
  297. package/dist/storage/repositories/improve-runs-repository.js +146 -0
  298. package/dist/storage/repositories/index-db.js +14 -8
  299. package/dist/storage/repositories/proposals-repository.js +220 -0
  300. package/dist/storage/repositories/recombine-repository.js +213 -0
  301. package/dist/storage/repositories/registry-cache.js +93 -0
  302. package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
  303. package/dist/storage/repositories/task-history-repository.js +93 -0
  304. package/dist/storage/sqlite-pragmas.js +146 -0
  305. package/dist/tasks/backends/cron.js +1 -1
  306. package/dist/tasks/backends/index.js +9 -0
  307. package/dist/tasks/backends/launchd.js +1 -1
  308. package/dist/tasks/backends/schtasks.js +1 -1
  309. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  310. package/dist/tasks/runner.js +15 -13
  311. package/dist/text-import-hook.mjs +0 -0
  312. package/dist/wiki/wiki.js +52 -11
  313. package/dist/workflows/cli.js +1 -0
  314. package/dist/workflows/db.js +3 -4
  315. package/dist/workflows/runtime/runs.js +43 -118
  316. package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
  317. package/dist/workflows/validate-summary.js +2 -7
  318. package/docs/README.md +69 -18
  319. package/docs/data-and-telemetry.md +5 -4
  320. package/docs/migration/release-notes/0.7.0.md +1 -1
  321. package/docs/migration/release-notes/0.9.0.md +39 -0
  322. package/package.json +10 -10
  323. package/dist/assets/tasks/core/update-stashes.yml +0 -4
  324. package/dist/commands/db-cli.js +0 -23
  325. package/dist/indexer/db/db-backup.js +0 -376
  326. package/dist/indexer/passes/staleness-detect.js +0 -488
@@ -2,1003 +2,44 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { resolveStashDir } from "../core/common.js";
7
+ import { loadConfig } from "../core/config/config.js";
5
8
  import { ConfigError, UsageError } from "../core/errors.js";
6
- import { appendEvent, readEvents } from "../core/events.js";
7
- import { buildTaskRunId, getLoggedRunIds, openLogsDatabase } from "../core/logs-db.js";
8
- import { getStateDbPathInDataDir } from "../core/paths.js";
9
- import { listExistingTableNames, openStateDatabase, queryCompletedTaskIntervals, queryImproveRuns, queryTaskHistory, } from "../core/state-db.js";
10
- import { parseSinceToIso } from "../core/time.js";
9
+ import { readEvents } from "../core/events.js";
10
+ import { openLogsDatabase } from "../core/logs-db.js";
11
+ import { getCacheDir, getConfigDir, getConfigPath, getDataDir, getStateDbPathInDataDir } from "../core/paths.js";
12
+ import { listExistingTableNames, openStateDatabase } from "../core/state-db.js";
13
+ import { DURATION_UNITS, parseDuration, parseSinceToIso } from "../core/time.js";
11
14
  import { readSemanticStatus } from "../indexer/search/semantic-status.js";
12
15
  import { getExecutionLogCandidates } from "../integrations/session-logs/index.js";
13
- import { LLM_USAGE_EVENT } from "../llm/usage-persist.js";
16
+ import { queryTaskHistory } from "../storage/repositories/task-history-repository.js";
17
+ import { collectImproveAdvisories } from "./health/advisories.js";
14
18
  import { HEALTH_CHECKS } from "./health/checks.js";
19
+ import { buildImproveSkipSummary, computeWallTimeStats, parseTaskMetadata, roundRate, summarizeImproveCompleted, summarizeImproveRuns, } from "./health/improve-metrics.js";
20
+ import { readLlmUsageAggregate } from "./health/llm-usage.js";
21
+ import { computeDegradationMetrics, computeDenominatorFixedCoverage, computeEnrichmentMintingRollup, probeStateDbRoundTrip, readCalibration, } from "./health/metrics.js";
22
+ import { collectStashExposureAdvisory } from "./health/stash-exposure.js";
23
+ import { collectSurfacesAdvisories } from "./health/surfaces.js";
24
+ import { buildPerRunSummaries } from "./health/task-runs.js";
25
+ import { ACTIVE_RUN_WARN_MS, IMPROVE_COMPLETED_EVENT, } from "./health/types.js";
26
+ import { buildWindowMetrics, computeDeltas, partitionLogBackedRows, resolveWindowCompare } from "./health/windows.js";
15
27
  const DEFAULT_SINCE_MS = 24 * 60 * 60 * 1000;
16
- const IMPROVE_COMPLETED_EVENT = "improve_completed";
17
- const HEALTH_PROBE_EVENT = "health_probe";
18
- const ACTIVE_RUN_WARN_MS = 15 * 60 * 1000;
19
28
  export function parseHealthSince(since) {
20
29
  if (since === undefined || since.trim() === "") {
21
30
  return new Date(Date.now() - DEFAULT_SINCE_MS).toISOString();
22
31
  }
23
32
  const trimmed = since.trim();
24
- const durationMatch = trimmed.match(/^(\d+)([dhm])$/i);
25
- if (durationMatch) {
26
- const amount = Number.parseInt(durationMatch[1] ?? "0", 10);
27
- const unit = (durationMatch[2] ?? "d").toLowerCase();
28
- if (!Number.isFinite(amount) || amount < 0) {
29
- throw new UsageError("--since must be a non-negative duration or timestamp.", "INVALID_FLAG_VALUE");
30
- }
31
- const multiplier = unit === "h" ? 60 * 60 * 1000 : unit === "m" ? 30 * 24 * 60 * 60 * 1000 : 24 * 60 * 60 * 1000;
32
- return new Date(Date.now() - amount * multiplier).toISOString();
33
+ // Unit grammar is the CLI-wide canonical map: `m` = minutes, `M` = months.
34
+ // (Historically `--since 5m` meant 5 months here; it now means 5 minutes,
35
+ // with `5M` for months unified with consolidate / `--window-compare`.)
36
+ // Not lower-cased: case distinguishes `m` (minutes) from `M` (months).
37
+ const durationMs = parseDuration(trimmed, DURATION_UNITS);
38
+ if (durationMs !== null) {
39
+ return new Date(Date.now() - durationMs).toISOString();
33
40
  }
34
41
  return parseSinceToIso(trimmed);
35
42
  }
36
- function roundRate(value) {
37
- return Number(value.toFixed(4));
38
- }
39
- function parseTaskMetadata(row) {
40
- try {
41
- return JSON.parse(row.metadata_json);
42
- }
43
- catch {
44
- return {};
45
- }
46
- }
47
- function createUnknownImproveMetrics() {
48
- return {
49
- invoked: 0,
50
- completed: 0,
51
- skipped: 0,
52
- skipReasons: {},
53
- plannedRefs: 0,
54
- profileFilteredRefs: 0,
55
- actions: {
56
- reflect: { ok: 0, failed: 0, cooldown: 0, skipped: 0, guardRejected: 0, skippedByReason: {} },
57
- distill: {
58
- queued: 0,
59
- llmFailed: 0,
60
- qualityRejected: 0,
61
- judgeRejected: 0,
62
- validatorRejected: 0,
63
- configDisabled: 0,
64
- skipped: 0,
65
- skippedByReason: {},
66
- deferred: 0,
67
- deferredByReason: {},
68
- },
69
- memoryPrune: 0,
70
- memoryInference: 0,
71
- graphExtraction: 0,
72
- error: 0,
73
- },
74
- autoAccept: { promoted: 0, validationFailed: 0 },
75
- reflectsWithErrorContext: 0,
76
- coverageGapCount: 0,
77
- evalCasesWritten: 0,
78
- deadUrlCount: 0,
79
- memorySummary: { eligible: 0, derived: 0 },
80
- memoryCleanup: {
81
- pruneCandidates: 0,
82
- contradictionCandidates: 0,
83
- beliefStateTransitions: 0,
84
- consolidationCandidates: 0,
85
- archived: 0,
86
- warnings: 0,
87
- },
88
- consolidation: {
89
- ran: false,
90
- processed: 0,
91
- promoted: 0,
92
- merged: 0,
93
- deleted: 0,
94
- contradicted: 0,
95
- judgedNoAction: 0,
96
- mergedSecondaries: 0,
97
- failedChunkMemories: 0,
98
- skipReasons: {},
99
- failedChunks: 0,
100
- totalChunks: 0,
101
- durationMs: 0,
102
- },
103
- memoryInference: {
104
- ran: false,
105
- considered: 0,
106
- cacheHits: 0,
107
- retryAttempts: 0,
108
- freshAttempts: 0,
109
- splitParents: 0,
110
- written: 0,
111
- skippedNoFacts: 0,
112
- skippedChildExists: 0,
113
- skippedAborted: 0,
114
- unaccounted: 0,
115
- htmlErrorCount: 0,
116
- yieldEligibleRuns: 0,
117
- yieldEligibleConsidered: 0,
118
- yieldEligibleWritten: 0,
119
- yieldRate: 0,
120
- durationMs: 0,
121
- writes: 0,
122
- },
123
- graphExtraction: {
124
- ran: false,
125
- extractedFiles: 0,
126
- entities: 0,
127
- relations: 0,
128
- cacheHits: 0,
129
- cacheMisses: 0,
130
- cacheHitRate: 0,
131
- truncations: 0,
132
- failures: 0,
133
- htmlErrors: 0,
134
- retryAttempts: 0,
135
- durationMs: 0,
136
- },
137
- sessionExtraction: {
138
- ran: false,
139
- sessionsScanned: 0,
140
- sessionsExtracted: 0,
141
- sessionsSkipped: 0,
142
- proposalsCreated: 0,
143
- warnings: 0,
144
- durationMs: 0,
145
- },
146
- wallTime: {
147
- count: 0,
148
- medianMs: 0,
149
- p95Ms: 0,
150
- minMs: 0,
151
- maxMs: 0,
152
- byPhase: {
153
- consolidation: { count: 0, totalMs: 0, medianMs: 0, p95Ms: 0 },
154
- memoryInference: { count: 0, totalMs: 0, medianMs: 0, p95Ms: 0 },
155
- graphExtraction: { count: 0, totalMs: 0, medianMs: 0, p95Ms: 0 },
156
- },
157
- },
158
- };
159
- }
160
- function toFiniteNumber(value) {
161
- if (typeof value === "number" && Number.isFinite(value))
162
- return value;
163
- if (typeof value === "string" && value.trim()) {
164
- const parsed = Number(value);
165
- if (Number.isFinite(parsed))
166
- return parsed;
167
- }
168
- return 0;
169
- }
170
- /**
171
- * Event-derived metrics. Only `completed` and skipReasons/invoked are sourced
172
- * from events in v2 — the richer fields come from {@link summarizeImproveRuns}.
173
- * The function still receives `improve_completed` events so that the completed
174
- * count reflects the canonical event stream (it lines up 1:1 with improve_runs
175
- * rows in practice, but the events table remains the system-of-record for the
176
- * existence of a run).
177
- */
178
- function summarizeImproveCompleted(events) {
179
- const metrics = createUnknownImproveMetrics();
180
- metrics.completed = events.length;
181
- return metrics;
182
- }
183
- /**
184
- * Project a single `improve_runs.result_json` envelope into an accumulator-shaped
185
- * ImproveHealthMetrics. The aggregator merges these per-row metrics into one
186
- * window-level metric.
187
- */
188
- function projectRunMetrics(result) {
189
- const metrics = createUnknownImproveMetrics();
190
- // plannedRefs (array of {ref, reason})
191
- const plannedRefs = result.plannedRefs;
192
- if (Array.isArray(plannedRefs))
193
- metrics.plannedRefs += plannedRefs.length;
194
- // profileFilteredRefs (array of {ref, reason}) — 2026-05-27: pre-filter
195
- // bucket from `collectEligibleRefs` so the metric reflects work the
196
- // planner dropped before signal-delta / per-pass dispatch.
197
- const profileFilteredRefs = result.profileFilteredRefs;
198
- if (Array.isArray(profileFilteredRefs))
199
- metrics.profileFilteredRefs += profileFilteredRefs.length;
200
- // actions: split reflect / distill by outcome, count others.
201
- const actions = result.actions;
202
- if (Array.isArray(actions)) {
203
- for (const action of actions) {
204
- const mode = typeof action.mode === "string" ? action.mode : "";
205
- switch (mode) {
206
- case "reflect":
207
- metrics.actions.reflect.ok += 1;
208
- break;
209
- case "reflect-failed":
210
- metrics.actions.reflect.failed += 1;
211
- break;
212
- case "reflect-cooldown":
213
- metrics.actions.reflect.cooldown += 1;
214
- break;
215
- case "reflect-skipped": {
216
- metrics.actions.reflect.skipped += 1;
217
- const r = action.result;
218
- const reason = typeof r?.reason === "string" && r.reason.trim() ? r.reason : "unknown";
219
- metrics.actions.reflect.skippedByReason[reason] = (metrics.actions.reflect.skippedByReason[reason] ?? 0) + 1;
220
- break;
221
- }
222
- case "reflect-guard-rejected":
223
- metrics.actions.reflect.guardRejected += 1;
224
- break;
225
- case "distill": {
226
- const r = action.result;
227
- const outcome = typeof r?.outcome === "string" ? r.outcome : "";
228
- switch (outcome) {
229
- case "queued":
230
- metrics.actions.distill.queued += 1;
231
- break;
232
- case "llm_failed":
233
- metrics.actions.distill.llmFailed += 1;
234
- break;
235
- case "quality_rejected":
236
- case "review_needed":
237
- metrics.actions.distill.qualityRejected += 1;
238
- metrics.actions.distill.judgeRejected += 1;
239
- break;
240
- case "validation_failed":
241
- metrics.actions.distill.qualityRejected += 1;
242
- metrics.actions.distill.validatorRejected += 1;
243
- break;
244
- case "config_disabled":
245
- metrics.actions.distill.configDisabled += 1;
246
- break;
247
- case "skipped": {
248
- // Previously dropped on the floor. The four sub-paths that emit
249
- // `outcome: "skipped"` (see distill.ts:893, 1024, 1120, 1576):
250
- // - recursive_lesson_input (type guard refused a lesson input)
251
- // - conflict_noop (LLM resolved destination conflict as NOOP)
252
- // - proposal-skipped cooldown / dedup at persistence
253
- // 465 events/7d in the user's live stack. The result message
254
- // typically encodes the reason; we also accept an explicit
255
- // `skipReason` field when downstream code sets it.
256
- metrics.actions.distill.deferred += 1;
257
- const explicitReason = typeof r?.skipReason === "string" ? r.skipReason : undefined;
258
- const msg = typeof r?.message === "string" ? r.message : "";
259
- let reason = explicitReason ?? "unknown";
260
- if (!explicitReason) {
261
- if (/lesson inputs/i.test(msg))
262
- reason = "recursive_lesson_input";
263
- else if (/NOOP/.test(msg))
264
- reason = "conflict_noop";
265
- else if (/cooldown/i.test(msg))
266
- reason = "proposal_cooldown";
267
- else if (/content[_ ]?hash/i.test(msg))
268
- reason = "content_hash_match";
269
- }
270
- metrics.actions.distill.deferredByReason[reason] =
271
- (metrics.actions.distill.deferredByReason[reason] ?? 0) + 1;
272
- break;
273
- }
274
- default:
275
- break;
276
- }
277
- break;
278
- }
279
- case "distill-skipped": {
280
- metrics.actions.distill.skipped += 1;
281
- const r = action.result;
282
- const reason = typeof r?.reason === "string" && r.reason.trim() ? r.reason : "unknown";
283
- metrics.actions.distill.skippedByReason[reason] = (metrics.actions.distill.skippedByReason[reason] ?? 0) + 1;
284
- break;
285
- }
286
- case "memory-prune":
287
- metrics.actions.memoryPrune += 1;
288
- break;
289
- case "memory-inference":
290
- metrics.actions.memoryInference += 1;
291
- break;
292
- case "graph-extraction":
293
- metrics.actions.graphExtraction += 1;
294
- break;
295
- case "error":
296
- metrics.actions.error += 1;
297
- break;
298
- }
299
- }
300
- }
301
- metrics.autoAccept.promoted += toFiniteNumber(result.gateAutoAcceptedCount);
302
- metrics.autoAccept.validationFailed += toFiniteNumber(result.gateAutoAcceptFailedCount);
303
- metrics.reflectsWithErrorContext += toFiniteNumber(result.reflectsWithErrorContext);
304
- if (Array.isArray(result.coverageGaps))
305
- metrics.coverageGapCount += result.coverageGaps.length;
306
- metrics.evalCasesWritten += toFiniteNumber(result.evalCasesWritten);
307
- if (Array.isArray(result.deadUrls))
308
- metrics.deadUrlCount += result.deadUrls.length;
309
- const memorySummary = result.memorySummary;
310
- if (memorySummary) {
311
- metrics.memorySummary.eligible += toFiniteNumber(memorySummary.eligible);
312
- metrics.memorySummary.derived += toFiniteNumber(memorySummary.derived);
313
- }
314
- const memoryCleanup = result.memoryCleanup;
315
- if (memoryCleanup) {
316
- if (Array.isArray(memoryCleanup.pruneCandidates))
317
- metrics.memoryCleanup.pruneCandidates += memoryCleanup.pruneCandidates.length;
318
- if (Array.isArray(memoryCleanup.contradictionCandidates))
319
- metrics.memoryCleanup.contradictionCandidates += memoryCleanup.contradictionCandidates.length;
320
- if (Array.isArray(memoryCleanup.beliefStateTransitions))
321
- metrics.memoryCleanup.beliefStateTransitions += memoryCleanup.beliefStateTransitions.length;
322
- if (Array.isArray(memoryCleanup.consolidationCandidates))
323
- metrics.memoryCleanup.consolidationCandidates += memoryCleanup.consolidationCandidates.length;
324
- if (Array.isArray(memoryCleanup.archived))
325
- metrics.memoryCleanup.archived += memoryCleanup.archived.length;
326
- if (Array.isArray(memoryCleanup.warnings))
327
- metrics.memoryCleanup.warnings += memoryCleanup.warnings.length;
328
- }
329
- const consolidation = result.consolidation;
330
- if (consolidation) {
331
- metrics.consolidation.processed += toFiniteNumber(consolidation.processed);
332
- metrics.consolidation.merged += toFiniteNumber(consolidation.merged);
333
- metrics.consolidation.deleted += toFiniteNumber(consolidation.deleted);
334
- metrics.consolidation.contradicted += toFiniteNumber(consolidation.contradicted);
335
- if (Array.isArray(consolidation.promoted))
336
- metrics.consolidation.promoted += consolidation.promoted.length;
337
- metrics.consolidation.failedChunks += toFiniteNumber(consolidation.failedChunks);
338
- metrics.consolidation.totalChunks += toFiniteNumber(consolidation.totalChunks);
339
- metrics.consolidation.durationMs += toFiniteNumber(consolidation.durationMs);
340
- metrics.consolidation.judgedNoAction += toFiniteNumber(consolidation.judgedNoAction);
341
- metrics.consolidation.mergedSecondaries += toFiniteNumber(consolidation.mergedSecondaries);
342
- metrics.consolidation.failedChunkMemories += toFiniteNumber(consolidation.failedChunkMemories);
343
- // Structured emitter (new on this branch): consolidate.ts now pushes
344
- // per-ref grouped `{ref, skips: [{op, reason}]}` entries to `skipReasons`
345
- // for every deterministic post-LLM rejection. Each ref appears once but
346
- // may carry multiple skips; aggregate every reason. Pre-fix envelopes have
347
- // neither field, so be defensive.
348
- const skipReasons = consolidation.skipReasons;
349
- if (Array.isArray(skipReasons)) {
350
- for (const entry of skipReasons) {
351
- if (!entry || typeof entry !== "object")
352
- continue;
353
- const skips = entry.skips;
354
- if (!Array.isArray(skips))
355
- continue;
356
- for (const skip of skips) {
357
- if (!skip || typeof skip !== "object")
358
- continue;
359
- const reason = skip.reason;
360
- if (typeof reason !== "string" || !reason.trim())
361
- continue;
362
- metrics.consolidation.skipReasons[reason] = (metrics.consolidation.skipReasons[reason] ?? 0) + 1;
363
- }
364
- }
365
- }
366
- }
367
- const memoryInference = result.memoryInference;
368
- if (memoryInference) {
369
- const considered = toFiniteNumber(memoryInference.considered);
370
- const writtenFacts = toFiniteNumber(memoryInference.writtenFacts);
371
- metrics.memoryInference.considered += considered;
372
- metrics.memoryInference.cacheHits += toFiniteNumber(memoryInference.cacheHits);
373
- metrics.memoryInference.retryAttempts += toFiniteNumber(memoryInference.retryAttempts);
374
- metrics.memoryInference.splitParents += toFiniteNumber(memoryInference.splitParents);
375
- metrics.memoryInference.written += writtenFacts;
376
- metrics.memoryInference.skippedNoFacts += toFiniteNumber(memoryInference.skippedNoFacts);
377
- metrics.memoryInference.skippedChildExists += toFiniteNumber(memoryInference.skippedChildExists);
378
- metrics.memoryInference.skippedAborted += toFiniteNumber(memoryInference.skippedAborted);
379
- metrics.memoryInference.unaccounted += toFiniteNumber(memoryInference.unaccounted);
380
- metrics.memoryInference.htmlErrorCount += toFiniteNumber(memoryInference.htmlErrorCount);
381
- // Yield-rate gating: pre-cache-feature envelopes lack the `cacheHits`
382
- // field entirely. Treating their `considered` as freshAttempts (since
383
- // cacheHits=0) is mathematically tempting but operationally wrong —
384
- // historical runs with the legacy schema have no cache instrumentation
385
- // and the SUM dragged the reported rate to ~14% in local data. Only
386
- // contribute to the yield aggregate when the envelope actually carries
387
- // the field. See investigation 2026-05-26.
388
- if (Object.hasOwn(memoryInference, "cacheHits")) {
389
- metrics.memoryInference.yieldEligibleRuns += 1;
390
- metrics.memoryInference.yieldEligibleConsidered += considered;
391
- metrics.memoryInference.yieldEligibleWritten += writtenFacts;
392
- }
393
- }
394
- metrics.memoryInference.durationMs += toFiniteNumber(result.memoryInferenceDurationMs);
395
- const graphExtraction = result.graphExtraction;
396
- if (graphExtraction) {
397
- const quality = graphExtraction.quality;
398
- if (quality)
399
- metrics.graphExtraction.extractedFiles += toFiniteNumber(quality.extractedFiles);
400
- metrics.graphExtraction.entities += toFiniteNumber(graphExtraction.totalEntities);
401
- metrics.graphExtraction.relations += toFiniteNumber(graphExtraction.totalRelations);
402
- const telemetry = graphExtraction.telemetry;
403
- if (telemetry) {
404
- metrics.graphExtraction.cacheHits += toFiniteNumber(telemetry.cacheHits);
405
- metrics.graphExtraction.cacheMisses += toFiniteNumber(telemetry.cacheMisses);
406
- metrics.graphExtraction.truncations += toFiniteNumber(telemetry.truncationCount);
407
- metrics.graphExtraction.failures += toFiniteNumber(telemetry.failureCount);
408
- metrics.graphExtraction.htmlErrors += toFiniteNumber(telemetry.htmlErrorCount);
409
- metrics.graphExtraction.retryAttempts += toFiniteNumber(telemetry.retryAttempts);
410
- }
411
- }
412
- metrics.graphExtraction.durationMs += toFiniteNumber(result.graphExtractionDurationMs);
413
- if (Array.isArray(result.extract)) {
414
- for (const e of result.extract) {
415
- metrics.sessionExtraction.sessionsScanned += toFiniteNumber(e.sessionsProcessed);
416
- metrics.sessionExtraction.sessionsSkipped += toFiniteNumber(e.sessionsSkipped);
417
- if (Array.isArray(e.sessions)) {
418
- metrics.sessionExtraction.sessionsExtracted += e.sessions.filter((s) => Array.isArray(s.proposalIds) && s.proposalIds.length > 0).length;
419
- }
420
- metrics.sessionExtraction.proposalsCreated += Array.isArray(e.proposals) ? e.proposals.length : 0;
421
- metrics.sessionExtraction.warnings += Array.isArray(e.warnings) ? e.warnings.length : 0;
422
- metrics.sessionExtraction.durationMs += toFiniteNumber(e.durationMs);
423
- }
424
- }
425
- return metrics;
426
- }
427
- /**
428
- * Finalize derived flags and rates on an accumulator. Used both for the
429
- * window-level aggregate and for each per-run row in --detail per-run mode
430
- * so the single-row metrics still expose `ran` / `yieldRate` / `cacheHitRate`.
431
- */
432
- function finalizeImproveMetrics(metrics) {
433
- metrics.consolidation.ran =
434
- metrics.consolidation.processed > 0 ||
435
- metrics.consolidation.durationMs > 0 ||
436
- metrics.consolidation.promoted > 0 ||
437
- metrics.consolidation.merged > 0 ||
438
- metrics.consolidation.deleted > 0 ||
439
- metrics.consolidation.contradicted > 0 ||
440
- metrics.consolidation.totalChunks > 0;
441
- metrics.memoryInference.ran =
442
- metrics.memoryInference.considered > 0 ||
443
- metrics.memoryInference.written > 0 ||
444
- metrics.memoryInference.durationMs > 0;
445
- metrics.memoryInference.writes = metrics.memoryInference.written;
446
- // Yield denominator excludes cache hits AND legacy (pre-cacheHits-field)
447
- // envelopes. Only runs whose envelope carries a `cacheHits` field
448
- // contribute to freshAttempts/yieldRate; legacy rows remain in
449
- // `considered`/`written` for totals but are excluded from the rate so
450
- // they cannot drag it down. See ImproveHealthMetrics.memoryInference
451
- // jsdoc for the rationale.
452
- metrics.memoryInference.freshAttempts = Math.max(0, metrics.memoryInference.yieldEligibleConsidered -
453
- metrics.memoryInference.cacheHits -
454
- metrics.memoryInference.skippedAborted);
455
- metrics.memoryInference.yieldRate =
456
- metrics.memoryInference.freshAttempts > 0
457
- ? roundRate(metrics.memoryInference.yieldEligibleWritten / metrics.memoryInference.freshAttempts)
458
- : 0;
459
- metrics.graphExtraction.ran =
460
- metrics.graphExtraction.extractedFiles > 0 ||
461
- metrics.graphExtraction.entities > 0 ||
462
- metrics.graphExtraction.durationMs > 0;
463
- const cacheTotal = metrics.graphExtraction.cacheHits + metrics.graphExtraction.cacheMisses;
464
- metrics.graphExtraction.cacheHitRate = cacheTotal > 0 ? roundRate(metrics.graphExtraction.cacheHits / cacheTotal) : 0;
465
- metrics.sessionExtraction.ran =
466
- metrics.sessionExtraction.sessionsScanned > 0 ||
467
- metrics.sessionExtraction.proposalsCreated > 0 ||
468
- metrics.sessionExtraction.durationMs > 0;
469
- }
470
- /**
471
- * Merge per-row metrics from `src` into accumulator `dst`. All numeric fields
472
- * are additive; cumulative rates are recomputed by finalizeImproveMetrics.
473
- */
474
- function mergeImproveMetrics(dst, src) {
475
- dst.plannedRefs += src.plannedRefs;
476
- dst.profileFilteredRefs += src.profileFilteredRefs;
477
- dst.actions.reflect.ok += src.actions.reflect.ok;
478
- dst.actions.reflect.failed += src.actions.reflect.failed;
479
- dst.actions.reflect.cooldown += src.actions.reflect.cooldown;
480
- dst.actions.reflect.skipped += src.actions.reflect.skipped;
481
- dst.actions.reflect.guardRejected += src.actions.reflect.guardRejected;
482
- for (const [reason, count] of Object.entries(src.actions.reflect.skippedByReason)) {
483
- dst.actions.reflect.skippedByReason[reason] = (dst.actions.reflect.skippedByReason[reason] ?? 0) + count;
484
- }
485
- dst.actions.distill.queued += src.actions.distill.queued;
486
- dst.actions.distill.llmFailed += src.actions.distill.llmFailed;
487
- dst.actions.distill.qualityRejected += src.actions.distill.qualityRejected;
488
- dst.actions.distill.judgeRejected += src.actions.distill.judgeRejected;
489
- dst.actions.distill.validatorRejected += src.actions.distill.validatorRejected;
490
- dst.actions.distill.configDisabled += src.actions.distill.configDisabled;
491
- dst.actions.distill.skipped += src.actions.distill.skipped;
492
- for (const [reason, count] of Object.entries(src.actions.distill.skippedByReason)) {
493
- dst.actions.distill.skippedByReason[reason] = (dst.actions.distill.skippedByReason[reason] ?? 0) + count;
494
- }
495
- dst.actions.distill.deferred += src.actions.distill.deferred;
496
- for (const [reason, count] of Object.entries(src.actions.distill.deferredByReason)) {
497
- dst.actions.distill.deferredByReason[reason] = (dst.actions.distill.deferredByReason[reason] ?? 0) + count;
498
- }
499
- dst.actions.memoryPrune += src.actions.memoryPrune;
500
- dst.actions.memoryInference += src.actions.memoryInference;
501
- dst.actions.graphExtraction += src.actions.graphExtraction;
502
- dst.actions.error += src.actions.error;
503
- dst.autoAccept.promoted += src.autoAccept.promoted;
504
- dst.autoAccept.validationFailed += src.autoAccept.validationFailed;
505
- dst.reflectsWithErrorContext += src.reflectsWithErrorContext;
506
- dst.coverageGapCount += src.coverageGapCount;
507
- dst.evalCasesWritten += src.evalCasesWritten;
508
- dst.deadUrlCount += src.deadUrlCount;
509
- dst.memorySummary.eligible += src.memorySummary.eligible;
510
- dst.memorySummary.derived += src.memorySummary.derived;
511
- dst.memoryCleanup.pruneCandidates += src.memoryCleanup.pruneCandidates;
512
- dst.memoryCleanup.contradictionCandidates += src.memoryCleanup.contradictionCandidates;
513
- dst.memoryCleanup.beliefStateTransitions += src.memoryCleanup.beliefStateTransitions;
514
- dst.memoryCleanup.consolidationCandidates += src.memoryCleanup.consolidationCandidates;
515
- dst.memoryCleanup.archived += src.memoryCleanup.archived;
516
- dst.memoryCleanup.warnings += src.memoryCleanup.warnings;
517
- dst.consolidation.processed += src.consolidation.processed;
518
- dst.consolidation.promoted += src.consolidation.promoted;
519
- dst.consolidation.merged += src.consolidation.merged;
520
- dst.consolidation.deleted += src.consolidation.deleted;
521
- dst.consolidation.contradicted += src.consolidation.contradicted;
522
- dst.consolidation.failedChunks += src.consolidation.failedChunks;
523
- dst.consolidation.totalChunks += src.consolidation.totalChunks;
524
- dst.consolidation.durationMs += src.consolidation.durationMs;
525
- dst.consolidation.judgedNoAction += src.consolidation.judgedNoAction;
526
- dst.consolidation.mergedSecondaries += src.consolidation.mergedSecondaries;
527
- dst.consolidation.failedChunkMemories += src.consolidation.failedChunkMemories;
528
- for (const [reason, count] of Object.entries(src.consolidation.skipReasons)) {
529
- dst.consolidation.skipReasons[reason] = (dst.consolidation.skipReasons[reason] ?? 0) + count;
530
- }
531
- dst.memoryInference.considered += src.memoryInference.considered;
532
- dst.memoryInference.cacheHits += src.memoryInference.cacheHits;
533
- dst.memoryInference.splitParents += src.memoryInference.splitParents;
534
- dst.memoryInference.written += src.memoryInference.written;
535
- dst.memoryInference.skippedNoFacts += src.memoryInference.skippedNoFacts;
536
- dst.memoryInference.skippedChildExists += src.memoryInference.skippedChildExists;
537
- dst.memoryInference.skippedAborted += src.memoryInference.skippedAborted;
538
- dst.memoryInference.unaccounted += src.memoryInference.unaccounted;
539
- dst.memoryInference.htmlErrorCount += src.memoryInference.htmlErrorCount;
540
- dst.memoryInference.yieldEligibleRuns += src.memoryInference.yieldEligibleRuns;
541
- dst.memoryInference.yieldEligibleConsidered += src.memoryInference.yieldEligibleConsidered;
542
- dst.memoryInference.yieldEligibleWritten += src.memoryInference.yieldEligibleWritten;
543
- dst.memoryInference.durationMs += src.memoryInference.durationMs;
544
- dst.graphExtraction.extractedFiles += src.graphExtraction.extractedFiles;
545
- dst.graphExtraction.entities += src.graphExtraction.entities;
546
- dst.graphExtraction.relations += src.graphExtraction.relations;
547
- dst.graphExtraction.cacheHits += src.graphExtraction.cacheHits;
548
- dst.graphExtraction.cacheMisses += src.graphExtraction.cacheMisses;
549
- dst.graphExtraction.truncations += src.graphExtraction.truncations;
550
- dst.graphExtraction.failures += src.graphExtraction.failures;
551
- dst.graphExtraction.htmlErrors += src.graphExtraction.htmlErrors;
552
- dst.graphExtraction.durationMs += src.graphExtraction.durationMs;
553
- dst.sessionExtraction.sessionsScanned += src.sessionExtraction.sessionsScanned;
554
- dst.sessionExtraction.sessionsExtracted += src.sessionExtraction.sessionsExtracted;
555
- dst.sessionExtraction.sessionsSkipped += src.sessionExtraction.sessionsSkipped;
556
- dst.sessionExtraction.proposalsCreated += src.sessionExtraction.proposalsCreated;
557
- dst.sessionExtraction.warnings += src.sessionExtraction.warnings;
558
- dst.sessionExtraction.durationMs += src.sessionExtraction.durationMs;
559
- }
560
- function summarizeImproveRuns(db, since, until) {
561
- const accum = createUnknownImproveMetrics();
562
- const rows = queryImproveRuns(db, since, until);
563
- // Per-phase wall-time samples. Each entry is one envelope's durationMs for
564
- // that phase. Phases that did not run on a given envelope are simply
565
- // omitted (NOT counted as 0) so the median/p95 reflect actual phase work.
566
- const phaseDurations = {
567
- consolidation: [],
568
- memoryInference: [],
569
- graphExtraction: [],
570
- };
571
- for (const row of rows) {
572
- let result;
573
- try {
574
- result = JSON.parse(row.result_json);
575
- }
576
- catch {
577
- continue;
578
- }
579
- const perRow = projectRunMetrics(result);
580
- mergeImproveMetrics(accum, perRow);
581
- // Collect per-phase durations directly off the envelope. consolidation's
582
- // duration lives inside the sub-object; memoryInference and graphExtraction
583
- // expose top-level *DurationMs keys (`memoryInferenceDurationMs`,
584
- // `graphExtractionDurationMs`) when they actually ran on that envelope.
585
- const consol = result.consolidation;
586
- const consolMs = toFiniteNumber(consol?.durationMs);
587
- if (consolMs > 0)
588
- phaseDurations.consolidation.push(consolMs);
589
- const memMs = toFiniteNumber(result.memoryInferenceDurationMs);
590
- if (memMs > 0)
591
- phaseDurations.memoryInference.push(memMs);
592
- const graphMs = toFiniteNumber(result.graphExtractionDurationMs);
593
- if (graphMs > 0)
594
- phaseDurations.graphExtraction.push(graphMs);
595
- }
596
- finalizeImproveMetrics(accum);
597
- accum.wallTime.byPhase = {
598
- consolidation: summarizePhaseDurations(phaseDurations.consolidation),
599
- memoryInference: summarizePhaseDurations(phaseDurations.memoryInference),
600
- graphExtraction: summarizePhaseDurations(phaseDurations.graphExtraction),
601
- };
602
- return { metrics: accum, runCount: rows.length };
603
- }
604
- /**
605
- * Aggregate a list of per-envelope phase durations into the
606
- * `wallTime.byPhase.*` shape: count, total, median, p95. Median/p95 use the
607
- * same nearest-rank picker as the top-level wallTime stats so the two are
608
- * comparable.
609
- */
610
- function summarizePhaseDurations(samples) {
611
- if (samples.length === 0)
612
- return { count: 0, totalMs: 0, medianMs: 0, p95Ms: 0 };
613
- const sorted = [...samples].sort((a, b) => a - b);
614
- const pick = (q) => sorted[Math.min(sorted.length - 1, Math.floor(q * sorted.length))] ?? 0;
615
- const totalMs = sorted.reduce((acc, n) => acc + n, 0);
616
- return {
617
- count: sorted.length,
618
- totalMs,
619
- medianMs: pick(0.5),
620
- p95Ms: pick(0.95),
621
- };
622
- }
623
- /**
624
- * Project an improve_runs row + wall-time lookup into a single ImproveRunSummary.
625
- * Used by `akm health --detail per-run`.
626
- */
627
- function projectImproveRunSummary(row, wallTimeMs) {
628
- let result = {};
629
- try {
630
- result = JSON.parse(row.result_json);
631
- }
632
- catch {
633
- // fall through with empty result so per-stage rollups are zeros
634
- }
635
- const perRow = projectRunMetrics(result);
636
- finalizeImproveMetrics(perRow);
637
- const orphansPurged = toFiniteNumber(result.orphansPurged);
638
- const lintSummary = result.lintSummary;
639
- const lintFixed = lintSummary ? toFiniteNumber(lintSummary.fixed) : 0;
640
- const lintFlagged = lintSummary ? toFiniteNumber(lintSummary.flagged) : 0;
641
- return {
642
- id: row.id,
643
- startedAt: row.started_at,
644
- completedAt: row.completed_at,
645
- wallTimeMs,
646
- ok: row.ok === 1,
647
- scope: {
648
- mode: row.scope_mode,
649
- ...(row.scope_value ? { value: row.scope_value } : {}),
650
- },
651
- actions: perRow.actions,
652
- memorySummary: perRow.memorySummary,
653
- memoryCleanup: perRow.memoryCleanup,
654
- consolidation: perRow.consolidation,
655
- memoryInference: perRow.memoryInference,
656
- graphExtraction: perRow.graphExtraction,
657
- reflectsWithErrorContext: perRow.reflectsWithErrorContext,
658
- evalCasesWritten: perRow.evalCasesWritten,
659
- orphansPurged,
660
- lintFixed,
661
- lintFlagged,
662
- };
663
- }
664
- /**
665
- * Load task_history intervals for `task_id='akm-improve'` in the window.
666
- * Returned sorted by startMs ascending so containment lookups can use a
667
- * linear scan (typical N is ~24/day; not worth a tree).
668
- *
669
- * The window filter is widened by 5 minutes on each side because the cron
670
- * task wraps `akm improve` — the task `started_at` fires at e.g. :07:01
671
- * while `recordImproveRun` writes the matching `improve_runs.started_at`
672
- * later (after config load, planning, etc.), so the improve_runs row can
673
- * be inside the window even when its enclosing task_history row started
674
- * just before the window opened.
675
- */
676
- function loadTaskIntervals(db, since, until) {
677
- const sinceMs = new Date(since).getTime();
678
- const untilMs = until ? new Date(until).getTime() : Number.POSITIVE_INFINITY;
679
- const widenedSince = new Date(sinceMs - 5 * 60 * 1000).toISOString();
680
- const widenedUntil = Number.isFinite(untilMs) ? new Date(untilMs + 5 * 60 * 1000).toISOString() : undefined;
681
- const rows = queryCompletedTaskIntervals(db, widenedSince, widenedUntil);
682
- const intervals = [];
683
- for (const row of rows) {
684
- const startMs = new Date(row.started_at).getTime();
685
- const endMs = new Date(row.completed_at).getTime();
686
- if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs < startMs)
687
- continue;
688
- intervals.push({ startMs, endMs, durationMs: endMs - startMs });
689
- }
690
- return intervals;
691
- }
692
- /**
693
- * Find the task_history interval that contains the given timestamp. The
694
- * task wraps `akm improve`, so `improve_runs.started_at` (when
695
- * `recordImproveRun` writes) always falls inside the enclosing task's
696
- * [started_at, completed_at]. Returns undefined when no interval
697
- * contains the timestamp (which happens for manually-invoked improve
698
- * runs not driven by the `akm-improve` task).
699
- *
700
- * Linear scan because N is small. We tolerate a 1s slop on the upper
701
- * bound to handle clock skew between the wrapper's `completed_at` write
702
- * and recordImproveRun's `started_at` write.
703
- */
704
- function findContainingTaskInterval(timestampMs, intervals) {
705
- const SLOP_MS = 1000;
706
- for (const interval of intervals) {
707
- if (timestampMs >= interval.startMs && timestampMs <= interval.endMs + SLOP_MS) {
708
- return interval;
709
- }
710
- }
711
- return undefined;
712
- }
713
- function buildPerRunSummaries(db, since, until) {
714
- const rows = queryImproveRuns(db, since, until);
715
- const taskIntervals = loadTaskIntervals(db, since, until);
716
- const summaries = [];
717
- for (const row of rows) {
718
- const startMs = new Date(row.started_at).getTime();
719
- const endMs = new Date(row.completed_at).getTime();
720
- // Prefer the improve_runs row's own (completed_at - started_at) delta:
721
- // recordImproveRun now persists distinct start/end timestamps, so the
722
- // row's own delta is the authoritative per-run wall time even for
723
- // manually-invoked `akm improve` runs with no enclosing task_history.
724
- // Only fall back to the task_history containing-interval join for legacy/
725
- // backfill rows where started_at == completed_at (row delta is 0).
726
- const hasRowDelta = Number.isFinite(startMs) && Number.isFinite(endMs) && endMs > startMs;
727
- let wallTimeMs;
728
- if (hasRowDelta) {
729
- wallTimeMs = endMs - startMs;
730
- }
731
- else {
732
- const interval = Number.isFinite(startMs) ? findContainingTaskInterval(startMs, taskIntervals) : undefined;
733
- wallTimeMs = interval?.durationMs ?? 0;
734
- }
735
- summaries.push(projectImproveRunSummary(row, wallTimeMs));
736
- }
737
- return summaries;
738
- }
739
- function emptyPhaseStats() {
740
- return {
741
- consolidation: { count: 0, totalMs: 0, medianMs: 0, p95Ms: 0 },
742
- memoryInference: { count: 0, totalMs: 0, medianMs: 0, p95Ms: 0 },
743
- graphExtraction: { count: 0, totalMs: 0, medianMs: 0, p95Ms: 0 },
744
- };
745
- }
746
- function computeWallTimeStats(durationsMs, byPhase) {
747
- const phase = byPhase ?? emptyPhaseStats();
748
- if (durationsMs.length === 0)
749
- return { count: 0, medianMs: 0, p95Ms: 0, minMs: 0, maxMs: 0, byPhase: phase };
750
- const sorted = [...durationsMs].sort((a, b) => a - b);
751
- const pick = (q) => sorted[Math.min(sorted.length - 1, Math.floor(q * sorted.length))] ?? 0;
752
- return {
753
- count: sorted.length,
754
- medianMs: pick(0.5),
755
- p95Ms: pick(0.95),
756
- minMs: sorted[0] ?? 0,
757
- maxMs: sorted[sorted.length - 1] ?? 0,
758
- byPhase: phase,
759
- };
760
- }
761
- function buildImproveSkipSummary(events) {
762
- const skipReasons = {};
763
- for (const event of events) {
764
- const reason = typeof event.metadata?.reason === "string" && event.metadata.reason.trim() ? event.metadata.reason : "unknown";
765
- skipReasons[reason] = (skipReasons[reason] ?? 0) + 1;
766
- }
767
- return { skipped: events.length, skipReasons };
768
- }
769
- function probeStateDbRoundTrip(stateDbPath) {
770
- const before = readEvents({}, { dbPath: stateDbPath }).nextOffset;
771
- const started = Date.now();
772
- appendEvent({ eventType: HEALTH_PROBE_EVENT, ref: "health:probe", metadata: { source: "akm health" } }, { dbPath: stateDbPath });
773
- const after = readEvents({ sinceOffset: before, type: HEALTH_PROBE_EVENT, ref: "health:probe" }, { dbPath: stateDbPath });
774
- const durationMs = Date.now() - started;
775
- if (after.events.length === 0 || after.nextOffset <= before) {
776
- return { ok: false, durationMs, error: "probe event was not readable after append" };
777
- }
778
- return { ok: true, durationMs };
779
- }
780
- /**
781
- * Parse a `--window-compare <duration>` shorthand into two adjacent windows
782
- * (current, prior). Duration syntax matches {@link parseHealthSince}.
783
- */
784
- function resolveWindowCompare(duration, now = () => Date.now()) {
785
- const trimmed = duration.trim();
786
- const durationMatch = trimmed.match(/^(\d+)([dhm])$/i);
787
- if (!durationMatch) {
788
- throw new UsageError("--window-compare must be a duration like '24h', '7d', or '30m'.", "INVALID_FLAG_VALUE");
789
- }
790
- const amount = Number.parseInt(durationMatch[1] ?? "0", 10);
791
- const unit = (durationMatch[2] ?? "h").toLowerCase();
792
- if (!Number.isFinite(amount) || amount <= 0) {
793
- throw new UsageError("--window-compare must be a positive duration.", "INVALID_FLAG_VALUE");
794
- }
795
- const multiplier = unit === "h" ? 60 * 60 * 1000 : unit === "m" ? 60 * 1000 : 24 * 60 * 60 * 1000;
796
- const ms = amount * multiplier;
797
- const nowMs = now();
798
- const currentSince = new Date(nowMs - ms).toISOString();
799
- const currentUntil = new Date(nowMs).toISOString();
800
- const priorSince = new Date(nowMs - 2 * ms).toISOString();
801
- const priorUntil = currentSince;
802
- return [
803
- { name: "current", since: currentSince, until: currentUntil },
804
- { name: "prior", since: priorSince, until: priorUntil },
805
- ];
806
- }
807
- /**
808
- * Parse a single repeatable `--windows` value of the form
809
- * `name=...,since=...,until=...`. All keys are optional EXCEPT name and since.
810
- */
811
- export function parseWindowSpec(raw) {
812
- const fields = {};
813
- for (const part of raw.split(",")) {
814
- const trimmed = part.trim();
815
- if (!trimmed)
816
- continue;
817
- const eq = trimmed.indexOf("=");
818
- if (eq < 0) {
819
- throw new UsageError(`--windows entry must be a comma-separated list of key=value pairs: ${raw}`, "INVALID_FLAG_VALUE");
820
- }
821
- const key = trimmed.slice(0, eq).trim();
822
- const value = trimmed.slice(eq + 1).trim();
823
- fields[key] = value;
824
- }
825
- if (!fields.name) {
826
- throw new UsageError(`--windows entry is missing required 'name': ${raw}`, "INVALID_FLAG_VALUE");
827
- }
828
- if (!fields.since) {
829
- throw new UsageError(`--windows entry is missing required 'since': ${raw}`, "INVALID_FLAG_VALUE");
830
- }
831
- return {
832
- name: fields.name,
833
- since: fields.since,
834
- ...(fields.until ? { until: fields.until } : {}),
835
- };
836
- }
837
- /** Hard-coded list of "interesting" metric paths for window-compare deltas. */
838
- const INTERESTING_DELTA_PATHS = [
839
- "improve.actions.reflect.failed",
840
- "improve.actions.reflect.guardRejected",
841
- "improve.actions.distill.llmFailed",
842
- "improve.actions.distill.queued",
843
- "improve.actions.distill.deferred",
844
- "improve.consolidation.promoted",
845
- "improve.memoryInference.written",
846
- "improve.memoryInference.yieldRate",
847
- "improve.memoryInference.skippedNoFacts",
848
- "improve.memoryInference.htmlErrorCount",
849
- "improve.graphExtraction.cacheHitRate",
850
- "improve.graphExtraction.failures",
851
- "improve.graphExtraction.htmlErrors",
852
- "improve.sessionExtraction.sessionsScanned",
853
- "improve.sessionExtraction.proposalsCreated",
854
- "improve.autoAccept.promoted",
855
- "improve.autoAccept.validationFailed",
856
- "improve.wallTime.medianMs",
857
- "improve.wallTime.p95Ms",
858
- ];
859
- function readNumericPath(obj, path) {
860
- const parts = path.split(".");
861
- let cursor = obj;
862
- for (const part of parts) {
863
- if (typeof cursor !== "object" || cursor === null)
864
- return 0;
865
- cursor = cursor[part];
866
- }
867
- return typeof cursor === "number" && Number.isFinite(cursor) ? cursor : 0;
868
- }
869
- function computeDeltas(first, last) {
870
- const out = {};
871
- for (const path of INTERESTING_DELTA_PATHS) {
872
- const from = readNumericPath(first, path);
873
- const to = readNumericPath(last, path);
874
- if (from === 0 && to === 0)
875
- continue;
876
- let pctChange;
877
- if (from === 0) {
878
- pctChange = to === 0 ? 0 : "+inf";
879
- }
880
- else {
881
- pctChange = Number((((to - from) / from) * 100).toFixed(2));
882
- }
883
- out[path] = { from, to, pctChange };
884
- }
885
- return out;
886
- }
887
- /**
888
- * Partition task_history rows into "should have a log" (non-null log_path) and
889
- * "log is actually backed". A run counts as backed when logs.db holds rows for
890
- * its run_id (#579 — the DB is the primary record); rows written before logs.db
891
- * existed fall back to the transitional on-disk file check. `logsDb` may be
892
- * undefined when logs.db could not be opened — then only the file check runs.
893
- */
894
- function partitionLogBackedRows(taskRows, logsDb) {
895
- const withLogs = taskRows.filter((row) => row.log_path !== null);
896
- const loggedRunIds = logsDb
897
- ? getLoggedRunIds(logsDb, withLogs.map((row) => buildTaskRunId(row.task_id, row.started_at)))
898
- : new Set();
899
- const backed = withLogs.filter((row) => loggedRunIds.has(buildTaskRunId(row.task_id, row.started_at)) ||
900
- (row.log_path !== null && fs.existsSync(row.log_path)));
901
- return { withLogs, backed };
902
- }
903
- /** Stage key used for `llm_usage` events recorded outside any stage scope. */
904
- const UNATTRIBUTED_STAGE = "unattributed";
905
- function emptyLlmUsageStageAggregate() {
906
- return {
907
- calls: 0,
908
- totalDurationMs: 0,
909
- promptTokens: 0,
910
- completionTokens: 0,
911
- totalTokens: 0,
912
- reasoningTokens: 0,
913
- };
914
- }
915
- function emptyLlmUsageAggregate() {
916
- return { ...emptyLlmUsageStageAggregate(), byStage: {} };
917
- }
918
- /**
919
- * Aggregate `llm_usage` events (#576) into a window total plus a per-stage
920
- * breakdown of call count, wall-time, and token usage. Token fields absent from
921
- * a best-effort record contribute 0. Calls with no `stage` land under
922
- * {@link UNATTRIBUTED_STAGE}.
923
- */
924
- function summarizeLlmUsage(events) {
925
- const aggregate = emptyLlmUsageAggregate();
926
- for (const event of events) {
927
- const meta = event.metadata ?? {};
928
- const stageKey = typeof meta.stage === "string" && meta.stage ? meta.stage : UNATTRIBUTED_STAGE;
929
- let stage = aggregate.byStage[stageKey];
930
- if (!stage) {
931
- stage = emptyLlmUsageStageAggregate();
932
- aggregate.byStage[stageKey] = stage;
933
- }
934
- const durationMs = toFiniteNumber(meta.durationMs);
935
- const promptTokens = toFiniteNumber(meta.promptTokens);
936
- const completionTokens = toFiniteNumber(meta.completionTokens);
937
- const totalTokens = toFiniteNumber(meta.totalTokens);
938
- const reasoningTokens = toFiniteNumber(meta.reasoningTokens);
939
- for (const target of [aggregate, stage]) {
940
- target.calls += 1;
941
- target.totalDurationMs += durationMs;
942
- target.promptTokens += promptTokens;
943
- target.completionTokens += completionTokens;
944
- target.totalTokens += totalTokens;
945
- target.reasoningTokens += reasoningTokens;
946
- }
947
- }
948
- return aggregate;
949
- }
950
- function readLlmUsageAggregate(stateDbPath, since, until) {
951
- const events = readEvents({ since, type: LLM_USAGE_EVENT }, { dbPath: stateDbPath }).events.filter((event) => {
952
- if (until === undefined)
953
- return true;
954
- return new Date(event.ts ?? since).getTime() < new Date(until).getTime();
955
- });
956
- return summarizeLlmUsage(events);
957
- }
958
- function buildWindowMetrics(db, stateDbPath, since, until, now = () => Date.now(), logsDb) {
959
- const taskRows = queryTaskHistory(db, { since }).filter((row) => {
960
- const startMs = new Date(row.started_at).getTime();
961
- const untilMs = new Date(until).getTime();
962
- return !Number.isFinite(untilMs) || startMs < untilMs;
963
- });
964
- const { withLogs: taskRowsWithLogs, backed: existingLogRows } = partitionLogBackedRows(taskRows, logsDb);
965
- const failedTaskRows = taskRows.filter((row) => row.status === "failed");
966
- const activeRows = taskRows.filter((row) => row.status === "active");
967
- const stuckActiveRuns = activeRows.filter((row) => now() - new Date(row.started_at).getTime() > ACTIVE_RUN_WARN_MS).length;
968
- const promptRows = taskRows.filter((row) => row.target_kind === "prompt");
969
- const promptFailures = promptRows.filter((row) => {
970
- const detail = parseTaskMetadata(row).detail;
971
- return typeof detail?.reason === "string" && detail.reason.length > 0;
972
- });
973
- const logBackingRate = taskRowsWithLogs.length === 0 ? 1 : existingLogRows.length / taskRowsWithLogs.length;
974
- const taskFailRate = taskRows.length === 0 ? 0 : failedTaskRows.length / taskRows.length;
975
- const agentFailureRate = promptRows.length === 0 ? 0 : promptFailures.length / promptRows.length;
976
- const improveInvoked = readEvents({ since, type: "improve_invoked" }, { dbPath: stateDbPath }).events.filter((event) => new Date(event.ts ?? since).getTime() < new Date(until).getTime()).length;
977
- const improveCompletedEvents = readEvents({ since, type: IMPROVE_COMPLETED_EVENT }, { dbPath: stateDbPath }).events.filter((event) => new Date(event.ts ?? since).getTime() < new Date(until).getTime());
978
- const improveSkippedEvents = readEvents({ since, type: "improve_skipped" }, { dbPath: stateDbPath }).events.filter((event) => new Date(event.ts ?? since).getTime() < new Date(until).getTime());
979
- const eventsMetrics = summarizeImproveCompleted(improveCompletedEvents);
980
- const { metrics: improveSummary, runCount } = summarizeImproveRuns(db, since, until);
981
- improveSummary.invoked = improveInvoked;
982
- improveSummary.completed = eventsMetrics.completed;
983
- const skipSummary = buildImproveSkipSummary(improveSkippedEvents);
984
- improveSummary.skipped = skipSummary.skipped;
985
- improveSummary.skipReasons = skipSummary.skipReasons;
986
- // Preserve the per-phase aggregation computed by summarizeImproveRuns and
987
- // derive top-level wall times from the same improve-runs window so counts
988
- // and percentiles stay aligned with per-run reporting.
989
- const perRunSummaries = buildPerRunSummaries(db, since, until);
990
- const wallTimes = perRunSummaries.map((run) => run.wallTimeMs).filter((ms) => Number.isFinite(ms) && ms > 0);
991
- improveSummary.wallTime = computeWallTimeStats(wallTimes, improveSummary.wallTime.byPhase);
992
- const metrics = {
993
- taskFailRate: roundRate(taskFailRate),
994
- agentFailureRate: roundRate(agentFailureRate),
995
- stuckActiveRuns,
996
- logBackingRate: roundRate(logBackingRate),
997
- probeRoundTripMs: null,
998
- llmUsage: readLlmUsageAggregate(stateDbPath, since, until),
999
- };
1000
- return { improve: improveSummary, metrics, runs: runCount };
1001
- }
1002
43
  function validateAkmHealthOptions(options) {
1003
44
  if (options.groupBy !== undefined && options.groupBy !== "run") {
1004
45
  throw new UsageError(`Invalid value for --group-by: ${options.groupBy}. Expected: run`, "INVALID_FLAG_VALUE");
@@ -1064,6 +105,20 @@ export function akmHealth(options = {}) {
1064
105
  const taskFailRate = taskRows.length === 0 ? 0 : failedTaskRows.length / taskRows.length;
1065
106
  const agentFailureRate = promptRows.length === 0 ? 0 : promptFailures.length / promptRows.length;
1066
107
  const semanticStatus = readSemanticStatus();
108
+ // For the embedding-endpoint advisory. Best-effort: an unloadable config
109
+ // leaves both undefined and the check falls back to its generic message.
110
+ let semanticSearchMode;
111
+ let embeddingEndpoint;
112
+ let egressConfigView;
113
+ try {
114
+ const config = loadConfig();
115
+ semanticSearchMode = config.semanticSearchMode;
116
+ embeddingEndpoint = config.embedding?.endpoint;
117
+ egressConfigView = config;
118
+ }
119
+ catch {
120
+ // fall through with undefined
121
+ }
1067
122
  const improveInvoked = readEvents({ since, type: "improve_invoked" }, { dbPath: stateDbPath }).events.length;
1068
123
  const improveCompletedEvents = readEvents({ since, type: IMPROVE_COMPLETED_EVENT }, { dbPath: stateDbPath }).events;
1069
124
  const improveSkippedEvents = readEvents({ since, type: "improve_skipped" }, { dbPath: stateDbPath }).events;
@@ -1077,6 +132,53 @@ export function akmHealth(options = {}) {
1077
132
  const perRunSummaries = buildPerRunSummaries(db, since);
1078
133
  const wallTimes = perRunSummaries.map((run) => run.wallTimeMs).filter((ms) => Number.isFinite(ms) && ms > 0);
1079
134
  improveSummary.wallTime = computeWallTimeStats(wallTimes, improveSummary.wallTime.byPhase);
135
+ improveSummary.calibration = readCalibration(db, since);
136
+ // WS-5: Compute denominator-fixed coverage and per-run degradation metrics
137
+ // for the main health path (not just window-compare mode).
138
+ const until = new Date(now()).toISOString();
139
+ const totalAssetsMain = improveSummary.memorySummary.eligible + improveSummary.memorySummary.derived;
140
+ improveSummary.coverage = computeDenominatorFixedCoverage(db, totalAssetsMain, improveSummary.memorySummary.eligible, since, until);
141
+ const degradationMain = computeDegradationMetrics(db, since, until);
142
+ if (degradationMain) {
143
+ improveSummary.degradation = degradationMain;
144
+ }
145
+ improveSummary.enrichmentMinting = computeEnrichmentMintingRollup(db, since, until);
146
+ advisories.push(...collectImproveAdvisories(db, stateDbPath, since, improveSummary));
147
+ // 08-F1: surface a `stash-git-exposure` advisory when env/secret assets are
148
+ // git-tracked AND a remote is configured (the leak moment). Best-effort.
149
+ // Cheap guard: only shell out to git when the stash has its OWN `.git` (or a
150
+ // test injected a fake seam), so the hot path never spawns for a non-git
151
+ // stash — the common unit-test case. Trade-off: a stash manually pointed at a
152
+ // bare subdirectory of a parent git repo (no `.git` of its own) is not
153
+ // checked. akm-init always creates `.git` at the stash root, so any
154
+ // akm-initialised stash is covered; this only skips hand-pointed nested ones.
155
+ try {
156
+ const exposureStashDir = options.stashDir ?? resolveStashDir();
157
+ if (options.stashExposureGit || fs.existsSync(path.join(exposureStashDir, ".git"))) {
158
+ const stashExposure = collectStashExposureAdvisory(exposureStashDir, options.stashExposureGit);
159
+ if (stashExposure)
160
+ advisories.push(stashExposure);
161
+ }
162
+ }
163
+ catch {
164
+ // Non-fatal — a git/probe failure must not abort the health report.
165
+ }
166
+ // 08 surfaces: the remaining read-only advisory group (secret-file-perms,
167
+ // binary-config-skew, orphan-stores, egress-endpoints). Best-effort — a
168
+ // filesystem probe failure must not abort the health report.
169
+ try {
170
+ advisories.push(...collectSurfacesAdvisories({
171
+ stashDir: options.stashDir ?? resolveStashDir(),
172
+ cacheDir: getCacheDir(),
173
+ dataDir: getDataDir(),
174
+ configDir: getConfigDir(),
175
+ configPath: getConfigPath(),
176
+ config: egressConfigView,
177
+ }));
178
+ }
179
+ catch {
180
+ // Non-fatal.
181
+ }
1080
182
  let sessionLogEntries = [];
1081
183
  try {
1082
184
  const sinceDays = Math.max(0, Math.ceil((now() - new Date(since).getTime()) / (24 * 60 * 60 * 1000)));
@@ -1101,11 +203,14 @@ export function akmHealth(options = {}) {
1101
203
  missingTables,
1102
204
  probe,
1103
205
  taskRowCount: taskRows.length,
206
+ taskFailRate,
1104
207
  taskRowsWithLogsCount: taskRowsWithLogs.length,
1105
208
  existingLogRowsCount: existingLogRows.length,
1106
209
  logBackingRate,
1107
210
  stuckActiveRuns,
1108
211
  semanticStatus,
212
+ semanticSearchMode,
213
+ embeddingEndpoint,
1109
214
  sessionLogEntries,
1110
215
  sessionExtraction: improveSummary.sessionExtraction,
1111
216
  autoAccept: improveSummary.autoAccept,
@@ -1205,102 +310,5 @@ export function akmHealth(options = {}) {
1205
310
  }
1206
311
  }
1207
312
  }
1208
- // ── Markdown renderers ───────────────────────────────────────────────────────
1209
- function padRight(s, width) {
1210
- return s.length >= width ? s : s + " ".repeat(width - s.length);
1211
- }
1212
- function renderTable(headers, rows) {
1213
- const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
1214
- const lines = [];
1215
- lines.push(headers.map((h, i) => padRight(h, widths[i] ?? 0)).join(" "));
1216
- for (const row of rows) {
1217
- lines.push(row.map((cell, i) => padRight(cell ?? "", widths[i] ?? 0)).join(" "));
1218
- }
1219
- return lines.join("\n");
1220
- }
1221
- /**
1222
- * Render `--detail per-run` rows as a TSV-ish aligned table. The column
1223
- * shape was originally inherited from the retired
1224
- * `scripts/improve-stats/runs-detail` bash helper; keep the same shape
1225
- * so operator muscle memory carries over.
1226
- *
1227
- * Columns: ts | ok | actions | refl_ok/fail/cd/skip |
1228
- * distill_q/llm-fail/qrej/cfg/skip | cons_proc/promo/merge/del |
1229
- * mem_cons/written/skip | graph_f/e/r | orphans | lint_f/fl
1230
- */
1231
- export function renderRunsDetailMd(runs) {
1232
- const headers = [
1233
- "ts",
1234
- "ok",
1235
- "actions",
1236
- "refl_ok/fail/cd/skip",
1237
- "distill_q/llm-fail/qrej/cfg/skip",
1238
- "cons_proc/promo/merge/del",
1239
- "mem_cons/written/skip",
1240
- "graph_f/e/r",
1241
- "orphans",
1242
- "lint_f/fl",
1243
- ];
1244
- const rows = runs.map((r) => {
1245
- const totalActions = r.actions.reflect.ok +
1246
- r.actions.reflect.failed +
1247
- r.actions.reflect.cooldown +
1248
- r.actions.reflect.skipped +
1249
- r.actions.distill.queued +
1250
- r.actions.distill.llmFailed +
1251
- r.actions.distill.qualityRejected +
1252
- r.actions.distill.configDisabled +
1253
- r.actions.distill.skipped +
1254
- r.actions.memoryPrune +
1255
- r.actions.memoryInference +
1256
- r.actions.graphExtraction +
1257
- r.actions.error;
1258
- return [
1259
- r.startedAt,
1260
- String(r.ok),
1261
- String(totalActions),
1262
- `${r.actions.reflect.ok}/${r.actions.reflect.failed}/${r.actions.reflect.cooldown}/${r.actions.reflect.skipped}`,
1263
- `${r.actions.distill.queued}/${r.actions.distill.llmFailed}/${r.actions.distill.qualityRejected}/${r.actions.distill.configDisabled}/${r.actions.distill.skipped}`,
1264
- `${r.consolidation.processed}/${r.consolidation.promoted}/${r.consolidation.merged}/${r.consolidation.deleted}`,
1265
- `${r.memoryInference.considered}/${r.memoryInference.written}/${r.memoryInference.skippedNoFacts}`,
1266
- `${r.graphExtraction.extractedFiles}/${r.graphExtraction.entities}/${r.graphExtraction.relations}`,
1267
- String(r.orphansPurged),
1268
- `${r.lintFixed}/${r.lintFlagged}`,
1269
- ];
1270
- });
1271
- return renderTable(headers, rows);
1272
- }
1273
- /**
1274
- * Render a window-compare comparison as a side-by-side metric table with a
1275
- * delta column. Bad-direction deltas (e.g. +pct on failed counts) get a `!`
1276
- * marker prefix.
1277
- */
1278
- export function renderWindowCompareMd(windows, deltas) {
1279
- if (windows.length === 0)
1280
- return "";
1281
- const headers = ["metric", ...windows.map((w) => w.name), "delta"];
1282
- const badIfPositive = new Set([
1283
- "improve.actions.reflect.failed",
1284
- "improve.actions.distill.llmFailed",
1285
- "improve.graphExtraction.failures",
1286
- "improve.wallTime.medianMs",
1287
- "improve.wallTime.p95Ms",
1288
- "improve.memoryInference.skippedNoFacts",
1289
- ]);
1290
- const rows = [];
1291
- for (const path of INTERESTING_DELTA_PATHS) {
1292
- const values = windows.map((w) => String(readNumericPath(w, path)));
1293
- const delta = deltas?.[path];
1294
- let deltaStr = "—";
1295
- if (delta) {
1296
- const pct = delta.pctChange;
1297
- const num = typeof pct === "number" ? pct : pct;
1298
- const sign = typeof num === "number" && num > 0 ? "+" : "";
1299
- const formatted = typeof num === "number" ? `${sign}${num}%` : String(num);
1300
- const marker = badIfPositive.has(path) && typeof num === "number" && num > 0 ? "!" : "";
1301
- deltaStr = marker + formatted;
1302
- }
1303
- rows.push([path, ...values, deltaStr]);
1304
- }
1305
- return renderTable(headers, rows);
1306
- }
313
+ // Markdown renderers (renderRunsDetailMd / renderWindowCompareMd) live in
314
+ // health/md-report.ts, mirroring the HTML extraction in health/html-report.ts.