openclaw-hybrid-memory 2026.5.310 → 2026.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (535) hide show
  1. package/api/plugin-runtime.ts +2 -0
  2. package/backends/facts-db/contradictions.ts +1 -1
  3. package/cli/cmd-extract-directives.ts +225 -11
  4. package/cli/cmd-extract-proposals.ts +5 -6
  5. package/cli/cmd-extract-reinforcement.ts +71 -0
  6. package/cli/cmd-feedback.ts +15 -9
  7. package/cli/commands/manage/register-reflection-pipeline.ts +247 -13
  8. package/cli/commands/manage/register-storage-maintenance.ts +224 -15
  9. package/cli/commands/manage/storage-stats-helpers.ts +13 -2
  10. package/cli/context.ts +9 -19
  11. package/cli/distill.ts +31 -1
  12. package/cli/register.ts +28 -38
  13. package/dist/api/plugin-runtime.js.map +1 -1
  14. package/dist/backends/agent-health-store.js.map +1 -1
  15. package/dist/backends/apitap-store.js.map +1 -1
  16. package/dist/backends/audit-store.js.map +1 -1
  17. package/dist/backends/base-sqlite-store.js.map +1 -1
  18. package/dist/backends/cost-tracker.js.map +1 -1
  19. package/dist/backends/credentials-db.js +2 -3
  20. package/dist/backends/credentials-db.js.map +1 -1
  21. package/dist/backends/crystallization-store.js.map +1 -1
  22. package/dist/backends/edict-store.js.map +1 -1
  23. package/dist/backends/event-bus.js.map +1 -1
  24. package/dist/backends/event-log.js.map +1 -1
  25. package/dist/backends/facts-db/cache-manager.js.map +1 -1
  26. package/dist/backends/facts-db/clusters.js.map +1 -1
  27. package/dist/backends/facts-db/contradictions.js +1 -1
  28. package/dist/backends/facts-db/contradictions.js.map +1 -1
  29. package/dist/backends/facts-db/crud.js.map +1 -1
  30. package/dist/backends/facts-db/db-connection.js.map +1 -1
  31. package/dist/backends/facts-db/entity-autolink.js.map +1 -1
  32. package/dist/backends/facts-db/entity-layer.js.map +1 -1
  33. package/dist/backends/facts-db/episodes.js.map +1 -1
  34. package/dist/backends/facts-db/fact-queries.js.map +1 -1
  35. package/dist/backends/facts-db/fact-read-queries.js.map +1 -1
  36. package/dist/backends/facts-db/facts-db-layer1.js.map +1 -1
  37. package/dist/backends/facts-db/facts-db-layer2.js.map +1 -1
  38. package/dist/backends/facts-db/facts-db-layer3.js.map +1 -1
  39. package/dist/backends/facts-db/fts-text.js.map +1 -1
  40. package/dist/backends/facts-db/generated-skills/policy.js.map +1 -1
  41. package/dist/backends/facts-db/generated-skills.js.map +1 -1
  42. package/dist/backends/facts-db/housekeeping.js.map +1 -1
  43. package/dist/backends/facts-db/links.js.map +1 -1
  44. package/dist/backends/facts-db/maintenance.js.map +1 -1
  45. package/dist/backends/facts-db/procedures/crud.js.map +1 -1
  46. package/dist/backends/facts-db/procedures/internal.js.map +1 -1
  47. package/dist/backends/facts-db/procedures/promotion.js.map +1 -1
  48. package/dist/backends/facts-db/procedures/search.js.map +1 -1
  49. package/dist/backends/facts-db/procedures/stats.js.map +1 -1
  50. package/dist/backends/facts-db/reinforcement.js.map +1 -1
  51. package/dist/backends/facts-db/row-mapper.js.map +1 -1
  52. package/dist/backends/facts-db/scan-cursors.js.map +1 -1
  53. package/dist/backends/facts-db/schema-bootstrap.js.map +1 -1
  54. package/dist/backends/facts-db/scope-sql.js.map +1 -1
  55. package/dist/backends/facts-db/search.js.map +1 -1
  56. package/dist/backends/facts-db/stats.js.map +1 -1
  57. package/dist/backends/facts-db/types.js.map +1 -1
  58. package/dist/backends/facts-db/variants.js.map +1 -1
  59. package/dist/backends/identity-reflection-store.js.map +1 -1
  60. package/dist/backends/issue-store.js.map +1 -1
  61. package/dist/backends/learnings-db.js.map +1 -1
  62. package/dist/backends/migrations/facts-migrations.js.map +1 -1
  63. package/dist/backends/migrations/procedures.js.map +1 -1
  64. package/dist/backends/narratives-db.js.map +1 -1
  65. package/dist/backends/persona-state-store.js.map +1 -1
  66. package/dist/backends/proposals-db.js.map +1 -1
  67. package/dist/backends/scope-filter-sql.js.map +1 -1
  68. package/dist/backends/sqlite-schema-meta.js.map +1 -1
  69. package/dist/backends/tool-proposal-store.js.map +1 -1
  70. package/dist/backends/vector-db/constants.js.map +1 -1
  71. package/dist/backends/vector-db/path-utils.js.map +1 -1
  72. package/dist/backends/vector-db/runtime-locks.js.map +1 -1
  73. package/dist/backends/vector-db/vector-db-class.js.map +1 -1
  74. package/dist/backends/wal.js.map +1 -1
  75. package/dist/backends/workflow-store.js.map +1 -1
  76. package/dist/benchmark/shadow-eval.js.map +1 -1
  77. package/dist/cli/active-tasks.js.map +1 -1
  78. package/dist/cli/backup.js.map +1 -1
  79. package/dist/cli/benchmark.js.map +1 -1
  80. package/dist/cli/cmd-backfill.js.map +1 -1
  81. package/dist/cli/cmd-config.js.map +1 -1
  82. package/dist/cli/cmd-credentials.js.map +1 -1
  83. package/dist/cli/cmd-demo.js.map +1 -1
  84. package/dist/cli/cmd-distill.js.map +1 -1
  85. package/dist/cli/cmd-doctor.js.map +1 -1
  86. package/dist/cli/cmd-examples.js.map +1 -1
  87. package/dist/cli/cmd-extract-daily.js.map +1 -1
  88. package/dist/cli/cmd-extract-directives.js +141 -10
  89. package/dist/cli/cmd-extract-directives.js.map +1 -1
  90. package/dist/cli/cmd-extract-procedures.js.map +1 -1
  91. package/dist/cli/cmd-extract-proposals.js +3 -2
  92. package/dist/cli/cmd-extract-proposals.js.map +1 -1
  93. package/dist/cli/cmd-extract-reinforcement.js +39 -0
  94. package/dist/cli/cmd-extract-reinforcement.js.map +1 -1
  95. package/dist/cli/cmd-extract-sessions.js.map +1 -1
  96. package/dist/cli/cmd-feedback.js +9 -4
  97. package/dist/cli/cmd-feedback.js.map +1 -1
  98. package/dist/cli/cmd-health.js.map +1 -1
  99. package/dist/cli/cmd-providers.js.map +1 -1
  100. package/dist/cli/cmd-selfcorrection.js.map +1 -1
  101. package/dist/cli/cmd-setup.js.map +1 -1
  102. package/dist/cli/cmd-status.js.map +1 -1
  103. package/dist/cli/cmd-store.js.map +1 -1
  104. package/dist/cli/cmd-user-friendly.js.map +1 -1
  105. package/dist/cli/cmd-verify.js.map +1 -1
  106. package/dist/cli/commands/manage/bindings.js.map +1 -1
  107. package/dist/cli/commands/manage/dream-cycle-followup.js.map +1 -1
  108. package/dist/cli/commands/manage/maintenance-heartbeat.js.map +1 -1
  109. package/dist/cli/commands/manage/register-agents-audit-runall.js.map +1 -1
  110. package/dist/cli/commands/manage/register-analyze-maintenance-logs.js.map +1 -1
  111. package/dist/cli/commands/manage/register-budget-proposals.js.map +1 -1
  112. package/dist/cli/commands/manage/register-config-cli.js.map +1 -1
  113. package/dist/cli/commands/manage/register-corrections-and-pipeline.js.map +1 -1
  114. package/dist/cli/commands/manage/register-corrections.js.map +1 -1
  115. package/dist/cli/commands/manage/register-council.js.map +1 -1
  116. package/dist/cli/commands/manage/register-credentials-scope.js.map +1 -1
  117. package/dist/cli/commands/manage/register-digest.js.map +1 -1
  118. package/dist/cli/commands/manage/register-lifecycle.js.map +1 -1
  119. package/dist/cli/commands/manage/register-procedure-lifecycle.js.map +1 -1
  120. package/dist/cli/commands/manage/register-reconcile-cron-ledgers.js.map +1 -1
  121. package/dist/cli/commands/manage/register-reflection-pipeline.js +144 -7
  122. package/dist/cli/commands/manage/register-reflection-pipeline.js.map +1 -1
  123. package/dist/cli/commands/manage/register-self-correction-feedback.js.map +1 -1
  124. package/dist/cli/commands/manage/register-storage-and-stats.js.map +1 -1
  125. package/dist/cli/commands/manage/register-storage-entities-decay.js.map +1 -1
  126. package/dist/cli/commands/manage/register-storage-graph-audit.js.map +1 -1
  127. package/dist/cli/commands/manage/register-storage-maintenance.js +152 -9
  128. package/dist/cli/commands/manage/register-storage-maintenance.js.map +1 -1
  129. package/dist/cli/commands/manage/register-validate-cron-exit.js.map +1 -1
  130. package/dist/cli/commands/manage/storage-stats-helpers.js +10 -3
  131. package/dist/cli/commands/manage/storage-stats-helpers.js.map +1 -1
  132. package/dist/cli/commands/register-manage-commands.js.map +1 -1
  133. package/dist/cli/config-feature-summaries.js.map +1 -1
  134. package/dist/cli/config-output-sink.js.map +1 -1
  135. package/dist/cli/distill-session-jsonl.js.map +1 -1
  136. package/dist/cli/distill.js +10 -1
  137. package/dist/cli/distill.js.map +1 -1
  138. package/dist/cli/global-verbose.js.map +1 -1
  139. package/dist/cli/goals.js.map +1 -1
  140. package/dist/cli/hybrid-mem-commander-utils.js.map +1 -1
  141. package/dist/cli/install/config-merge.js.map +1 -1
  142. package/dist/cli/install/cron-jobs.js.map +1 -1
  143. package/dist/cli/install/embedding-detect.js.map +1 -1
  144. package/dist/cli/install/run-install.js.map +1 -1
  145. package/dist/cli/install/workspace.js.map +1 -1
  146. package/dist/cli/proposals.js.map +1 -1
  147. package/dist/cli/register.js.map +1 -1
  148. package/dist/cli/shared.js.map +1 -1
  149. package/dist/cli/skills.js.map +1 -1
  150. package/dist/cli/task-queue-status.js.map +1 -1
  151. package/dist/cli/verified.js.map +1 -1
  152. package/dist/cli/verify/fact-count.js.map +1 -1
  153. package/dist/cli/verify/openclaw-config.js.map +1 -1
  154. package/dist/cli/verify/plugin-config-credentials.js.map +1 -1
  155. package/dist/cli/verify/sections/config-cron.js.map +1 -1
  156. package/dist/cli/verify/sections/embeddings.js.map +1 -1
  157. package/dist/cli/verify/sections/infrastructure.js.map +1 -1
  158. package/dist/cli/verify/sections/llm-models.js.map +1 -1
  159. package/dist/cli/verify/sections/reconcile.js.map +1 -1
  160. package/dist/cli/verify/verify-run-state.js.map +1 -1
  161. package/dist/cli/verify-llm-azure-auth.js.map +1 -1
  162. package/dist/cli/verify.js.map +1 -1
  163. package/dist/config/hybrid-schema.js.map +1 -1
  164. package/dist/config/index.js.map +1 -1
  165. package/dist/config/maintenance-fallback-policy.js.map +1 -1
  166. package/dist/config/parsers/capture.js.map +1 -1
  167. package/dist/config/parsers/core.js.map +1 -1
  168. package/dist/config/parsers/features.js.map +1 -1
  169. package/dist/config/parsers/index.js.map +1 -1
  170. package/dist/config/parsers/maintenance.js.map +1 -1
  171. package/dist/config/parsers/retrieval.js.map +1 -1
  172. package/dist/config/parsers/sensors.js.map +1 -1
  173. package/dist/config/skill-sections.js.map +1 -1
  174. package/dist/config/skill-size-limits.js.map +1 -1
  175. package/dist/config/types/agents.js.map +1 -1
  176. package/dist/config/types/bootstrap.js.map +1 -1
  177. package/dist/config/types/core.js.map +1 -1
  178. package/dist/config/types/index.js.map +1 -1
  179. package/dist/config/utils.js.map +1 -1
  180. package/dist/index-help.js.map +1 -1
  181. package/dist/index-testing-exports.js.map +1 -1
  182. package/dist/index.d.ts +1 -1
  183. package/dist/index.js +2 -2
  184. package/dist/index.js.map +1 -1
  185. package/dist/lifecycle/hook-resolution-api.js.map +1 -1
  186. package/dist/lifecycle/hooks.js +0 -1
  187. package/dist/lifecycle/hooks.js.map +1 -1
  188. package/dist/lifecycle/resolve-agent-id.js.map +1 -1
  189. package/dist/lifecycle/session-state.js.map +1 -1
  190. package/dist/lifecycle/stage-active-task.js.map +1 -1
  191. package/dist/lifecycle/stage-auth-failure.js.map +1 -1
  192. package/dist/lifecycle/stage-capture/run-capture.js.map +1 -1
  193. package/dist/lifecycle/stage-capture.js.map +1 -1
  194. package/dist/lifecycle/stage-cleanup.js.map +1 -1
  195. package/dist/lifecycle/stage-credential-hint.js.map +1 -1
  196. package/dist/lifecycle/stage-frustration.js.map +1 -1
  197. package/dist/lifecycle/stage-goal-stewardship.js.map +1 -1
  198. package/dist/lifecycle/stage-goal-subagent.js.map +1 -1
  199. package/dist/lifecycle/stage-injection.js +1 -1
  200. package/dist/lifecycle/stage-injection.js.map +1 -1
  201. package/dist/lifecycle/stage-recall/run-recall.js.map +1 -1
  202. package/dist/lifecycle/stage-recall.js.map +1 -1
  203. package/dist/lifecycle/stage-setup.js.map +1 -1
  204. package/dist/routes/dashboard/collectors.js.map +1 -1
  205. package/dist/routes/dashboard/html.js.map +1 -1
  206. package/dist/routes/dashboard/server.js.map +1 -1
  207. package/dist/routes/dashboard-graph.js.map +1 -1
  208. package/dist/routes/graphql-resolvers.js.map +1 -1
  209. package/dist/routes/graphql-server.js.map +1 -1
  210. package/dist/services/active-task-checkpoint.js.map +1 -1
  211. package/dist/services/active-task-injection.js.map +1 -1
  212. package/dist/services/active-task.js.map +1 -1
  213. package/dist/services/adaptive-catch-up-pacing.js +25 -0
  214. package/dist/services/adaptive-catch-up-pacing.js.map +1 -0
  215. package/dist/services/adaptive-maintenance-llm.js.map +1 -1
  216. package/dist/services/adaptive-model-limits.js.map +1 -1
  217. package/dist/services/ambient-retrieval.js.map +1 -1
  218. package/dist/services/apitap-service.js.map +1 -1
  219. package/dist/services/audit-health-exit-info.js.map +1 -1
  220. package/dist/services/audit-health-json.js.map +1 -1
  221. package/dist/services/auth-failure-detect.js.map +1 -1
  222. package/dist/services/auto-capture.js.map +1 -1
  223. package/dist/services/auto-classifier.js.map +1 -1
  224. package/dist/services/auto-skills-audit.js.map +1 -1
  225. package/dist/services/bootstrap-optional.js.map +1 -1
  226. package/dist/services/bootstrap-priority.js.map +1 -1
  227. package/dist/services/bootstrap.js.map +1 -1
  228. package/dist/services/capture-provenance.js.map +1 -1
  229. package/dist/services/capture-utils.js.map +1 -1
  230. package/dist/services/chat.js +22 -3
  231. package/dist/services/chat.js.map +1 -1
  232. package/dist/services/classification-scope.js.map +1 -1
  233. package/dist/services/classification.js.map +1 -1
  234. package/dist/services/cli-sql-dump.js.map +1 -1
  235. package/dist/services/consolidation.js.map +1 -1
  236. package/dist/services/context-audit.js +1 -1
  237. package/dist/services/context-audit.js.map +1 -1
  238. package/dist/services/context-budget.js.map +1 -1
  239. package/dist/services/context-engine.js.map +1 -1
  240. package/dist/services/contextual-variants.js.map +1 -1
  241. package/dist/services/continuous-verifier.js.map +1 -1
  242. package/dist/services/contradiction-adjudicator.js.map +1 -1
  243. package/dist/services/cost-context.js.map +1 -1
  244. package/dist/services/cost-feature-labels.js.map +1 -1
  245. package/dist/services/credential-migration.js.map +1 -1
  246. package/dist/services/credential-scanner.js.map +1 -1
  247. package/dist/services/credential-validation.js.map +1 -1
  248. package/dist/services/cron-exit-validator.js.map +1 -1
  249. package/dist/services/cron-guard.js.map +1 -1
  250. package/dist/services/cron-job-bash-harness.js +52 -5
  251. package/dist/services/cron-job-bash-harness.js.map +1 -1
  252. package/dist/services/cron-maintenance-reconciler.js +1 -3
  253. package/dist/services/cron-maintenance-reconciler.js.map +1 -1
  254. package/dist/services/cross-agent-learning.js.map +1 -1
  255. package/dist/services/crystallization-proposer.js.map +1 -1
  256. package/dist/services/dedupe-policy.js.map +1 -1
  257. package/dist/services/deprecated-cron-commands.js.map +1 -1
  258. package/dist/services/directive-extract.js.map +1 -1
  259. package/dist/services/document-chunker.js.map +1 -1
  260. package/dist/services/document-grader.js.map +1 -1
  261. package/dist/services/dream-cycle.js.map +1 -1
  262. package/dist/services/embedding-migration.js.map +1 -1
  263. package/dist/services/embedding-registry.js.map +1 -1
  264. package/dist/services/embeddings/chain-provider.js.map +1 -1
  265. package/dist/services/embeddings/factory.js.map +1 -1
  266. package/dist/services/embeddings/fallback-provider.js.map +1 -1
  267. package/dist/services/embeddings/ollama-provider.js.map +1 -1
  268. package/dist/services/embeddings/onnx-provider.js.map +1 -1
  269. package/dist/services/embeddings/openai-provider.js.map +1 -1
  270. package/dist/services/embeddings/shared.js +3 -3
  271. package/dist/services/embeddings/shared.js.map +1 -1
  272. package/dist/services/embeddings/types.js.map +1 -1
  273. package/dist/services/entity-enrichment-adaptive.js +128 -0
  274. package/dist/services/entity-enrichment-adaptive.js.map +1 -0
  275. package/dist/services/entity-enrichment-cli.js +389 -42
  276. package/dist/services/entity-enrichment-cli.js.map +1 -1
  277. package/dist/services/entity-enrichment.js +31 -5
  278. package/dist/services/entity-enrichment.js.map +1 -1
  279. package/dist/services/error-reporter/noisy-errors.js.map +1 -1
  280. package/dist/services/error-reporter/sanitize.js.map +1 -1
  281. package/dist/services/error-reporter.js.map +1 -1
  282. package/dist/services/event-hub-repair.js.map +1 -1
  283. package/dist/services/export-memory.js.map +1 -1
  284. package/dist/services/fact-extraction.js.map +1 -1
  285. package/dist/services/feedback-effectiveness.js.map +1 -1
  286. package/dist/services/find-duplicates.js.map +1 -1
  287. package/dist/services/frustration-detector.js.map +1 -1
  288. package/dist/services/fts-search.js.map +1 -1
  289. package/dist/services/gap-detector.js.map +1 -1
  290. package/dist/services/generated-skill-lifecycle.js.map +1 -1
  291. package/dist/services/generated-skill-validation.js.map +1 -1
  292. package/dist/services/goal-active-task-mirror.js.map +1 -1
  293. package/dist/services/goal-circuit-breaker.js.map +1 -1
  294. package/dist/services/goal-health.js.map +1 -1
  295. package/dist/services/goal-registry.js.map +1 -1
  296. package/dist/services/goal-stewardship-heartbeat.js.map +1 -1
  297. package/dist/services/goal-stewardship-llm-triage.js.map +1 -1
  298. package/dist/services/goal-stewardship-verify-cron.js.map +1 -1
  299. package/dist/services/goal-stewardship.js.map +1 -1
  300. package/dist/services/goal-subagent.js.map +1 -1
  301. package/dist/services/graph-retrieval.js.map +1 -1
  302. package/dist/services/humanizer-score.js.map +1 -1
  303. package/dist/services/hybrid-mem-cron-default-job-steps.js.map +1 -1
  304. package/dist/services/hyde-helper.js.map +1 -1
  305. package/dist/services/identity-reflection.js.map +1 -1
  306. package/dist/services/implicit-feedback-extract.js.map +1 -1
  307. package/dist/services/index.js.map +1 -1
  308. package/dist/services/ingest-utils.js.map +1 -1
  309. package/dist/services/intent-template.js.map +1 -1
  310. package/dist/services/json-array-parser.js.map +1 -1
  311. package/dist/services/knowledge-gaps.js.map +1 -1
  312. package/dist/services/language-keywords-build.js.map +1 -1
  313. package/dist/services/lifecycle/github-adapter.js.map +1 -1
  314. package/dist/services/llm-rate-limit-headers.js +1 -2
  315. package/dist/services/llm-rate-limit-headers.js.map +1 -1
  316. package/dist/services/maintenance-auto-fix.js.map +1 -1
  317. package/dist/services/maintenance-log-analyzer.js +7 -1
  318. package/dist/services/maintenance-log-analyzer.js.map +1 -1
  319. package/dist/services/maintenance-timestamp.js.map +1 -1
  320. package/dist/services/memory-diagnostics.js.map +1 -1
  321. package/dist/services/memory-index.js.map +1 -1
  322. package/dist/services/merge-results.js.map +1 -1
  323. package/dist/services/model-capabilities.js.map +1 -1
  324. package/dist/services/model-pricing.js.map +1 -1
  325. package/dist/services/narrative-recall.js.map +1 -1
  326. package/dist/services/openclaw-session-artifact.js.map +1 -1
  327. package/dist/services/passive-observer.js.map +1 -1
  328. package/dist/services/pattern-detector-hash.js.map +1 -1
  329. package/dist/services/pattern-detector.js.map +1 -1
  330. package/dist/services/pending-autopilot/foundation.js.map +1 -1
  331. package/dist/services/pending-autopilot/redaction.js.map +1 -1
  332. package/dist/services/pending-autopilot/store.js.map +1 -1
  333. package/dist/services/pending-autopilot/types.js.map +1 -1
  334. package/dist/services/pending-digest-autopilot-cron.js.map +1 -1
  335. package/dist/services/pending-digest-autopilot.js.map +1 -1
  336. package/dist/services/pending-review-digest.js.map +1 -1
  337. package/dist/services/persona-proposal-triage.js.map +1 -1
  338. package/dist/services/persona-state-promotion.js.map +1 -1
  339. package/dist/services/post-compaction-recall.js.map +1 -1
  340. package/dist/services/pre-consolidation-flush.js.map +1 -1
  341. package/dist/services/pre-finalization-guard.js.map +1 -1
  342. package/dist/services/procedure-cluster.js.map +1 -1
  343. package/dist/services/procedure-extractor.js.map +1 -1
  344. package/dist/services/procedure-promotion/duplicate-skill-cache.js.map +1 -1
  345. package/dist/services/procedure-promotion-policy.js.map +1 -1
  346. package/dist/services/procedure-selection-metrics.js.map +1 -1
  347. package/dist/services/procedure-skill-eval.js.map +1 -1
  348. package/dist/services/procedure-skill-generator.js.map +1 -1
  349. package/dist/services/procedure-skill-recipe.js.map +1 -1
  350. package/dist/services/procedure-skill-shrink.js.map +1 -1
  351. package/dist/services/procedure-skill-workflow.js.map +1 -1
  352. package/dist/services/provenance.js.map +1 -1
  353. package/dist/services/public-export-bundle.js.map +1 -1
  354. package/dist/services/python-bridge.js.map +1 -1
  355. package/dist/services/query-expander.js.map +1 -1
  356. package/dist/services/query-validator.js.map +1 -1
  357. package/dist/services/recall-pipeline.js.map +1 -1
  358. package/dist/services/recall-timing.js.map +1 -1
  359. package/dist/services/recent-http-attempts.js.map +1 -1
  360. package/dist/services/reflection/shared.js.map +1 -1
  361. package/dist/services/reflection.js.map +1 -1
  362. package/dist/services/reinforcement-extract.js.map +1 -1
  363. package/dist/services/reranker.js.map +1 -1
  364. package/dist/services/responses-adapter.js.map +1 -1
  365. package/dist/services/retrieval-aliases.js.map +1 -1
  366. package/dist/services/retrieval-mode-policy.js.map +1 -1
  367. package/dist/services/retrieval-orchestrator/packing.js.map +1 -1
  368. package/dist/services/retrieval-orchestrator.d.ts +2 -3
  369. package/dist/services/retrieval-orchestrator.js.map +1 -1
  370. package/dist/services/rrf-fusion.js.map +1 -1
  371. package/dist/services/self-correction-extract.js.map +1 -1
  372. package/dist/services/session-observability.js.map +1 -1
  373. package/dist/services/session-pre-filter.js.map +1 -1
  374. package/dist/services/shortest-path.js.map +1 -1
  375. package/dist/services/skill-allowed-tools.js.map +1 -1
  376. package/dist/services/skill-creator-validator.js.map +1 -1
  377. package/dist/services/skill-crystallizer-helpers.js.map +1 -1
  378. package/dist/services/skill-crystallizer.js.map +1 -1
  379. package/dist/services/skill-description-builder.js.map +1 -1
  380. package/dist/services/skill-eval-synthesizer.js.map +1 -1
  381. package/dist/services/skill-examples-builder.js.map +1 -1
  382. package/dist/services/skill-frontmatter.js.map +1 -1
  383. package/dist/services/skill-name-validator.js.map +1 -1
  384. package/dist/services/skill-prompt-injection.js.map +1 -1
  385. package/dist/services/skill-reference-sidecar.js.map +1 -1
  386. package/dist/services/skill-script-bundler.js.map +1 -1
  387. package/dist/services/skill-validator.js.map +1 -1
  388. package/dist/services/startup-memory-attribution.js.map +1 -1
  389. package/dist/services/task-hygiene.js.map +1 -1
  390. package/dist/services/task-ledger/canonical.js.map +1 -1
  391. package/dist/services/task-ledger-facts.js.map +1 -1
  392. package/dist/services/task-queue-leases.js.map +1 -1
  393. package/dist/services/task-queue-watchdog.js.map +1 -1
  394. package/dist/services/tool-effectiveness.js.map +1 -1
  395. package/dist/services/tool-proposer.js.map +1 -1
  396. package/dist/services/tools-md-section.js.map +1 -1
  397. package/dist/services/topic-clusters.js.map +1 -1
  398. package/dist/services/trajectory-tracker.js.map +1 -1
  399. package/dist/services/vector-backend-observability.js.map +1 -1
  400. package/dist/services/vector-lifecycle-audit.js.map +1 -1
  401. package/dist/services/vector-maintenance.js.map +1 -1
  402. package/dist/services/vector-search.js.map +1 -1
  403. package/dist/services/verification-store.js.map +1 -1
  404. package/dist/services/verified-fact-triage.js.map +1 -1
  405. package/dist/services/wal-helpers.js.map +1 -1
  406. package/dist/services/workflow-tracker.js.map +1 -1
  407. package/dist/setup/bootstrap-databases.js.map +1 -1
  408. package/dist/setup/cli-context/cli-services.js.map +1 -1
  409. package/dist/setup/cli-context/help-text.js.map +1 -1
  410. package/dist/setup/cli-context/metadata.js.map +1 -1
  411. package/dist/setup/cli-context/register-cli-with-help.js.map +1 -1
  412. package/dist/setup/cli-context/register-full.js.map +1 -1
  413. package/dist/setup/cli-context/register-help.js.map +1 -1
  414. package/dist/setup/cost-instrumentation.js.map +1 -1
  415. package/dist/setup/hybrid-memory-generation-state.js.map +1 -1
  416. package/dist/setup/hybrid-memory-reload-coordinator.js +13 -13
  417. package/dist/setup/hybrid-memory-reload-coordinator.js.map +1 -1
  418. package/dist/setup/plugin-service.js.map +1 -1
  419. package/dist/setup/provider-router.js.map +1 -1
  420. package/dist/setup/register-context-engine.js.map +1 -1
  421. package/dist/setup/register-hooks.js.map +1 -1
  422. package/dist/setup/register-plugin.js +25 -21
  423. package/dist/setup/register-plugin.js.map +1 -1
  424. package/dist/setup/register-tools.js.map +1 -1
  425. package/dist/setup/reregister-policy.js +2 -2
  426. package/dist/setup/reregister-policy.js.map +1 -1
  427. package/dist/setup/tool-installers.js.map +1 -1
  428. package/dist/setup/workspace-bootstrap.js.map +1 -1
  429. package/dist/src/worker/narratives.js.map +1 -1
  430. package/dist/tools/apitap-tools.js.map +1 -1
  431. package/dist/tools/credential-tools.js.map +1 -1
  432. package/dist/tools/crystallization-tools.js.map +1 -1
  433. package/dist/tools/dashboard-routes.js.map +1 -1
  434. package/dist/tools/document-tools.js.map +1 -1
  435. package/dist/tools/goal-tools.js.map +1 -1
  436. package/dist/tools/graph-tools.js.map +1 -1
  437. package/dist/tools/issue-tools.js.map +1 -1
  438. package/dist/tools/memory/build-runtime.js.map +1 -1
  439. package/dist/tools/memory/helpers.js.map +1 -1
  440. package/dist/tools/memory/register-checkpoint-tools.js.map +1 -1
  441. package/dist/tools/memory/register-directory-tools.js.map +1 -1
  442. package/dist/tools/memory/register-edict-tools.js.map +1 -1
  443. package/dist/tools/memory/register-episode-tools.js.map +1 -1
  444. package/dist/tools/memory/register-recall-tools.js.map +1 -1
  445. package/dist/tools/memory/register-store-tools.js.map +1 -1
  446. package/dist/tools/memory-tools.js.map +1 -1
  447. package/dist/tools/persona-tools.js.map +1 -1
  448. package/dist/tools/provenance-tools.js.map +1 -1
  449. package/dist/tools/public-api-routes.js.map +1 -1
  450. package/dist/tools/safe-register-http-route.js.map +1 -1
  451. package/dist/tools/self-extension-tools.js.map +1 -1
  452. package/dist/tools/task-hygiene-tools.js.map +1 -1
  453. package/dist/tools/utility-tools.js.map +1 -1
  454. package/dist/tools/verification-tools.js.map +1 -1
  455. package/dist/tools/workflow-tools.js.map +1 -1
  456. package/dist/types/issue-types.js.map +1 -1
  457. package/dist/types/learnings-types.js.map +1 -1
  458. package/dist/types/memory.js.map +1 -1
  459. package/dist/utils/apim-gateway-fetch.js.map +1 -1
  460. package/dist/utils/atomic-write.js.map +1 -1
  461. package/dist/utils/auth-failover.js.map +1 -1
  462. package/dist/utils/auth.js.map +1 -1
  463. package/dist/utils/compaction-model-watchdog.js.map +1 -1
  464. package/dist/utils/consolidation-controls.js.map +1 -1
  465. package/dist/utils/constants.js.map +1 -1
  466. package/dist/utils/date-detector.js.map +1 -1
  467. package/dist/utils/dates.js.map +1 -1
  468. package/dist/utils/decay.js.map +1 -1
  469. package/dist/utils/duration.js.map +1 -1
  470. package/dist/utils/embed-call.js.map +1 -1
  471. package/dist/utils/entity-lookup-resolve.js.map +1 -1
  472. package/dist/utils/entity-mention-quality.js.map +1 -1
  473. package/dist/utils/entity-stopwords.js.map +1 -1
  474. package/dist/utils/env-manager.js.map +1 -1
  475. package/dist/utils/error-tracking.js.map +1 -1
  476. package/dist/utils/event-loop-yield.js.map +1 -1
  477. package/dist/utils/extract-last-user-message.js.map +1 -1
  478. package/dist/utils/extraction-from-template.js.map +1 -1
  479. package/dist/utils/fact-embeddings.js.map +1 -1
  480. package/dist/utils/file-snapshot.js.map +1 -1
  481. package/dist/utils/format.js.map +1 -1
  482. package/dist/utils/fs.js.map +1 -1
  483. package/dist/utils/gh-repo-arg.js.map +1 -1
  484. package/dist/utils/hybrid-mem-json-cli.js.map +1 -1
  485. package/dist/utils/language-keywords.js.map +1 -1
  486. package/dist/utils/lifecycle-generation.js.map +1 -1
  487. package/dist/utils/llm-json-array.js.map +1 -1
  488. package/dist/utils/llm-selection.js.map +1 -1
  489. package/dist/utils/logger.js.map +1 -1
  490. package/dist/utils/model-provider-family.js.map +1 -1
  491. package/dist/utils/model-tier.js.map +1 -1
  492. package/dist/utils/openclaw-agent-defaults.js.map +1 -1
  493. package/dist/utils/path.js.map +1 -1
  494. package/dist/utils/plugin-root.js.map +1 -1
  495. package/dist/utils/plugin-update-check.js.map +1 -1
  496. package/dist/utils/procedure-risk.js.map +1 -1
  497. package/dist/utils/progress-indicators.js.map +1 -1
  498. package/dist/utils/prompt-loader.js.map +1 -1
  499. package/dist/utils/provenance.js.map +1 -1
  500. package/dist/utils/provider-detection.js.map +1 -1
  501. package/dist/utils/registration-superseded.js.map +1 -1
  502. package/dist/utils/salience.js.map +1 -1
  503. package/dist/utils/sanitize-messages.js.map +1 -1
  504. package/dist/utils/scope-filter.js.map +1 -1
  505. package/dist/utils/skill-discovery.js.map +1 -1
  506. package/dist/utils/sqlite-file-perms.js.map +1 -1
  507. package/dist/utils/sqlite-outcome-compat.js.map +1 -1
  508. package/dist/utils/sqlite-transaction.js.map +1 -1
  509. package/dist/utils/stable-stringify.js.map +1 -1
  510. package/dist/utils/subagent-ended-utils.js.map +1 -1
  511. package/dist/utils/tags.js.map +1 -1
  512. package/dist/utils/text.js.map +1 -1
  513. package/dist/utils/timeout.js.map +1 -1
  514. package/dist/utils/typebox.js.map +1 -1
  515. package/dist/utils/version-check.js.map +1 -1
  516. package/dist/utils/wal-replay.js.map +1 -1
  517. package/dist/versionInfo.js.map +1 -1
  518. package/index.ts +2 -2
  519. package/lifecycle/hooks.ts +0 -1
  520. package/npm-shrinkwrap.json +487 -186
  521. package/openclaw.plugin.json +1 -1
  522. package/package.json +2 -2
  523. package/services/adaptive-catch-up-pacing.ts +28 -0
  524. package/services/chat.ts +34 -1
  525. package/services/cron-job-bash-harness.ts +52 -5
  526. package/services/embeddings/shared.ts +5 -2
  527. package/services/entity-enrichment-adaptive.ts +245 -0
  528. package/services/entity-enrichment-cli.ts +553 -47
  529. package/services/entity-enrichment.ts +43 -2
  530. package/services/llm-rate-limit-headers.ts +1 -4
  531. package/services/maintenance-log-analyzer.ts +13 -9
  532. package/services/reinforcement-extract.ts +19 -0
  533. package/setup/hybrid-memory-reload-coordinator.ts +26 -0
  534. package/setup/register-plugin.ts +62 -32
  535. package/setup/reregister-policy.ts +10 -5
@@ -1 +1 @@
1
- {"version":3,"file":"generated-skills.js","names":[],"sources":["../../../backends/facts-db/generated-skills.ts"],"sourcesContent":["import { createHash, randomUUID } from \"node:crypto\";\nimport { statSync } from \"node:fs\";\nimport { basename, isAbsolute, join, win32 } from \"node:path\";\nimport type { DatabaseSync } from \"node:sqlite\";\nimport { determineRiskLevel, parseRecipeOrRaw } from \"../../utils/procedure-risk.js\";\nimport type {\n GeneratedSkillLifecycleState,\n GeneratedSkillTelemetryDecision,\n GeneratedSkillTelemetryEntry,\n GeneratedSkillTelemetryOutcome,\n MemoryScope,\n ProcedureEntry,\n} from \"../../types/memory.js\";\nimport { getEnv } from \"../../utils/env-manager.js\";\nimport {\n DEFAULT_GENERATED_SKILL_LIFECYCLE_POLICY,\n effectiveDemoteThresholdsForRisk,\n} from \"./generated-skills/policy.js\";\nexport {\n DEFAULT_GENERATED_SKILL_LIFECYCLE_POLICY,\n effectiveDemoteThresholdsForRisk,\n} from \"./generated-skills/policy.js\";\nexport type {\n GeneratedSkillLifecyclePolicy,\n GeneratedSkillTelemetryFlags,\n GeneratedSkillTelemetryMetrics,\n GeneratedSkillTelemetryRecordInput,\n GeneratedSkillTelemetryReport,\n GeneratedSkillTelemetryReportRow,\n} from \"./generated-skills/policy.js\";\nimport type {\n GeneratedSkillLifecyclePolicy,\n GeneratedSkillTelemetryFlags,\n GeneratedSkillTelemetryMetrics,\n GeneratedSkillTelemetryRecordInput,\n GeneratedSkillTelemetryReport,\n GeneratedSkillTelemetryReportRow,\n} from \"./generated-skills/policy.js\";\nimport { procedureRowToEntry } from \"./procedures.js\";\n\nconst MAX_REQUEST_SUMMARY_LENGTH = 240;\n\nfunction isAbsoluteSkillPath(skillPath: string): boolean {\n return isAbsolute(skillPath) || win32.isAbsolute(skillPath);\n}\n\nfunction hasWindowsDrivePrefix(skillPath: string): boolean {\n return /^[A-Za-z]:[/\\\\]/u.test(skillPath);\n}\n\nfunction usesWindowsPathSemantics(skillPath: string): boolean {\n return hasWindowsDrivePrefix(skillPath) || skillPath.includes(\"\\\\\");\n}\n\nfunction skillPathBasename(skillPath: string): string {\n return usesWindowsPathSemantics(skillPath) ? win32.basename(skillPath) : basename(skillPath);\n}\n\nfunction resolveSkillMdPath(skillPath: string, workspaceRoot: string): { dirPath: string; skillMdPath: string } {\n const resolvedPath = isAbsoluteSkillPath(skillPath) ? skillPath : join(workspaceRoot, skillPath);\n const usesWindowsSeparators = usesWindowsPathSemantics(resolvedPath);\n const hasSkillMdSuffix = /(?:^|[/\\\\])SKILL\\.md$/u.test(resolvedPath);\n const dirPath = hasSkillMdSuffix ? resolvedPath.replace(/(?:^|[/\\\\])SKILL\\.md$/u, \"\") : resolvedPath;\n const skillMdPath = hasSkillMdSuffix\n ? resolvedPath\n : usesWindowsSeparators\n ? win32.join(dirPath, \"SKILL.md\")\n : join(dirPath, \"SKILL.md\");\n return { dirPath, skillMdPath };\n}\n\ntype GeneratedSkillTelemetryRow = {\n id: string;\n procedure_id: string;\n skill_name: string;\n skill_version: number;\n request_hash: string | null;\n request_summary: string | null;\n decision: GeneratedSkillTelemetryDecision;\n confidence: number | null;\n reason: string | null;\n task_outcome: GeneratedSkillTelemetryOutcome | null;\n user_correction: number;\n correction_reason: string | null;\n false_negative_signal: number;\n caused_rework: number;\n saved_tool_calls: number | null;\n saved_time_ms: number | null;\n scope: MemoryScope | null;\n scope_target: string | null;\n agent_id: string | null;\n session_id: string | null;\n created_at: number;\n};\n\nfunction mapGeneratedSkillTelemetryRow(row: GeneratedSkillTelemetryRow): GeneratedSkillTelemetryEntry {\n return {\n id: row.id,\n procedureId: row.procedure_id,\n skillName: row.skill_name,\n skillVersion: row.skill_version,\n requestHash: row.request_hash,\n requestSummary: row.request_summary,\n decision: row.decision,\n confidence: row.confidence,\n reason: row.reason,\n taskOutcome: row.task_outcome,\n userCorrection: row.user_correction === 1,\n correctionReason: row.correction_reason,\n falseNegativeSignal: row.false_negative_signal === 1,\n causedRework: row.caused_rework === 1,\n savedToolCalls: row.saved_tool_calls,\n savedTimeMs: row.saved_time_ms,\n scope: row.scope,\n scopeTarget: row.scope_target,\n agentId: row.agent_id,\n sessionId: row.session_id,\n createdAt: row.created_at,\n };\n}\n\nfunction normalizeSummary(summary: string | null | undefined): string | null {\n if (typeof summary !== \"string\") return null;\n const normalized = summary.replace(/\\s+/g, \" \").trim();\n return normalized.length > 0 ? normalized.slice(0, MAX_REQUEST_SUMMARY_LENGTH) : null;\n}\n\nfunction requestHashFromInput(\n requestHash: string | null | undefined,\n requestSummary: string | null | undefined,\n): string | null {\n if (typeof requestHash === \"string\" && requestHash.trim().length > 0) return requestHash.trim();\n const normalizedSummary = normalizeSummary(requestSummary);\n if (!normalizedSummary) return null;\n return createHash(\"sha256\").update(normalizedSummary).digest(\"hex\").slice(0, 16);\n}\n\n/** Per-procedure counters maintained incrementally on telemetry insert (#1415 / #1400). */\ntype TelemetryRollupRow = {\n gst_sel_total: number;\n gst_near_miss_total: number;\n gst_fp_signals_total: number;\n gst_fn_signals_total: number;\n gst_user_correction_total: number;\n gst_outcome_success: number;\n gst_outcome_failure: number;\n gst_outcome_partial: number;\n gst_outcome_unknown: number;\n gst_success_clear_total: number;\n gst_considered_total: number;\n gst_skipped_total: number;\n gst_saved_tool_calls_sum: number;\n gst_saved_time_ms_sum: number;\n gst_last_selected_at: number | null;\n};\n\nfunction readTelemetryRollup(db: DatabaseSync, procedureId: string): TelemetryRollupRow | null {\n const row = db\n .prepare(\n `SELECT gst_sel_total, gst_near_miss_total, gst_fp_signals_total, gst_fn_signals_total,\n gst_user_correction_total, gst_outcome_success, gst_outcome_failure, gst_outcome_partial,\n gst_outcome_unknown, gst_success_clear_total, gst_considered_total, gst_skipped_total,\n gst_saved_tool_calls_sum, gst_saved_time_ms_sum, gst_last_selected_at\n FROM procedures WHERE id = ?`,\n )\n .get(procedureId) as Record<string, unknown> | undefined;\n if (!row) return null;\n return {\n gst_sel_total: (row.gst_sel_total as number) ?? 0,\n gst_near_miss_total: (row.gst_near_miss_total as number) ?? 0,\n gst_fp_signals_total: (row.gst_fp_signals_total as number) ?? 0,\n gst_fn_signals_total: (row.gst_fn_signals_total as number) ?? 0,\n gst_user_correction_total: (row.gst_user_correction_total as number) ?? 0,\n gst_outcome_success: (row.gst_outcome_success as number) ?? 0,\n gst_outcome_failure: (row.gst_outcome_failure as number) ?? 0,\n gst_outcome_partial: (row.gst_outcome_partial as number) ?? 0,\n gst_outcome_unknown: (row.gst_outcome_unknown as number) ?? 0,\n gst_success_clear_total: (row.gst_success_clear_total as number) ?? 0,\n gst_considered_total: (row.gst_considered_total as number) ?? 0,\n gst_skipped_total: (row.gst_skipped_total as number) ?? 0,\n gst_saved_tool_calls_sum: (row.gst_saved_tool_calls_sum as number) ?? 0,\n gst_saved_time_ms_sum: (row.gst_saved_time_ms_sum as number) ?? 0,\n gst_last_selected_at: (row.gst_last_selected_at as number | null) ?? null,\n };\n}\n\n/** Recompute rollup columns from `generated_skill_telemetry` (e.g. after correction edits). */\nexport function rebuildGeneratedSkillTelemetryRollupsForProcedure(db: DatabaseSync, procedureId: string): void {\n db.prepare(\n `UPDATE procedures AS p\n SET gst_sel_total = (SELECT COUNT(*) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id AND t.decision = 'selected'),\n gst_near_miss_total = (SELECT COUNT(*) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id AND t.decision != 'selected'),\n gst_fp_signals_total = (SELECT COUNT(*) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id AND (t.user_correction = 1 OR t.caused_rework = 1)),\n gst_fn_signals_total = (SELECT COUNT(*) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id AND t.false_negative_signal = 1),\n gst_user_correction_total = (SELECT COUNT(*) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id AND t.user_correction = 1),\n gst_outcome_success = (SELECT COUNT(*) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id AND t.decision = 'selected' AND t.task_outcome = 'success'),\n gst_outcome_failure = (SELECT COUNT(*) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id AND t.decision = 'selected' AND t.task_outcome = 'failure'),\n gst_outcome_partial = (SELECT COUNT(*) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id AND t.decision = 'selected' AND t.task_outcome = 'partial'),\n gst_outcome_unknown = (SELECT COUNT(*) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id AND t.decision = 'selected' AND (t.task_outcome IS NULL OR t.task_outcome = '' OR t.task_outcome NOT IN ('success','failure','partial'))),\n gst_success_clear_total = (SELECT COUNT(*) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id AND t.decision = 'selected' AND t.task_outcome = 'success' AND t.user_correction = 0),\n gst_considered_total = (SELECT COUNT(*) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id AND t.decision = 'considered'),\n gst_skipped_total = (SELECT COUNT(*) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id AND t.decision = 'skipped'),\n gst_saved_tool_calls_sum = (SELECT COALESCE(SUM(CASE WHEN t.saved_tool_calls > 0 THEN t.saved_tool_calls ELSE 0 END), 0) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id),\n gst_saved_time_ms_sum = (SELECT COALESCE(SUM(CASE WHEN t.saved_time_ms > 0 THEN t.saved_time_ms ELSE 0 END), 0) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id),\n gst_last_selected_at = (SELECT MAX(t.created_at) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id AND t.decision = 'selected')\n WHERE p.id = ?`,\n ).run(procedureId);\n}\n\nfunction applyTelemetryRollupDelta(\n db: DatabaseSync,\n procedureId: string,\n input: GeneratedSkillTelemetryRecordInput,\n savedCalls: number,\n savedTimeMs: number,\n nowSec: number,\n): void {\n let dSel = 0;\n let dNear = 0;\n let dFp = 0;\n let dFn = 0;\n let dUserCorr = 0;\n let dSucc = 0;\n let dFail = 0;\n let dPart = 0;\n let dUnk = 0;\n let dClear = 0;\n let dConsidered = 0;\n let dSkipped = 0;\n if (input.userCorrection === true || input.causedRework === true) dFp = 1;\n if (input.decision === \"selected\") {\n dSel = 1;\n const outcome = input.taskOutcome;\n if (outcome === \"success\") {\n dSucc = 1;\n if (input.userCorrection !== true) dClear = 1;\n } else if (outcome === \"failure\") {\n dFail = 1;\n } else if (outcome === \"partial\") {\n dPart = 1;\n } else {\n dUnk = 1;\n }\n } else {\n dNear = 1;\n if (input.decision === \"considered\") dConsidered = 1;\n if (input.decision === \"skipped\") dSkipped = 1;\n }\n if (input.falseNegativeSignal === true) dFn = 1;\n if (input.userCorrection === true) dUserCorr = 1;\n const positiveSavedCalls = savedCalls > 0 ? savedCalls : 0;\n const positiveSavedTimeMs = savedTimeMs > 0 ? savedTimeMs : 0;\n const lastSelectedAt = input.decision === \"selected\" ? nowSec : 0;\n db.prepare(\n `UPDATE procedures SET\n gst_sel_total = gst_sel_total + ?,\n gst_near_miss_total = gst_near_miss_total + ?,\n gst_fp_signals_total = gst_fp_signals_total + ?,\n gst_fn_signals_total = gst_fn_signals_total + ?,\n gst_user_correction_total = gst_user_correction_total + ?,\n gst_outcome_success = gst_outcome_success + ?,\n gst_outcome_failure = gst_outcome_failure + ?,\n gst_outcome_partial = gst_outcome_partial + ?,\n gst_outcome_unknown = gst_outcome_unknown + ?,\n gst_success_clear_total = gst_success_clear_total + ?,\n gst_considered_total = gst_considered_total + ?,\n gst_skipped_total = gst_skipped_total + ?,\n gst_saved_tool_calls_sum = gst_saved_tool_calls_sum + ?,\n gst_saved_time_ms_sum = gst_saved_time_ms_sum + ?,\n gst_last_selected_at = CASE WHEN ? > COALESCE(gst_last_selected_at, 0) THEN ? ELSE gst_last_selected_at END\n WHERE id = ?`,\n ).run(\n dSel,\n dNear,\n dFp,\n dFn,\n dUserCorr,\n dSucc,\n dFail,\n dPart,\n dUnk,\n dClear,\n dConsidered,\n dSkipped,\n positiveSavedCalls,\n positiveSavedTimeMs,\n lastSelectedAt,\n lastSelectedAt,\n procedureId,\n );\n}\n\nfunction countSelectedActivationsSince(db: DatabaseSync, procedureId: string, sinceSec: number): number {\n const row = db\n .prepare(\n `SELECT COUNT(*) as c FROM generated_skill_telemetry\n WHERE procedure_id = ? AND decision = 'selected' AND created_at >= ?`,\n )\n .get(procedureId, sinceSec) as { c: number };\n return row?.c ?? 0;\n}\n\nfunction generatedSkillRows(db: DatabaseSync): Array<Record<string, unknown>> {\n return db\n .prepare(\n `SELECT *\n FROM procedures\n WHERE promoted_to_skill = 1\n AND skill_path IS NOT NULL\n AND TRIM(skill_path) <> ''\n ORDER BY COALESCE(skill_generated_at, promoted_at, updated_at, created_at) DESC, task_pattern ASC`,\n )\n .all() as Array<Record<string, unknown>>;\n}\n\nfunction findGeneratedSkillProcedureMatches(db: DatabaseSync, skillName: string): ProcedureEntry[] {\n const trimmed = skillName.trim();\n if (trimmed.length === 0) return [];\n const matches: ProcedureEntry[] = [];\n for (const row of generatedSkillRows(db)) {\n const proc = procedureRowToEntry(db, row);\n if (!proc.skillPath?.trim()) continue;\n const path = proc.skillPath.trim();\n if (path === trimmed || skillPathBasename(path) === trimmed) matches.push(proc);\n }\n return matches;\n}\n\nfunction findGeneratedSkillProcedure(db: DatabaseSync, skillName: string): ProcedureEntry | null {\n return findGeneratedSkillProcedureMatches(db, skillName)[0] ?? null;\n}\n\nfunction findGeneratedSkillProcedureStrict(db: DatabaseSync, skillName: string): ProcedureEntry | null {\n const trimmed = skillName.trim();\n const matches = findGeneratedSkillProcedureMatches(db, trimmed);\n if (matches.length === 0) return null;\n if (matches.length > 1) {\n throw new Error(\n `Ambiguous skill name \"${trimmed}\": multiple promoted procedures match; pass procedureId to disambiguate`,\n );\n }\n return matches[0] ?? null;\n}\n\nfunction findProcedureById(db: DatabaseSync, procedureId: string): ProcedureEntry | null {\n const row = db.prepare(\"SELECT * FROM procedures WHERE id = ? LIMIT 1\").get(procedureId) as\n | Record<string, unknown>\n | undefined;\n return row ? procedureRowToEntry(db, row) : null;\n}\n\nexport function listGeneratedSkillProcedures(db: DatabaseSync): ProcedureEntry[] {\n return generatedSkillRows(db).map((row) => procedureRowToEntry(db, row));\n}\n\nexport function listGeneratedSkillTelemetry(\n db: DatabaseSync,\n skillName?: string,\n limit = 50,\n): GeneratedSkillTelemetryEntry[] {\n const rows = skillName\n ? (db\n .prepare(\n `SELECT *\n FROM generated_skill_telemetry\n WHERE skill_name = ?\n ORDER BY created_at DESC\n LIMIT ?`,\n )\n .all(skillName, limit) as GeneratedSkillTelemetryRow[])\n : (db\n .prepare(\n `SELECT *\n FROM generated_skill_telemetry\n ORDER BY created_at DESC\n LIMIT ?`,\n )\n .all(limit) as GeneratedSkillTelemetryRow[]);\n return rows.map(mapGeneratedSkillTelemetryRow);\n}\n\nexport function recordGeneratedSkillTelemetry(\n db: DatabaseSync,\n input: GeneratedSkillTelemetryRecordInput,\n policy: GeneratedSkillLifecyclePolicy = DEFAULT_GENERATED_SKILL_LIFECYCLE_POLICY,\n): GeneratedSkillTelemetryEntry {\n let proc: ProcedureEntry | null = null;\n if (input.procedureId != null) {\n const candidate = findProcedureById(db, input.procedureId);\n if (!candidate) throw new Error(`Procedure not found for telemetry: ${input.procedureId}`);\n if (candidate.promotedToSkill !== 1 || !candidate.skillPath?.trim()) {\n throw new Error(`Procedure ${input.procedureId} is not a promoted generated skill`);\n }\n const skill = input.skillName.trim();\n const path = candidate.skillPath.trim();\n if (path !== skill && skillPathBasename(path) !== skill) {\n throw new Error(`Procedure ${input.procedureId} skill_path does not match skill name \"${input.skillName}\"`);\n }\n proc = candidate;\n } else {\n proc = findGeneratedSkillProcedureStrict(db, input.skillName);\n }\n if (!proc) throw new Error(`Generated skill not found: ${input.skillName}`);\n if (input.userCorrection === true) {\n const normalizedCorrectionReason = normalizeSummary(input.correctionReason);\n if (!normalizedCorrectionReason) {\n throw new Error(\"user correction telemetry requires a non-empty correctionReason\");\n }\n }\n const now = input.createdAt ?? Math.floor(Date.now() / 1000);\n const normalizedSummary = normalizeSummary(input.requestSummary);\n const id = randomUUID();\n db.prepare(\n `INSERT INTO generated_skill_telemetry (\n id, procedure_id, skill_name, skill_version, request_hash, request_summary, decision, confidence, reason,\n task_outcome, user_correction, correction_reason, false_negative_signal, caused_rework, saved_tool_calls,\n saved_time_ms, scope, scope_target, agent_id, session_id, created_at\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n ).run(\n id,\n proc.id,\n proc.skillPath ? skillPathBasename(proc.skillPath) : input.skillName,\n input.skillVersion ?? proc.skillVersion ?? 1,\n requestHashFromInput(input.requestHash, normalizedSummary),\n normalizedSummary,\n input.decision,\n input.confidence ?? null,\n normalizeSummary(input.reason),\n input.taskOutcome ?? null,\n input.userCorrection === true ? 1 : 0,\n normalizeSummary(input.correctionReason),\n input.falseNegativeSignal === true ? 1 : 0,\n input.causedRework === true ? 1 : 0,\n input.savedToolCalls ?? null,\n input.savedTimeMs ?? null,\n input.scope ?? null,\n input.scopeTarget ?? null,\n input.agentId ?? null,\n input.sessionId ?? null,\n now,\n );\n applyTelemetryRollupDelta(db, proc.id, input, input.savedToolCalls ?? 0, input.savedTimeMs ?? 0, now);\n refreshGeneratedSkillLifecycleState(\n db,\n proc.skillPath ? skillPathBasename(proc.skillPath) : input.skillName,\n policy,\n now,\n proc.id,\n );\n return mapGeneratedSkillTelemetryRow(\n db.prepare(\"SELECT * FROM generated_skill_telemetry WHERE id = ?\").get(id) as GeneratedSkillTelemetryRow,\n );\n}\n\nconst REPEATED_CORRECTION_WINDOW_SEC = 30 * 24 * 60 * 60;\n\nfunction countRepeatedUserCorrectionBuckets(activations: GeneratedSkillTelemetryEntry[], now: number): number {\n const windowStart = now - REPEATED_CORRECTION_WINDOW_SEC;\n const byHash = new Map<string, number>();\n for (const activation of activations) {\n if (!activation.userCorrection || activation.createdAt < windowStart) continue;\n const hash = activation.requestHash;\n if (typeof hash !== \"string\" || hash.length === 0) continue;\n byHash.set(hash, (byHash.get(hash) ?? 0) + 1);\n }\n let repeatedBuckets = 0;\n for (const count of byHash.values()) {\n if (count >= 2) repeatedBuckets++;\n }\n return repeatedBuckets;\n}\n\nexport function markGeneratedSkillTelemetryFalsePositive(\n db: DatabaseSync,\n activationId: string,\n correctionReason: string,\n policy: GeneratedSkillLifecyclePolicy = DEFAULT_GENERATED_SKILL_LIFECYCLE_POLICY,\n): GeneratedSkillTelemetryEntry | null {\n const row = db.prepare(\"SELECT * FROM generated_skill_telemetry WHERE id = ?\").get(activationId) as\n | GeneratedSkillTelemetryRow\n | undefined;\n if (!row) return null;\n db.prepare(\n `UPDATE generated_skill_telemetry\n SET user_correction = 1,\n correction_reason = COALESCE(?, correction_reason)\n WHERE id = ?`,\n ).run(normalizeSummary(correctionReason), activationId);\n rebuildGeneratedSkillTelemetryRollupsForProcedure(db, row.procedure_id);\n refreshGeneratedSkillLifecycleState(db, row.skill_name, policy, undefined, row.procedure_id);\n return mapGeneratedSkillTelemetryRow(\n db.prepare(\"SELECT * FROM generated_skill_telemetry WHERE id = ?\").get(activationId) as GeneratedSkillTelemetryRow,\n );\n}\n\nexport function setGeneratedSkillLifecycleState(\n db: DatabaseSync,\n skillName: string,\n state: GeneratedSkillLifecycleState,\n reason: string | null,\n at = Math.floor(Date.now() / 1000),\n procedureId?: string,\n): ProcedureEntry | null {\n let proc: ProcedureEntry | null = null;\n if (procedureId != null) {\n proc = findProcedureById(db, procedureId);\n if (proc && (proc.promotedToSkill !== 1 || !proc.skillPath?.trim())) {\n return null;\n }\n } else {\n proc = findGeneratedSkillProcedure(db, skillName);\n }\n if (!proc) return null;\n if (state === \"experimental\") {\n // When a skill is reset/unblocked back to experimental, update skill_generated_at to mark\n // the new evaluation window start. This prevents pre-reset telemetry from counting against\n // the skill's overTriggering and promotionCandidate evaluations.\n db.prepare(\n `UPDATE procedures\n SET skill_state = ?,\n skill_state_reason = ?,\n skill_generated_at = ?,\n skill_state_changed_at = ?,\n updated_at = ?\n WHERE id = ?`,\n ).run(state, normalizeSummary(reason), at, at, at, proc.id);\n } else {\n db.prepare(\n `UPDATE procedures\n SET skill_state = ?,\n skill_state_reason = ?,\n skill_state_changed_at = ?,\n updated_at = ?\n WHERE id = ?`,\n ).run(state, normalizeSummary(reason), at, at, proc.id);\n }\n return getGeneratedSkillByName(db, skillName, procedureId);\n}\n\nexport function getGeneratedSkillByName(\n db: DatabaseSync,\n skillName: string,\n procedureId?: string,\n): ProcedureEntry | null {\n if (procedureId != null) {\n const proc = findProcedureById(db, procedureId);\n if (proc && (proc.promotedToSkill !== 1 || !proc.skillPath?.trim())) {\n return null;\n }\n return proc;\n }\n return findGeneratedSkillProcedure(db, skillName);\n}\n\nfunction skillTelemetryEntries(\n db: DatabaseSync,\n skillName: string,\n procedureId?: string,\n): GeneratedSkillTelemetryEntry[] {\n const query = procedureId\n ? `SELECT *\n FROM generated_skill_telemetry\n WHERE skill_name = ? AND procedure_id = ?\n ORDER BY created_at DESC`\n : `SELECT *\n FROM generated_skill_telemetry\n WHERE skill_name = ?\n ORDER BY created_at DESC`;\n const params = procedureId ? [skillName, procedureId] : [skillName];\n return (db.prepare(query).all(...params) as GeneratedSkillTelemetryRow[]).map(mapGeneratedSkillTelemetryRow);\n}\n\nfunction skillTelemetryRecentEntries(\n db: DatabaseSync,\n skillName: string,\n limit: number,\n procedureId?: string,\n): GeneratedSkillTelemetryEntry[] {\n const query = procedureId\n ? `SELECT id, procedure_id, skill_name, skill_version, request_hash, request_summary, decision, confidence,\n reason, task_outcome, user_correction, correction_reason, false_negative_signal, caused_rework,\n saved_tool_calls, saved_time_ms, scope, scope_target, agent_id, session_id, created_at\n FROM generated_skill_telemetry\n WHERE skill_name = ? AND procedure_id = ?\n ORDER BY created_at DESC\n LIMIT ?`\n : `SELECT id, procedure_id, skill_name, skill_version, request_hash, request_summary, decision, confidence,\n reason, task_outcome, user_correction, correction_reason, false_negative_signal, caused_rework,\n saved_tool_calls, saved_time_ms, scope, scope_target, agent_id, session_id, created_at\n FROM generated_skill_telemetry\n WHERE skill_name = ?\n ORDER BY created_at DESC\n LIMIT ?`;\n const params = procedureId ? [skillName, procedureId, limit] : [skillName, limit];\n return (db.prepare(query).all(...params) as GeneratedSkillTelemetryRow[]).map(mapGeneratedSkillTelemetryRow);\n}\n\ntype RawTelemetryCounts = {\n activationCountPerWeek: number;\n activationCountTotal: number;\n nearMissCount: number;\n falsePositiveSignals: number;\n falseNegativeSignals: number;\n correctionCount: number;\n repeatedCorrectionCount: number;\n lastUsedAt: number | null;\n successCount: number;\n failureCount: number;\n partialCount: number;\n unknownCount: number;\n successfulUsesWithoutCorrection: number;\n consideredCount: number;\n skippedCount: number;\n savedToolCalls: number;\n savedTimeMs: number;\n cleanUsesAfterDemotion: number;\n};\n\nfunction deriveMetricsAndFlags(\n counts: RawTelemetryCounts,\n proc: ProcedureEntry,\n policy: GeneratedSkillLifecyclePolicy,\n now: number,\n): { metrics: GeneratedSkillTelemetryMetrics; flags: GeneratedSkillTelemetryFlags } {\n const knownOutcomeTotal = counts.successCount + counts.failureCount + counts.partialCount + counts.unknownCount;\n const successRate = knownOutcomeTotal > 0 ? counts.successCount / knownOutcomeTotal : null;\n const failureRate = knownOutcomeTotal > 0 ? counts.failureCount / knownOutcomeTotal : null;\n const partialRate = knownOutcomeTotal > 0 ? counts.partialCount / knownOutcomeTotal : null;\n const unknownRate = knownOutcomeTotal > 0 ? counts.unknownCount / knownOutcomeTotal : null;\n const exposureTotal = counts.activationCountTotal + counts.nearMissCount;\n const falsePositiveRate = exposureTotal > 0 ? counts.falsePositiveSignals / exposureTotal : null;\n const riskLevel = determineRiskLevel(proc, parseRecipeOrRaw(proc.recipeJson));\n const { falsePositiveRate: demoteFpRate, minSamples: demoteMinSamples } = effectiveDemoteThresholdsForRisk(\n riskLevel,\n policy,\n );\n const generatedAt = proc.skillGeneratedAt ?? proc.promotedAt ?? proc.updatedAt ?? proc.createdAt ?? now;\n const archiveBaselineAt = proc.skillState === \"demoted\" ? (proc.skillStateChangedAt ?? generatedAt) : generatedAt;\n const archiveCandidate =\n counts.activationCountTotal === 0 && archiveBaselineAt <= now - policy.archiveAfterUnusedDays * 24 * 60 * 60;\n const promotionCandidate =\n proc.skillState !== \"demoted\" &&\n proc.skillState !== \"archived\" &&\n proc.skillState !== \"trusted\" &&\n proc.skillState !== \"uninstalled\" &&\n proc.skillState !== \"rejected\" &&\n counts.successfulUsesWithoutCorrection >= policy.promoteAfterSuccessfulUses;\n const overTriggering =\n exposureTotal >= demoteMinSamples && falsePositiveRate != null && falsePositiveRate >= demoteFpRate;\n const revisionCandidate =\n counts.nearMissCount >= policy.revisionNearMissThreshold && counts.skippedCount >= counts.consideredCount;\n const unblockAfterCleanUses = policy.unblockAfterCleanUses ?? 0;\n const unblockCandidate =\n unblockAfterCleanUses > 0 &&\n (proc.skillState === \"demoted\" || proc.skillState === \"archived\") &&\n counts.cleanUsesAfterDemotion >= unblockAfterCleanUses;\n\n return {\n metrics: {\n activationCountPerWeek: counts.activationCountPerWeek,\n activationCountTotal: counts.activationCountTotal,\n nearMissCount: counts.nearMissCount,\n falsePositiveSignals: counts.falsePositiveSignals,\n falseNegativeSignals: counts.falseNegativeSignals,\n correctionCount: counts.correctionCount,\n repeatedCorrectionCount: counts.repeatedCorrectionCount,\n lastUsedAt: counts.lastUsedAt,\n successCount: counts.successCount,\n failureCount: counts.failureCount,\n partialCount: counts.partialCount,\n unknownCount: counts.unknownCount,\n successRate,\n failureRate,\n partialRate,\n unknownRate,\n successfulUsesWithoutCorrection: counts.successfulUsesWithoutCorrection,\n consideredCount: counts.consideredCount,\n skippedCount: counts.skippedCount,\n savedToolCalls: counts.savedToolCalls,\n savedTimeMs: counts.savedTimeMs,\n falsePositiveRate,\n cleanUsesAfterDemotion: counts.cleanUsesAfterDemotion,\n },\n flags: {\n promotionCandidate,\n overTriggering,\n revisionCandidate,\n neverUsed: counts.activationCountTotal === 0,\n archiveCandidate,\n unblockCandidate,\n },\n };\n}\n\nfunction summarizeSkillTelemetry(\n proc: ProcedureEntry,\n activations: GeneratedSkillTelemetryEntry[],\n policy: GeneratedSkillLifecyclePolicy,\n now: number,\n): {\n metrics: GeneratedSkillTelemetryMetrics;\n flags: GeneratedSkillTelemetryFlags;\n riskLevel: \"low\" | \"medium\" | \"high\";\n} {\n const weekAgo = now - 7 * 24 * 60 * 60;\n\n // For experimental and trusted skills, evaluate only activations since skill_generated_at.\n // For demoted/archived skills, evaluate only activations since skill_state_changed_at.\n // This prevents pre-reset telemetry from blocking promotion or re-triggering demotion\n // after an operator manually resets or the system auto-unblocks a skill.\n const isExperimentalOrTrusted =\n proc.skillState === \"experimental\" || proc.skillState === \"trusted\" || proc.skillState === \"draft\";\n const isDemotedOrArchived = proc.skillState === \"demoted\" || proc.skillState === \"archived\";\n let evalWindowStart = 0;\n if (isExperimentalOrTrusted && proc.skillGeneratedAt !== null && proc.skillGeneratedAt !== undefined) {\n evalWindowStart = proc.skillGeneratedAt;\n } else if (isDemotedOrArchived && proc.skillStateChangedAt !== null && proc.skillStateChangedAt !== undefined) {\n evalWindowStart = proc.skillStateChangedAt;\n }\n const evalActivations = evalWindowStart > 0 ? activations.filter((a) => a.createdAt >= evalWindowStart) : activations;\n\n let activationCountPerWeek = 0;\n let activationCountTotal = 0;\n let nearMissCount = 0;\n let falsePositiveSignals = 0;\n let falseNegativeSignals = 0;\n let correctionCount = 0;\n let lastUsedAt: number | null = null;\n let successCount = 0;\n let failureCount = 0;\n let partialCount = 0;\n let unknownCount = 0;\n let successfulUsesWithoutCorrection = 0;\n let consideredCount = 0;\n let skippedCount = 0;\n let savedToolCalls = 0;\n let savedTimeMs = 0;\n\n // For unblock tracking: count clean (uncorrected) successful activations since the\n // most recent demotion/archive state change. Use skill_state_changed_at as the\n // reset baseline — it tracks state transitions exclusively and is not affected by\n // other procedure updates (e.g., upsertProcedure).\n const demotedOrArchivedAt =\n proc.skillState === \"demoted\" || proc.skillState === \"archived\"\n ? (proc.skillStateChangedAt ?? proc.updatedAt ?? 0)\n : null;\n let cleanUsesAfterDemotion = 0;\n\n for (const activation of evalActivations) {\n const calls = activation.savedToolCalls ?? 0;\n const timeMs = activation.savedTimeMs ?? 0;\n savedToolCalls += calls > 0 ? calls : 0;\n savedTimeMs += timeMs > 0 ? timeMs : 0;\n if (activation.userCorrection || activation.causedRework) falsePositiveSignals++;\n if (activation.userCorrection) correctionCount++;\n if (activation.decision === \"selected\") {\n activationCountTotal++;\n if (activation.createdAt >= weekAgo) activationCountPerWeek++;\n lastUsedAt = lastUsedAt == null ? activation.createdAt : Math.max(lastUsedAt, activation.createdAt);\n if (activation.taskOutcome === \"success\") {\n successCount++;\n if (!activation.userCorrection) {\n successfulUsesWithoutCorrection++;\n // Count clean uses recorded after a demotion reset\n if (demotedOrArchivedAt != null && activation.createdAt > demotedOrArchivedAt) {\n cleanUsesAfterDemotion++;\n }\n }\n } else if (activation.taskOutcome === \"failure\") {\n failureCount++;\n } else if (activation.taskOutcome === \"partial\") {\n partialCount++;\n } else {\n unknownCount++;\n }\n } else {\n nearMissCount++;\n if (activation.decision === \"considered\") consideredCount++;\n if (activation.decision === \"skipped\") skippedCount++;\n }\n if (activation.falseNegativeSignal) falseNegativeSignals++;\n }\n const repeatedCorrectionCount = countRepeatedUserCorrectionBuckets(evalActivations, now);\n\n const knownOutcomeTotal = successCount + failureCount + partialCount + unknownCount;\n const successRate = knownOutcomeTotal > 0 ? successCount / knownOutcomeTotal : null;\n const failureRate = knownOutcomeTotal > 0 ? failureCount / knownOutcomeTotal : null;\n const partialRate = knownOutcomeTotal > 0 ? partialCount / knownOutcomeTotal : null;\n const unknownRate = knownOutcomeTotal > 0 ? unknownCount / knownOutcomeTotal : null;\n const exposureTotal = activationCountTotal + nearMissCount;\n const falsePositiveRate = exposureTotal > 0 ? falsePositiveSignals / exposureTotal : null;\n const riskLevel = determineRiskLevel(proc, parseRecipeOrRaw(proc.recipeJson));\n const { falsePositiveRate: demoteFpRate, minSamples: demoteMinSamples } = effectiveDemoteThresholdsForRisk(\n riskLevel,\n policy,\n );\n const generatedAt = proc.skillGeneratedAt ?? proc.promotedAt ?? proc.updatedAt ?? proc.createdAt ?? now;\n const archiveBaselineAt = proc.skillState === \"demoted\" ? (proc.skillStateChangedAt ?? generatedAt) : generatedAt;\n const lastActivityAt = lastUsedAt ?? archiveBaselineAt;\n const promotionCandidate =\n proc.skillState !== \"demoted\" &&\n proc.skillState !== \"archived\" &&\n proc.skillState !== \"trusted\" &&\n proc.skillState !== \"uninstalled\" &&\n proc.skillState !== \"rejected\" &&\n successfulUsesWithoutCorrection >= policy.promoteAfterSuccessfulUses;\n const unblockAfterCleanUses = policy.unblockAfterCleanUses ?? 0;\n const unblockCandidate =\n unblockAfterCleanUses > 0 &&\n (proc.skillState === \"demoted\" || proc.skillState === \"archived\") &&\n cleanUsesAfterDemotion >= unblockAfterCleanUses;\n const archiveCandidate =\n !promotionCandidate && !unblockCandidate && now - lastActivityAt >= policy.archiveAfterUnusedDays * 24 * 60 * 60;\n const overTriggering =\n exposureTotal >= demoteMinSamples && falsePositiveRate != null && falsePositiveRate >= demoteFpRate;\n const revisionCandidate = nearMissCount >= policy.revisionNearMissThreshold && skippedCount >= consideredCount;\n\n return {\n metrics: {\n activationCountPerWeek,\n activationCountTotal,\n nearMissCount,\n falsePositiveSignals,\n falseNegativeSignals,\n correctionCount,\n repeatedCorrectionCount,\n lastUsedAt,\n successCount,\n failureCount,\n partialCount,\n unknownCount,\n successRate,\n failureRate,\n partialRate,\n unknownRate,\n successfulUsesWithoutCorrection,\n consideredCount,\n skippedCount,\n savedToolCalls,\n savedTimeMs,\n falsePositiveRate,\n cleanUsesAfterDemotion,\n },\n flags: {\n promotionCandidate,\n overTriggering,\n revisionCandidate,\n neverUsed: activationCountTotal === 0,\n archiveCandidate,\n unblockCandidate,\n },\n riskLevel,\n };\n}\n\nfunction _summarizeSkillTelemetryFromRollups(\n db: DatabaseSync,\n proc: ProcedureEntry,\n canonicalSkillName: string,\n policy: GeneratedSkillLifecyclePolicy,\n now: number,\n): {\n metrics: GeneratedSkillTelemetryMetrics;\n flags: GeneratedSkillTelemetryFlags;\n riskLevel: \"low\" | \"medium\" | \"high\";\n} {\n const r = readTelemetryRollup(db, proc.id);\n if (!r) {\n return summarizeSkillTelemetry(proc, skillTelemetryEntries(db, canonicalSkillName, proc.id), policy, now);\n }\n const weekAgo = now - 7 * 24 * 60 * 60;\n const activationCountPerWeek = countSelectedActivationsSince(db, proc.id, weekAgo);\n const activationCountTotal = r.gst_sel_total;\n const nearMissCount = r.gst_near_miss_total;\n const falsePositiveSignals = r.gst_fp_signals_total;\n const falseNegativeSignals = r.gst_fn_signals_total;\n const correctionCount = r.gst_user_correction_total;\n const lastUsedAt = r.gst_last_selected_at;\n const successCount = r.gst_outcome_success;\n const failureCount = r.gst_outcome_failure;\n const partialCount = r.gst_outcome_partial;\n const unknownCount = r.gst_outcome_unknown;\n const successfulUsesWithoutCorrection = r.gst_success_clear_total;\n const consideredCount = r.gst_considered_total;\n const skippedCount = r.gst_skipped_total;\n const savedToolCalls = r.gst_saved_tool_calls_sum;\n const savedTimeMs = r.gst_saved_time_ms_sum;\n const repeatedCorrectionCount = countRepeatedUserCorrectionBuckets(\n skillTelemetryEntries(db, canonicalSkillName, proc.id),\n now,\n );\n const demotedOrArchivedAt =\n proc.skillState === \"demoted\" || proc.skillState === \"archived\"\n ? (proc.skillStateChangedAt ?? proc.updatedAt ?? 0)\n : null;\n let cleanUsesAfterDemotion = 0;\n if (demotedOrArchivedAt != null) {\n const postDemotionActivations = skillTelemetryEntries(db, canonicalSkillName, proc.id).filter(\n (a) =>\n a.decision === \"selected\" &&\n a.taskOutcome === \"success\" &&\n !a.userCorrection &&\n a.createdAt > demotedOrArchivedAt,\n );\n cleanUsesAfterDemotion = postDemotionActivations.length;\n }\n const riskLevel = determineRiskLevel(proc, parseRecipeOrRaw(proc.recipeJson));\n const summary = deriveMetricsAndFlags(\n {\n activationCountPerWeek,\n activationCountTotal,\n nearMissCount,\n falsePositiveSignals,\n falseNegativeSignals,\n correctionCount,\n repeatedCorrectionCount,\n lastUsedAt,\n successCount,\n failureCount,\n partialCount,\n unknownCount,\n successfulUsesWithoutCorrection,\n consideredCount,\n skippedCount,\n savedToolCalls,\n savedTimeMs,\n cleanUsesAfterDemotion,\n },\n proc,\n policy,\n now,\n );\n return { ...summary, riskLevel };\n}\n\nfunction desiredLifecycleTransition(\n currentState: GeneratedSkillLifecycleState,\n flags: GeneratedSkillTelemetryFlags,\n metrics: GeneratedSkillTelemetryMetrics,\n policy: GeneratedSkillLifecyclePolicy,\n riskLevel: \"low\" | \"medium\" | \"high\",\n): { state: GeneratedSkillLifecycleState; reason: string } | null {\n // Uninstalled and rejected are terminal — no automatic transitions out.\n if (currentState === \"uninstalled\" || currentState === \"rejected\") return null;\n\n if (currentState !== \"archived\" && flags.archiveCandidate) {\n return {\n state: \"archived\",\n reason: `auto-archived after ${policy.archiveAfterUnusedDays} days without selected-activation activity`,\n };\n }\n // Allow automatic unblocking of demoted/archived skills when they accumulate\n // enough clean uses after the demotion reset AND are not currently overtriggering.\n // Evaluate this before the overTriggering check so archived skills with high historical\n // FP rates can transition directly to experimental rather than demoted, but only if\n // the current post-demotion false-positive rate is acceptable.\n if (flags.unblockCandidate && !flags.overTriggering) {\n return {\n state: \"experimental\",\n reason: `auto-unblocked after ${metrics.cleanUsesAfterDemotion} clean activations since demotion`,\n };\n }\n if (currentState !== \"demoted\" && currentState !== \"archived\" && flags.overTriggering) {\n const { falsePositiveRate: demoteFpRate, minSamples: demoteMinSamples } = effectiveDemoteThresholdsForRisk(\n riskLevel,\n policy,\n );\n return {\n state: \"demoted\",\n reason: `auto-demoted (${riskLevel} risk): FP rate ${Math.round((metrics.falsePositiveRate ?? 0) * 100)}% reached threshold ${Math.round(demoteFpRate * 100)}% over ${demoteMinSamples}+ activations`,\n };\n }\n if (currentState === \"experimental\" && flags.promotionCandidate) {\n return {\n state: \"trusted\",\n reason: `auto-promoted after ${metrics.successfulUsesWithoutCorrection} successful activations without correction`,\n };\n }\n return null;\n}\n\nexport function refreshGeneratedSkillLifecycleState(\n db: DatabaseSync,\n skillName: string,\n policy?: GeneratedSkillLifecyclePolicy,\n now?: number,\n procedureId?: string,\n): ProcedureEntry | null;\nexport function refreshGeneratedSkillLifecycleState(\n db: DatabaseSync,\n skillName: string,\n policy: GeneratedSkillLifecyclePolicy,\n now: number,\n returnMetrics: true,\n procedureId?: string,\n): {\n proc: ProcedureEntry;\n metrics: GeneratedSkillTelemetryMetrics;\n flags: GeneratedSkillTelemetryFlags;\n riskLevel: \"low\" | \"medium\" | \"high\";\n} | null;\nexport function refreshGeneratedSkillLifecycleState(\n db: DatabaseSync,\n skillName: string,\n policy: GeneratedSkillLifecyclePolicy = DEFAULT_GENERATED_SKILL_LIFECYCLE_POLICY,\n now = Math.floor(Date.now() / 1000),\n returnMetricsOrProcedureId: boolean | string = false,\n procedureId?: string,\n):\n | ProcedureEntry\n | {\n proc: ProcedureEntry;\n metrics: GeneratedSkillTelemetryMetrics;\n flags: GeneratedSkillTelemetryFlags;\n riskLevel: \"low\" | \"medium\" | \"high\";\n }\n | null {\n const returnMetrics = returnMetricsOrProcedureId === true;\n const resolvedProcedureId = typeof returnMetricsOrProcedureId === \"string\" ? returnMetricsOrProcedureId : procedureId;\n const proc =\n resolvedProcedureId != null\n ? findProcedureById(db, resolvedProcedureId)\n : findGeneratedSkillProcedure(db, skillName);\n if (!proc?.skillPath) return null;\n const canonicalSkillName = skillPathBasename(proc.skillPath);\n const activations = skillTelemetryEntries(db, canonicalSkillName, proc.id);\n const { metrics, flags, riskLevel } = summarizeSkillTelemetry(proc, activations, policy, now);\n const currentState = proc.skillState ?? \"experimental\";\n const transition = desiredLifecycleTransition(currentState, flags, metrics, policy, riskLevel);\n const updatedProc =\n transition == null\n ? proc\n : (setGeneratedSkillLifecycleState(db, canonicalSkillName, transition.state, transition.reason, now, proc.id) ??\n proc);\n if (returnMetrics) {\n return { proc: updatedProc, metrics, flags, riskLevel };\n }\n return updatedProc;\n}\n\nexport function buildGeneratedSkillTelemetryReport(\n db: DatabaseSync,\n options?: {\n skillName?: string;\n policy?: Partial<GeneratedSkillLifecyclePolicy>;\n recentActivationLimit?: number;\n now?: number;\n },\n): GeneratedSkillTelemetryReport {\n const policy = {\n ...DEFAULT_GENERATED_SKILL_LIFECYCLE_POLICY,\n ...(options?.policy ?? {}),\n };\n const now = options?.now ?? Math.floor(Date.now() / 1000);\n const recentLimit = options?.recentActivationLimit ?? 10;\n const procedures = listGeneratedSkillProcedures(db).filter((proc) => {\n if (!options?.skillName) return true;\n return proc.skillPath != null && skillPathBasename(proc.skillPath) === options.skillName;\n });\n const refreshResults = new Map<\n string,\n {\n proc: ProcedureEntry;\n metrics: GeneratedSkillTelemetryMetrics;\n flags: GeneratedSkillTelemetryFlags;\n riskLevel: \"low\" | \"medium\" | \"high\";\n }\n >();\n for (const proc of procedures) {\n if (proc.skillPath) {\n const skillName = skillPathBasename(proc.skillPath);\n const result = refreshGeneratedSkillLifecycleState(db, skillName, policy, now, true, proc.id);\n if (result) refreshResults.set(proc.id, result);\n }\n }\n const rows = listGeneratedSkillProcedures(db)\n .filter((proc) => {\n if (!options?.skillName) return true;\n return proc.skillPath != null && skillPathBasename(proc.skillPath) === options.skillName;\n })\n .map((proc) => {\n const skillName = skillPathBasename(proc.skillPath ?? proc.taskPattern);\n const cachedResult = refreshResults.get(proc.id);\n let procFresh: ProcedureEntry = proc;\n let metrics: GeneratedSkillTelemetryMetrics;\n let flags: GeneratedSkillTelemetryFlags;\n let riskLevel: \"low\" | \"medium\" | \"high\";\n if (cachedResult) {\n ({ proc: procFresh, metrics, flags, riskLevel } = cachedResult);\n } else {\n const activations = skillTelemetryEntries(db, skillName, proc.id);\n const summary = summarizeSkillTelemetry(proc, activations, policy, now);\n metrics = summary.metrics;\n flags = summary.flags;\n riskLevel = summary.riskLevel;\n }\n const currentState = procFresh.skillState ?? \"experimental\";\n const stateReason = procFresh.skillStateReason ?? null;\n const generatedAt = procFresh.skillGeneratedAt ?? procFresh.promotedAt ?? null;\n let recommendation: \"promote\" | \"demote\" | \"archive\" | \"revise\" | \"unblock\" | \"observe\";\n\n if (currentState === \"uninstalled\" || currentState === \"rejected\") {\n recommendation = \"observe\";\n } else if (currentState === \"experimental\" && stateReason?.startsWith(\"auto-unblocked\")) {\n recommendation = \"unblock\";\n } else if (currentState === \"archived\" && flags.archiveCandidate) {\n recommendation = \"observe\";\n } else if (currentState === \"demoted\" && flags.overTriggering) {\n recommendation = \"observe\";\n } else if (flags.unblockCandidate && !flags.overTriggering) {\n recommendation = \"unblock\";\n } else if (flags.archiveCandidate) {\n recommendation = \"archive\";\n } else if (flags.overTriggering) {\n recommendation = \"demote\";\n } else if (flags.promotionCandidate) {\n recommendation = \"promote\";\n } else if (flags.revisionCandidate) {\n recommendation = \"revise\";\n } else {\n recommendation = \"observe\";\n }\n return {\n procedureId: procFresh.id,\n skillName,\n skillPath: procFresh.skillPath ?? skillName,\n skillVersion: procFresh.skillVersion ?? 1,\n riskLevel,\n state: currentState,\n stateReason,\n generatedAt,\n metrics,\n flags,\n recommendation,\n recentActivations: skillTelemetryRecentEntries(db, skillName, recentLimit, procFresh.id),\n } satisfies GeneratedSkillTelemetryReportRow;\n })\n .sort((a, b) => a.skillName.localeCompare(b.skillName));\n return {\n generatedAt: new Date(now * 1000).toISOString(),\n policy,\n totalSkills: rows.length,\n rows,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Disk reconciler (#1391) — detect DB rows whose skill file no longer exists\n// ---------------------------------------------------------------------------\n\nexport type GeneratedSkillDoctorRow = {\n procedureId: string;\n skillName: string;\n skillPath: string;\n state: GeneratedSkillLifecycleState;\n issue: \"missing_on_disk\" | \"stat_error\";\n resolvedAbsolutePath: string;\n fixed: boolean;\n errorCode?: string;\n errorMessage?: string;\n};\n\nexport type GeneratedSkillDoctorReport = {\n checkedAt: string;\n totalChecked: number;\n issues: GeneratedSkillDoctorRow[];\n fixedCount: number;\n failedFixCount: number;\n fixedProcedureIds: string[];\n};\n\n/**\n * Reconcile the DB's generated-skill records against disk.\n *\n * For each procedure row that is marked promoted (`promoted_to_skill = 1`) and has a\n * `skill_path`, this function requires the resolved `SKILL.md` file to exist on disk.\n * Rows whose SKILL.md is missing are reported as `\"missing_on_disk\"` and, when `fix` is\n * true, non-terminal rows are transitioned to the `\"uninstalled\"` state. Rejected rows\n * remain rejected because that terminal operator decision must not be overwritten by\n * disk reconciliation.\n *\n * @param workspaceRoot Absolute path used to resolve relative skill paths. Defaults to `OPENCLAW_WORKSPACE` env var, then `process.cwd()`.\n * @param fix When true, update missing rows to `uninstalled` in the DB.\n */\nexport function reconcileGeneratedSkillDiskState(\n db: DatabaseSync,\n opts: { workspaceRoot?: string; fix?: boolean; now?: number } = {},\n): GeneratedSkillDoctorReport {\n const workspaceRoot = opts.workspaceRoot ?? getEnv(\"OPENCLAW_WORKSPACE\") ?? process.cwd();\n const fix = opts.fix ?? false;\n const now = opts.now ?? Math.floor(Date.now() / 1000);\n const procedures = listGeneratedSkillProcedures(db);\n const issues: GeneratedSkillDoctorRow[] = [];\n const fixedProcedureIds: string[] = [];\n let totalChecked = 0;\n\n for (const proc of procedures) {\n if (!proc.skillPath) continue;\n // Skip terminal rows — they are already correctly modelled and must not be\n // overwritten by disk reconciliation.\n if (proc.skillState === \"uninstalled\" || proc.skillState === \"rejected\") continue;\n\n totalChecked++;\n\n // Accept either a SKILL.md file path or the parent directory, but always\n // require the actual SKILL.md file to exist. A bare directory is not an\n // installed generated skill.\n const { dirPath, skillMdPath } = resolveSkillMdPath(proc.skillPath, workspaceRoot);\n\n let statError: unknown;\n let missing = false;\n try {\n missing = !statSync(skillMdPath).isFile();\n } catch (err) {\n statError = err;\n missing = true;\n }\n if (!missing) continue;\n\n // Derive skill name from the directory component of the path (not the raw skill_path\n // which might include a SKILL.md suffix or be a full directory path).\n const skillName = skillPathBasename(dirPath);\n const errorCode =\n typeof statError === \"object\" && statError !== null && \"code\" in statError\n ? String((statError as { code?: unknown }).code)\n : undefined;\n const isMissingOnDisk = !statError || errorCode === \"ENOENT\";\n let fixed = false;\n if (fix && isMissingOnDisk) {\n // Use the full skill_path for the lookup so findGeneratedSkillProcedure\n // matches on exact path regardless of basename edge cases.\n const updated = setGeneratedSkillLifecycleState(\n db,\n proc.skillPath,\n \"uninstalled\",\n \"operator-confirmed doctor --fix: skill file no longer exists on disk\",\n now,\n );\n fixed = updated?.skillState === \"uninstalled\";\n if (fixed) fixedProcedureIds.push(proc.id);\n }\n issues.push({\n procedureId: proc.id,\n skillName,\n skillPath: proc.skillPath,\n state: proc.skillState ?? \"experimental\",\n issue: isMissingOnDisk ? \"missing_on_disk\" : \"stat_error\",\n resolvedAbsolutePath: skillMdPath,\n fixed,\n ...(errorCode ? { errorCode } : {}),\n ...(statError instanceof Error ? { errorMessage: statError.message } : {}),\n });\n }\n\n return {\n checkedAt: new Date(now * 1000).toISOString(),\n totalChecked,\n issues,\n fixedCount: fixedProcedureIds.length,\n failedFixCount: fix ? issues.filter((issue) => !issue.fixed).length : 0,\n fixedProcedureIds,\n };\n}\n"],"mappings":";;;;;;;;;AAwCA,MAAM,6BAA6B;AAEnC,SAAS,oBAAoB,WAA4B;CACvD,OAAO,WAAW,UAAU,IAAI,MAAM,WAAW,UAAU;;AAG7D,SAAS,sBAAsB,WAA4B;CACzD,OAAO,mBAAmB,KAAK,UAAU;;AAG3C,SAAS,yBAAyB,WAA4B;CAC5D,OAAO,sBAAsB,UAAU,IAAI,UAAU,SAAS,KAAK;;AAGrE,SAAS,kBAAkB,WAA2B;CACpD,OAAO,yBAAyB,UAAU,GAAG,MAAM,SAAS,UAAU,GAAG,SAAS,UAAU;;AAG9F,SAAS,mBAAmB,WAAmB,eAAiE;CAC9G,MAAM,eAAe,oBAAoB,UAAU,GAAG,YAAY,KAAK,eAAe,UAAU;CAChG,MAAM,wBAAwB,yBAAyB,aAAa;CACpE,MAAM,mBAAmB,yBAAyB,KAAK,aAAa;CACpE,MAAM,UAAU,mBAAmB,aAAa,QAAQ,0BAA0B,GAAG,GAAG;CAMxF,OAAO;EAAE;EAAS,aALE,mBAChB,eACA,wBACE,MAAM,KAAK,SAAS,WAAW,GAC/B,KAAK,SAAS,WAAW;EACA;;AA2BjC,SAAS,8BAA8B,KAA+D;CACpG,OAAO;EACL,IAAI,IAAI;EACR,aAAa,IAAI;EACjB,WAAW,IAAI;EACf,cAAc,IAAI;EAClB,aAAa,IAAI;EACjB,gBAAgB,IAAI;EACpB,UAAU,IAAI;EACd,YAAY,IAAI;EAChB,QAAQ,IAAI;EACZ,aAAa,IAAI;EACjB,gBAAgB,IAAI,oBAAoB;EACxC,kBAAkB,IAAI;EACtB,qBAAqB,IAAI,0BAA0B;EACnD,cAAc,IAAI,kBAAkB;EACpC,gBAAgB,IAAI;EACpB,aAAa,IAAI;EACjB,OAAO,IAAI;EACX,aAAa,IAAI;EACjB,SAAS,IAAI;EACb,WAAW,IAAI;EACf,WAAW,IAAI;EAChB;;AAGH,SAAS,iBAAiB,SAAmD;CAC3E,IAAI,OAAO,YAAY,UAAU,OAAO;CACxC,MAAM,aAAa,QAAQ,QAAQ,QAAQ,IAAI,CAAC,MAAM;CACtD,OAAO,WAAW,SAAS,IAAI,WAAW,MAAM,GAAG,2BAA2B,GAAG;;AAGnF,SAAS,qBACP,aACA,gBACe;CACf,IAAI,OAAO,gBAAgB,YAAY,YAAY,MAAM,CAAC,SAAS,GAAG,OAAO,YAAY,MAAM;CAC/F,MAAM,oBAAoB,iBAAiB,eAAe;CAC1D,IAAI,CAAC,mBAAmB,OAAO;CAC/B,OAAO,WAAW,SAAS,CAAC,OAAO,kBAAkB,CAAC,OAAO,MAAM,CAAC,MAAM,GAAG,GAAG;;;AAqDlF,SAAgB,kDAAkD,IAAkB,aAA2B;CAC7G,GAAG,QACD;;;;;;;;;;;;;;;;qBAiBD,CAAC,IAAI,YAAY;;AAGpB,SAAS,0BACP,IACA,aACA,OACA,YACA,aACA,QACM;CACN,IAAI,OAAO;CACX,IAAI,QAAQ;CACZ,IAAI,MAAM;CACV,IAAI,MAAM;CACV,IAAI,YAAY;CAChB,IAAI,QAAQ;CACZ,IAAI,QAAQ;CACZ,IAAI,QAAQ;CACZ,IAAI,OAAO;CACX,IAAI,SAAS;CACb,IAAI,cAAc;CAClB,IAAI,WAAW;CACf,IAAI,MAAM,mBAAmB,QAAQ,MAAM,iBAAiB,MAAM,MAAM;CACxE,IAAI,MAAM,aAAa,YAAY;EACjC,OAAO;EACP,MAAM,UAAU,MAAM;EACtB,IAAI,YAAY,WAAW;GACzB,QAAQ;GACR,IAAI,MAAM,mBAAmB,MAAM,SAAS;SACvC,IAAI,YAAY,WACrB,QAAQ;OACH,IAAI,YAAY,WACrB,QAAQ;OAER,OAAO;QAEJ;EACL,QAAQ;EACR,IAAI,MAAM,aAAa,cAAc,cAAc;EACnD,IAAI,MAAM,aAAa,WAAW,WAAW;;CAE/C,IAAI,MAAM,wBAAwB,MAAM,MAAM;CAC9C,IAAI,MAAM,mBAAmB,MAAM,YAAY;CAC/C,MAAM,qBAAqB,aAAa,IAAI,aAAa;CACzD,MAAM,sBAAsB,cAAc,IAAI,cAAc;CAC5D,MAAM,iBAAiB,MAAM,aAAa,aAAa,SAAS;CAChE,GAAG,QACD;;;;;;;;;;;;;;;;mBAiBD,CAAC,IACA,MACA,OACA,KACA,KACA,WACA,OACA,OACA,OACA,MACA,QACA,aACA,UACA,oBACA,qBACA,gBACA,gBACA,YACD;;AAaH,SAAS,mBAAmB,IAAkD;CAC5E,OAAO,GACJ,QACC;;;;;2GAMD,CACA,KAAK;;AAGV,SAAS,mCAAmC,IAAkB,WAAqC;CACjG,MAAM,UAAU,UAAU,MAAM;CAChC,IAAI,QAAQ,WAAW,GAAG,OAAO,EAAE;CACnC,MAAM,UAA4B,EAAE;CACpC,KAAK,MAAM,OAAO,mBAAmB,GAAG,EAAE;EACxC,MAAM,OAAO,oBAAoB,IAAI,IAAI;EACzC,IAAI,CAAC,KAAK,WAAW,MAAM,EAAE;EAC7B,MAAM,OAAO,KAAK,UAAU,MAAM;EAClC,IAAI,SAAS,WAAW,kBAAkB,KAAK,KAAK,SAAS,QAAQ,KAAK,KAAK;;CAEjF,OAAO;;AAGT,SAAS,4BAA4B,IAAkB,WAA0C;CAC/F,OAAO,mCAAmC,IAAI,UAAU,CAAC,MAAM;;AAGjE,SAAS,kCAAkC,IAAkB,WAA0C;CACrG,MAAM,UAAU,UAAU,MAAM;CAChC,MAAM,UAAU,mCAAmC,IAAI,QAAQ;CAC/D,IAAI,QAAQ,WAAW,GAAG,OAAO;CACjC,IAAI,QAAQ,SAAS,GACnB,MAAM,IAAI,MACR,yBAAyB,QAAQ,yEAClC;CAEH,OAAO,QAAQ,MAAM;;AAGvB,SAAS,kBAAkB,IAAkB,aAA4C;CACvF,MAAM,MAAM,GAAG,QAAQ,gDAAgD,CAAC,IAAI,YAAY;CAGxF,OAAO,MAAM,oBAAoB,IAAI,IAAI,GAAG;;AAG9C,SAAgB,6BAA6B,IAAoC;CAC/E,OAAO,mBAAmB,GAAG,CAAC,KAAK,QAAQ,oBAAoB,IAAI,IAAI,CAAC;;AAG1E,SAAgB,4BACd,IACA,WACA,QAAQ,IACwB;CAmBhC,QAlBa,YACR,GACE,QACC;;;;qBAKD,CACA,IAAI,WAAW,MAAM,GACvB,GACE,QACC;;;qBAID,CACA,IAAI,MAAM,EACL,IAAI,8BAA8B;;AAGhD,SAAgB,8BACd,IACA,OACA,SAAwC,0CACV;CAC9B,IAAI,OAA8B;CAClC,IAAI,MAAM,eAAe,MAAM;EAC7B,MAAM,YAAY,kBAAkB,IAAI,MAAM,YAAY;EAC1D,IAAI,CAAC,WAAW,MAAM,IAAI,MAAM,sCAAsC,MAAM,cAAc;EAC1F,IAAI,UAAU,oBAAoB,KAAK,CAAC,UAAU,WAAW,MAAM,EACjE,MAAM,IAAI,MAAM,aAAa,MAAM,YAAY,oCAAoC;EAErF,MAAM,QAAQ,MAAM,UAAU,MAAM;EACpC,MAAM,OAAO,UAAU,UAAU,MAAM;EACvC,IAAI,SAAS,SAAS,kBAAkB,KAAK,KAAK,OAChD,MAAM,IAAI,MAAM,aAAa,MAAM,YAAY,yCAAyC,MAAM,UAAU,GAAG;EAE7G,OAAO;QAEP,OAAO,kCAAkC,IAAI,MAAM,UAAU;CAE/D,IAAI,CAAC,MAAM,MAAM,IAAI,MAAM,8BAA8B,MAAM,YAAY;CAC3E,IAAI,MAAM,mBAAmB;MAEvB,CAD+B,iBAAiB,MAAM,iBAC3B,EAC7B,MAAM,IAAI,MAAM,kEAAkE;;CAGtF,MAAM,MAAM,MAAM,aAAa,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK;CAC5D,MAAM,oBAAoB,iBAAiB,MAAM,eAAe;CAChE,MAAM,KAAK,YAAY;CACvB,GAAG,QACD;;;;+EAKD,CAAC,IACA,IACA,KAAK,IACL,KAAK,YAAY,kBAAkB,KAAK,UAAU,GAAG,MAAM,WAC3D,MAAM,gBAAgB,KAAK,gBAAgB,GAC3C,qBAAqB,MAAM,aAAa,kBAAkB,EAC1D,mBACA,MAAM,UACN,MAAM,cAAc,MACpB,iBAAiB,MAAM,OAAO,EAC9B,MAAM,eAAe,MACrB,MAAM,mBAAmB,OAAO,IAAI,GACpC,iBAAiB,MAAM,iBAAiB,EACxC,MAAM,wBAAwB,OAAO,IAAI,GACzC,MAAM,iBAAiB,OAAO,IAAI,GAClC,MAAM,kBAAkB,MACxB,MAAM,eAAe,MACrB,MAAM,SAAS,MACf,MAAM,eAAe,MACrB,MAAM,WAAW,MACjB,MAAM,aAAa,MACnB,IACD;CACD,0BAA0B,IAAI,KAAK,IAAI,OAAO,MAAM,kBAAkB,GAAG,MAAM,eAAe,GAAG,IAAI;CACrG,oCACE,IACA,KAAK,YAAY,kBAAkB,KAAK,UAAU,GAAG,MAAM,WAC3D,QACA,KACA,KAAK,GACN;CACD,OAAO,8BACL,GAAG,QAAQ,uDAAuD,CAAC,IAAI,GAAG,CAC3E;;AAGH,MAAM,iCAAiC,MAAU,KAAK;AAEtD,SAAS,mCAAmC,aAA6C,KAAqB;CAC5G,MAAM,cAAc,MAAM;CAC1B,MAAM,yBAAS,IAAI,KAAqB;CACxC,KAAK,MAAM,cAAc,aAAa;EACpC,IAAI,CAAC,WAAW,kBAAkB,WAAW,YAAY,aAAa;EACtE,MAAM,OAAO,WAAW;EACxB,IAAI,OAAO,SAAS,YAAY,KAAK,WAAW,GAAG;EACnD,OAAO,IAAI,OAAO,OAAO,IAAI,KAAK,IAAI,KAAK,EAAE;;CAE/C,IAAI,kBAAkB;CACtB,KAAK,MAAM,SAAS,OAAO,QAAQ,EACjC,IAAI,SAAS,GAAG;CAElB,OAAO;;AAGT,SAAgB,yCACd,IACA,cACA,kBACA,SAAwC,0CACH;CACrC,MAAM,MAAM,GAAG,QAAQ,uDAAuD,CAAC,IAAI,aAAa;CAGhG,IAAI,CAAC,KAAK,OAAO;CACjB,GAAG,QACD;;;oBAID,CAAC,IAAI,iBAAiB,iBAAiB,EAAE,aAAa;CACvD,kDAAkD,IAAI,IAAI,aAAa;CACvE,oCAAoC,IAAI,IAAI,YAAY,QAAQ,KAAA,GAAW,IAAI,aAAa;CAC5F,OAAO,8BACL,GAAG,QAAQ,uDAAuD,CAAC,IAAI,aAAa,CACrF;;AAGH,SAAgB,gCACd,IACA,WACA,OACA,QACA,KAAK,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK,EAClC,aACuB;CACvB,IAAI,OAA8B;CAClC,IAAI,eAAe,MAAM;EACvB,OAAO,kBAAkB,IAAI,YAAY;EACzC,IAAI,SAAS,KAAK,oBAAoB,KAAK,CAAC,KAAK,WAAW,MAAM,GAChE,OAAO;QAGT,OAAO,4BAA4B,IAAI,UAAU;CAEnD,IAAI,CAAC,MAAM,OAAO;CAClB,IAAI,UAAU,gBAIZ,GAAG,QACD;;;;;;qBAOD,CAAC,IAAI,OAAO,iBAAiB,OAAO,EAAE,IAAI,IAAI,IAAI,KAAK,GAAG;MAE3D,GAAG,QACD;;;;;sBAMD,CAAC,IAAI,OAAO,iBAAiB,OAAO,EAAE,IAAI,IAAI,KAAK,GAAG;CAEzD,OAAO,wBAAwB,IAAI,WAAW,YAAY;;AAG5D,SAAgB,wBACd,IACA,WACA,aACuB;CACvB,IAAI,eAAe,MAAM;EACvB,MAAM,OAAO,kBAAkB,IAAI,YAAY;EAC/C,IAAI,SAAS,KAAK,oBAAoB,KAAK,CAAC,KAAK,WAAW,MAAM,GAChE,OAAO;EAET,OAAO;;CAET,OAAO,4BAA4B,IAAI,UAAU;;AAGnD,SAAS,sBACP,IACA,WACA,aACgC;CAChC,MAAM,QAAQ,cACV;;;oCAIA;;;;CAIJ,MAAM,SAAS,cAAc,CAAC,WAAW,YAAY,GAAG,CAAC,UAAU;CACnE,OAAQ,GAAG,QAAQ,MAAM,CAAC,IAAI,GAAG,OAAO,CAAkC,IAAI,8BAA8B;;AAG9G,SAAS,4BACP,IACA,WACA,OACA,aACgC;CAChC,MAAM,QAAQ,cACV;;;;;;qBAOA;;;;;;;CAOJ,MAAM,SAAS,cAAc;EAAC;EAAW;EAAa;EAAM,GAAG,CAAC,WAAW,MAAM;CACjF,OAAQ,GAAG,QAAQ,MAAM,CAAC,IAAI,GAAG,OAAO,CAAkC,IAAI,8BAA8B;;AAoG9G,SAAS,wBACP,MACA,aACA,QACA,KAKA;CACA,MAAM,UAAU,MAAM,QAAc;CAMpC,MAAM,0BACJ,KAAK,eAAe,kBAAkB,KAAK,eAAe,aAAa,KAAK,eAAe;CAC7F,MAAM,sBAAsB,KAAK,eAAe,aAAa,KAAK,eAAe;CACjF,IAAI,kBAAkB;CACtB,IAAI,2BAA2B,KAAK,qBAAqB,QAAQ,KAAK,qBAAqB,KAAA,GACzF,kBAAkB,KAAK;MAClB,IAAI,uBAAuB,KAAK,wBAAwB,QAAQ,KAAK,wBAAwB,KAAA,GAClG,kBAAkB,KAAK;CAEzB,MAAM,kBAAkB,kBAAkB,IAAI,YAAY,QAAQ,MAAM,EAAE,aAAa,gBAAgB,GAAG;CAE1G,IAAI,yBAAyB;CAC7B,IAAI,uBAAuB;CAC3B,IAAI,gBAAgB;CACpB,IAAI,uBAAuB;CAC3B,IAAI,uBAAuB;CAC3B,IAAI,kBAAkB;CACtB,IAAI,aAA4B;CAChC,IAAI,eAAe;CACnB,IAAI,eAAe;CACnB,IAAI,eAAe;CACnB,IAAI,eAAe;CACnB,IAAI,kCAAkC;CACtC,IAAI,kBAAkB;CACtB,IAAI,eAAe;CACnB,IAAI,iBAAiB;CACrB,IAAI,cAAc;CAMlB,MAAM,sBACJ,KAAK,eAAe,aAAa,KAAK,eAAe,aAChD,KAAK,uBAAuB,KAAK,aAAa,IAC/C;CACN,IAAI,yBAAyB;CAE7B,KAAK,MAAM,cAAc,iBAAiB;EACxC,MAAM,QAAQ,WAAW,kBAAkB;EAC3C,MAAM,SAAS,WAAW,eAAe;EACzC,kBAAkB,QAAQ,IAAI,QAAQ;EACtC,eAAe,SAAS,IAAI,SAAS;EACrC,IAAI,WAAW,kBAAkB,WAAW,cAAc;EAC1D,IAAI,WAAW,gBAAgB;EAC/B,IAAI,WAAW,aAAa,YAAY;GACtC;GACA,IAAI,WAAW,aAAa,SAAS;GACrC,aAAa,cAAc,OAAO,WAAW,YAAY,KAAK,IAAI,YAAY,WAAW,UAAU;GACnG,IAAI,WAAW,gBAAgB,WAAW;IACxC;IACA,IAAI,CAAC,WAAW,gBAAgB;KAC9B;KAEA,IAAI,uBAAuB,QAAQ,WAAW,YAAY,qBACxD;;UAGC,IAAI,WAAW,gBAAgB,WACpC;QACK,IAAI,WAAW,gBAAgB,WACpC;QAEA;SAEG;GACL;GACA,IAAI,WAAW,aAAa,cAAc;GAC1C,IAAI,WAAW,aAAa,WAAW;;EAEzC,IAAI,WAAW,qBAAqB;;CAEtC,MAAM,0BAA0B,mCAAmC,iBAAiB,IAAI;CAExF,MAAM,oBAAoB,eAAe,eAAe,eAAe;CACvE,MAAM,cAAc,oBAAoB,IAAI,eAAe,oBAAoB;CAC/E,MAAM,cAAc,oBAAoB,IAAI,eAAe,oBAAoB;CAC/E,MAAM,cAAc,oBAAoB,IAAI,eAAe,oBAAoB;CAC/E,MAAM,cAAc,oBAAoB,IAAI,eAAe,oBAAoB;CAC/E,MAAM,gBAAgB,uBAAuB;CAC7C,MAAM,oBAAoB,gBAAgB,IAAI,uBAAuB,gBAAgB;CACrF,MAAM,YAAY,mBAAmB,MAAM,iBAAiB,KAAK,WAAW,CAAC;CAC7E,MAAM,EAAE,mBAAmB,cAAc,YAAY,qBAAqB,iCACxE,WACA,OACD;CACD,MAAM,cAAc,KAAK,oBAAoB,KAAK,cAAc,KAAK,aAAa,KAAK,aAAa;CACpG,MAAM,oBAAoB,KAAK,eAAe,YAAa,KAAK,uBAAuB,cAAe;CACtG,MAAM,iBAAiB,cAAc;CACrC,MAAM,qBACJ,KAAK,eAAe,aACpB,KAAK,eAAe,cACpB,KAAK,eAAe,aACpB,KAAK,eAAe,iBACpB,KAAK,eAAe,cACpB,mCAAmC,OAAO;CAC5C,MAAM,wBAAwB,OAAO,yBAAyB;CAC9D,MAAM,mBACJ,wBAAwB,MACvB,KAAK,eAAe,aAAa,KAAK,eAAe,eACtD,0BAA0B;CAC5B,MAAM,mBACJ,CAAC,sBAAsB,CAAC,oBAAoB,MAAM,kBAAkB,OAAO,yBAAyB,KAAK,KAAK;CAChH,MAAM,iBACJ,iBAAiB,oBAAoB,qBAAqB,QAAQ,qBAAqB;CACzF,MAAM,oBAAoB,iBAAiB,OAAO,6BAA6B,gBAAgB;CAE/F,OAAO;EACL,SAAS;GACP;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACD;EACD,OAAO;GACL;GACA;GACA;GACA,WAAW,yBAAyB;GACpC;GACA;GACD;EACD;EACD;;AAmFH,SAAS,2BACP,cACA,OACA,SACA,QACA,WACgE;CAEhE,IAAI,iBAAiB,iBAAiB,iBAAiB,YAAY,OAAO;CAE1E,IAAI,iBAAiB,cAAc,MAAM,kBACvC,OAAO;EACL,OAAO;EACP,QAAQ,uBAAuB,OAAO,uBAAuB;EAC9D;CAOH,IAAI,MAAM,oBAAoB,CAAC,MAAM,gBACnC,OAAO;EACL,OAAO;EACP,QAAQ,wBAAwB,QAAQ,uBAAuB;EAChE;CAEH,IAAI,iBAAiB,aAAa,iBAAiB,cAAc,MAAM,gBAAgB;EACrF,MAAM,EAAE,mBAAmB,cAAc,YAAY,qBAAqB,iCACxE,WACA,OACD;EACD,OAAO;GACL,OAAO;GACP,QAAQ,iBAAiB,UAAU,kBAAkB,KAAK,OAAO,QAAQ,qBAAqB,KAAK,IAAI,CAAC,sBAAsB,KAAK,MAAM,eAAe,IAAI,CAAC,SAAS,iBAAiB;GACxL;;CAEH,IAAI,iBAAiB,kBAAkB,MAAM,oBAC3C,OAAO;EACL,OAAO;EACP,QAAQ,uBAAuB,QAAQ,gCAAgC;EACxE;CAEH,OAAO;;AAuBT,SAAgB,oCACd,IACA,WACA,SAAwC,0CACxC,MAAM,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK,EACnC,6BAA+C,OAC/C,aASO;CACP,MAAM,gBAAgB,+BAA+B;CACrD,MAAM,sBAAsB,OAAO,+BAA+B,WAAW,6BAA6B;CAC1G,MAAM,OACJ,uBAAuB,OACnB,kBAAkB,IAAI,oBAAoB,GAC1C,4BAA4B,IAAI,UAAU;CAChD,IAAI,CAAC,MAAM,WAAW,OAAO;CAC7B,MAAM,qBAAqB,kBAAkB,KAAK,UAAU;CAE5D,MAAM,EAAE,SAAS,OAAO,cAAc,wBAAwB,MAD1C,sBAAsB,IAAI,oBAAoB,KAAK,GACQ,EAAE,QAAQ,IAAI;CAE7F,MAAM,aAAa,2BADE,KAAK,cAAc,gBACoB,OAAO,SAAS,QAAQ,UAAU;CAC9F,MAAM,cACJ,cAAc,OACV,OACC,gCAAgC,IAAI,oBAAoB,WAAW,OAAO,WAAW,QAAQ,KAAK,KAAK,GAAG,IAC3G;CACN,IAAI,eACF,OAAO;EAAE,MAAM;EAAa;EAAS;EAAO;EAAW;CAEzD,OAAO;;AAGT,SAAgB,mCACd,IACA,SAM+B;CAC/B,MAAM,SAAS;EACb,GAAG;EACH,GAAI,SAAS,UAAU,EAAE;EAC1B;CACD,MAAM,MAAM,SAAS,OAAO,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK;CACzD,MAAM,cAAc,SAAS,yBAAyB;CACtD,MAAM,aAAa,6BAA6B,GAAG,CAAC,QAAQ,SAAS;EACnE,IAAI,CAAC,SAAS,WAAW,OAAO;EAChC,OAAO,KAAK,aAAa,QAAQ,kBAAkB,KAAK,UAAU,KAAK,QAAQ;GAC/E;CACF,MAAM,iCAAiB,IAAI,KAQxB;CACH,KAAK,MAAM,QAAQ,YACjB,IAAI,KAAK,WAAW;EAElB,MAAM,SAAS,oCAAoC,IADjC,kBAAkB,KAAK,UACuB,EAAE,QAAQ,KAAK,MAAM,KAAK,GAAG;EAC7F,IAAI,QAAQ,eAAe,IAAI,KAAK,IAAI,OAAO;;CAGnD,MAAM,OAAO,6BAA6B,GAAG,CAC1C,QAAQ,SAAS;EAChB,IAAI,CAAC,SAAS,WAAW,OAAO;EAChC,OAAO,KAAK,aAAa,QAAQ,kBAAkB,KAAK,UAAU,KAAK,QAAQ;GAC/E,CACD,KAAK,SAAS;EACb,MAAM,YAAY,kBAAkB,KAAK,aAAa,KAAK,YAAY;EACvE,MAAM,eAAe,eAAe,IAAI,KAAK,GAAG;EAChD,IAAI,YAA4B;EAChC,IAAI;EACJ,IAAI;EACJ,IAAI;EACJ,IAAI,cACF,CAAC,CAAE,MAAM,WAAW,SAAS,OAAO,aAAc;OAC7C;GAEL,MAAM,UAAU,wBAAwB,MADpB,sBAAsB,IAAI,WAAW,KAAK,GACL,EAAE,QAAQ,IAAI;GACvE,UAAU,QAAQ;GAClB,QAAQ,QAAQ;GAChB,YAAY,QAAQ;;EAEtB,MAAM,eAAe,UAAU,cAAc;EAC7C,MAAM,cAAc,UAAU,oBAAoB;EAClD,MAAM,cAAc,UAAU,oBAAoB,UAAU,cAAc;EAC1E,IAAI;EAEJ,IAAI,iBAAiB,iBAAiB,iBAAiB,YACrD,iBAAiB;OACZ,IAAI,iBAAiB,kBAAkB,aAAa,WAAW,iBAAiB,EACrF,iBAAiB;OACZ,IAAI,iBAAiB,cAAc,MAAM,kBAC9C,iBAAiB;OACZ,IAAI,iBAAiB,aAAa,MAAM,gBAC7C,iBAAiB;OACZ,IAAI,MAAM,oBAAoB,CAAC,MAAM,gBAC1C,iBAAiB;OACZ,IAAI,MAAM,kBACf,iBAAiB;OACZ,IAAI,MAAM,gBACf,iBAAiB;OACZ,IAAI,MAAM,oBACf,iBAAiB;OACZ,IAAI,MAAM,mBACf,iBAAiB;OAEjB,iBAAiB;EAEnB,OAAO;GACL,aAAa,UAAU;GACvB;GACA,WAAW,UAAU,aAAa;GAClC,cAAc,UAAU,gBAAgB;GACxC;GACA,OAAO;GACP;GACA;GACA;GACA;GACA;GACA,mBAAmB,4BAA4B,IAAI,WAAW,aAAa,UAAU,GAAG;GACzF;GACD,CACD,MAAM,GAAG,MAAM,EAAE,UAAU,cAAc,EAAE,UAAU,CAAC;CACzD,OAAO;EACL,8BAAa,IAAI,KAAK,MAAM,IAAK,EAAC,aAAa;EAC/C;EACA,aAAa,KAAK;EAClB;EACD;;;;;;;;;;;;;;;AAyCH,SAAgB,iCACd,IACA,OAAgE,EAAE,EACtC;CAC5B,MAAM,gBAAgB,KAAK,iBAAiB,OAAO,qBAAqB,IAAI,QAAQ,KAAK;CACzF,MAAM,MAAM,KAAK,OAAO;CACxB,MAAM,MAAM,KAAK,OAAO,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK;CACrD,MAAM,aAAa,6BAA6B,GAAG;CACnD,MAAM,SAAoC,EAAE;CAC5C,MAAM,oBAA8B,EAAE;CACtC,IAAI,eAAe;CAEnB,KAAK,MAAM,QAAQ,YAAY;EAC7B,IAAI,CAAC,KAAK,WAAW;EAGrB,IAAI,KAAK,eAAe,iBAAiB,KAAK,eAAe,YAAY;EAEzE;EAKA,MAAM,EAAE,SAAS,gBAAgB,mBAAmB,KAAK,WAAW,cAAc;EAElF,IAAI;EACJ,IAAI,UAAU;EACd,IAAI;GACF,UAAU,CAAC,SAAS,YAAY,CAAC,QAAQ;WAClC,KAAK;GACZ,YAAY;GACZ,UAAU;;EAEZ,IAAI,CAAC,SAAS;EAId,MAAM,YAAY,kBAAkB,QAAQ;EAC5C,MAAM,YACJ,OAAO,cAAc,YAAY,cAAc,QAAQ,UAAU,YAC7D,OAAQ,UAAiC,KAAK,GAC9C,KAAA;EACN,MAAM,kBAAkB,CAAC,aAAa,cAAc;EACpD,IAAI,QAAQ;EACZ,IAAI,OAAO,iBAAiB;GAU1B,QAPgB,gCACd,IACA,KAAK,WACL,eACA,wEACA,IAEa,EAAE,eAAe;GAChC,IAAI,OAAO,kBAAkB,KAAK,KAAK,GAAG;;EAE5C,OAAO,KAAK;GACV,aAAa,KAAK;GAClB;GACA,WAAW,KAAK;GAChB,OAAO,KAAK,cAAc;GAC1B,OAAO,kBAAkB,oBAAoB;GAC7C,sBAAsB;GACtB;GACA,GAAI,YAAY,EAAE,WAAW,GAAG,EAAE;GAClC,GAAI,qBAAqB,QAAQ,EAAE,cAAc,UAAU,SAAS,GAAG,EAAE;GAC1E,CAAC;;CAGJ,OAAO;EACL,4BAAW,IAAI,KAAK,MAAM,IAAK,EAAC,aAAa;EAC7C;EACA;EACA,YAAY,kBAAkB;EAC9B,gBAAgB,MAAM,OAAO,QAAQ,UAAU,CAAC,MAAM,MAAM,CAAC,SAAS;EACtE;EACD"}
1
+ {"version":3,"file":"generated-skills.js","names":[],"sources":["../../../backends/facts-db/generated-skills.ts"],"sourcesContent":["import { createHash, randomUUID } from \"node:crypto\";\nimport { statSync } from \"node:fs\";\nimport { basename, isAbsolute, join, win32 } from \"node:path\";\nimport type { DatabaseSync } from \"node:sqlite\";\nimport { determineRiskLevel, parseRecipeOrRaw } from \"../../utils/procedure-risk.js\";\nimport type {\n GeneratedSkillLifecycleState,\n GeneratedSkillTelemetryDecision,\n GeneratedSkillTelemetryEntry,\n GeneratedSkillTelemetryOutcome,\n MemoryScope,\n ProcedureEntry,\n} from \"../../types/memory.js\";\nimport { getEnv } from \"../../utils/env-manager.js\";\nimport {\n DEFAULT_GENERATED_SKILL_LIFECYCLE_POLICY,\n effectiveDemoteThresholdsForRisk,\n} from \"./generated-skills/policy.js\";\nexport {\n DEFAULT_GENERATED_SKILL_LIFECYCLE_POLICY,\n effectiveDemoteThresholdsForRisk,\n} from \"./generated-skills/policy.js\";\nexport type {\n GeneratedSkillLifecyclePolicy,\n GeneratedSkillTelemetryFlags,\n GeneratedSkillTelemetryMetrics,\n GeneratedSkillTelemetryRecordInput,\n GeneratedSkillTelemetryReport,\n GeneratedSkillTelemetryReportRow,\n} from \"./generated-skills/policy.js\";\nimport type {\n GeneratedSkillLifecyclePolicy,\n GeneratedSkillTelemetryFlags,\n GeneratedSkillTelemetryMetrics,\n GeneratedSkillTelemetryRecordInput,\n GeneratedSkillTelemetryReport,\n GeneratedSkillTelemetryReportRow,\n} from \"./generated-skills/policy.js\";\nimport { procedureRowToEntry } from \"./procedures.js\";\n\nconst MAX_REQUEST_SUMMARY_LENGTH = 240;\n\nfunction isAbsoluteSkillPath(skillPath: string): boolean {\n return isAbsolute(skillPath) || win32.isAbsolute(skillPath);\n}\n\nfunction hasWindowsDrivePrefix(skillPath: string): boolean {\n return /^[A-Za-z]:[/\\\\]/u.test(skillPath);\n}\n\nfunction usesWindowsPathSemantics(skillPath: string): boolean {\n return hasWindowsDrivePrefix(skillPath) || skillPath.includes(\"\\\\\");\n}\n\nfunction skillPathBasename(skillPath: string): string {\n return usesWindowsPathSemantics(skillPath) ? win32.basename(skillPath) : basename(skillPath);\n}\n\nfunction resolveSkillMdPath(skillPath: string, workspaceRoot: string): { dirPath: string; skillMdPath: string } {\n const resolvedPath = isAbsoluteSkillPath(skillPath) ? skillPath : join(workspaceRoot, skillPath);\n const usesWindowsSeparators = usesWindowsPathSemantics(resolvedPath);\n const hasSkillMdSuffix = /(?:^|[/\\\\])SKILL\\.md$/u.test(resolvedPath);\n const dirPath = hasSkillMdSuffix ? resolvedPath.replace(/(?:^|[/\\\\])SKILL\\.md$/u, \"\") : resolvedPath;\n const skillMdPath = hasSkillMdSuffix\n ? resolvedPath\n : usesWindowsSeparators\n ? win32.join(dirPath, \"SKILL.md\")\n : join(dirPath, \"SKILL.md\");\n return { dirPath, skillMdPath };\n}\n\ntype GeneratedSkillTelemetryRow = {\n id: string;\n procedure_id: string;\n skill_name: string;\n skill_version: number;\n request_hash: string | null;\n request_summary: string | null;\n decision: GeneratedSkillTelemetryDecision;\n confidence: number | null;\n reason: string | null;\n task_outcome: GeneratedSkillTelemetryOutcome | null;\n user_correction: number;\n correction_reason: string | null;\n false_negative_signal: number;\n caused_rework: number;\n saved_tool_calls: number | null;\n saved_time_ms: number | null;\n scope: MemoryScope | null;\n scope_target: string | null;\n agent_id: string | null;\n session_id: string | null;\n created_at: number;\n};\n\nfunction mapGeneratedSkillTelemetryRow(row: GeneratedSkillTelemetryRow): GeneratedSkillTelemetryEntry {\n return {\n id: row.id,\n procedureId: row.procedure_id,\n skillName: row.skill_name,\n skillVersion: row.skill_version,\n requestHash: row.request_hash,\n requestSummary: row.request_summary,\n decision: row.decision,\n confidence: row.confidence,\n reason: row.reason,\n taskOutcome: row.task_outcome,\n userCorrection: row.user_correction === 1,\n correctionReason: row.correction_reason,\n falseNegativeSignal: row.false_negative_signal === 1,\n causedRework: row.caused_rework === 1,\n savedToolCalls: row.saved_tool_calls,\n savedTimeMs: row.saved_time_ms,\n scope: row.scope,\n scopeTarget: row.scope_target,\n agentId: row.agent_id,\n sessionId: row.session_id,\n createdAt: row.created_at,\n };\n}\n\nfunction normalizeSummary(summary: string | null | undefined): string | null {\n if (typeof summary !== \"string\") return null;\n const normalized = summary.replace(/\\s+/g, \" \").trim();\n return normalized.length > 0 ? normalized.slice(0, MAX_REQUEST_SUMMARY_LENGTH) : null;\n}\n\nfunction requestHashFromInput(\n requestHash: string | null | undefined,\n requestSummary: string | null | undefined,\n): string | null {\n if (typeof requestHash === \"string\" && requestHash.trim().length > 0) return requestHash.trim();\n const normalizedSummary = normalizeSummary(requestSummary);\n if (!normalizedSummary) return null;\n return createHash(\"sha256\").update(normalizedSummary).digest(\"hex\").slice(0, 16);\n}\n\n/** Per-procedure counters maintained incrementally on telemetry insert (#1415 / #1400). */\ntype TelemetryRollupRow = {\n gst_sel_total: number;\n gst_near_miss_total: number;\n gst_fp_signals_total: number;\n gst_fn_signals_total: number;\n gst_user_correction_total: number;\n gst_outcome_success: number;\n gst_outcome_failure: number;\n gst_outcome_partial: number;\n gst_outcome_unknown: number;\n gst_success_clear_total: number;\n gst_considered_total: number;\n gst_skipped_total: number;\n gst_saved_tool_calls_sum: number;\n gst_saved_time_ms_sum: number;\n gst_last_selected_at: number | null;\n};\n\nfunction readTelemetryRollup(db: DatabaseSync, procedureId: string): TelemetryRollupRow | null {\n const row = db\n .prepare(\n `SELECT gst_sel_total, gst_near_miss_total, gst_fp_signals_total, gst_fn_signals_total,\n gst_user_correction_total, gst_outcome_success, gst_outcome_failure, gst_outcome_partial,\n gst_outcome_unknown, gst_success_clear_total, gst_considered_total, gst_skipped_total,\n gst_saved_tool_calls_sum, gst_saved_time_ms_sum, gst_last_selected_at\n FROM procedures WHERE id = ?`,\n )\n .get(procedureId) as Record<string, unknown> | undefined;\n if (!row) return null;\n return {\n gst_sel_total: (row.gst_sel_total as number) ?? 0,\n gst_near_miss_total: (row.gst_near_miss_total as number) ?? 0,\n gst_fp_signals_total: (row.gst_fp_signals_total as number) ?? 0,\n gst_fn_signals_total: (row.gst_fn_signals_total as number) ?? 0,\n gst_user_correction_total: (row.gst_user_correction_total as number) ?? 0,\n gst_outcome_success: (row.gst_outcome_success as number) ?? 0,\n gst_outcome_failure: (row.gst_outcome_failure as number) ?? 0,\n gst_outcome_partial: (row.gst_outcome_partial as number) ?? 0,\n gst_outcome_unknown: (row.gst_outcome_unknown as number) ?? 0,\n gst_success_clear_total: (row.gst_success_clear_total as number) ?? 0,\n gst_considered_total: (row.gst_considered_total as number) ?? 0,\n gst_skipped_total: (row.gst_skipped_total as number) ?? 0,\n gst_saved_tool_calls_sum: (row.gst_saved_tool_calls_sum as number) ?? 0,\n gst_saved_time_ms_sum: (row.gst_saved_time_ms_sum as number) ?? 0,\n gst_last_selected_at: (row.gst_last_selected_at as number | null) ?? null,\n };\n}\n\n/** Recompute rollup columns from `generated_skill_telemetry` (e.g. after correction edits). */\nexport function rebuildGeneratedSkillTelemetryRollupsForProcedure(db: DatabaseSync, procedureId: string): void {\n db.prepare(\n `UPDATE procedures AS p\n SET gst_sel_total = (SELECT COUNT(*) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id AND t.decision = 'selected'),\n gst_near_miss_total = (SELECT COUNT(*) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id AND t.decision != 'selected'),\n gst_fp_signals_total = (SELECT COUNT(*) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id AND (t.user_correction = 1 OR t.caused_rework = 1)),\n gst_fn_signals_total = (SELECT COUNT(*) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id AND t.false_negative_signal = 1),\n gst_user_correction_total = (SELECT COUNT(*) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id AND t.user_correction = 1),\n gst_outcome_success = (SELECT COUNT(*) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id AND t.decision = 'selected' AND t.task_outcome = 'success'),\n gst_outcome_failure = (SELECT COUNT(*) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id AND t.decision = 'selected' AND t.task_outcome = 'failure'),\n gst_outcome_partial = (SELECT COUNT(*) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id AND t.decision = 'selected' AND t.task_outcome = 'partial'),\n gst_outcome_unknown = (SELECT COUNT(*) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id AND t.decision = 'selected' AND (t.task_outcome IS NULL OR t.task_outcome = '' OR t.task_outcome NOT IN ('success','failure','partial'))),\n gst_success_clear_total = (SELECT COUNT(*) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id AND t.decision = 'selected' AND t.task_outcome = 'success' AND t.user_correction = 0),\n gst_considered_total = (SELECT COUNT(*) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id AND t.decision = 'considered'),\n gst_skipped_total = (SELECT COUNT(*) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id AND t.decision = 'skipped'),\n gst_saved_tool_calls_sum = (SELECT COALESCE(SUM(CASE WHEN t.saved_tool_calls > 0 THEN t.saved_tool_calls ELSE 0 END), 0) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id),\n gst_saved_time_ms_sum = (SELECT COALESCE(SUM(CASE WHEN t.saved_time_ms > 0 THEN t.saved_time_ms ELSE 0 END), 0) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id),\n gst_last_selected_at = (SELECT MAX(t.created_at) FROM generated_skill_telemetry t WHERE t.procedure_id = p.id AND t.decision = 'selected')\n WHERE p.id = ?`,\n ).run(procedureId);\n}\n\nfunction applyTelemetryRollupDelta(\n db: DatabaseSync,\n procedureId: string,\n input: GeneratedSkillTelemetryRecordInput,\n savedCalls: number,\n savedTimeMs: number,\n nowSec: number,\n): void {\n let dSel = 0;\n let dNear = 0;\n let dFp = 0;\n let dFn = 0;\n let dUserCorr = 0;\n let dSucc = 0;\n let dFail = 0;\n let dPart = 0;\n let dUnk = 0;\n let dClear = 0;\n let dConsidered = 0;\n let dSkipped = 0;\n if (input.userCorrection === true || input.causedRework === true) dFp = 1;\n if (input.decision === \"selected\") {\n dSel = 1;\n const outcome = input.taskOutcome;\n if (outcome === \"success\") {\n dSucc = 1;\n if (input.userCorrection !== true) dClear = 1;\n } else if (outcome === \"failure\") {\n dFail = 1;\n } else if (outcome === \"partial\") {\n dPart = 1;\n } else {\n dUnk = 1;\n }\n } else {\n dNear = 1;\n if (input.decision === \"considered\") dConsidered = 1;\n if (input.decision === \"skipped\") dSkipped = 1;\n }\n if (input.falseNegativeSignal === true) dFn = 1;\n if (input.userCorrection === true) dUserCorr = 1;\n const positiveSavedCalls = savedCalls > 0 ? savedCalls : 0;\n const positiveSavedTimeMs = savedTimeMs > 0 ? savedTimeMs : 0;\n const lastSelectedAt = input.decision === \"selected\" ? nowSec : 0;\n db.prepare(\n `UPDATE procedures SET\n gst_sel_total = gst_sel_total + ?,\n gst_near_miss_total = gst_near_miss_total + ?,\n gst_fp_signals_total = gst_fp_signals_total + ?,\n gst_fn_signals_total = gst_fn_signals_total + ?,\n gst_user_correction_total = gst_user_correction_total + ?,\n gst_outcome_success = gst_outcome_success + ?,\n gst_outcome_failure = gst_outcome_failure + ?,\n gst_outcome_partial = gst_outcome_partial + ?,\n gst_outcome_unknown = gst_outcome_unknown + ?,\n gst_success_clear_total = gst_success_clear_total + ?,\n gst_considered_total = gst_considered_total + ?,\n gst_skipped_total = gst_skipped_total + ?,\n gst_saved_tool_calls_sum = gst_saved_tool_calls_sum + ?,\n gst_saved_time_ms_sum = gst_saved_time_ms_sum + ?,\n gst_last_selected_at = CASE WHEN ? > COALESCE(gst_last_selected_at, 0) THEN ? ELSE gst_last_selected_at END\n WHERE id = ?`,\n ).run(\n dSel,\n dNear,\n dFp,\n dFn,\n dUserCorr,\n dSucc,\n dFail,\n dPart,\n dUnk,\n dClear,\n dConsidered,\n dSkipped,\n positiveSavedCalls,\n positiveSavedTimeMs,\n lastSelectedAt,\n lastSelectedAt,\n procedureId,\n );\n}\n\nfunction countSelectedActivationsSince(db: DatabaseSync, procedureId: string, sinceSec: number): number {\n const row = db\n .prepare(\n `SELECT COUNT(*) as c FROM generated_skill_telemetry\n WHERE procedure_id = ? AND decision = 'selected' AND created_at >= ?`,\n )\n .get(procedureId, sinceSec) as { c: number };\n return row?.c ?? 0;\n}\n\nfunction generatedSkillRows(db: DatabaseSync): Array<Record<string, unknown>> {\n return db\n .prepare(\n `SELECT *\n FROM procedures\n WHERE promoted_to_skill = 1\n AND skill_path IS NOT NULL\n AND TRIM(skill_path) <> ''\n ORDER BY COALESCE(skill_generated_at, promoted_at, updated_at, created_at) DESC, task_pattern ASC`,\n )\n .all() as Array<Record<string, unknown>>;\n}\n\nfunction findGeneratedSkillProcedureMatches(db: DatabaseSync, skillName: string): ProcedureEntry[] {\n const trimmed = skillName.trim();\n if (trimmed.length === 0) return [];\n const matches: ProcedureEntry[] = [];\n for (const row of generatedSkillRows(db)) {\n const proc = procedureRowToEntry(db, row);\n if (!proc.skillPath?.trim()) continue;\n const path = proc.skillPath.trim();\n if (path === trimmed || skillPathBasename(path) === trimmed) matches.push(proc);\n }\n return matches;\n}\n\nfunction findGeneratedSkillProcedure(db: DatabaseSync, skillName: string): ProcedureEntry | null {\n return findGeneratedSkillProcedureMatches(db, skillName)[0] ?? null;\n}\n\nfunction findGeneratedSkillProcedureStrict(db: DatabaseSync, skillName: string): ProcedureEntry | null {\n const trimmed = skillName.trim();\n const matches = findGeneratedSkillProcedureMatches(db, trimmed);\n if (matches.length === 0) return null;\n if (matches.length > 1) {\n throw new Error(\n `Ambiguous skill name \"${trimmed}\": multiple promoted procedures match; pass procedureId to disambiguate`,\n );\n }\n return matches[0] ?? null;\n}\n\nfunction findProcedureById(db: DatabaseSync, procedureId: string): ProcedureEntry | null {\n const row = db.prepare(\"SELECT * FROM procedures WHERE id = ? LIMIT 1\").get(procedureId) as\n | Record<string, unknown>\n | undefined;\n return row ? procedureRowToEntry(db, row) : null;\n}\n\nexport function listGeneratedSkillProcedures(db: DatabaseSync): ProcedureEntry[] {\n return generatedSkillRows(db).map((row) => procedureRowToEntry(db, row));\n}\n\nexport function listGeneratedSkillTelemetry(\n db: DatabaseSync,\n skillName?: string,\n limit = 50,\n): GeneratedSkillTelemetryEntry[] {\n const rows = skillName\n ? (db\n .prepare(\n `SELECT *\n FROM generated_skill_telemetry\n WHERE skill_name = ?\n ORDER BY created_at DESC\n LIMIT ?`,\n )\n .all(skillName, limit) as GeneratedSkillTelemetryRow[])\n : (db\n .prepare(\n `SELECT *\n FROM generated_skill_telemetry\n ORDER BY created_at DESC\n LIMIT ?`,\n )\n .all(limit) as GeneratedSkillTelemetryRow[]);\n return rows.map(mapGeneratedSkillTelemetryRow);\n}\n\nexport function recordGeneratedSkillTelemetry(\n db: DatabaseSync,\n input: GeneratedSkillTelemetryRecordInput,\n policy: GeneratedSkillLifecyclePolicy = DEFAULT_GENERATED_SKILL_LIFECYCLE_POLICY,\n): GeneratedSkillTelemetryEntry {\n let proc: ProcedureEntry | null = null;\n if (input.procedureId != null) {\n const candidate = findProcedureById(db, input.procedureId);\n if (!candidate) throw new Error(`Procedure not found for telemetry: ${input.procedureId}`);\n if (candidate.promotedToSkill !== 1 || !candidate.skillPath?.trim()) {\n throw new Error(`Procedure ${input.procedureId} is not a promoted generated skill`);\n }\n const skill = input.skillName.trim();\n const path = candidate.skillPath.trim();\n if (path !== skill && skillPathBasename(path) !== skill) {\n throw new Error(`Procedure ${input.procedureId} skill_path does not match skill name \"${input.skillName}\"`);\n }\n proc = candidate;\n } else {\n proc = findGeneratedSkillProcedureStrict(db, input.skillName);\n }\n if (!proc) throw new Error(`Generated skill not found: ${input.skillName}`);\n if (input.userCorrection === true) {\n const normalizedCorrectionReason = normalizeSummary(input.correctionReason);\n if (!normalizedCorrectionReason) {\n throw new Error(\"user correction telemetry requires a non-empty correctionReason\");\n }\n }\n const now = input.createdAt ?? Math.floor(Date.now() / 1000);\n const normalizedSummary = normalizeSummary(input.requestSummary);\n const id = randomUUID();\n db.prepare(\n `INSERT INTO generated_skill_telemetry (\n id, procedure_id, skill_name, skill_version, request_hash, request_summary, decision, confidence, reason,\n task_outcome, user_correction, correction_reason, false_negative_signal, caused_rework, saved_tool_calls,\n saved_time_ms, scope, scope_target, agent_id, session_id, created_at\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n ).run(\n id,\n proc.id,\n proc.skillPath ? skillPathBasename(proc.skillPath) : input.skillName,\n input.skillVersion ?? proc.skillVersion ?? 1,\n requestHashFromInput(input.requestHash, normalizedSummary),\n normalizedSummary,\n input.decision,\n input.confidence ?? null,\n normalizeSummary(input.reason),\n input.taskOutcome ?? null,\n input.userCorrection === true ? 1 : 0,\n normalizeSummary(input.correctionReason),\n input.falseNegativeSignal === true ? 1 : 0,\n input.causedRework === true ? 1 : 0,\n input.savedToolCalls ?? null,\n input.savedTimeMs ?? null,\n input.scope ?? null,\n input.scopeTarget ?? null,\n input.agentId ?? null,\n input.sessionId ?? null,\n now,\n );\n applyTelemetryRollupDelta(db, proc.id, input, input.savedToolCalls ?? 0, input.savedTimeMs ?? 0, now);\n refreshGeneratedSkillLifecycleState(\n db,\n proc.skillPath ? skillPathBasename(proc.skillPath) : input.skillName,\n policy,\n now,\n proc.id,\n );\n return mapGeneratedSkillTelemetryRow(\n db.prepare(\"SELECT * FROM generated_skill_telemetry WHERE id = ?\").get(id) as GeneratedSkillTelemetryRow,\n );\n}\n\nconst REPEATED_CORRECTION_WINDOW_SEC = 30 * 24 * 60 * 60;\n\nfunction countRepeatedUserCorrectionBuckets(activations: GeneratedSkillTelemetryEntry[], now: number): number {\n const windowStart = now - REPEATED_CORRECTION_WINDOW_SEC;\n const byHash = new Map<string, number>();\n for (const activation of activations) {\n if (!activation.userCorrection || activation.createdAt < windowStart) continue;\n const hash = activation.requestHash;\n if (typeof hash !== \"string\" || hash.length === 0) continue;\n byHash.set(hash, (byHash.get(hash) ?? 0) + 1);\n }\n let repeatedBuckets = 0;\n for (const count of byHash.values()) {\n if (count >= 2) repeatedBuckets++;\n }\n return repeatedBuckets;\n}\n\nexport function markGeneratedSkillTelemetryFalsePositive(\n db: DatabaseSync,\n activationId: string,\n correctionReason: string,\n policy: GeneratedSkillLifecyclePolicy = DEFAULT_GENERATED_SKILL_LIFECYCLE_POLICY,\n): GeneratedSkillTelemetryEntry | null {\n const row = db.prepare(\"SELECT * FROM generated_skill_telemetry WHERE id = ?\").get(activationId) as\n | GeneratedSkillTelemetryRow\n | undefined;\n if (!row) return null;\n db.prepare(\n `UPDATE generated_skill_telemetry\n SET user_correction = 1,\n correction_reason = COALESCE(?, correction_reason)\n WHERE id = ?`,\n ).run(normalizeSummary(correctionReason), activationId);\n rebuildGeneratedSkillTelemetryRollupsForProcedure(db, row.procedure_id);\n refreshGeneratedSkillLifecycleState(db, row.skill_name, policy, undefined, row.procedure_id);\n return mapGeneratedSkillTelemetryRow(\n db.prepare(\"SELECT * FROM generated_skill_telemetry WHERE id = ?\").get(activationId) as GeneratedSkillTelemetryRow,\n );\n}\n\nexport function setGeneratedSkillLifecycleState(\n db: DatabaseSync,\n skillName: string,\n state: GeneratedSkillLifecycleState,\n reason: string | null,\n at = Math.floor(Date.now() / 1000),\n procedureId?: string,\n): ProcedureEntry | null {\n let proc: ProcedureEntry | null = null;\n if (procedureId != null) {\n proc = findProcedureById(db, procedureId);\n if (proc && (proc.promotedToSkill !== 1 || !proc.skillPath?.trim())) {\n return null;\n }\n } else {\n proc = findGeneratedSkillProcedure(db, skillName);\n }\n if (!proc) return null;\n if (state === \"experimental\") {\n // When a skill is reset/unblocked back to experimental, update skill_generated_at to mark\n // the new evaluation window start. This prevents pre-reset telemetry from counting against\n // the skill's overTriggering and promotionCandidate evaluations.\n db.prepare(\n `UPDATE procedures\n SET skill_state = ?,\n skill_state_reason = ?,\n skill_generated_at = ?,\n skill_state_changed_at = ?,\n updated_at = ?\n WHERE id = ?`,\n ).run(state, normalizeSummary(reason), at, at, at, proc.id);\n } else {\n db.prepare(\n `UPDATE procedures\n SET skill_state = ?,\n skill_state_reason = ?,\n skill_state_changed_at = ?,\n updated_at = ?\n WHERE id = ?`,\n ).run(state, normalizeSummary(reason), at, at, proc.id);\n }\n return getGeneratedSkillByName(db, skillName, procedureId);\n}\n\nexport function getGeneratedSkillByName(\n db: DatabaseSync,\n skillName: string,\n procedureId?: string,\n): ProcedureEntry | null {\n if (procedureId != null) {\n const proc = findProcedureById(db, procedureId);\n if (proc && (proc.promotedToSkill !== 1 || !proc.skillPath?.trim())) {\n return null;\n }\n return proc;\n }\n return findGeneratedSkillProcedure(db, skillName);\n}\n\nfunction skillTelemetryEntries(\n db: DatabaseSync,\n skillName: string,\n procedureId?: string,\n): GeneratedSkillTelemetryEntry[] {\n const query = procedureId\n ? `SELECT *\n FROM generated_skill_telemetry\n WHERE skill_name = ? AND procedure_id = ?\n ORDER BY created_at DESC`\n : `SELECT *\n FROM generated_skill_telemetry\n WHERE skill_name = ?\n ORDER BY created_at DESC`;\n const params = procedureId ? [skillName, procedureId] : [skillName];\n return (db.prepare(query).all(...params) as GeneratedSkillTelemetryRow[]).map(mapGeneratedSkillTelemetryRow);\n}\n\nfunction skillTelemetryRecentEntries(\n db: DatabaseSync,\n skillName: string,\n limit: number,\n procedureId?: string,\n): GeneratedSkillTelemetryEntry[] {\n const query = procedureId\n ? `SELECT id, procedure_id, skill_name, skill_version, request_hash, request_summary, decision, confidence,\n reason, task_outcome, user_correction, correction_reason, false_negative_signal, caused_rework,\n saved_tool_calls, saved_time_ms, scope, scope_target, agent_id, session_id, created_at\n FROM generated_skill_telemetry\n WHERE skill_name = ? AND procedure_id = ?\n ORDER BY created_at DESC\n LIMIT ?`\n : `SELECT id, procedure_id, skill_name, skill_version, request_hash, request_summary, decision, confidence,\n reason, task_outcome, user_correction, correction_reason, false_negative_signal, caused_rework,\n saved_tool_calls, saved_time_ms, scope, scope_target, agent_id, session_id, created_at\n FROM generated_skill_telemetry\n WHERE skill_name = ?\n ORDER BY created_at DESC\n LIMIT ?`;\n const params = procedureId ? [skillName, procedureId, limit] : [skillName, limit];\n return (db.prepare(query).all(...params) as GeneratedSkillTelemetryRow[]).map(mapGeneratedSkillTelemetryRow);\n}\n\ntype RawTelemetryCounts = {\n activationCountPerWeek: number;\n activationCountTotal: number;\n nearMissCount: number;\n falsePositiveSignals: number;\n falseNegativeSignals: number;\n correctionCount: number;\n repeatedCorrectionCount: number;\n lastUsedAt: number | null;\n successCount: number;\n failureCount: number;\n partialCount: number;\n unknownCount: number;\n successfulUsesWithoutCorrection: number;\n consideredCount: number;\n skippedCount: number;\n savedToolCalls: number;\n savedTimeMs: number;\n cleanUsesAfterDemotion: number;\n};\n\nfunction deriveMetricsAndFlags(\n counts: RawTelemetryCounts,\n proc: ProcedureEntry,\n policy: GeneratedSkillLifecyclePolicy,\n now: number,\n): { metrics: GeneratedSkillTelemetryMetrics; flags: GeneratedSkillTelemetryFlags } {\n const knownOutcomeTotal = counts.successCount + counts.failureCount + counts.partialCount + counts.unknownCount;\n const successRate = knownOutcomeTotal > 0 ? counts.successCount / knownOutcomeTotal : null;\n const failureRate = knownOutcomeTotal > 0 ? counts.failureCount / knownOutcomeTotal : null;\n const partialRate = knownOutcomeTotal > 0 ? counts.partialCount / knownOutcomeTotal : null;\n const unknownRate = knownOutcomeTotal > 0 ? counts.unknownCount / knownOutcomeTotal : null;\n const exposureTotal = counts.activationCountTotal + counts.nearMissCount;\n const falsePositiveRate = exposureTotal > 0 ? counts.falsePositiveSignals / exposureTotal : null;\n const riskLevel = determineRiskLevel(proc, parseRecipeOrRaw(proc.recipeJson));\n const { falsePositiveRate: demoteFpRate, minSamples: demoteMinSamples } = effectiveDemoteThresholdsForRisk(\n riskLevel,\n policy,\n );\n const generatedAt = proc.skillGeneratedAt ?? proc.promotedAt ?? proc.updatedAt ?? proc.createdAt ?? now;\n const archiveBaselineAt = proc.skillState === \"demoted\" ? (proc.skillStateChangedAt ?? generatedAt) : generatedAt;\n const archiveCandidate =\n counts.activationCountTotal === 0 && archiveBaselineAt <= now - policy.archiveAfterUnusedDays * 24 * 60 * 60;\n const promotionCandidate =\n proc.skillState !== \"demoted\" &&\n proc.skillState !== \"archived\" &&\n proc.skillState !== \"trusted\" &&\n proc.skillState !== \"uninstalled\" &&\n proc.skillState !== \"rejected\" &&\n counts.successfulUsesWithoutCorrection >= policy.promoteAfterSuccessfulUses;\n const overTriggering =\n exposureTotal >= demoteMinSamples && falsePositiveRate != null && falsePositiveRate >= demoteFpRate;\n const revisionCandidate =\n counts.nearMissCount >= policy.revisionNearMissThreshold && counts.skippedCount >= counts.consideredCount;\n const unblockAfterCleanUses = policy.unblockAfterCleanUses ?? 0;\n const unblockCandidate =\n unblockAfterCleanUses > 0 &&\n (proc.skillState === \"demoted\" || proc.skillState === \"archived\") &&\n counts.cleanUsesAfterDemotion >= unblockAfterCleanUses;\n\n return {\n metrics: {\n activationCountPerWeek: counts.activationCountPerWeek,\n activationCountTotal: counts.activationCountTotal,\n nearMissCount: counts.nearMissCount,\n falsePositiveSignals: counts.falsePositiveSignals,\n falseNegativeSignals: counts.falseNegativeSignals,\n correctionCount: counts.correctionCount,\n repeatedCorrectionCount: counts.repeatedCorrectionCount,\n lastUsedAt: counts.lastUsedAt,\n successCount: counts.successCount,\n failureCount: counts.failureCount,\n partialCount: counts.partialCount,\n unknownCount: counts.unknownCount,\n successRate,\n failureRate,\n partialRate,\n unknownRate,\n successfulUsesWithoutCorrection: counts.successfulUsesWithoutCorrection,\n consideredCount: counts.consideredCount,\n skippedCount: counts.skippedCount,\n savedToolCalls: counts.savedToolCalls,\n savedTimeMs: counts.savedTimeMs,\n falsePositiveRate,\n cleanUsesAfterDemotion: counts.cleanUsesAfterDemotion,\n },\n flags: {\n promotionCandidate,\n overTriggering,\n revisionCandidate,\n neverUsed: counts.activationCountTotal === 0,\n archiveCandidate,\n unblockCandidate,\n },\n };\n}\n\nfunction summarizeSkillTelemetry(\n proc: ProcedureEntry,\n activations: GeneratedSkillTelemetryEntry[],\n policy: GeneratedSkillLifecyclePolicy,\n now: number,\n): {\n metrics: GeneratedSkillTelemetryMetrics;\n flags: GeneratedSkillTelemetryFlags;\n riskLevel: \"low\" | \"medium\" | \"high\";\n} {\n const weekAgo = now - 7 * 24 * 60 * 60;\n\n // For experimental and trusted skills, evaluate only activations since skill_generated_at.\n // For demoted/archived skills, evaluate only activations since skill_state_changed_at.\n // This prevents pre-reset telemetry from blocking promotion or re-triggering demotion\n // after an operator manually resets or the system auto-unblocks a skill.\n const isExperimentalOrTrusted =\n proc.skillState === \"experimental\" || proc.skillState === \"trusted\" || proc.skillState === \"draft\";\n const isDemotedOrArchived = proc.skillState === \"demoted\" || proc.skillState === \"archived\";\n let evalWindowStart = 0;\n if (isExperimentalOrTrusted && proc.skillGeneratedAt !== null && proc.skillGeneratedAt !== undefined) {\n evalWindowStart = proc.skillGeneratedAt;\n } else if (isDemotedOrArchived && proc.skillStateChangedAt !== null && proc.skillStateChangedAt !== undefined) {\n evalWindowStart = proc.skillStateChangedAt;\n }\n const evalActivations = evalWindowStart > 0 ? activations.filter((a) => a.createdAt >= evalWindowStart) : activations;\n\n let activationCountPerWeek = 0;\n let activationCountTotal = 0;\n let nearMissCount = 0;\n let falsePositiveSignals = 0;\n let falseNegativeSignals = 0;\n let correctionCount = 0;\n let lastUsedAt: number | null = null;\n let successCount = 0;\n let failureCount = 0;\n let partialCount = 0;\n let unknownCount = 0;\n let successfulUsesWithoutCorrection = 0;\n let consideredCount = 0;\n let skippedCount = 0;\n let savedToolCalls = 0;\n let savedTimeMs = 0;\n\n // For unblock tracking: count clean (uncorrected) successful activations since the\n // most recent demotion/archive state change. Use skill_state_changed_at as the\n // reset baseline — it tracks state transitions exclusively and is not affected by\n // other procedure updates (e.g., upsertProcedure).\n const demotedOrArchivedAt =\n proc.skillState === \"demoted\" || proc.skillState === \"archived\"\n ? (proc.skillStateChangedAt ?? proc.updatedAt ?? 0)\n : null;\n let cleanUsesAfterDemotion = 0;\n\n for (const activation of evalActivations) {\n const calls = activation.savedToolCalls ?? 0;\n const timeMs = activation.savedTimeMs ?? 0;\n savedToolCalls += calls > 0 ? calls : 0;\n savedTimeMs += timeMs > 0 ? timeMs : 0;\n if (activation.userCorrection || activation.causedRework) falsePositiveSignals++;\n if (activation.userCorrection) correctionCount++;\n if (activation.decision === \"selected\") {\n activationCountTotal++;\n if (activation.createdAt >= weekAgo) activationCountPerWeek++;\n lastUsedAt = lastUsedAt == null ? activation.createdAt : Math.max(lastUsedAt, activation.createdAt);\n if (activation.taskOutcome === \"success\") {\n successCount++;\n if (!activation.userCorrection) {\n successfulUsesWithoutCorrection++;\n // Count clean uses recorded after a demotion reset\n if (demotedOrArchivedAt != null && activation.createdAt > demotedOrArchivedAt) {\n cleanUsesAfterDemotion++;\n }\n }\n } else if (activation.taskOutcome === \"failure\") {\n failureCount++;\n } else if (activation.taskOutcome === \"partial\") {\n partialCount++;\n } else {\n unknownCount++;\n }\n } else {\n nearMissCount++;\n if (activation.decision === \"considered\") consideredCount++;\n if (activation.decision === \"skipped\") skippedCount++;\n }\n if (activation.falseNegativeSignal) falseNegativeSignals++;\n }\n const repeatedCorrectionCount = countRepeatedUserCorrectionBuckets(evalActivations, now);\n\n const knownOutcomeTotal = successCount + failureCount + partialCount + unknownCount;\n const successRate = knownOutcomeTotal > 0 ? successCount / knownOutcomeTotal : null;\n const failureRate = knownOutcomeTotal > 0 ? failureCount / knownOutcomeTotal : null;\n const partialRate = knownOutcomeTotal > 0 ? partialCount / knownOutcomeTotal : null;\n const unknownRate = knownOutcomeTotal > 0 ? unknownCount / knownOutcomeTotal : null;\n const exposureTotal = activationCountTotal + nearMissCount;\n const falsePositiveRate = exposureTotal > 0 ? falsePositiveSignals / exposureTotal : null;\n const riskLevel = determineRiskLevel(proc, parseRecipeOrRaw(proc.recipeJson));\n const { falsePositiveRate: demoteFpRate, minSamples: demoteMinSamples } = effectiveDemoteThresholdsForRisk(\n riskLevel,\n policy,\n );\n const generatedAt = proc.skillGeneratedAt ?? proc.promotedAt ?? proc.updatedAt ?? proc.createdAt ?? now;\n const archiveBaselineAt = proc.skillState === \"demoted\" ? (proc.skillStateChangedAt ?? generatedAt) : generatedAt;\n const lastActivityAt = lastUsedAt ?? archiveBaselineAt;\n const promotionCandidate =\n proc.skillState !== \"demoted\" &&\n proc.skillState !== \"archived\" &&\n proc.skillState !== \"trusted\" &&\n proc.skillState !== \"uninstalled\" &&\n proc.skillState !== \"rejected\" &&\n successfulUsesWithoutCorrection >= policy.promoteAfterSuccessfulUses;\n const unblockAfterCleanUses = policy.unblockAfterCleanUses ?? 0;\n const unblockCandidate =\n unblockAfterCleanUses > 0 &&\n (proc.skillState === \"demoted\" || proc.skillState === \"archived\") &&\n cleanUsesAfterDemotion >= unblockAfterCleanUses;\n const archiveCandidate =\n !promotionCandidate && !unblockCandidate && now - lastActivityAt >= policy.archiveAfterUnusedDays * 24 * 60 * 60;\n const overTriggering =\n exposureTotal >= demoteMinSamples && falsePositiveRate != null && falsePositiveRate >= demoteFpRate;\n const revisionCandidate = nearMissCount >= policy.revisionNearMissThreshold && skippedCount >= consideredCount;\n\n return {\n metrics: {\n activationCountPerWeek,\n activationCountTotal,\n nearMissCount,\n falsePositiveSignals,\n falseNegativeSignals,\n correctionCount,\n repeatedCorrectionCount,\n lastUsedAt,\n successCount,\n failureCount,\n partialCount,\n unknownCount,\n successRate,\n failureRate,\n partialRate,\n unknownRate,\n successfulUsesWithoutCorrection,\n consideredCount,\n skippedCount,\n savedToolCalls,\n savedTimeMs,\n falsePositiveRate,\n cleanUsesAfterDemotion,\n },\n flags: {\n promotionCandidate,\n overTriggering,\n revisionCandidate,\n neverUsed: activationCountTotal === 0,\n archiveCandidate,\n unblockCandidate,\n },\n riskLevel,\n };\n}\n\nfunction _summarizeSkillTelemetryFromRollups(\n db: DatabaseSync,\n proc: ProcedureEntry,\n canonicalSkillName: string,\n policy: GeneratedSkillLifecyclePolicy,\n now: number,\n): {\n metrics: GeneratedSkillTelemetryMetrics;\n flags: GeneratedSkillTelemetryFlags;\n riskLevel: \"low\" | \"medium\" | \"high\";\n} {\n const r = readTelemetryRollup(db, proc.id);\n if (!r) {\n return summarizeSkillTelemetry(proc, skillTelemetryEntries(db, canonicalSkillName, proc.id), policy, now);\n }\n const weekAgo = now - 7 * 24 * 60 * 60;\n const activationCountPerWeek = countSelectedActivationsSince(db, proc.id, weekAgo);\n const activationCountTotal = r.gst_sel_total;\n const nearMissCount = r.gst_near_miss_total;\n const falsePositiveSignals = r.gst_fp_signals_total;\n const falseNegativeSignals = r.gst_fn_signals_total;\n const correctionCount = r.gst_user_correction_total;\n const lastUsedAt = r.gst_last_selected_at;\n const successCount = r.gst_outcome_success;\n const failureCount = r.gst_outcome_failure;\n const partialCount = r.gst_outcome_partial;\n const unknownCount = r.gst_outcome_unknown;\n const successfulUsesWithoutCorrection = r.gst_success_clear_total;\n const consideredCount = r.gst_considered_total;\n const skippedCount = r.gst_skipped_total;\n const savedToolCalls = r.gst_saved_tool_calls_sum;\n const savedTimeMs = r.gst_saved_time_ms_sum;\n const repeatedCorrectionCount = countRepeatedUserCorrectionBuckets(\n skillTelemetryEntries(db, canonicalSkillName, proc.id),\n now,\n );\n const demotedOrArchivedAt =\n proc.skillState === \"demoted\" || proc.skillState === \"archived\"\n ? (proc.skillStateChangedAt ?? proc.updatedAt ?? 0)\n : null;\n let cleanUsesAfterDemotion = 0;\n if (demotedOrArchivedAt != null) {\n const postDemotionActivations = skillTelemetryEntries(db, canonicalSkillName, proc.id).filter(\n (a) =>\n a.decision === \"selected\" &&\n a.taskOutcome === \"success\" &&\n !a.userCorrection &&\n a.createdAt > demotedOrArchivedAt,\n );\n cleanUsesAfterDemotion = postDemotionActivations.length;\n }\n const riskLevel = determineRiskLevel(proc, parseRecipeOrRaw(proc.recipeJson));\n const summary = deriveMetricsAndFlags(\n {\n activationCountPerWeek,\n activationCountTotal,\n nearMissCount,\n falsePositiveSignals,\n falseNegativeSignals,\n correctionCount,\n repeatedCorrectionCount,\n lastUsedAt,\n successCount,\n failureCount,\n partialCount,\n unknownCount,\n successfulUsesWithoutCorrection,\n consideredCount,\n skippedCount,\n savedToolCalls,\n savedTimeMs,\n cleanUsesAfterDemotion,\n },\n proc,\n policy,\n now,\n );\n return { ...summary, riskLevel };\n}\n\nfunction desiredLifecycleTransition(\n currentState: GeneratedSkillLifecycleState,\n flags: GeneratedSkillTelemetryFlags,\n metrics: GeneratedSkillTelemetryMetrics,\n policy: GeneratedSkillLifecyclePolicy,\n riskLevel: \"low\" | \"medium\" | \"high\",\n): { state: GeneratedSkillLifecycleState; reason: string } | null {\n // Uninstalled and rejected are terminal — no automatic transitions out.\n if (currentState === \"uninstalled\" || currentState === \"rejected\") return null;\n\n if (currentState !== \"archived\" && flags.archiveCandidate) {\n return {\n state: \"archived\",\n reason: `auto-archived after ${policy.archiveAfterUnusedDays} days without selected-activation activity`,\n };\n }\n // Allow automatic unblocking of demoted/archived skills when they accumulate\n // enough clean uses after the demotion reset AND are not currently overtriggering.\n // Evaluate this before the overTriggering check so archived skills with high historical\n // FP rates can transition directly to experimental rather than demoted, but only if\n // the current post-demotion false-positive rate is acceptable.\n if (flags.unblockCandidate && !flags.overTriggering) {\n return {\n state: \"experimental\",\n reason: `auto-unblocked after ${metrics.cleanUsesAfterDemotion} clean activations since demotion`,\n };\n }\n if (currentState !== \"demoted\" && currentState !== \"archived\" && flags.overTriggering) {\n const { falsePositiveRate: demoteFpRate, minSamples: demoteMinSamples } = effectiveDemoteThresholdsForRisk(\n riskLevel,\n policy,\n );\n return {\n state: \"demoted\",\n reason: `auto-demoted (${riskLevel} risk): FP rate ${Math.round((metrics.falsePositiveRate ?? 0) * 100)}% reached threshold ${Math.round(demoteFpRate * 100)}% over ${demoteMinSamples}+ activations`,\n };\n }\n if (currentState === \"experimental\" && flags.promotionCandidate) {\n return {\n state: \"trusted\",\n reason: `auto-promoted after ${metrics.successfulUsesWithoutCorrection} successful activations without correction`,\n };\n }\n return null;\n}\n\nexport function refreshGeneratedSkillLifecycleState(\n db: DatabaseSync,\n skillName: string,\n policy?: GeneratedSkillLifecyclePolicy,\n now?: number,\n procedureId?: string,\n): ProcedureEntry | null;\nexport function refreshGeneratedSkillLifecycleState(\n db: DatabaseSync,\n skillName: string,\n policy: GeneratedSkillLifecyclePolicy,\n now: number,\n returnMetrics: true,\n procedureId?: string,\n): {\n proc: ProcedureEntry;\n metrics: GeneratedSkillTelemetryMetrics;\n flags: GeneratedSkillTelemetryFlags;\n riskLevel: \"low\" | \"medium\" | \"high\";\n} | null;\nexport function refreshGeneratedSkillLifecycleState(\n db: DatabaseSync,\n skillName: string,\n policy: GeneratedSkillLifecyclePolicy = DEFAULT_GENERATED_SKILL_LIFECYCLE_POLICY,\n now = Math.floor(Date.now() / 1000),\n returnMetricsOrProcedureId: boolean | string = false,\n procedureId?: string,\n):\n | ProcedureEntry\n | {\n proc: ProcedureEntry;\n metrics: GeneratedSkillTelemetryMetrics;\n flags: GeneratedSkillTelemetryFlags;\n riskLevel: \"low\" | \"medium\" | \"high\";\n }\n | null {\n const returnMetrics = returnMetricsOrProcedureId === true;\n const resolvedProcedureId = typeof returnMetricsOrProcedureId === \"string\" ? returnMetricsOrProcedureId : procedureId;\n const proc =\n resolvedProcedureId != null\n ? findProcedureById(db, resolvedProcedureId)\n : findGeneratedSkillProcedure(db, skillName);\n if (!proc?.skillPath) return null;\n const canonicalSkillName = skillPathBasename(proc.skillPath);\n const activations = skillTelemetryEntries(db, canonicalSkillName, proc.id);\n const { metrics, flags, riskLevel } = summarizeSkillTelemetry(proc, activations, policy, now);\n const currentState = proc.skillState ?? \"experimental\";\n const transition = desiredLifecycleTransition(currentState, flags, metrics, policy, riskLevel);\n const updatedProc =\n transition == null\n ? proc\n : (setGeneratedSkillLifecycleState(db, canonicalSkillName, transition.state, transition.reason, now, proc.id) ??\n proc);\n if (returnMetrics) {\n return { proc: updatedProc, metrics, flags, riskLevel };\n }\n return updatedProc;\n}\n\nexport function buildGeneratedSkillTelemetryReport(\n db: DatabaseSync,\n options?: {\n skillName?: string;\n policy?: Partial<GeneratedSkillLifecyclePolicy>;\n recentActivationLimit?: number;\n now?: number;\n },\n): GeneratedSkillTelemetryReport {\n const policy = {\n ...DEFAULT_GENERATED_SKILL_LIFECYCLE_POLICY,\n ...(options?.policy ?? {}),\n };\n const now = options?.now ?? Math.floor(Date.now() / 1000);\n const recentLimit = options?.recentActivationLimit ?? 10;\n const procedures = listGeneratedSkillProcedures(db).filter((proc) => {\n if (!options?.skillName) return true;\n return proc.skillPath != null && skillPathBasename(proc.skillPath) === options.skillName;\n });\n const refreshResults = new Map<\n string,\n {\n proc: ProcedureEntry;\n metrics: GeneratedSkillTelemetryMetrics;\n flags: GeneratedSkillTelemetryFlags;\n riskLevel: \"low\" | \"medium\" | \"high\";\n }\n >();\n for (const proc of procedures) {\n if (proc.skillPath) {\n const skillName = skillPathBasename(proc.skillPath);\n const result = refreshGeneratedSkillLifecycleState(db, skillName, policy, now, true, proc.id);\n if (result) refreshResults.set(proc.id, result);\n }\n }\n const rows = listGeneratedSkillProcedures(db)\n .filter((proc) => {\n if (!options?.skillName) return true;\n return proc.skillPath != null && skillPathBasename(proc.skillPath) === options.skillName;\n })\n .map((proc) => {\n const skillName = skillPathBasename(proc.skillPath ?? proc.taskPattern);\n const cachedResult = refreshResults.get(proc.id);\n let procFresh: ProcedureEntry = proc;\n let metrics: GeneratedSkillTelemetryMetrics;\n let flags: GeneratedSkillTelemetryFlags;\n let riskLevel: \"low\" | \"medium\" | \"high\";\n if (cachedResult) {\n ({ proc: procFresh, metrics, flags, riskLevel } = cachedResult);\n } else {\n const activations = skillTelemetryEntries(db, skillName, proc.id);\n const summary = summarizeSkillTelemetry(proc, activations, policy, now);\n metrics = summary.metrics;\n flags = summary.flags;\n riskLevel = summary.riskLevel;\n }\n const currentState = procFresh.skillState ?? \"experimental\";\n const stateReason = procFresh.skillStateReason ?? null;\n const generatedAt = procFresh.skillGeneratedAt ?? procFresh.promotedAt ?? null;\n let recommendation: \"promote\" | \"demote\" | \"archive\" | \"revise\" | \"unblock\" | \"observe\";\n\n if (currentState === \"uninstalled\" || currentState === \"rejected\") {\n recommendation = \"observe\";\n } else if (currentState === \"experimental\" && stateReason?.startsWith(\"auto-unblocked\")) {\n recommendation = \"unblock\";\n } else if (currentState === \"archived\" && flags.archiveCandidate) {\n recommendation = \"observe\";\n } else if (currentState === \"demoted\" && flags.overTriggering) {\n recommendation = \"observe\";\n } else if (flags.unblockCandidate && !flags.overTriggering) {\n recommendation = \"unblock\";\n } else if (flags.archiveCandidate) {\n recommendation = \"archive\";\n } else if (flags.overTriggering) {\n recommendation = \"demote\";\n } else if (flags.promotionCandidate) {\n recommendation = \"promote\";\n } else if (flags.revisionCandidate) {\n recommendation = \"revise\";\n } else {\n recommendation = \"observe\";\n }\n return {\n procedureId: procFresh.id,\n skillName,\n skillPath: procFresh.skillPath ?? skillName,\n skillVersion: procFresh.skillVersion ?? 1,\n riskLevel,\n state: currentState,\n stateReason,\n generatedAt,\n metrics,\n flags,\n recommendation,\n recentActivations: skillTelemetryRecentEntries(db, skillName, recentLimit, procFresh.id),\n } satisfies GeneratedSkillTelemetryReportRow;\n })\n .sort((a, b) => a.skillName.localeCompare(b.skillName));\n return {\n generatedAt: new Date(now * 1000).toISOString(),\n policy,\n totalSkills: rows.length,\n rows,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Disk reconciler (#1391) — detect DB rows whose skill file no longer exists\n// ---------------------------------------------------------------------------\n\nexport type GeneratedSkillDoctorRow = {\n procedureId: string;\n skillName: string;\n skillPath: string;\n state: GeneratedSkillLifecycleState;\n issue: \"missing_on_disk\" | \"stat_error\";\n resolvedAbsolutePath: string;\n fixed: boolean;\n errorCode?: string;\n errorMessage?: string;\n};\n\nexport type GeneratedSkillDoctorReport = {\n checkedAt: string;\n totalChecked: number;\n issues: GeneratedSkillDoctorRow[];\n fixedCount: number;\n failedFixCount: number;\n fixedProcedureIds: string[];\n};\n\n/**\n * Reconcile the DB's generated-skill records against disk.\n *\n * For each procedure row that is marked promoted (`promoted_to_skill = 1`) and has a\n * `skill_path`, this function requires the resolved `SKILL.md` file to exist on disk.\n * Rows whose SKILL.md is missing are reported as `\"missing_on_disk\"` and, when `fix` is\n * true, non-terminal rows are transitioned to the `\"uninstalled\"` state. Rejected rows\n * remain rejected because that terminal operator decision must not be overwritten by\n * disk reconciliation.\n *\n * @param workspaceRoot Absolute path used to resolve relative skill paths. Defaults to `OPENCLAW_WORKSPACE` env var, then `process.cwd()`.\n * @param fix When true, update missing rows to `uninstalled` in the DB.\n */\nexport function reconcileGeneratedSkillDiskState(\n db: DatabaseSync,\n opts: { workspaceRoot?: string; fix?: boolean; now?: number } = {},\n): GeneratedSkillDoctorReport {\n const workspaceRoot = opts.workspaceRoot ?? getEnv(\"OPENCLAW_WORKSPACE\") ?? process.cwd();\n const fix = opts.fix ?? false;\n const now = opts.now ?? Math.floor(Date.now() / 1000);\n const procedures = listGeneratedSkillProcedures(db);\n const issues: GeneratedSkillDoctorRow[] = [];\n const fixedProcedureIds: string[] = [];\n let totalChecked = 0;\n\n for (const proc of procedures) {\n if (!proc.skillPath) continue;\n // Skip terminal rows — they are already correctly modelled and must not be\n // overwritten by disk reconciliation.\n if (proc.skillState === \"uninstalled\" || proc.skillState === \"rejected\") continue;\n\n totalChecked++;\n\n // Accept either a SKILL.md file path or the parent directory, but always\n // require the actual SKILL.md file to exist. A bare directory is not an\n // installed generated skill.\n const { dirPath, skillMdPath } = resolveSkillMdPath(proc.skillPath, workspaceRoot);\n\n let statError: unknown;\n let missing = false;\n try {\n missing = !statSync(skillMdPath).isFile();\n } catch (err) {\n statError = err;\n missing = true;\n }\n if (!missing) continue;\n\n // Derive skill name from the directory component of the path (not the raw skill_path\n // which might include a SKILL.md suffix or be a full directory path).\n const skillName = skillPathBasename(dirPath);\n const errorCode =\n typeof statError === \"object\" && statError !== null && \"code\" in statError\n ? String((statError as { code?: unknown }).code)\n : undefined;\n const isMissingOnDisk = !statError || errorCode === \"ENOENT\";\n let fixed = false;\n if (fix && isMissingOnDisk) {\n // Use the full skill_path for the lookup so findGeneratedSkillProcedure\n // matches on exact path regardless of basename edge cases.\n const updated = setGeneratedSkillLifecycleState(\n db,\n proc.skillPath,\n \"uninstalled\",\n \"operator-confirmed doctor --fix: skill file no longer exists on disk\",\n now,\n );\n fixed = updated?.skillState === \"uninstalled\";\n if (fixed) fixedProcedureIds.push(proc.id);\n }\n issues.push({\n procedureId: proc.id,\n skillName,\n skillPath: proc.skillPath,\n state: proc.skillState ?? \"experimental\",\n issue: isMissingOnDisk ? \"missing_on_disk\" : \"stat_error\",\n resolvedAbsolutePath: skillMdPath,\n fixed,\n ...(errorCode ? { errorCode } : {}),\n ...(statError instanceof Error ? { errorMessage: statError.message } : {}),\n });\n }\n\n return {\n checkedAt: new Date(now * 1000).toISOString(),\n totalChecked,\n issues,\n fixedCount: fixedProcedureIds.length,\n failedFixCount: fix ? issues.filter((issue) => !issue.fixed).length : 0,\n fixedProcedureIds,\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"housekeeping.js","names":[],"sources":["../../../backends/facts-db/housekeeping.ts"],"sourcesContent":["/**\n * Misc DB ops: pruning logs, stats helpers, scope pruning (Issue #954 split).\n */\nimport { randomUUID } from \"node:crypto\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport type { DatabaseSync } from \"node:sqlite\";\n\nimport type { MemoryEntry, ScopeFilter } from \"../../types/memory.js\";\nimport { getLanguageKeywordsFilePath } from \"../../utils/language-keywords.js\";\nimport { createTransaction } from \"../../utils/sqlite-transaction.js\";\nimport { rebuildFtsIndex } from \"../../services/fts-search.js\";\nimport { rowToMemoryEntry } from \"./row-mapper.js\";\n\nexport function pruneOrphanedLinks(db: DatabaseSync): number {\n const result = db\n .prepare(\n `DELETE FROM memory_links\n WHERE NOT EXISTS (SELECT 1 FROM facts WHERE facts.id = memory_links.source_fact_id)\n OR NOT EXISTS (SELECT 1 FROM facts WHERE facts.id = memory_links.target_fact_id)`,\n )\n .run();\n return Number(result.changes ?? 0);\n}\n\n/**\n * SQL COUNT of active (non-superseded, non-expired) facts for a given category.\n * Replaces the previous O(n) approach of loading all rows and filtering in JS.\n */\nexport function countActiveFactsByCategory(db: DatabaseSync, category: string): number {\n const nowSec = Math.floor(Date.now() / 1000);\n const row = db\n .prepare(\n `SELECT COUNT(*) as count FROM facts\n WHERE category = ?\n AND superseded_at IS NULL\n AND (expires_at IS NULL OR expires_at > ?)`,\n )\n .get(category, nowSec) as { count: number } | undefined;\n return row?.count ?? 0;\n}\n\nexport function pruneLogTables(db: DatabaseSync, retentionDays: number): number {\n if (retentionDays <= 0) return 0;\n const cutoff = Math.floor(Date.now() / 1000) - retentionDays * 86400;\n const tx = createTransaction(db, () => {\n const recall = db.prepare(\"DELETE FROM recall_log WHERE occurred_at < ?\").run(cutoff);\n const reinforcement = db.prepare(\"DELETE FROM reinforcement_log WHERE occurred_at < ?\").run(cutoff);\n const feedback = db.prepare(\"DELETE FROM feedback_trajectories WHERE created_at < ?\").run(cutoff);\n return Number(recall.changes ?? 0) + Number(reinforcement.changes ?? 0) + Number(feedback.changes ?? 0);\n });\n return tx();\n}\n\n/** Run standard FTS5 optimize maintenance on `facts_fts`. */\nexport function optimizeFts(db: DatabaseSync): void {\n db.exec(`INSERT INTO facts_fts(facts_fts) VALUES('optimize')`);\n}\n\nconst EXPECTED_FTS_TRIGGERS = [\"facts_ai\", \"facts_au\", \"facts_ad\"] as const;\n\nexport type FtsConsistencySnapshot = {\n factsCount: number;\n ftsCount: number;\n ftsTableExists: boolean;\n ftsTableSql: string | null;\n hasTagsColumn: boolean;\n hasWhyColumn: boolean;\n presentTriggers: string[];\n missingTriggers: string[];\n missingFactsInFts: number;\n extraFtsRows: number;\n missingFactIdsSample: string[];\n extraFtsRowidsSample: number[];\n};\n\nexport type FtsTriggerProbeResult = {\n ok: boolean;\n insertVisible: boolean;\n updateVisible: boolean;\n deleteVisibleAfterDelete: boolean;\n error?: string;\n};\n\nfunction scalarCount(db: DatabaseSync, query: string, ...params: (string | number)[]): number {\n const row = db.prepare(query).get(...params) as { cnt: number } | undefined;\n return Number(row?.cnt ?? 0);\n}\n\nexport function getFtsConsistencySnapshot(db: DatabaseSync): FtsConsistencySnapshot {\n const factsCount = scalarCount(db, \"SELECT COUNT(*) AS cnt FROM facts\");\n const ftsRow = db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='facts_fts'`).get() as\n | { sql: string | null }\n | undefined;\n const ftsTableExists = !!ftsRow;\n const ftsTableSql = ftsRow?.sql ?? null;\n const hasTagsColumn = /\\btags\\b/i.test(ftsTableSql ?? \"\");\n const hasWhyColumn = /\\bwhy\\b/i.test(ftsTableSql ?? \"\");\n\n const triggerRows = db\n .prepare(\n `SELECT name FROM sqlite_master\n WHERE type = 'trigger' AND name IN ('facts_ai', 'facts_au', 'facts_ad')`,\n )\n .all() as Array<{ name: string }>;\n const presentTriggers = triggerRows.map((r) => r.name).sort();\n const missingTriggers = EXPECTED_FTS_TRIGGERS.filter((name) => !presentTriggers.includes(name));\n\n if (!ftsTableExists) {\n return {\n factsCount,\n ftsCount: 0,\n ftsTableExists,\n ftsTableSql,\n hasTagsColumn,\n hasWhyColumn,\n presentTriggers,\n missingTriggers,\n missingFactsInFts: factsCount,\n extraFtsRows: 0,\n missingFactIdsSample: [],\n extraFtsRowidsSample: [],\n };\n }\n\n const ftsCount = scalarCount(db, \"SELECT COUNT(*) AS cnt FROM facts_fts\");\n const missingFactsInFts = scalarCount(\n db,\n `SELECT COUNT(*) AS cnt\n FROM facts f\n WHERE NOT EXISTS (SELECT 1 FROM facts_fts t WHERE t.rowid = f.rowid)`,\n );\n const extraFtsRows = scalarCount(\n db,\n `SELECT COUNT(*) AS cnt\n FROM facts_fts t\n WHERE NOT EXISTS (SELECT 1 FROM facts f WHERE f.rowid = t.rowid)`,\n );\n const missingFactIdsSampleRows = db\n .prepare(\n `SELECT f.id\n FROM facts f\n WHERE NOT EXISTS (SELECT 1 FROM facts_fts t WHERE t.rowid = f.rowid)\n LIMIT 5`,\n )\n .all() as Array<{ id: string }>;\n const extraFtsRowidsSampleRows = db\n .prepare(\n `SELECT t.rowid\n FROM facts_fts t\n WHERE NOT EXISTS (SELECT 1 FROM facts f WHERE f.rowid = t.rowid)\n LIMIT 5`,\n )\n .all() as Array<{ rowid: number }>;\n\n return {\n factsCount,\n ftsCount,\n ftsTableExists,\n ftsTableSql,\n hasTagsColumn,\n hasWhyColumn,\n presentTriggers,\n missingTriggers,\n missingFactsInFts,\n extraFtsRows,\n missingFactIdsSample: missingFactIdsSampleRows.map((row) => row.id),\n extraFtsRowidsSample: extraFtsRowidsSampleRows.map((row) => Number(row.rowid)),\n };\n}\n\nexport function runFtsTriggerProbe(db: DatabaseSync): FtsTriggerProbeResult {\n const savepoint = `hm_fts_probe_${randomUUID().replace(/-/g, \"\")}`;\n const probeId = randomUUID();\n const tokenBase = randomUUID().replace(/-/g, \"\");\n const insertToken = `hmdoctorins${tokenBase.slice(0, 12)}`;\n const updateToken = `hmdoctorupd${tokenBase.slice(12, 24)}`;\n const nowSec = Math.floor(Date.now() / 1000);\n\n let insertVisible = false;\n let updateVisible = false;\n let deleteVisibleAfterDelete = true;\n\n db.exec(`SAVEPOINT ${savepoint}`);\n try {\n db.prepare(\n `INSERT INTO facts(id, text, category, importance, source, created_at)\n VALUES(?, ?, 'fact', 0.01, 'doctor-fts-probe', ?)`,\n ).run(probeId, insertToken, nowSec);\n\n const rowidRow = db.prepare(\"SELECT rowid FROM facts WHERE id = ?\").get(probeId) as { rowid: number } | undefined;\n if (!rowidRow?.rowid) {\n return {\n ok: false,\n insertVisible,\n updateVisible,\n deleteVisibleAfterDelete,\n error: \"probe insert rowid not found\",\n };\n }\n const rowid = Number(rowidRow.rowid);\n\n const insertMatchCount = scalarCount(\n db,\n \"SELECT COUNT(*) AS cnt FROM facts_fts WHERE rowid = ? AND facts_fts MATCH ?\",\n rowid,\n insertToken,\n );\n insertVisible = insertMatchCount === 1;\n\n db.prepare(\"UPDATE facts SET text = ? WHERE id = ?\").run(updateToken, probeId);\n const updateMatchCount = scalarCount(\n db,\n \"SELECT COUNT(*) AS cnt FROM facts_fts WHERE rowid = ? AND facts_fts MATCH ?\",\n rowid,\n updateToken,\n );\n const staleMatchCount = scalarCount(\n db,\n \"SELECT COUNT(*) AS cnt FROM facts_fts WHERE rowid = ? AND facts_fts MATCH ?\",\n rowid,\n insertToken,\n );\n updateVisible = updateMatchCount === 1 && staleMatchCount === 0;\n\n db.prepare(\"DELETE FROM facts WHERE id = ?\").run(probeId);\n const postDeleteCount = scalarCount(db, \"SELECT COUNT(*) AS cnt FROM facts_fts WHERE rowid = ?\", rowid);\n deleteVisibleAfterDelete = postDeleteCount > 0;\n\n return {\n ok: insertVisible && updateVisible && !deleteVisibleAfterDelete,\n insertVisible,\n updateVisible,\n deleteVisibleAfterDelete,\n };\n } catch (error) {\n return {\n ok: false,\n insertVisible,\n updateVisible,\n deleteVisibleAfterDelete,\n error: String(error),\n };\n } finally {\n try {\n db.exec(`ROLLBACK TO ${savepoint}`);\n } catch {\n // ignore cleanup errors\n }\n try {\n db.exec(`RELEASE ${savepoint}`);\n } catch {\n // ignore cleanup errors\n }\n }\n}\n\nexport function rebuildFtsIndexFromFacts(db: DatabaseSync): number {\n const tx = createTransaction(db, () => rebuildFtsIndex(db));\n return tx();\n}\n\nexport function freelistSpaceStats(db: DatabaseSync): {\n freelistCount: number;\n pageSize: number;\n freedBytes: number;\n freedMB: number;\n} {\n const freelistCountResult = db.prepare(\"PRAGMA freelist_count\").get() as { freelist_count: number } | undefined;\n const pageSizeResult = db.prepare(\"PRAGMA page_size\").get() as { page_size: number } | undefined;\n const freelistCount = freelistCountResult?.freelist_count ?? 0;\n const pageSize = pageSizeResult?.page_size ?? 4096;\n const freedBytes = freelistCount * pageSize;\n return {\n freelistCount,\n pageSize,\n freedBytes,\n freedMB: freedBytes / (1024 * 1024),\n };\n}\n\nexport function checkpointWalTruncate(db: DatabaseSync): void {\n db.exec(\"PRAGMA wal_checkpoint(TRUNCATE)\");\n}\n\nexport function vacuumAndCheckpoint(db: DatabaseSync): void {\n db.exec(\"VACUUM\");\n checkpointWalTruncate(db);\n}\n\nexport function statsReflection(db: DatabaseSync): {\n reflectionPatternsCount: number;\n reflectionRulesCount: number;\n} {\n const patternsRow = db\n .prepare(\n `SELECT COUNT(*) as count FROM facts WHERE superseded_at IS NULL AND source = 'reflection' AND category = 'pattern'`,\n )\n .get() as { count: number } | undefined;\n const rulesRow = db\n .prepare(\n `SELECT COUNT(*) as count FROM facts WHERE superseded_at IS NULL AND source = 'reflection' AND category = 'rule'`,\n )\n .get() as { count: number } | undefined;\n return {\n reflectionPatternsCount: patternsRow?.count ?? 0,\n reflectionRulesCount: rulesRow?.count ?? 0,\n };\n}\n\nexport function selfCorrectionIncidentsCount(db: DatabaseSync): number {\n const row = db\n .prepare(`SELECT COUNT(*) as count FROM facts WHERE superseded_at IS NULL AND source = 'self-correction'`)\n .get() as { count: number } | undefined;\n return row?.count ?? 0;\n}\n\nexport function countSupersededFacts(db: DatabaseSync): number {\n const row = db.prepare(\"SELECT COUNT(*) as count FROM facts WHERE superseded_at IS NOT NULL\").get() as\n | { count: number }\n | undefined;\n return row?.count ?? 0;\n}\n\nexport function countVerifiedFacts(db: DatabaseSync): number {\n try {\n const row = db.prepare(\"SELECT COUNT(*) as count FROM verified_facts\").get() as { count: number } | undefined;\n return row?.count ?? 0;\n } catch {\n return 0;\n }\n}\n\n/** Number of active (non-superseded, non-expired) facts created at-or-after `sinceSec`. */\nexport function countActiveFactsCreatedSince(db: DatabaseSync, sinceSec: number): number {\n const nowSec = Math.floor(Date.now() / 1000);\n const row = db\n .prepare(\n `SELECT COUNT(*) as count FROM facts\n WHERE superseded_at IS NULL\n AND (expires_at IS NULL OR expires_at > ?)\n AND created_at >= ?`,\n )\n .get(nowSec, sinceSec) as { count: number } | undefined;\n return row?.count ?? 0;\n}\n\n/** Recent ingestion activity for the rich `stats` view. Cheap (3× COUNT). */\nexport function recentActivity(db: DatabaseSync): {\n last24h: number;\n last7d: number;\n last30d: number;\n newestCreatedAtSec: number | null;\n oldestActiveCreatedAtSec: number | null;\n} {\n const nowSec = Math.floor(Date.now() / 1000);\n const day = 86400;\n const last24h = countActiveFactsCreatedSince(db, nowSec - day);\n const last7d = countActiveFactsCreatedSince(db, nowSec - 7 * day);\n const last30d = countActiveFactsCreatedSince(db, nowSec - 30 * day);\n const minMaxRow = db\n .prepare(\n `SELECT MIN(created_at) as min_ts, MAX(created_at) as max_ts FROM facts\n WHERE superseded_at IS NULL AND (expires_at IS NULL OR expires_at > ?)`,\n )\n .get(nowSec) as { min_ts: number | null; max_ts: number | null } | undefined;\n return {\n last24h,\n last7d,\n last30d,\n newestCreatedAtSec: minMaxRow?.max_ts ?? null,\n oldestActiveCreatedAtSec: minMaxRow?.min_ts ?? null,\n };\n}\n\nexport function countBySource(db: DatabaseSync, source: string): number {\n const row = db\n .prepare(\"SELECT COUNT(*) as count FROM facts WHERE superseded_at IS NULL AND source = ?\")\n .get(source) as { count: number } | undefined;\n return row?.count ?? 0;\n}\n\nexport function languageKeywordsCount(): number {\n const filePath = getLanguageKeywordsFilePath();\n if (!filePath || !existsSync(filePath)) return 0;\n\n try {\n const data = JSON.parse(readFileSync(filePath, \"utf-8\"));\n let count = 0;\n\n const translations = data.translations ?? {};\n for (const lang of Object.values(translations)) {\n for (const [_key, val] of Object.entries(lang as Record<string, unknown>)) {\n if (Array.isArray(val)) count += val.length;\n }\n }\n\n const triggerStructures = data.triggerStructures ?? {};\n for (const val of Object.values(triggerStructures)) {\n if (Array.isArray(val)) count += val.length;\n }\n\n const directiveSignals = data.directiveSignalsByCategory ?? {};\n for (const val of Object.values(directiveSignals)) {\n if (Array.isArray(val)) count += val.length;\n }\n\n const reinforcementCategories = data.reinforcementCategories ?? {};\n for (const val of Object.values(reinforcementCategories)) {\n if (Array.isArray(val)) count += val.length;\n }\n\n return count;\n } catch {\n return 0;\n }\n}\n\nexport function statsBySource(db: DatabaseSync): Record<string, number> {\n const rows = db\n .prepare(\"SELECT source, COUNT(*) as count FROM facts WHERE superseded_at IS NULL GROUP BY source\")\n .all() as Array<{ source: string; count: number }>;\n return Object.fromEntries(rows.map((r) => [r.source, r.count]));\n}\n\nexport function uniqueScopes(db: DatabaseSync): Array<{ scope: string; scopeTarget: string | null }> {\n const rows = db\n .prepare(\"SELECT DISTINCT scope, scope_target as scopeTarget FROM facts WHERE scope IS NOT NULL\")\n .all() as Array<{ scope: string; scopeTarget: string | null }>;\n return rows;\n}\n\nexport function scopeStats(db: DatabaseSync): Array<{\n scope: string;\n scopeTarget: string | null;\n count: number;\n}> {\n const rows = db\n .prepare(\n \"SELECT scope, scope_target as scopeTarget, COUNT(*) as count FROM facts WHERE scope IS NOT NULL GROUP BY scope, scope_target\",\n )\n .all() as Array<{\n scope: string;\n scopeTarget: string | null;\n count: number;\n }>;\n return rows;\n}\n\n/**\n * Return all fact IDs that `pruneScopedFacts(scopeFilter)` would delete.\n * Use this *before* calling `pruneScopedFacts` to gather IDs for vector cleanup.\n * Excludes pinned facts in `verified_facts` (same guard as the DELETE).\n */\nexport function listScopedFactIdsPendingPrune(db: DatabaseSync, scopeFilter: ScopeFilter): string[] {\n const conditions: string[] = [];\n const params: (string | null)[] = [];\n\n if (scopeFilter.global) {\n conditions.push(`(scope = 'global')`);\n }\n if (scopeFilter.userId !== undefined) {\n conditions.push(`(scope = 'user' AND scope_target = ?)`);\n params.push(scopeFilter.userId);\n }\n if (scopeFilter.agentId !== undefined) {\n conditions.push(`(scope = 'agent' AND scope_target = ?)`);\n params.push(scopeFilter.agentId);\n }\n if (scopeFilter.sessionId !== undefined) {\n conditions.push(`(scope = 'session' AND scope_target = ?)`);\n params.push(scopeFilter.sessionId);\n }\n\n if (conditions.length === 0) return [];\n\n const rows = db\n .prepare(\n `SELECT id FROM facts\n WHERE (${conditions.join(\" OR \")})\n AND id NOT IN (SELECT fact_id FROM verified_facts)`,\n )\n .all(...params) as Array<{ id: string }>;\n return rows.map((r) => r.id);\n}\n\nexport function pruneScopedFacts(db: DatabaseSync, scopeFilter: ScopeFilter): number {\n const conditions: string[] = [];\n const params: (string | null)[] = [];\n\n if (scopeFilter.global) {\n conditions.push(`(scope = 'global')`);\n }\n if (scopeFilter.userId !== undefined) {\n conditions.push(`(scope = 'user' AND scope_target = ?)`);\n params.push(scopeFilter.userId);\n }\n if (scopeFilter.agentId !== undefined) {\n conditions.push(`(scope = 'agent' AND scope_target = ?)`);\n params.push(scopeFilter.agentId);\n }\n if (scopeFilter.sessionId !== undefined) {\n conditions.push(`(scope = 'session' AND scope_target = ?)`);\n params.push(scopeFilter.sessionId);\n }\n\n if (conditions.length === 0) return 0;\n\n const linkCleanupQuery = `DELETE FROM memory_links\n WHERE target_fact_id IN (\n SELECT id FROM facts WHERE (${conditions.join(\" OR \")})\n AND id NOT IN (SELECT fact_id FROM verified_facts)\n )\n AND link_type != 'DERIVED_FROM'`;\n db.prepare(linkCleanupQuery).run(...params);\n\n const query = `DELETE FROM facts WHERE (${conditions.join(\" OR \")})\n AND id NOT IN (SELECT fact_id FROM verified_facts)`;\n const result = db.prepare(query).run(...params);\n return Number(result.changes ?? 0);\n}\n\nexport function findSessionFactsForPromotion(\n db: DatabaseSync,\n thresholdDays: number,\n minImportance: number,\n): MemoryEntry[] {\n const nowSec = Math.floor(Date.now() / 1000);\n const thresholdSec = nowSec - thresholdDays * 86400;\n const rows = db\n .prepare(\n `SELECT * FROM facts\n WHERE scope = 'session'\n AND importance >= ?\n AND created_at <= ?\n AND superseded_at IS NULL\n AND (expires_at IS NULL OR expires_at > ?)`,\n )\n .all(minImportance, thresholdSec, nowSec) as Record<string, unknown>[];\n return rows.map((r) => rowToMemoryEntry(r));\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"housekeeping.js","names":[],"sources":["../../../backends/facts-db/housekeeping.ts"],"sourcesContent":["/**\n * Misc DB ops: pruning logs, stats helpers, scope pruning (Issue #954 split).\n */\nimport { randomUUID } from \"node:crypto\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport type { DatabaseSync } from \"node:sqlite\";\n\nimport type { MemoryEntry, ScopeFilter } from \"../../types/memory.js\";\nimport { getLanguageKeywordsFilePath } from \"../../utils/language-keywords.js\";\nimport { createTransaction } from \"../../utils/sqlite-transaction.js\";\nimport { rebuildFtsIndex } from \"../../services/fts-search.js\";\nimport { rowToMemoryEntry } from \"./row-mapper.js\";\n\nexport function pruneOrphanedLinks(db: DatabaseSync): number {\n const result = db\n .prepare(\n `DELETE FROM memory_links\n WHERE NOT EXISTS (SELECT 1 FROM facts WHERE facts.id = memory_links.source_fact_id)\n OR NOT EXISTS (SELECT 1 FROM facts WHERE facts.id = memory_links.target_fact_id)`,\n )\n .run();\n return Number(result.changes ?? 0);\n}\n\n/**\n * SQL COUNT of active (non-superseded, non-expired) facts for a given category.\n * Replaces the previous O(n) approach of loading all rows and filtering in JS.\n */\nexport function countActiveFactsByCategory(db: DatabaseSync, category: string): number {\n const nowSec = Math.floor(Date.now() / 1000);\n const row = db\n .prepare(\n `SELECT COUNT(*) as count FROM facts\n WHERE category = ?\n AND superseded_at IS NULL\n AND (expires_at IS NULL OR expires_at > ?)`,\n )\n .get(category, nowSec) as { count: number } | undefined;\n return row?.count ?? 0;\n}\n\nexport function pruneLogTables(db: DatabaseSync, retentionDays: number): number {\n if (retentionDays <= 0) return 0;\n const cutoff = Math.floor(Date.now() / 1000) - retentionDays * 86400;\n const tx = createTransaction(db, () => {\n const recall = db.prepare(\"DELETE FROM recall_log WHERE occurred_at < ?\").run(cutoff);\n const reinforcement = db.prepare(\"DELETE FROM reinforcement_log WHERE occurred_at < ?\").run(cutoff);\n const feedback = db.prepare(\"DELETE FROM feedback_trajectories WHERE created_at < ?\").run(cutoff);\n return Number(recall.changes ?? 0) + Number(reinforcement.changes ?? 0) + Number(feedback.changes ?? 0);\n });\n return tx();\n}\n\n/** Run standard FTS5 optimize maintenance on `facts_fts`. */\nexport function optimizeFts(db: DatabaseSync): void {\n db.exec(`INSERT INTO facts_fts(facts_fts) VALUES('optimize')`);\n}\n\nconst EXPECTED_FTS_TRIGGERS = [\"facts_ai\", \"facts_au\", \"facts_ad\"] as const;\n\nexport type FtsConsistencySnapshot = {\n factsCount: number;\n ftsCount: number;\n ftsTableExists: boolean;\n ftsTableSql: string | null;\n hasTagsColumn: boolean;\n hasWhyColumn: boolean;\n presentTriggers: string[];\n missingTriggers: string[];\n missingFactsInFts: number;\n extraFtsRows: number;\n missingFactIdsSample: string[];\n extraFtsRowidsSample: number[];\n};\n\nexport type FtsTriggerProbeResult = {\n ok: boolean;\n insertVisible: boolean;\n updateVisible: boolean;\n deleteVisibleAfterDelete: boolean;\n error?: string;\n};\n\nfunction scalarCount(db: DatabaseSync, query: string, ...params: (string | number)[]): number {\n const row = db.prepare(query).get(...params) as { cnt: number } | undefined;\n return Number(row?.cnt ?? 0);\n}\n\nexport function getFtsConsistencySnapshot(db: DatabaseSync): FtsConsistencySnapshot {\n const factsCount = scalarCount(db, \"SELECT COUNT(*) AS cnt FROM facts\");\n const ftsRow = db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='facts_fts'`).get() as\n | { sql: string | null }\n | undefined;\n const ftsTableExists = !!ftsRow;\n const ftsTableSql = ftsRow?.sql ?? null;\n const hasTagsColumn = /\\btags\\b/i.test(ftsTableSql ?? \"\");\n const hasWhyColumn = /\\bwhy\\b/i.test(ftsTableSql ?? \"\");\n\n const triggerRows = db\n .prepare(\n `SELECT name FROM sqlite_master\n WHERE type = 'trigger' AND name IN ('facts_ai', 'facts_au', 'facts_ad')`,\n )\n .all() as Array<{ name: string }>;\n const presentTriggers = triggerRows.map((r) => r.name).sort();\n const missingTriggers = EXPECTED_FTS_TRIGGERS.filter((name) => !presentTriggers.includes(name));\n\n if (!ftsTableExists) {\n return {\n factsCount,\n ftsCount: 0,\n ftsTableExists,\n ftsTableSql,\n hasTagsColumn,\n hasWhyColumn,\n presentTriggers,\n missingTriggers,\n missingFactsInFts: factsCount,\n extraFtsRows: 0,\n missingFactIdsSample: [],\n extraFtsRowidsSample: [],\n };\n }\n\n const ftsCount = scalarCount(db, \"SELECT COUNT(*) AS cnt FROM facts_fts\");\n const missingFactsInFts = scalarCount(\n db,\n `SELECT COUNT(*) AS cnt\n FROM facts f\n WHERE NOT EXISTS (SELECT 1 FROM facts_fts t WHERE t.rowid = f.rowid)`,\n );\n const extraFtsRows = scalarCount(\n db,\n `SELECT COUNT(*) AS cnt\n FROM facts_fts t\n WHERE NOT EXISTS (SELECT 1 FROM facts f WHERE f.rowid = t.rowid)`,\n );\n const missingFactIdsSampleRows = db\n .prepare(\n `SELECT f.id\n FROM facts f\n WHERE NOT EXISTS (SELECT 1 FROM facts_fts t WHERE t.rowid = f.rowid)\n LIMIT 5`,\n )\n .all() as Array<{ id: string }>;\n const extraFtsRowidsSampleRows = db\n .prepare(\n `SELECT t.rowid\n FROM facts_fts t\n WHERE NOT EXISTS (SELECT 1 FROM facts f WHERE f.rowid = t.rowid)\n LIMIT 5`,\n )\n .all() as Array<{ rowid: number }>;\n\n return {\n factsCount,\n ftsCount,\n ftsTableExists,\n ftsTableSql,\n hasTagsColumn,\n hasWhyColumn,\n presentTriggers,\n missingTriggers,\n missingFactsInFts,\n extraFtsRows,\n missingFactIdsSample: missingFactIdsSampleRows.map((row) => row.id),\n extraFtsRowidsSample: extraFtsRowidsSampleRows.map((row) => Number(row.rowid)),\n };\n}\n\nexport function runFtsTriggerProbe(db: DatabaseSync): FtsTriggerProbeResult {\n const savepoint = `hm_fts_probe_${randomUUID().replace(/-/g, \"\")}`;\n const probeId = randomUUID();\n const tokenBase = randomUUID().replace(/-/g, \"\");\n const insertToken = `hmdoctorins${tokenBase.slice(0, 12)}`;\n const updateToken = `hmdoctorupd${tokenBase.slice(12, 24)}`;\n const nowSec = Math.floor(Date.now() / 1000);\n\n let insertVisible = false;\n let updateVisible = false;\n let deleteVisibleAfterDelete = true;\n\n db.exec(`SAVEPOINT ${savepoint}`);\n try {\n db.prepare(\n `INSERT INTO facts(id, text, category, importance, source, created_at)\n VALUES(?, ?, 'fact', 0.01, 'doctor-fts-probe', ?)`,\n ).run(probeId, insertToken, nowSec);\n\n const rowidRow = db.prepare(\"SELECT rowid FROM facts WHERE id = ?\").get(probeId) as { rowid: number } | undefined;\n if (!rowidRow?.rowid) {\n return {\n ok: false,\n insertVisible,\n updateVisible,\n deleteVisibleAfterDelete,\n error: \"probe insert rowid not found\",\n };\n }\n const rowid = Number(rowidRow.rowid);\n\n const insertMatchCount = scalarCount(\n db,\n \"SELECT COUNT(*) AS cnt FROM facts_fts WHERE rowid = ? AND facts_fts MATCH ?\",\n rowid,\n insertToken,\n );\n insertVisible = insertMatchCount === 1;\n\n db.prepare(\"UPDATE facts SET text = ? WHERE id = ?\").run(updateToken, probeId);\n const updateMatchCount = scalarCount(\n db,\n \"SELECT COUNT(*) AS cnt FROM facts_fts WHERE rowid = ? AND facts_fts MATCH ?\",\n rowid,\n updateToken,\n );\n const staleMatchCount = scalarCount(\n db,\n \"SELECT COUNT(*) AS cnt FROM facts_fts WHERE rowid = ? AND facts_fts MATCH ?\",\n rowid,\n insertToken,\n );\n updateVisible = updateMatchCount === 1 && staleMatchCount === 0;\n\n db.prepare(\"DELETE FROM facts WHERE id = ?\").run(probeId);\n const postDeleteCount = scalarCount(db, \"SELECT COUNT(*) AS cnt FROM facts_fts WHERE rowid = ?\", rowid);\n deleteVisibleAfterDelete = postDeleteCount > 0;\n\n return {\n ok: insertVisible && updateVisible && !deleteVisibleAfterDelete,\n insertVisible,\n updateVisible,\n deleteVisibleAfterDelete,\n };\n } catch (error) {\n return {\n ok: false,\n insertVisible,\n updateVisible,\n deleteVisibleAfterDelete,\n error: String(error),\n };\n } finally {\n try {\n db.exec(`ROLLBACK TO ${savepoint}`);\n } catch {\n // ignore cleanup errors\n }\n try {\n db.exec(`RELEASE ${savepoint}`);\n } catch {\n // ignore cleanup errors\n }\n }\n}\n\nexport function rebuildFtsIndexFromFacts(db: DatabaseSync): number {\n const tx = createTransaction(db, () => rebuildFtsIndex(db));\n return tx();\n}\n\nexport function freelistSpaceStats(db: DatabaseSync): {\n freelistCount: number;\n pageSize: number;\n freedBytes: number;\n freedMB: number;\n} {\n const freelistCountResult = db.prepare(\"PRAGMA freelist_count\").get() as { freelist_count: number } | undefined;\n const pageSizeResult = db.prepare(\"PRAGMA page_size\").get() as { page_size: number } | undefined;\n const freelistCount = freelistCountResult?.freelist_count ?? 0;\n const pageSize = pageSizeResult?.page_size ?? 4096;\n const freedBytes = freelistCount * pageSize;\n return {\n freelistCount,\n pageSize,\n freedBytes,\n freedMB: freedBytes / (1024 * 1024),\n };\n}\n\nexport function checkpointWalTruncate(db: DatabaseSync): void {\n db.exec(\"PRAGMA wal_checkpoint(TRUNCATE)\");\n}\n\nexport function vacuumAndCheckpoint(db: DatabaseSync): void {\n db.exec(\"VACUUM\");\n checkpointWalTruncate(db);\n}\n\nexport function statsReflection(db: DatabaseSync): {\n reflectionPatternsCount: number;\n reflectionRulesCount: number;\n} {\n const patternsRow = db\n .prepare(\n `SELECT COUNT(*) as count FROM facts WHERE superseded_at IS NULL AND source = 'reflection' AND category = 'pattern'`,\n )\n .get() as { count: number } | undefined;\n const rulesRow = db\n .prepare(\n `SELECT COUNT(*) as count FROM facts WHERE superseded_at IS NULL AND source = 'reflection' AND category = 'rule'`,\n )\n .get() as { count: number } | undefined;\n return {\n reflectionPatternsCount: patternsRow?.count ?? 0,\n reflectionRulesCount: rulesRow?.count ?? 0,\n };\n}\n\nexport function selfCorrectionIncidentsCount(db: DatabaseSync): number {\n const row = db\n .prepare(`SELECT COUNT(*) as count FROM facts WHERE superseded_at IS NULL AND source = 'self-correction'`)\n .get() as { count: number } | undefined;\n return row?.count ?? 0;\n}\n\nexport function countSupersededFacts(db: DatabaseSync): number {\n const row = db.prepare(\"SELECT COUNT(*) as count FROM facts WHERE superseded_at IS NOT NULL\").get() as\n | { count: number }\n | undefined;\n return row?.count ?? 0;\n}\n\nexport function countVerifiedFacts(db: DatabaseSync): number {\n try {\n const row = db.prepare(\"SELECT COUNT(*) as count FROM verified_facts\").get() as { count: number } | undefined;\n return row?.count ?? 0;\n } catch {\n return 0;\n }\n}\n\n/** Number of active (non-superseded, non-expired) facts created at-or-after `sinceSec`. */\nexport function countActiveFactsCreatedSince(db: DatabaseSync, sinceSec: number): number {\n const nowSec = Math.floor(Date.now() / 1000);\n const row = db\n .prepare(\n `SELECT COUNT(*) as count FROM facts\n WHERE superseded_at IS NULL\n AND (expires_at IS NULL OR expires_at > ?)\n AND created_at >= ?`,\n )\n .get(nowSec, sinceSec) as { count: number } | undefined;\n return row?.count ?? 0;\n}\n\n/** Recent ingestion activity for the rich `stats` view. Cheap (3× COUNT). */\nexport function recentActivity(db: DatabaseSync): {\n last24h: number;\n last7d: number;\n last30d: number;\n newestCreatedAtSec: number | null;\n oldestActiveCreatedAtSec: number | null;\n} {\n const nowSec = Math.floor(Date.now() / 1000);\n const day = 86400;\n const last24h = countActiveFactsCreatedSince(db, nowSec - day);\n const last7d = countActiveFactsCreatedSince(db, nowSec - 7 * day);\n const last30d = countActiveFactsCreatedSince(db, nowSec - 30 * day);\n const minMaxRow = db\n .prepare(\n `SELECT MIN(created_at) as min_ts, MAX(created_at) as max_ts FROM facts\n WHERE superseded_at IS NULL AND (expires_at IS NULL OR expires_at > ?)`,\n )\n .get(nowSec) as { min_ts: number | null; max_ts: number | null } | undefined;\n return {\n last24h,\n last7d,\n last30d,\n newestCreatedAtSec: minMaxRow?.max_ts ?? null,\n oldestActiveCreatedAtSec: minMaxRow?.min_ts ?? null,\n };\n}\n\nexport function countBySource(db: DatabaseSync, source: string): number {\n const row = db\n .prepare(\"SELECT COUNT(*) as count FROM facts WHERE superseded_at IS NULL AND source = ?\")\n .get(source) as { count: number } | undefined;\n return row?.count ?? 0;\n}\n\nexport function languageKeywordsCount(): number {\n const filePath = getLanguageKeywordsFilePath();\n if (!filePath || !existsSync(filePath)) return 0;\n\n try {\n const data = JSON.parse(readFileSync(filePath, \"utf-8\"));\n let count = 0;\n\n const translations = data.translations ?? {};\n for (const lang of Object.values(translations)) {\n for (const [_key, val] of Object.entries(lang as Record<string, unknown>)) {\n if (Array.isArray(val)) count += val.length;\n }\n }\n\n const triggerStructures = data.triggerStructures ?? {};\n for (const val of Object.values(triggerStructures)) {\n if (Array.isArray(val)) count += val.length;\n }\n\n const directiveSignals = data.directiveSignalsByCategory ?? {};\n for (const val of Object.values(directiveSignals)) {\n if (Array.isArray(val)) count += val.length;\n }\n\n const reinforcementCategories = data.reinforcementCategories ?? {};\n for (const val of Object.values(reinforcementCategories)) {\n if (Array.isArray(val)) count += val.length;\n }\n\n return count;\n } catch {\n return 0;\n }\n}\n\nexport function statsBySource(db: DatabaseSync): Record<string, number> {\n const rows = db\n .prepare(\"SELECT source, COUNT(*) as count FROM facts WHERE superseded_at IS NULL GROUP BY source\")\n .all() as Array<{ source: string; count: number }>;\n return Object.fromEntries(rows.map((r) => [r.source, r.count]));\n}\n\nexport function uniqueScopes(db: DatabaseSync): Array<{ scope: string; scopeTarget: string | null }> {\n const rows = db\n .prepare(\"SELECT DISTINCT scope, scope_target as scopeTarget FROM facts WHERE scope IS NOT NULL\")\n .all() as Array<{ scope: string; scopeTarget: string | null }>;\n return rows;\n}\n\nexport function scopeStats(db: DatabaseSync): Array<{\n scope: string;\n scopeTarget: string | null;\n count: number;\n}> {\n const rows = db\n .prepare(\n \"SELECT scope, scope_target as scopeTarget, COUNT(*) as count FROM facts WHERE scope IS NOT NULL GROUP BY scope, scope_target\",\n )\n .all() as Array<{\n scope: string;\n scopeTarget: string | null;\n count: number;\n }>;\n return rows;\n}\n\n/**\n * Return all fact IDs that `pruneScopedFacts(scopeFilter)` would delete.\n * Use this *before* calling `pruneScopedFacts` to gather IDs for vector cleanup.\n * Excludes pinned facts in `verified_facts` (same guard as the DELETE).\n */\nexport function listScopedFactIdsPendingPrune(db: DatabaseSync, scopeFilter: ScopeFilter): string[] {\n const conditions: string[] = [];\n const params: (string | null)[] = [];\n\n if (scopeFilter.global) {\n conditions.push(`(scope = 'global')`);\n }\n if (scopeFilter.userId !== undefined) {\n conditions.push(`(scope = 'user' AND scope_target = ?)`);\n params.push(scopeFilter.userId);\n }\n if (scopeFilter.agentId !== undefined) {\n conditions.push(`(scope = 'agent' AND scope_target = ?)`);\n params.push(scopeFilter.agentId);\n }\n if (scopeFilter.sessionId !== undefined) {\n conditions.push(`(scope = 'session' AND scope_target = ?)`);\n params.push(scopeFilter.sessionId);\n }\n\n if (conditions.length === 0) return [];\n\n const rows = db\n .prepare(\n `SELECT id FROM facts\n WHERE (${conditions.join(\" OR \")})\n AND id NOT IN (SELECT fact_id FROM verified_facts)`,\n )\n .all(...params) as Array<{ id: string }>;\n return rows.map((r) => r.id);\n}\n\nexport function pruneScopedFacts(db: DatabaseSync, scopeFilter: ScopeFilter): number {\n const conditions: string[] = [];\n const params: (string | null)[] = [];\n\n if (scopeFilter.global) {\n conditions.push(`(scope = 'global')`);\n }\n if (scopeFilter.userId !== undefined) {\n conditions.push(`(scope = 'user' AND scope_target = ?)`);\n params.push(scopeFilter.userId);\n }\n if (scopeFilter.agentId !== undefined) {\n conditions.push(`(scope = 'agent' AND scope_target = ?)`);\n params.push(scopeFilter.agentId);\n }\n if (scopeFilter.sessionId !== undefined) {\n conditions.push(`(scope = 'session' AND scope_target = ?)`);\n params.push(scopeFilter.sessionId);\n }\n\n if (conditions.length === 0) return 0;\n\n const linkCleanupQuery = `DELETE FROM memory_links\n WHERE target_fact_id IN (\n SELECT id FROM facts WHERE (${conditions.join(\" OR \")})\n AND id NOT IN (SELECT fact_id FROM verified_facts)\n )\n AND link_type != 'DERIVED_FROM'`;\n db.prepare(linkCleanupQuery).run(...params);\n\n const query = `DELETE FROM facts WHERE (${conditions.join(\" OR \")})\n AND id NOT IN (SELECT fact_id FROM verified_facts)`;\n const result = db.prepare(query).run(...params);\n return Number(result.changes ?? 0);\n}\n\nexport function findSessionFactsForPromotion(\n db: DatabaseSync,\n thresholdDays: number,\n minImportance: number,\n): MemoryEntry[] {\n const nowSec = Math.floor(Date.now() / 1000);\n const thresholdSec = nowSec - thresholdDays * 86400;\n const rows = db\n .prepare(\n `SELECT * FROM facts\n WHERE scope = 'session'\n AND importance >= ?\n AND created_at <= ?\n AND superseded_at IS NULL\n AND (expires_at IS NULL OR expires_at > ?)`,\n )\n .all(minImportance, thresholdSec, nowSec) as Record<string, unknown>[];\n return rows.map((r) => rowToMemoryEntry(r));\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"links.js","names":[],"sources":["../../../backends/facts-db/links.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { DatabaseSync, SQLInputValue } from \"node:sqlite\";\n\nimport { createTransaction } from \"../../utils/sqlite-transaction.js\";\n\nimport type { MemoryLinkType } from \"./types.js\";\n\nexport function createLink(\n db: DatabaseSync,\n sourceFactId: string,\n targetFactId: string,\n linkType: MemoryLinkType,\n strength = 1.0,\n): string {\n if ((linkType as string) === \"DERIVED_FROM\") {\n throw new Error(\"DERIVED_FROM provenance is stored on facts.provenance_json, not memory_links\");\n }\n const id = randomUUID();\n const now = Math.floor(Date.now() / 1000);\n db.prepare(\n \"INSERT INTO memory_links (id, source_fact_id, target_fact_id, link_type, strength, created_at) VALUES (?, ?, ?, ?, ?, ?)\",\n ).run(id, sourceFactId, targetFactId, linkType, Math.max(0, Math.min(1, strength)), now);\n return id;\n}\n\nexport function createOrStrengthenRelatedLink(\n db: DatabaseSync,\n factIdA: string,\n factIdB: string,\n deltaStrength = 0.1,\n): void {\n if (factIdA === factIdB) return;\n const [source, target] = factIdA < factIdB ? [factIdA, factIdB] : [factIdB, factIdA];\n\n const existing = db\n .prepare(\n `SELECT id, strength FROM memory_links WHERE source_fact_id = ? AND target_fact_id = ? AND link_type = 'RELATED_TO'`,\n )\n .get(source, target) as { id: string; strength: number } | undefined;\n\n const newStrength = Math.min(1, (existing?.strength ?? 0) + deltaStrength);\n if (existing) {\n db.prepare(\"UPDATE memory_links SET strength = ? WHERE id = ?\").run(newStrength, existing.id);\n } else {\n createLink(db, source, target, \"RELATED_TO\", newStrength);\n }\n}\n\nexport function strengthenRelatedLinksBatch(db: DatabaseSync, pairs: [string, string][], deltaStrength = 0.1): void {\n if (pairs.length === 0) return;\n const selectStmt = db.prepare(\n `SELECT id, strength FROM memory_links WHERE source_fact_id = ? AND target_fact_id = ? AND link_type = 'RELATED_TO'`,\n );\n const updateStmt = db.prepare(\"UPDATE memory_links SET strength = ? WHERE id = ?\");\n const insertStmt = db.prepare(\n `INSERT INTO memory_links (id, source_fact_id, target_fact_id, link_type, strength, created_at) VALUES (?, ?, ?, 'RELATED_TO', ?, ?)`,\n );\n const now = Math.floor(Date.now() / 1000);\n const tx = createTransaction(db, () => {\n for (const [factIdA, factIdB] of pairs) {\n if (factIdA === factIdB) continue;\n const [source, target] = factIdA < factIdB ? [factIdA, factIdB] : [factIdB, factIdA];\n const existing = selectStmt.get(source, target) as { id: string; strength: number } | undefined;\n const newStrength = Math.max(0, Math.min(1, (existing?.strength ?? 0) + deltaStrength));\n if (existing) {\n updateStmt.run(newStrength, existing.id);\n } else {\n insertStmt.run(randomUUID(), source, target, newStrength, now);\n }\n }\n });\n tx();\n}\n\nexport function getLinksFrom(\n db: DatabaseSync,\n factId: string,\n): Array<{ id: string; targetFactId: string; linkType: string; strength: number }> {\n const rows = db\n .prepare(\"SELECT id, target_fact_id, link_type, strength FROM memory_links WHERE source_fact_id = ?\")\n .all(factId) as Array<{ id: string; target_fact_id: string; link_type: string; strength: number }>;\n return rows.map((r) => ({\n id: r.id,\n targetFactId: r.target_fact_id,\n linkType: r.link_type,\n strength: r.strength,\n }));\n}\n\nexport function getLinksTo(\n db: DatabaseSync,\n factId: string,\n): Array<{ id: string; sourceFactId: string; linkType: string; strength: number }> {\n const rows = db\n .prepare(\"SELECT id, source_fact_id, link_type, strength FROM memory_links WHERE target_fact_id = ?\")\n .all(factId) as Array<{ id: string; source_fact_id: string; link_type: string; strength: number }>;\n return rows.map((r) => ({\n id: r.id,\n sourceFactId: r.source_fact_id,\n linkType: r.link_type,\n strength: r.strength,\n }));\n}\n\n/** Optional out-params for hub-guard observability (Issue #1192). */\nexport type GraphConnectedStats = {\n nodesConsidered: number;\n nodesSkipped: number;\n hubsSkipped: number;\n};\n\n/**\n * Refresh denormalized `out_degree` / `in_degree` columns on `facts` (#1192). Counts only\n * traversable links — `CONTRADICTS` and `DERIVED_FROM` are excluded so the values match\n * what the BFS hub guard uses. Called from the dream-cycle so the per-traversal `COUNT(*)`\n * fallback path is rarely hit on warm stores.\n *\n * Returns the number of facts whose degree was updated (rows in `facts`).\n */\nexport function refreshFactDegrees(db: DatabaseSync): { updated: number } {\n const tx = createTransaction(db, () => {\n db.exec(`\n UPDATE facts SET\n out_degree = COALESCE(\n (SELECT COUNT(*) FROM memory_links ml\n WHERE ml.source_fact_id = facts.id\n AND ml.link_type NOT IN ('CONTRADICTS', 'DERIVED_FROM')),\n 0\n ),\n in_degree = COALESCE(\n (SELECT COUNT(*) FROM memory_links ml\n WHERE ml.target_fact_id = facts.id\n AND ml.link_type NOT IN ('CONTRADICTS', 'DERIVED_FROM')),\n 0\n )\n `);\n });\n tx();\n const row = db.prepare(\"SELECT COUNT(*) AS cnt FROM facts\").get() as { cnt: number } | undefined;\n return { updated: Number(row?.cnt ?? 0) };\n}\n\nexport function getConnectedFactIds(\n db: DatabaseSync,\n factIds: string[],\n maxDepth: number,\n options?: { hubDegreeCap?: number | null; stats?: GraphConnectedStats },\n): string[] {\n if (factIds.length === 0 || maxDepth < 1) return [...factIds];\n\n // `null` means no cap; only fall back to 500 when the option is omitted (`undefined`).\n const hubDegreeCap = options?.hubDegreeCap === undefined ? 500 : options.hubDegreeCap;\n const stats = options?.stats;\n const seen = new Set(factIds);\n let frontier = [...factIds];\n\n const outStmt = db.prepare(\n `SELECT target_fact_id AS id FROM memory_links WHERE source_fact_id = ? AND link_type NOT IN ('CONTRADICTS', 'DERIVED_FROM') ORDER BY strength DESC, created_at DESC LIMIT ?`,\n );\n const inStmt = db.prepare(\n `SELECT source_fact_id AS id FROM memory_links WHERE target_fact_id = ? AND link_type NOT IN ('CONTRADICTS', 'DERIVED_FROM') ORDER BY strength DESC, created_at DESC LIMIT ?`,\n );\n // #1192: prefer the denormalized columns when present (refreshed by the dream-cycle).\n // Falls back to the legacy COUNT(*) path when columns are missing or zero (which can\n // happen on a brand-new store before the first dream-cycle has run).\n // `prepare` fails if `facts` does not exist (e.g. minimal in-memory tests); skip denorm then.\n let denormDegreeStmt: ReturnType<DatabaseSync[\"prepare\"]> | null = null;\n try {\n denormDegreeStmt = db.prepare(\n \"SELECT COALESCE(out_degree, 0) + COALESCE(in_degree, 0) AS degree FROM facts WHERE id = ? LIMIT 1\",\n );\n } catch {\n denormDegreeStmt = null;\n }\n const fallbackDegreeStmt = db.prepare(\n `SELECT\n (SELECT COUNT(*) FROM memory_links WHERE source_fact_id = ? AND link_type NOT IN ('CONTRADICTS', 'DERIVED_FROM')) +\n (SELECT COUNT(*) FROM memory_links WHERE target_fact_id = ? AND link_type NOT IN ('CONTRADICTS', 'DERIVED_FROM')) AS degree`,\n );\n const degreeCache = new Map<string, number>();\n const liveDegreeCache = new Map<string, number>();\n const degreeOf = (id: string): number => {\n const cached = degreeCache.get(id);\n if (cached != null) return cached;\n let degree = 0;\n try {\n if (denormDegreeStmt) {\n const row = denormDegreeStmt.get(id) as { degree: number } | undefined;\n degree = Number(row?.degree ?? 0);\n }\n } catch {\n degree = 0;\n }\n if (degree === 0) {\n const row = fallbackDegreeStmt.get(id, id) as { degree: number } | undefined;\n degree = Number(row?.degree ?? 0);\n }\n degreeCache.set(id, degree);\n return degree;\n };\n /** Live in+out traversable edge count — safe for sizing SQL LIMIT when hub cap is off (see #1192). */\n const liveCombinedDegree = (id: string): number => {\n const cached = liveDegreeCache.get(id);\n if (cached != null) return cached;\n const row = fallbackDegreeStmt.get(id, id) as { degree: number } | undefined;\n const degree = Number(row?.degree ?? 0);\n liveDegreeCache.set(id, degree);\n return degree;\n };\n\n for (let depth = 0; depth < maxDepth && frontier.length > 0; depth++) {\n const nextFrontier: string[] = [];\n for (const id of frontier) {\n const degree = degreeOf(id);\n if (hubDegreeCap != null && degree > hubDegreeCap) {\n if (stats) {\n stats.hubsSkipped += 1;\n stats.nodesSkipped += degree;\n }\n continue;\n }\n // No hub cap: size LIMIT from live edge counts. Denormalized totals can lag dream-cycle\n // refreshes; using them here could truncate neighbours even though `out+in` is a valid upper\n // bound per direction when fresh. With hub cap, +1 avoids edge truncation vs the skip threshold.\n const limit = hubDegreeCap == null ? Math.max(liveCombinedDegree(id), 1) : Math.max(hubDegreeCap + 1, 1);\n const neighbours = [\n ...(outStmt.all(id, limit) as Array<{ id: string }>),\n ...(inStmt.all(id, limit) as Array<{ id: string }>),\n ];\n if (stats) stats.nodesConsidered += neighbours.length;\n for (const row of neighbours) {\n if (seen.has(row.id)) continue;\n seen.add(row.id);\n nextFrontier.push(row.id);\n }\n }\n frontier = nextFrontier;\n }\n\n return [...seen];\n}\n\n/**\n * Perform graph expansion using a recursive CTE, returning expanded nodes with hop count and path info.\n * This replaces the iterative N+1 BFS pattern with a single optimized SQL query.\n *\n * @param db - The database connection\n * @param seedFactIds - Array of seed fact IDs to start expansion from\n * @param maxDepth - Maximum traversal depth\n * @returns Array of expanded nodes with factId, seedId, hopCount, and path (JSON array of link steps)\n */\nexport function expandGraphWithCTE(\n db: DatabaseSync,\n seedFactIds: string[],\n maxDepth: number,\n options?: {\n asOf?: number;\n scopeFilter?: { userId?: string; agentId?: string; sessionId?: string };\n hubDegreeCap?: number | null;\n },\n): Array<{\n factId: string;\n seedId: string;\n hopCount: number;\n path: string; // JSON array of link steps\n}> {\n if (seedFactIds.length === 0 || maxDepth < 1) {\n return seedFactIds.map((id) => ({\n factId: id,\n seedId: id,\n hopCount: 0,\n path: \"[]\",\n }));\n }\n\n const asOf = options?.asOf ?? null;\n const scopeFilter = options?.scopeFilter;\n const hubDegreeCap = options?.hubDegreeCap === undefined ? 500 : options.hubDegreeCap;\n let factJoinOut = \"\";\n let factWhereOut = \"\";\n let factJoinIn = \"\";\n let factWhereIn = \"\";\n const filterParamsOut: SQLInputValue[] = [];\n const filterParamsIn: SQLInputValue[] = [];\n\n if (asOf != null || scopeFilter) {\n factJoinOut = \"JOIN facts f ON f.id = ml.target_fact_id\";\n factJoinIn = \"JOIN facts f ON f.id = ml.source_fact_id\";\n\n let baseWhere = \"\";\n if (asOf != null) {\n baseWhere += \" AND COALESCE(f.valid_from, f.created_at) <= ? AND (f.valid_until IS NULL OR f.valid_until > ?)\";\n filterParamsOut.push(asOf, asOf);\n filterParamsIn.push(asOf, asOf);\n }\n if (scopeFilter && (scopeFilter.userId || scopeFilter.agentId || scopeFilter.sessionId)) {\n baseWhere += ` AND (f.scope = 'global' OR (f.scope = 'user' AND f.scope_target = ?) OR (f.scope = 'agent' AND f.scope_target = ?) OR (f.scope = 'session' AND f.scope_target = ?))`;\n filterParamsOut.push(scopeFilter.userId ?? null, scopeFilter.agentId ?? null, scopeFilter.sessionId ?? null);\n filterParamsIn.push(scopeFilter.userId ?? null, scopeFilter.agentId ?? null, scopeFilter.sessionId ?? null);\n }\n factWhereOut = baseWhere;\n factWhereIn = baseWhere;\n }\n\n // Use recursive CTE to traverse the graph in a single query.\n // We track: current node, seed that originated this path, hop count, and JSON path.\n // #1192: prefer the denormalized `out_degree`/`in_degree` columns on `facts` (refreshed by the\n // dream-cycle) so the per-traversal `COUNT(*)` (O(edges) per hop) is avoided. The COALESCE\n // fallback ensures correctness on stores whose dream-cycle has not yet refreshed the columns\n // (or for legacy data with NULL/zero degrees) by checking `> 0` and only then short-circuiting.\n // Hub degree cap applies to the neighbor being entered (outgoing → `ml.target_fact_id`, incoming →\n // `ml.source_fact_id`), matching `getConnectedFactIds` which skips expanding from overloaded nodes.\n const degreeCheckOut =\n hubDegreeCap == null\n ? \"\"\n : `AND (\n SELECT\n CASE WHEN facts.out_degree IS NOT NULL AND facts.in_degree IS NOT NULL\n THEN (COALESCE(facts.out_degree, 0) + COALESCE(facts.in_degree, 0))\n ELSE (\n (SELECT COUNT(*) FROM memory_links sub WHERE sub.source_fact_id = ml.target_fact_id AND sub.link_type NOT IN ('CONTRADICTS', 'DERIVED_FROM')) +\n (SELECT COUNT(*) FROM memory_links sub WHERE sub.target_fact_id = ml.target_fact_id AND sub.link_type NOT IN ('CONTRADICTS', 'DERIVED_FROM'))\n )\n END\n FROM facts WHERE id = ml.target_fact_id\n ) <= ?`;\n const degreeCheckIn =\n hubDegreeCap == null\n ? \"\"\n : `AND (\n SELECT\n CASE WHEN facts.out_degree IS NOT NULL AND facts.in_degree IS NOT NULL\n THEN (COALESCE(facts.out_degree, 0) + COALESCE(facts.in_degree, 0))\n ELSE (\n (SELECT COUNT(*) FROM memory_links sub WHERE sub.source_fact_id = ml.source_fact_id AND sub.link_type NOT IN ('CONTRADICTS', 'DERIVED_FROM')) +\n (SELECT COUNT(*) FROM memory_links sub WHERE sub.target_fact_id = ml.source_fact_id AND sub.link_type NOT IN ('CONTRADICTS', 'DERIVED_FROM'))\n )\n END\n FROM facts WHERE id = ml.source_fact_id\n ) <= ?`;\n const query = `\n WITH RECURSIVE graph_expansion(\n fact_id,\n seed_id,\n hop_count,\n path_json,\n visited_ids\n ) AS (\n -- Base case: seed facts at hop 0\n SELECT\n value AS fact_id,\n value AS seed_id,\n 0 AS hop_count,\n '[]' AS path_json,\n ',' || value || ',' AS visited_ids\n FROM json_each(?)\n\n UNION ALL\n\n -- Recursive case: expand from frontier (outgoing links)\n SELECT\n ml.target_fact_id AS fact_id,\n ge.seed_id,\n ge.hop_count + 1 AS hop_count,\n json_insert(\n ge.path_json,\n '$[#]',\n json_object(\n 'fromFactId', ge.fact_id,\n 'toFactId', ml.target_fact_id,\n 'linkType', ml.link_type,\n 'strength', ml.strength\n )\n ) AS path_json,\n ge.visited_ids || ml.target_fact_id || ',' AS visited_ids\n FROM graph_expansion ge\n JOIN memory_links ml ON ml.source_fact_id = ge.fact_id\n ${factJoinOut}\n WHERE\n ge.hop_count < ?\n AND ml.link_type NOT IN ('CONTRADICTS', 'DERIVED_FROM')\n -- Avoid cycles: only visit each node once per path\n AND ge.visited_ids NOT LIKE '%,' || ml.target_fact_id || ',%'\n ${degreeCheckOut}\n ${factWhereOut}\n\n UNION ALL\n\n -- Recursive case: expand from frontier (incoming links)\n SELECT\n ml.source_fact_id AS fact_id,\n ge.seed_id,\n ge.hop_count + 1 AS hop_count,\n json_insert(\n ge.path_json,\n '$[#]',\n json_object(\n 'fromFactId', ge.fact_id,\n 'toFactId', ml.source_fact_id,\n 'linkType', ml.link_type,\n 'strength', ml.strength\n )\n ) AS path_json,\n ge.visited_ids || ml.source_fact_id || ',' AS visited_ids\n FROM graph_expansion ge\n JOIN memory_links ml ON ml.target_fact_id = ge.fact_id\n ${factJoinIn}\n WHERE\n ge.hop_count < ?\n AND ml.link_type NOT IN ('CONTRADICTS', 'DERIVED_FROM')\n -- Avoid cycles: only visit each node once per path\n AND ge.visited_ids NOT LIKE '%,' || ml.source_fact_id || ',%'\n ${degreeCheckIn}\n ${factWhereIn}\n ),\n -- Aggregate to find shortest path to each node\n shortest_paths AS (\n SELECT\n fact_id,\n seed_id,\n hop_count,\n path_json,\n ROW_NUMBER() OVER (PARTITION BY fact_id ORDER BY hop_count ASC) AS rn\n FROM graph_expansion\n )\n SELECT\n fact_id,\n seed_id,\n hop_count,\n path_json AS path\n FROM shortest_paths\n WHERE rn = 1\n ORDER BY hop_count ASC, fact_id ASC\n `;\n\n const hubParams = hubDegreeCap == null ? [] : [hubDegreeCap, hubDegreeCap];\n const rows = db\n .prepare(query)\n .all(\n JSON.stringify(seedFactIds),\n maxDepth,\n ...hubParams.slice(0, 1),\n ...filterParamsOut,\n maxDepth,\n ...hubParams.slice(1, 2),\n ...filterParamsIn,\n ) as Array<{\n fact_id: string;\n seed_id: string;\n hop_count: number;\n path: string;\n }>;\n\n return rows.map((r) => ({\n factId: r.fact_id,\n seedId: r.seed_id,\n hopCount: r.hop_count,\n path: r.path,\n }));\n}\n"],"mappings":";;;AAOA,SAAgB,WACd,IACA,cACA,cACA,UACA,WAAW,GACH;CACR,IAAK,aAAwB,gBAC3B,MAAM,IAAI,MAAM,+EAA+E;CAEjG,MAAM,KAAK,YAAY;CACvB,MAAM,MAAM,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK;CACzC,GAAG,QACD,2HACD,CAAC,IAAI,IAAI,cAAc,cAAc,UAAU,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,SAAS,CAAC,EAAE,IAAI;CACxF,OAAO;;AAGT,SAAgB,8BACd,IACA,SACA,SACA,gBAAgB,IACV;CACN,IAAI,YAAY,SAAS;CACzB,MAAM,CAAC,QAAQ,UAAU,UAAU,UAAU,CAAC,SAAS,QAAQ,GAAG,CAAC,SAAS,QAAQ;CAEpF,MAAM,WAAW,GACd,QACC,qHACD,CACA,IAAI,QAAQ,OAAO;CAEtB,MAAM,cAAc,KAAK,IAAI,IAAI,UAAU,YAAY,KAAK,cAAc;CAC1E,IAAI,UACF,GAAG,QAAQ,oDAAoD,CAAC,IAAI,aAAa,SAAS,GAAG;MAE7F,WAAW,IAAI,QAAQ,QAAQ,cAAc,YAAY;;AAI7D,SAAgB,4BAA4B,IAAkB,OAA2B,gBAAgB,IAAW;CAClH,IAAI,MAAM,WAAW,GAAG;CACxB,MAAM,aAAa,GAAG,QACpB,qHACD;CACD,MAAM,aAAa,GAAG,QAAQ,oDAAoD;CAClF,MAAM,aAAa,GAAG,QACpB,sIACD;CACD,MAAM,MAAM,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK;CAczC,kBAb6B,UAAU;EACrC,KAAK,MAAM,CAAC,SAAS,YAAY,OAAO;GACtC,IAAI,YAAY,SAAS;GACzB,MAAM,CAAC,QAAQ,UAAU,UAAU,UAAU,CAAC,SAAS,QAAQ,GAAG,CAAC,SAAS,QAAQ;GACpF,MAAM,WAAW,WAAW,IAAI,QAAQ,OAAO;GAC/C,MAAM,cAAc,KAAK,IAAI,GAAG,KAAK,IAAI,IAAI,UAAU,YAAY,KAAK,cAAc,CAAC;GACvF,IAAI,UACF,WAAW,IAAI,aAAa,SAAS,GAAG;QAExC,WAAW,IAAI,YAAY,EAAE,QAAQ,QAAQ,aAAa,IAAI;;GAIlE,EAAE;;AAGN,SAAgB,aACd,IACA,QACiF;CAIjF,OAHa,GACV,QAAQ,4FAA4F,CACpG,IAAI,OACI,CAAC,KAAK,OAAO;EACtB,IAAI,EAAE;EACN,cAAc,EAAE;EAChB,UAAU,EAAE;EACZ,UAAU,EAAE;EACb,EAAE;;AAGL,SAAgB,WACd,IACA,QACiF;CAIjF,OAHa,GACV,QAAQ,4FAA4F,CACpG,IAAI,OACI,CAAC,KAAK,OAAO;EACtB,IAAI,EAAE;EACN,cAAc,EAAE;EAChB,UAAU,EAAE;EACZ,UAAU,EAAE;EACb,EAAE;;;;;;;;;;AAkBL,SAAgB,mBAAmB,IAAuC;CAkBxE,kBAjB6B,UAAU;EACrC,GAAG,KAAK;;;;;;;;;;;;;;MAcN;GAEF,EAAE;CACJ,MAAM,MAAM,GAAG,QAAQ,oCAAoC,CAAC,KAAK;CACjE,OAAO,EAAE,SAAS,OAAO,KAAK,OAAO,EAAE,EAAE;;AAG3C,SAAgB,oBACd,IACA,SACA,UACA,SACU;CACV,IAAI,QAAQ,WAAW,KAAK,WAAW,GAAG,OAAO,CAAC,GAAG,QAAQ;CAG7D,MAAM,eAAe,SAAS,iBAAiB,KAAA,IAAY,MAAM,QAAQ;CACzE,MAAM,QAAQ,SAAS;CACvB,MAAM,OAAO,IAAI,IAAI,QAAQ;CAC7B,IAAI,WAAW,CAAC,GAAG,QAAQ;CAE3B,MAAM,UAAU,GAAG,QACjB,8KACD;CACD,MAAM,SAAS,GAAG,QAChB,8KACD;CAKD,IAAI,mBAA+D;CACnE,IAAI;EACF,mBAAmB,GAAG,QACpB,oGACD;SACK;EACN,mBAAmB;;CAErB,MAAM,qBAAqB,GAAG,QAC5B;;oIAGD;CACD,MAAM,8BAAc,IAAI,KAAqB;CAC7C,MAAM,kCAAkB,IAAI,KAAqB;CACjD,MAAM,YAAY,OAAuB;EACvC,MAAM,SAAS,YAAY,IAAI,GAAG;EAClC,IAAI,UAAU,MAAM,OAAO;EAC3B,IAAI,SAAS;EACb,IAAI;GACF,IAAI,kBAAkB;IACpB,MAAM,MAAM,iBAAiB,IAAI,GAAG;IACpC,SAAS,OAAO,KAAK,UAAU,EAAE;;UAE7B;GACN,SAAS;;EAEX,IAAI,WAAW,GAAG;GAChB,MAAM,MAAM,mBAAmB,IAAI,IAAI,GAAG;GAC1C,SAAS,OAAO,KAAK,UAAU,EAAE;;EAEnC,YAAY,IAAI,IAAI,OAAO;EAC3B,OAAO;;;CAGT,MAAM,sBAAsB,OAAuB;EACjD,MAAM,SAAS,gBAAgB,IAAI,GAAG;EACtC,IAAI,UAAU,MAAM,OAAO;EAC3B,MAAM,MAAM,mBAAmB,IAAI,IAAI,GAAG;EAC1C,MAAM,SAAS,OAAO,KAAK,UAAU,EAAE;EACvC,gBAAgB,IAAI,IAAI,OAAO;EAC/B,OAAO;;CAGT,KAAK,IAAI,QAAQ,GAAG,QAAQ,YAAY,SAAS,SAAS,GAAG,SAAS;EACpE,MAAM,eAAyB,EAAE;EACjC,KAAK,MAAM,MAAM,UAAU;GACzB,MAAM,SAAS,SAAS,GAAG;GAC3B,IAAI,gBAAgB,QAAQ,SAAS,cAAc;IACjD,IAAI,OAAO;KACT,MAAM,eAAe;KACrB,MAAM,gBAAgB;;IAExB;;GAKF,MAAM,QAAQ,gBAAgB,OAAO,KAAK,IAAI,mBAAmB,GAAG,EAAE,EAAE,GAAG,KAAK,IAAI,eAAe,GAAG,EAAE;GACxG,MAAM,aAAa,CACjB,GAAI,QAAQ,IAAI,IAAI,MAAM,EAC1B,GAAI,OAAO,IAAI,IAAI,MAAM,CAC1B;GACD,IAAI,OAAO,MAAM,mBAAmB,WAAW;GAC/C,KAAK,MAAM,OAAO,YAAY;IAC5B,IAAI,KAAK,IAAI,IAAI,GAAG,EAAE;IACtB,KAAK,IAAI,IAAI,GAAG;IAChB,aAAa,KAAK,IAAI,GAAG;;;EAG7B,WAAW;;CAGb,OAAO,CAAC,GAAG,KAAK;;;;;;;;;;;AAYlB,SAAgB,mBACd,IACA,aACA,UACA,SAUC;CACD,IAAI,YAAY,WAAW,KAAK,WAAW,GACzC,OAAO,YAAY,KAAK,QAAQ;EAC9B,QAAQ;EACR,QAAQ;EACR,UAAU;EACV,MAAM;EACP,EAAE;CAGL,MAAM,OAAO,SAAS,QAAQ;CAC9B,MAAM,cAAc,SAAS;CAC7B,MAAM,eAAe,SAAS,iBAAiB,KAAA,IAAY,MAAM,QAAQ;CACzE,IAAI,cAAc;CAClB,IAAI,eAAe;CACnB,IAAI,aAAa;CACjB,IAAI,cAAc;CAClB,MAAM,kBAAmC,EAAE;CAC3C,MAAM,iBAAkC,EAAE;CAE1C,IAAI,QAAQ,QAAQ,aAAa;EAC/B,cAAc;EACd,aAAa;EAEb,IAAI,YAAY;EAChB,IAAI,QAAQ,MAAM;GAChB,aAAa;GACb,gBAAgB,KAAK,MAAM,KAAK;GAChC,eAAe,KAAK,MAAM,KAAK;;EAEjC,IAAI,gBAAgB,YAAY,UAAU,YAAY,WAAW,YAAY,YAAY;GACvF,aAAa;GACb,gBAAgB,KAAK,YAAY,UAAU,MAAM,YAAY,WAAW,MAAM,YAAY,aAAa,KAAK;GAC5G,eAAe,KAAK,YAAY,UAAU,MAAM,YAAY,WAAW,MAAM,YAAY,aAAa,KAAK;;EAE7G,eAAe;EACf,cAAc;;CAuChB,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAqCR,YAAY;;;;;;UAhEhB,gBAAgB,OACZ,KACA;;;;;;;;;;cAoEiB;UACf,aAAa;;;;;;;;;;;;;;;;;;;;;;QAsBf,WAAW;;;;;;UA/Ef,gBAAgB,OACZ,KACA;;;;;;;;;;cAmFgB;UACd,YAAY;;;;;;;;;;;;;;;;;;;;;CAsBpB,MAAM,YAAY,gBAAgB,OAAO,EAAE,GAAG,CAAC,cAAc,aAAa;CAkB1E,OAjBa,GACV,QAAQ,MAAM,CACd,IACC,KAAK,UAAU,YAAY,EAC3B,UACA,GAAG,UAAU,MAAM,GAAG,EAAE,EACxB,GAAG,iBACH,UACA,GAAG,UAAU,MAAM,GAAG,EAAE,EACxB,GAAG,eAQI,CAAC,KAAK,OAAO;EACtB,QAAQ,EAAE;EACV,QAAQ,EAAE;EACV,UAAU,EAAE;EACZ,MAAM,EAAE;EACT,EAAE"}
1
+ {"version":3,"file":"links.js","names":[],"sources":["../../../backends/facts-db/links.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { DatabaseSync, SQLInputValue } from \"node:sqlite\";\n\nimport { createTransaction } from \"../../utils/sqlite-transaction.js\";\n\nimport type { MemoryLinkType } from \"./types.js\";\n\nexport function createLink(\n db: DatabaseSync,\n sourceFactId: string,\n targetFactId: string,\n linkType: MemoryLinkType,\n strength = 1.0,\n): string {\n if ((linkType as string) === \"DERIVED_FROM\") {\n throw new Error(\"DERIVED_FROM provenance is stored on facts.provenance_json, not memory_links\");\n }\n const id = randomUUID();\n const now = Math.floor(Date.now() / 1000);\n db.prepare(\n \"INSERT INTO memory_links (id, source_fact_id, target_fact_id, link_type, strength, created_at) VALUES (?, ?, ?, ?, ?, ?)\",\n ).run(id, sourceFactId, targetFactId, linkType, Math.max(0, Math.min(1, strength)), now);\n return id;\n}\n\nexport function createOrStrengthenRelatedLink(\n db: DatabaseSync,\n factIdA: string,\n factIdB: string,\n deltaStrength = 0.1,\n): void {\n if (factIdA === factIdB) return;\n const [source, target] = factIdA < factIdB ? [factIdA, factIdB] : [factIdB, factIdA];\n\n const existing = db\n .prepare(\n `SELECT id, strength FROM memory_links WHERE source_fact_id = ? AND target_fact_id = ? AND link_type = 'RELATED_TO'`,\n )\n .get(source, target) as { id: string; strength: number } | undefined;\n\n const newStrength = Math.min(1, (existing?.strength ?? 0) + deltaStrength);\n if (existing) {\n db.prepare(\"UPDATE memory_links SET strength = ? WHERE id = ?\").run(newStrength, existing.id);\n } else {\n createLink(db, source, target, \"RELATED_TO\", newStrength);\n }\n}\n\nexport function strengthenRelatedLinksBatch(db: DatabaseSync, pairs: [string, string][], deltaStrength = 0.1): void {\n if (pairs.length === 0) return;\n const selectStmt = db.prepare(\n `SELECT id, strength FROM memory_links WHERE source_fact_id = ? AND target_fact_id = ? AND link_type = 'RELATED_TO'`,\n );\n const updateStmt = db.prepare(\"UPDATE memory_links SET strength = ? WHERE id = ?\");\n const insertStmt = db.prepare(\n `INSERT INTO memory_links (id, source_fact_id, target_fact_id, link_type, strength, created_at) VALUES (?, ?, ?, 'RELATED_TO', ?, ?)`,\n );\n const now = Math.floor(Date.now() / 1000);\n const tx = createTransaction(db, () => {\n for (const [factIdA, factIdB] of pairs) {\n if (factIdA === factIdB) continue;\n const [source, target] = factIdA < factIdB ? [factIdA, factIdB] : [factIdB, factIdA];\n const existing = selectStmt.get(source, target) as { id: string; strength: number } | undefined;\n const newStrength = Math.max(0, Math.min(1, (existing?.strength ?? 0) + deltaStrength));\n if (existing) {\n updateStmt.run(newStrength, existing.id);\n } else {\n insertStmt.run(randomUUID(), source, target, newStrength, now);\n }\n }\n });\n tx();\n}\n\nexport function getLinksFrom(\n db: DatabaseSync,\n factId: string,\n): Array<{ id: string; targetFactId: string; linkType: string; strength: number }> {\n const rows = db\n .prepare(\"SELECT id, target_fact_id, link_type, strength FROM memory_links WHERE source_fact_id = ?\")\n .all(factId) as Array<{ id: string; target_fact_id: string; link_type: string; strength: number }>;\n return rows.map((r) => ({\n id: r.id,\n targetFactId: r.target_fact_id,\n linkType: r.link_type,\n strength: r.strength,\n }));\n}\n\nexport function getLinksTo(\n db: DatabaseSync,\n factId: string,\n): Array<{ id: string; sourceFactId: string; linkType: string; strength: number }> {\n const rows = db\n .prepare(\"SELECT id, source_fact_id, link_type, strength FROM memory_links WHERE target_fact_id = ?\")\n .all(factId) as Array<{ id: string; source_fact_id: string; link_type: string; strength: number }>;\n return rows.map((r) => ({\n id: r.id,\n sourceFactId: r.source_fact_id,\n linkType: r.link_type,\n strength: r.strength,\n }));\n}\n\n/** Optional out-params for hub-guard observability (Issue #1192). */\nexport type GraphConnectedStats = {\n nodesConsidered: number;\n nodesSkipped: number;\n hubsSkipped: number;\n};\n\n/**\n * Refresh denormalized `out_degree` / `in_degree` columns on `facts` (#1192). Counts only\n * traversable links — `CONTRADICTS` and `DERIVED_FROM` are excluded so the values match\n * what the BFS hub guard uses. Called from the dream-cycle so the per-traversal `COUNT(*)`\n * fallback path is rarely hit on warm stores.\n *\n * Returns the number of facts whose degree was updated (rows in `facts`).\n */\nexport function refreshFactDegrees(db: DatabaseSync): { updated: number } {\n const tx = createTransaction(db, () => {\n db.exec(`\n UPDATE facts SET\n out_degree = COALESCE(\n (SELECT COUNT(*) FROM memory_links ml\n WHERE ml.source_fact_id = facts.id\n AND ml.link_type NOT IN ('CONTRADICTS', 'DERIVED_FROM')),\n 0\n ),\n in_degree = COALESCE(\n (SELECT COUNT(*) FROM memory_links ml\n WHERE ml.target_fact_id = facts.id\n AND ml.link_type NOT IN ('CONTRADICTS', 'DERIVED_FROM')),\n 0\n )\n `);\n });\n tx();\n const row = db.prepare(\"SELECT COUNT(*) AS cnt FROM facts\").get() as { cnt: number } | undefined;\n return { updated: Number(row?.cnt ?? 0) };\n}\n\nexport function getConnectedFactIds(\n db: DatabaseSync,\n factIds: string[],\n maxDepth: number,\n options?: { hubDegreeCap?: number | null; stats?: GraphConnectedStats },\n): string[] {\n if (factIds.length === 0 || maxDepth < 1) return [...factIds];\n\n // `null` means no cap; only fall back to 500 when the option is omitted (`undefined`).\n const hubDegreeCap = options?.hubDegreeCap === undefined ? 500 : options.hubDegreeCap;\n const stats = options?.stats;\n const seen = new Set(factIds);\n let frontier = [...factIds];\n\n const outStmt = db.prepare(\n `SELECT target_fact_id AS id FROM memory_links WHERE source_fact_id = ? AND link_type NOT IN ('CONTRADICTS', 'DERIVED_FROM') ORDER BY strength DESC, created_at DESC LIMIT ?`,\n );\n const inStmt = db.prepare(\n `SELECT source_fact_id AS id FROM memory_links WHERE target_fact_id = ? AND link_type NOT IN ('CONTRADICTS', 'DERIVED_FROM') ORDER BY strength DESC, created_at DESC LIMIT ?`,\n );\n // #1192: prefer the denormalized columns when present (refreshed by the dream-cycle).\n // Falls back to the legacy COUNT(*) path when columns are missing or zero (which can\n // happen on a brand-new store before the first dream-cycle has run).\n // `prepare` fails if `facts` does not exist (e.g. minimal in-memory tests); skip denorm then.\n let denormDegreeStmt: ReturnType<DatabaseSync[\"prepare\"]> | null = null;\n try {\n denormDegreeStmt = db.prepare(\n \"SELECT COALESCE(out_degree, 0) + COALESCE(in_degree, 0) AS degree FROM facts WHERE id = ? LIMIT 1\",\n );\n } catch {\n denormDegreeStmt = null;\n }\n const fallbackDegreeStmt = db.prepare(\n `SELECT\n (SELECT COUNT(*) FROM memory_links WHERE source_fact_id = ? AND link_type NOT IN ('CONTRADICTS', 'DERIVED_FROM')) +\n (SELECT COUNT(*) FROM memory_links WHERE target_fact_id = ? AND link_type NOT IN ('CONTRADICTS', 'DERIVED_FROM')) AS degree`,\n );\n const degreeCache = new Map<string, number>();\n const liveDegreeCache = new Map<string, number>();\n const degreeOf = (id: string): number => {\n const cached = degreeCache.get(id);\n if (cached != null) return cached;\n let degree = 0;\n try {\n if (denormDegreeStmt) {\n const row = denormDegreeStmt.get(id) as { degree: number } | undefined;\n degree = Number(row?.degree ?? 0);\n }\n } catch {\n degree = 0;\n }\n if (degree === 0) {\n const row = fallbackDegreeStmt.get(id, id) as { degree: number } | undefined;\n degree = Number(row?.degree ?? 0);\n }\n degreeCache.set(id, degree);\n return degree;\n };\n /** Live in+out traversable edge count — safe for sizing SQL LIMIT when hub cap is off (see #1192). */\n const liveCombinedDegree = (id: string): number => {\n const cached = liveDegreeCache.get(id);\n if (cached != null) return cached;\n const row = fallbackDegreeStmt.get(id, id) as { degree: number } | undefined;\n const degree = Number(row?.degree ?? 0);\n liveDegreeCache.set(id, degree);\n return degree;\n };\n\n for (let depth = 0; depth < maxDepth && frontier.length > 0; depth++) {\n const nextFrontier: string[] = [];\n for (const id of frontier) {\n const degree = degreeOf(id);\n if (hubDegreeCap != null && degree > hubDegreeCap) {\n if (stats) {\n stats.hubsSkipped += 1;\n stats.nodesSkipped += degree;\n }\n continue;\n }\n // No hub cap: size LIMIT from live edge counts. Denormalized totals can lag dream-cycle\n // refreshes; using them here could truncate neighbours even though `out+in` is a valid upper\n // bound per direction when fresh. With hub cap, +1 avoids edge truncation vs the skip threshold.\n const limit = hubDegreeCap == null ? Math.max(liveCombinedDegree(id), 1) : Math.max(hubDegreeCap + 1, 1);\n const neighbours = [\n ...(outStmt.all(id, limit) as Array<{ id: string }>),\n ...(inStmt.all(id, limit) as Array<{ id: string }>),\n ];\n if (stats) stats.nodesConsidered += neighbours.length;\n for (const row of neighbours) {\n if (seen.has(row.id)) continue;\n seen.add(row.id);\n nextFrontier.push(row.id);\n }\n }\n frontier = nextFrontier;\n }\n\n return [...seen];\n}\n\n/**\n * Perform graph expansion using a recursive CTE, returning expanded nodes with hop count and path info.\n * This replaces the iterative N+1 BFS pattern with a single optimized SQL query.\n *\n * @param db - The database connection\n * @param seedFactIds - Array of seed fact IDs to start expansion from\n * @param maxDepth - Maximum traversal depth\n * @returns Array of expanded nodes with factId, seedId, hopCount, and path (JSON array of link steps)\n */\nexport function expandGraphWithCTE(\n db: DatabaseSync,\n seedFactIds: string[],\n maxDepth: number,\n options?: {\n asOf?: number;\n scopeFilter?: { userId?: string; agentId?: string; sessionId?: string };\n hubDegreeCap?: number | null;\n },\n): Array<{\n factId: string;\n seedId: string;\n hopCount: number;\n path: string; // JSON array of link steps\n}> {\n if (seedFactIds.length === 0 || maxDepth < 1) {\n return seedFactIds.map((id) => ({\n factId: id,\n seedId: id,\n hopCount: 0,\n path: \"[]\",\n }));\n }\n\n const asOf = options?.asOf ?? null;\n const scopeFilter = options?.scopeFilter;\n const hubDegreeCap = options?.hubDegreeCap === undefined ? 500 : options.hubDegreeCap;\n let factJoinOut = \"\";\n let factWhereOut = \"\";\n let factJoinIn = \"\";\n let factWhereIn = \"\";\n const filterParamsOut: SQLInputValue[] = [];\n const filterParamsIn: SQLInputValue[] = [];\n\n if (asOf != null || scopeFilter) {\n factJoinOut = \"JOIN facts f ON f.id = ml.target_fact_id\";\n factJoinIn = \"JOIN facts f ON f.id = ml.source_fact_id\";\n\n let baseWhere = \"\";\n if (asOf != null) {\n baseWhere += \" AND COALESCE(f.valid_from, f.created_at) <= ? AND (f.valid_until IS NULL OR f.valid_until > ?)\";\n filterParamsOut.push(asOf, asOf);\n filterParamsIn.push(asOf, asOf);\n }\n if (scopeFilter && (scopeFilter.userId || scopeFilter.agentId || scopeFilter.sessionId)) {\n baseWhere += ` AND (f.scope = 'global' OR (f.scope = 'user' AND f.scope_target = ?) OR (f.scope = 'agent' AND f.scope_target = ?) OR (f.scope = 'session' AND f.scope_target = ?))`;\n filterParamsOut.push(scopeFilter.userId ?? null, scopeFilter.agentId ?? null, scopeFilter.sessionId ?? null);\n filterParamsIn.push(scopeFilter.userId ?? null, scopeFilter.agentId ?? null, scopeFilter.sessionId ?? null);\n }\n factWhereOut = baseWhere;\n factWhereIn = baseWhere;\n }\n\n // Use recursive CTE to traverse the graph in a single query.\n // We track: current node, seed that originated this path, hop count, and JSON path.\n // #1192: prefer the denormalized `out_degree`/`in_degree` columns on `facts` (refreshed by the\n // dream-cycle) so the per-traversal `COUNT(*)` (O(edges) per hop) is avoided. The COALESCE\n // fallback ensures correctness on stores whose dream-cycle has not yet refreshed the columns\n // (or for legacy data with NULL/zero degrees) by checking `> 0` and only then short-circuiting.\n // Hub degree cap applies to the neighbor being entered (outgoing → `ml.target_fact_id`, incoming →\n // `ml.source_fact_id`), matching `getConnectedFactIds` which skips expanding from overloaded nodes.\n const degreeCheckOut =\n hubDegreeCap == null\n ? \"\"\n : `AND (\n SELECT\n CASE WHEN facts.out_degree IS NOT NULL AND facts.in_degree IS NOT NULL\n THEN (COALESCE(facts.out_degree, 0) + COALESCE(facts.in_degree, 0))\n ELSE (\n (SELECT COUNT(*) FROM memory_links sub WHERE sub.source_fact_id = ml.target_fact_id AND sub.link_type NOT IN ('CONTRADICTS', 'DERIVED_FROM')) +\n (SELECT COUNT(*) FROM memory_links sub WHERE sub.target_fact_id = ml.target_fact_id AND sub.link_type NOT IN ('CONTRADICTS', 'DERIVED_FROM'))\n )\n END\n FROM facts WHERE id = ml.target_fact_id\n ) <= ?`;\n const degreeCheckIn =\n hubDegreeCap == null\n ? \"\"\n : `AND (\n SELECT\n CASE WHEN facts.out_degree IS NOT NULL AND facts.in_degree IS NOT NULL\n THEN (COALESCE(facts.out_degree, 0) + COALESCE(facts.in_degree, 0))\n ELSE (\n (SELECT COUNT(*) FROM memory_links sub WHERE sub.source_fact_id = ml.source_fact_id AND sub.link_type NOT IN ('CONTRADICTS', 'DERIVED_FROM')) +\n (SELECT COUNT(*) FROM memory_links sub WHERE sub.target_fact_id = ml.source_fact_id AND sub.link_type NOT IN ('CONTRADICTS', 'DERIVED_FROM'))\n )\n END\n FROM facts WHERE id = ml.source_fact_id\n ) <= ?`;\n const query = `\n WITH RECURSIVE graph_expansion(\n fact_id,\n seed_id,\n hop_count,\n path_json,\n visited_ids\n ) AS (\n -- Base case: seed facts at hop 0\n SELECT\n value AS fact_id,\n value AS seed_id,\n 0 AS hop_count,\n '[]' AS path_json,\n ',' || value || ',' AS visited_ids\n FROM json_each(?)\n\n UNION ALL\n\n -- Recursive case: expand from frontier (outgoing links)\n SELECT\n ml.target_fact_id AS fact_id,\n ge.seed_id,\n ge.hop_count + 1 AS hop_count,\n json_insert(\n ge.path_json,\n '$[#]',\n json_object(\n 'fromFactId', ge.fact_id,\n 'toFactId', ml.target_fact_id,\n 'linkType', ml.link_type,\n 'strength', ml.strength\n )\n ) AS path_json,\n ge.visited_ids || ml.target_fact_id || ',' AS visited_ids\n FROM graph_expansion ge\n JOIN memory_links ml ON ml.source_fact_id = ge.fact_id\n ${factJoinOut}\n WHERE\n ge.hop_count < ?\n AND ml.link_type NOT IN ('CONTRADICTS', 'DERIVED_FROM')\n -- Avoid cycles: only visit each node once per path\n AND ge.visited_ids NOT LIKE '%,' || ml.target_fact_id || ',%'\n ${degreeCheckOut}\n ${factWhereOut}\n\n UNION ALL\n\n -- Recursive case: expand from frontier (incoming links)\n SELECT\n ml.source_fact_id AS fact_id,\n ge.seed_id,\n ge.hop_count + 1 AS hop_count,\n json_insert(\n ge.path_json,\n '$[#]',\n json_object(\n 'fromFactId', ge.fact_id,\n 'toFactId', ml.source_fact_id,\n 'linkType', ml.link_type,\n 'strength', ml.strength\n )\n ) AS path_json,\n ge.visited_ids || ml.source_fact_id || ',' AS visited_ids\n FROM graph_expansion ge\n JOIN memory_links ml ON ml.target_fact_id = ge.fact_id\n ${factJoinIn}\n WHERE\n ge.hop_count < ?\n AND ml.link_type NOT IN ('CONTRADICTS', 'DERIVED_FROM')\n -- Avoid cycles: only visit each node once per path\n AND ge.visited_ids NOT LIKE '%,' || ml.source_fact_id || ',%'\n ${degreeCheckIn}\n ${factWhereIn}\n ),\n -- Aggregate to find shortest path to each node\n shortest_paths AS (\n SELECT\n fact_id,\n seed_id,\n hop_count,\n path_json,\n ROW_NUMBER() OVER (PARTITION BY fact_id ORDER BY hop_count ASC) AS rn\n FROM graph_expansion\n )\n SELECT\n fact_id,\n seed_id,\n hop_count,\n path_json AS path\n FROM shortest_paths\n WHERE rn = 1\n ORDER BY hop_count ASC, fact_id ASC\n `;\n\n const hubParams = hubDegreeCap == null ? [] : [hubDegreeCap, hubDegreeCap];\n const rows = db\n .prepare(query)\n .all(\n JSON.stringify(seedFactIds),\n maxDepth,\n ...hubParams.slice(0, 1),\n ...filterParamsOut,\n maxDepth,\n ...hubParams.slice(1, 2),\n ...filterParamsIn,\n ) as Array<{\n fact_id: string;\n seed_id: string;\n hop_count: number;\n path: string;\n }>;\n\n return rows.map((r) => ({\n factId: r.fact_id,\n seedId: r.seed_id,\n hopCount: r.hop_count,\n path: r.path,\n }));\n}\n"],"mappings":";;;AAOA,SAAgB,WACd,IACA,cACA,cACA,UACA,WAAW,GACH;CACR,IAAK,aAAwB,gBAC3B,MAAM,IAAI,MAAM,8EAA8E;CAEhG,MAAM,KAAK,WAAW;CACtB,MAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;CACxC,GAAG,QACD,0HACF,EAAE,IAAI,IAAI,cAAc,cAAc,UAAU,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,QAAQ,CAAC,GAAG,GAAG;CACvF,OAAO;AACT;AAEA,SAAgB,8BACd,IACA,SACA,SACA,gBAAgB,IACV;CACN,IAAI,YAAY,SAAS;CACzB,MAAM,CAAC,QAAQ,UAAU,UAAU,UAAU,CAAC,SAAS,OAAO,IAAI,CAAC,SAAS,OAAO;CAEnF,MAAM,WAAW,GACd,QACC,oHACF,EACC,IAAI,QAAQ,MAAM;CAErB,MAAM,cAAc,KAAK,IAAI,IAAI,UAAU,YAAY,KAAK,aAAa;CACzE,IAAI,UACF,GAAG,QAAQ,mDAAmD,EAAE,IAAI,aAAa,SAAS,EAAE;MAE5F,WAAW,IAAI,QAAQ,QAAQ,cAAc,WAAW;AAE5D;AAEA,SAAgB,4BAA4B,IAAkB,OAA2B,gBAAgB,IAAW;CAClH,IAAI,MAAM,WAAW,GAAG;CACxB,MAAM,aAAa,GAAG,QACpB,oHACF;CACA,MAAM,aAAa,GAAG,QAAQ,mDAAmD;CACjF,MAAM,aAAa,GAAG,QACpB,qIACF;CACA,MAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;CAcxC,kBAb6B,UAAU;EACrC,KAAK,MAAM,CAAC,SAAS,YAAY,OAAO;GACtC,IAAI,YAAY,SAAS;GACzB,MAAM,CAAC,QAAQ,UAAU,UAAU,UAAU,CAAC,SAAS,OAAO,IAAI,CAAC,SAAS,OAAO;GACnF,MAAM,WAAW,WAAW,IAAI,QAAQ,MAAM;GAC9C,MAAM,cAAc,KAAK,IAAI,GAAG,KAAK,IAAI,IAAI,UAAU,YAAY,KAAK,aAAa,CAAC;GACtF,IAAI,UACF,WAAW,IAAI,aAAa,SAAS,EAAE;QAEvC,WAAW,IAAI,WAAW,GAAG,QAAQ,QAAQ,aAAa,GAAG;EAEjE;CACF,CACC,EAAE;AACL;AAEA,SAAgB,aACd,IACA,QACiF;CAIjF,OAHa,GACV,QAAQ,2FAA2F,EACnG,IAAI,MACG,EAAE,KAAK,OAAO;EACtB,IAAI,EAAE;EACN,cAAc,EAAE;EAChB,UAAU,EAAE;EACZ,UAAU,EAAE;CACd,EAAE;AACJ;AAEA,SAAgB,WACd,IACA,QACiF;CAIjF,OAHa,GACV,QAAQ,2FAA2F,EACnG,IAAI,MACG,EAAE,KAAK,OAAO;EACtB,IAAI,EAAE;EACN,cAAc,EAAE;EAChB,UAAU,EAAE;EACZ,UAAU,EAAE;CACd,EAAE;AACJ;;;;;;;;;AAiBA,SAAgB,mBAAmB,IAAuC;CAkBxE,kBAjB6B,UAAU;EACrC,GAAG,KAAK;;;;;;;;;;;;;;KAcP;CACH,CACC,EAAE;CACH,MAAM,MAAM,GAAG,QAAQ,mCAAmC,EAAE,IAAI;CAChE,OAAO,EAAE,SAAS,OAAO,KAAK,OAAO,CAAC,EAAE;AAC1C;AAEA,SAAgB,oBACd,IACA,SACA,UACA,SACU;CACV,IAAI,QAAQ,WAAW,KAAK,WAAW,GAAG,OAAO,CAAC,GAAG,OAAO;CAG5D,MAAM,eAAe,SAAS,iBAAiB,KAAA,IAAY,MAAM,QAAQ;CACzE,MAAM,QAAQ,SAAS;CACvB,MAAM,OAAO,IAAI,IAAI,OAAO;CAC5B,IAAI,WAAW,CAAC,GAAG,OAAO;CAE1B,MAAM,UAAU,GAAG,QACjB,6KACF;CACA,MAAM,SAAS,GAAG,QAChB,6KACF;CAKA,IAAI,mBAA+D;CACnE,IAAI;EACF,mBAAmB,GAAG,QACpB,mGACF;CACF,QAAQ;EACN,mBAAmB;CACrB;CACA,MAAM,qBAAqB,GAAG,QAC5B;;mIAGF;CACA,MAAM,8BAAc,IAAI,IAAoB;CAC5C,MAAM,kCAAkB,IAAI,IAAoB;CAChD,MAAM,YAAY,OAAuB;EACvC,MAAM,SAAS,YAAY,IAAI,EAAE;EACjC,IAAI,UAAU,MAAM,OAAO;EAC3B,IAAI,SAAS;EACb,IAAI;GACF,IAAI,kBAAkB;IACpB,MAAM,MAAM,iBAAiB,IAAI,EAAE;IACnC,SAAS,OAAO,KAAK,UAAU,CAAC;GAClC;EACF,QAAQ;GACN,SAAS;EACX;EACA,IAAI,WAAW,GAAG;GAChB,MAAM,MAAM,mBAAmB,IAAI,IAAI,EAAE;GACzC,SAAS,OAAO,KAAK,UAAU,CAAC;EAClC;EACA,YAAY,IAAI,IAAI,MAAM;EAC1B,OAAO;CACT;;CAEA,MAAM,sBAAsB,OAAuB;EACjD,MAAM,SAAS,gBAAgB,IAAI,EAAE;EACrC,IAAI,UAAU,MAAM,OAAO;EAC3B,MAAM,MAAM,mBAAmB,IAAI,IAAI,EAAE;EACzC,MAAM,SAAS,OAAO,KAAK,UAAU,CAAC;EACtC,gBAAgB,IAAI,IAAI,MAAM;EAC9B,OAAO;CACT;CAEA,KAAK,IAAI,QAAQ,GAAG,QAAQ,YAAY,SAAS,SAAS,GAAG,SAAS;EACpE,MAAM,eAAyB,CAAC;EAChC,KAAK,MAAM,MAAM,UAAU;GACzB,MAAM,SAAS,SAAS,EAAE;GAC1B,IAAI,gBAAgB,QAAQ,SAAS,cAAc;IACjD,IAAI,OAAO;KACT,MAAM,eAAe;KACrB,MAAM,gBAAgB;IACxB;IACA;GACF;GAIA,MAAM,QAAQ,gBAAgB,OAAO,KAAK,IAAI,mBAAmB,EAAE,GAAG,CAAC,IAAI,KAAK,IAAI,eAAe,GAAG,CAAC;GACvG,MAAM,aAAa,CACjB,GAAI,QAAQ,IAAI,IAAI,KAAK,GACzB,GAAI,OAAO,IAAI,IAAI,KAAK,CAC1B;GACA,IAAI,OAAO,MAAM,mBAAmB,WAAW;GAC/C,KAAK,MAAM,OAAO,YAAY;IAC5B,IAAI,KAAK,IAAI,IAAI,EAAE,GAAG;IACtB,KAAK,IAAI,IAAI,EAAE;IACf,aAAa,KAAK,IAAI,EAAE;GAC1B;EACF;EACA,WAAW;CACb;CAEA,OAAO,CAAC,GAAG,IAAI;AACjB;;;;;;;;;;AAWA,SAAgB,mBACd,IACA,aACA,UACA,SAUC;CACD,IAAI,YAAY,WAAW,KAAK,WAAW,GACzC,OAAO,YAAY,KAAK,QAAQ;EAC9B,QAAQ;EACR,QAAQ;EACR,UAAU;EACV,MAAM;CACR,EAAE;CAGJ,MAAM,OAAO,SAAS,QAAQ;CAC9B,MAAM,cAAc,SAAS;CAC7B,MAAM,eAAe,SAAS,iBAAiB,KAAA,IAAY,MAAM,QAAQ;CACzE,IAAI,cAAc;CAClB,IAAI,eAAe;CACnB,IAAI,aAAa;CACjB,IAAI,cAAc;CAClB,MAAM,kBAAmC,CAAC;CAC1C,MAAM,iBAAkC,CAAC;CAEzC,IAAI,QAAQ,QAAQ,aAAa;EAC/B,cAAc;EACd,aAAa;EAEb,IAAI,YAAY;EAChB,IAAI,QAAQ,MAAM;GAChB,aAAa;GACb,gBAAgB,KAAK,MAAM,IAAI;GAC/B,eAAe,KAAK,MAAM,IAAI;EAChC;EACA,IAAI,gBAAgB,YAAY,UAAU,YAAY,WAAW,YAAY,YAAY;GACvF,aAAa;GACb,gBAAgB,KAAK,YAAY,UAAU,MAAM,YAAY,WAAW,MAAM,YAAY,aAAa,IAAI;GAC3G,eAAe,KAAK,YAAY,UAAU,MAAM,YAAY,WAAW,MAAM,YAAY,aAAa,IAAI;EAC5G;EACA,eAAe;EACf,cAAc;CAChB;CAsCA,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAqCR,YAAY;;;;;;UAhEhB,gBAAgB,OACZ,KACA;;;;;;;;;;cAoEiB;UACf,aAAa;;;;;;;;;;;;;;;;;;;;;;QAsBf,WAAW;;;;;;UA/Ef,gBAAgB,OACZ,KACA;;;;;;;;;;cAmFgB;UACd,YAAY;;;;;;;;;;;;;;;;;;;;;CAsBpB,MAAM,YAAY,gBAAgB,OAAO,CAAC,IAAI,CAAC,cAAc,YAAY;CAkBzE,OAjBa,GACV,QAAQ,KAAK,EACb,IACC,KAAK,UAAU,WAAW,GAC1B,UACA,GAAG,UAAU,MAAM,GAAG,CAAC,GACvB,GAAG,iBACH,UACA,GAAG,UAAU,MAAM,GAAG,CAAC,GACvB,GAAG,cAQG,EAAE,KAAK,OAAO;EACtB,QAAQ,EAAE;EACV,QAAQ,EAAE;EACV,UAAU,EAAE;EACZ,MAAM,EAAE;CACV,EAAE;AACJ"}