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":"crystallization-store.js","names":[],"sources":["../../backends/crystallization-store.ts"],"sourcesContent":["/**\n * Crystallization Store — SQLite backend for workflow crystallization proposals (Issue #208).\n *\n * Stores pending/approved/rejected skill crystallization proposals derived from\n * workflow patterns. Human approval is required before any skill is written to disk.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport { mkdirSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { DatabaseSync } from \"node:sqlite\";\nimport type { SQLInputValue } from \"node:sqlite\";\n\nimport type { SkillProposalValidationResult } from \"../services/generated-skill-validation.js\";\nimport { computeEvidenceHash } from \"../services/pattern-detector-hash.js\";\nimport { createTransaction } from \"../utils/sqlite-transaction.js\";\nimport type { WorkflowPattern } from \"./workflow-store.js\";\nimport { BaseSqliteStore } from \"./base-sqlite-store.js\";\nimport { escapeLikeLiteralForBackslashEscape } from \"./facts-db/entity-layer.js\";\nimport { readSchemaVersion, runVersionedSchemaMigration } from \"./sqlite-schema-meta.js\";\n\n/** Increment when adding a new idempotent migration step in `runSchemaMigrations`. */\nexport const CRYSTALLIZATION_STORE_SCHEMA_VERSION = 2;\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Proposal lifecycle.\n *\n * Notes:\n * - We keep compatibility aliases in filtering/counting (\"pending\"/\"approved\")\n * to avoid breaking older CLI/digest code paths.\n */\nexport type CrystallizationStatus =\n | \"candidate\"\n | \"drafted\"\n | \"validated\"\n | \"approved\"\n | \"installed\"\n | \"quarantined\"\n | \"rejected\"\n | \"superseded\";\n\ntype CrystallizationStatusFilter = CrystallizationStatus | \"pending\" | \"approved\";\n\n/** Values accepted by `skills queue --status` and `CrystallizationStore.list({ status })`. */\nexport type CrystallizationQueueStatusFilter = CrystallizationStatusFilter | \"ready\" | \"needs-override\";\n\nexport const CRYSTALLIZATION_QUEUE_STATUS_FILTERS: ReadonlyArray<CrystallizationQueueStatusFilter> = [\n \"candidate\",\n \"drafted\",\n \"validated\",\n \"approved\",\n \"installed\",\n \"quarantined\",\n \"rejected\",\n \"superseded\",\n \"pending\",\n \"ready\",\n \"needs-override\",\n];\n\nexport function isCrystallizationQueueStatusFilter(s: string): s is CrystallizationQueueStatusFilter {\n return (CRYSTALLIZATION_QUEUE_STATUS_FILTERS as readonly string[]).includes(s);\n}\n\nexport function assertCrystallizationQueueStatusFilter(status: string | undefined): void {\n if (status === undefined) return;\n if (!isCrystallizationQueueStatusFilter(status)) {\n throw new Error(`Invalid status filter: \"${status}\". Allowed: ${CRYSTALLIZATION_QUEUE_STATUS_FILTERS.join(\", \")}`);\n }\n}\n\nexport type SkillProposalRecommendedOutput = \"SKILL.md only\";\n\nexport type SkillProposalCard = {\n name: string;\n category: string;\n description: string;\n observed_runs: number;\n successful_runs: number;\n failed_runs: number;\n captures: string[];\n why_useful: string;\n risks: string[];\n confidence: number;\n recommended_output: SkillProposalRecommendedOutput;\n provenance: {\n source: \"workflow-pattern\";\n pattern_id: string;\n evidence_hash: string;\n tool_sequence: string[];\n example_goals: string[];\n };\n};\n\nexport interface CrystallizationProposal {\n id: string;\n patternId: string;\n /** Stable hash of the non-metric evidence used to generate this proposal. */\n evidenceHash: string;\n skillName: string;\n skillContent: string;\n status: CrystallizationStatus;\n /** JSON-encoded WorkflowPattern for reference */\n patternSnapshot: string;\n /** JSON-encoded proposal card (see issue #208/#??? proposal lifecycle). */\n proposalCardJson?: string;\n category?: string;\n description?: string;\n confidence?: number;\n recommendedOutput?: SkillProposalRecommendedOutput;\n /** Reason provided when rejecting */\n rejectionReason?: string;\n /** Path where the skill was written on approval */\n outputPath?: string;\n approvedAt?: string;\n installedAt?: string;\n supersededAt?: string;\n supersededBy?: string;\n /** Stored generated-skill validation / eval snapshot (optional). */\n validationResult?: SkillProposalValidationResult;\n createdAt: string;\n updatedAt: string;\n}\n\ninterface CreateProposalInput {\n patternId: string;\n evidenceHash: string;\n skillName: string;\n skillContent: string;\n patternSnapshot: string;\n proposalCardJson?: string;\n category?: string;\n description?: string;\n confidence?: number;\n recommendedOutput?: SkillProposalRecommendedOutput;\n /** Initial state (default: drafted). */\n status?: CrystallizationStatus;\n /** Optional rejection reason when creating already-rejected records (validator gate). */\n rejectionReason?: string;\n validationResult?: SkillProposalValidationResult;\n}\n\ninterface ProposalFilter {\n status?: CrystallizationQueueStatusFilter;\n skillName?: string;\n limit?: number;\n}\n\ntype ApproveWithinCapResult =\n | { kind: \"approved\"; proposal: CrystallizationProposal }\n | { kind: \"limit-reached\" }\n | { kind: \"not-approvable\" };\n\ntype CreateApprovedWithinCapResult =\n | { kind: \"approved\"; proposal: CrystallizationProposal }\n | { kind: \"limit-reached\" };\n\n// ---------------------------------------------------------------------------\n// CrystallizationStore\n// ---------------------------------------------------------------------------\n\nexport class CrystallizationStore extends BaseSqliteStore {\n constructor(dbPath: string) {\n mkdirSync(dirname(dbPath), { recursive: true });\n const db = new DatabaseSync(dbPath);\n super(db, { deferClose: true });\n this.registerSqlFunctions();\n\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS crystallization_proposals (\n id TEXT PRIMARY KEY,\n pattern_id TEXT NOT NULL,\n evidence_hash TEXT,\n skill_name TEXT NOT NULL,\n skill_content TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n pattern_snapshot TEXT NOT NULL DEFAULT '{}',\n proposal_card_json TEXT,\n category TEXT,\n description TEXT,\n confidence REAL,\n recommended_output TEXT,\n rejection_reason TEXT,\n output_path TEXT,\n approved_at TEXT,\n installed_at TEXT,\n superseded_at TEXT,\n superseded_by TEXT,\n validation_result TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n );\n\n CREATE INDEX IF NOT EXISTS idx_cp_status ON crystallization_proposals(status);\n CREATE INDEX IF NOT EXISTS idx_cp_pattern_id ON crystallization_proposals(pattern_id);\n CREATE INDEX IF NOT EXISTS idx_cp_skill_name ON crystallization_proposals(skill_name);\n `);\n\n this.runSchemaMigrations();\n }\n\n protected getSubsystemName(): string {\n return \"crystallization-store\";\n }\n\n private registerSqlFunctions(): void {\n this.liveDb.function(\"normalizedCrystallizationTimestamp\", { deterministic: true }, (primary, fallback) => {\n return normalizeCrystallizationTimestamp(primary) ?? normalizeCrystallizationTimestamp(fallback) ?? 0;\n });\n }\n\n private runSchemaMigrations(): void {\n const namespace = \"crystallization\";\n let v = readSchemaVersion(this.liveDb, namespace);\n while (v < CRYSTALLIZATION_STORE_SCHEMA_VERSION) {\n const next = v + 1;\n if (next === 1) {\n runVersionedSchemaMigration(this.liveDb, namespace, next, () => migrateCrystallizationSchemaV1(this.liveDb));\n } else if (next === 2) {\n runVersionedSchemaMigration(this.liveDb, namespace, next, () => migrateCrystallizationSchemaV2(this.liveDb));\n } else {\n throw new Error(`crystallization-store: unsupported schema migration target ${next}`);\n }\n v = next;\n }\n }\n\n private expandStatusFilter(status: CrystallizationStatusFilter): CrystallizationStatus[] {\n if (status === \"pending\") return [\"drafted\", \"validated\"];\n if (status === \"approved\") return [\"approved\", \"installed\"];\n const canonical: CrystallizationStatus[] = [\n \"candidate\",\n \"drafted\",\n \"validated\",\n \"approved\",\n \"installed\",\n \"quarantined\",\n \"rejected\",\n \"superseded\",\n ];\n const single = status as CrystallizationStatus;\n if (!canonical.includes(single)) {\n throw new Error(\n `Invalid crystallization status filter: \"${status}\". Allowed: ${CRYSTALLIZATION_QUEUE_STATUS_FILTERS.join(\", \")}`,\n );\n }\n return [single];\n }\n\n // -------------------------------------------------------------------------\n // create\n // -------------------------------------------------------------------------\n\n create(input: CreateProposalInput): CrystallizationProposal {\n return this.runWithDb(\"create\", () => {\n const id = randomUUID();\n const now = new Date().toISOString();\n this.insertProposalInternal(id, input, input.status ?? \"drafted\", now);\n\n // biome-ignore lint/style/noNonNullAssertion: Known to exist\n return this.getByIdInternal(id)!;\n });\n }\n\n createApprovedWithinCap(input: CreateProposalInput, maxCrystallized: number): CreateApprovedWithinCapResult {\n return this.runWithDb(\"createApprovedWithinCap\", () => {\n const tx = createTransaction(\n this.liveDb,\n (): CreateApprovedWithinCapResult => {\n const row = this.liveDb\n .prepare(\"SELECT COUNT(*) as n FROM crystallization_proposals WHERE status IN ('approved', 'installed')\")\n .get() as { n: number };\n if (row.n >= maxCrystallized) {\n return { kind: \"limit-reached\" };\n }\n\n const id = randomUUID();\n const now = new Date().toISOString();\n this.insertProposalInternal(id, input, \"approved\", now, now);\n const proposal = this.getByIdInternal(id);\n if (!proposal) {\n throw new Error(\"Failed to read created crystallization proposal\");\n }\n return { kind: \"approved\", proposal };\n },\n \"IMMEDIATE\",\n );\n return tx();\n });\n }\n\n private insertProposalInternal(\n id: string,\n input: CreateProposalInput,\n status: CrystallizationStatus,\n now: string,\n approvedAt?: string,\n ): void {\n this.liveDb\n .prepare(\n `INSERT INTO crystallization_proposals\n (id, pattern_id, evidence_hash, skill_name, skill_content, status, pattern_snapshot, proposal_card_json, category, description, confidence, recommended_output, rejection_reason, validation_result, approved_at, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .run(\n id,\n input.patternId,\n input.evidenceHash,\n input.skillName,\n input.skillContent,\n status,\n input.patternSnapshot,\n input.proposalCardJson ?? null,\n input.category ?? null,\n input.description ?? null,\n input.confidence ?? null,\n input.recommendedOutput ?? null,\n input.rejectionReason ?? null,\n input.validationResult ? JSON.stringify(input.validationResult) : null,\n approvedAt ?? null,\n now,\n now,\n );\n }\n\n // -------------------------------------------------------------------------\n // getById\n // -------------------------------------------------------------------------\n\n getById(id: string): CrystallizationProposal | null {\n return this.runWithDb(\"getById\", () => this.getByIdInternal(id));\n }\n\n private getByIdInternal(id: string): CrystallizationProposal | null {\n const row = this.liveDb.prepare(\"SELECT * FROM crystallization_proposals WHERE id = ?\").get(id) as\n | Record<string, unknown>\n | undefined;\n if (!row) return null;\n return this.rowToProposal(row);\n }\n\n // -------------------------------------------------------------------------\n // getByPatternId — find proposal for a given pattern id\n // -------------------------------------------------------------------------\n\n getByPatternId(patternId: string): CrystallizationProposal | null {\n return this.runWithDb(\"getByPatternId\", () => {\n const row = this.liveDb\n .prepare(\n \"SELECT * FROM crystallization_proposals WHERE pattern_id = ? ORDER BY created_at DESC, rowid DESC LIMIT 1\",\n )\n .get(patternId) as Record<string, unknown> | undefined;\n if (!row) return null;\n return this.rowToProposal(row);\n });\n }\n\n // -------------------------------------------------------------------------\n // list\n // -------------------------------------------------------------------------\n\n list(filter?: ProposalFilter): CrystallizationProposal[] {\n return this.runWithDb(\"list\", () => {\n assertCrystallizationQueueStatusFilter(filter?.status);\n\n let query = \"SELECT * FROM crystallization_proposals WHERE 1=1\";\n const params: SQLInputValue[] = [];\n\n if (filter?.status === \"ready\") {\n query +=\n \" AND status = 'validated' AND COALESCE(json_extract(validation_result, '$.approvalDecision'), '') = 'allow'\";\n } else if (filter?.status === \"needs-override\") {\n query +=\n \" AND status = 'validated' AND json_extract(validation_result, '$.approvalDecision') = 'allow-with-override'\";\n } else if (filter?.status) {\n const statuses = this.expandStatusFilter(filter.status as CrystallizationStatusFilter);\n if (statuses.length > 0) {\n query += ` AND status IN (${statuses.map(() => \"?\").join(\",\")})`;\n params.push(...statuses);\n }\n }\n if (filter?.skillName) {\n query += \" AND skill_name LIKE ? ESCAPE '\\\\'\";\n params.push(`%${escapeLikeLiteralForBackslashEscape(filter.skillName)}%`);\n }\n\n query += \" ORDER BY created_at DESC\";\n\n if (filter?.limit && filter.limit > 0) {\n query += \" LIMIT ?\";\n params.push(filter.limit);\n }\n\n const rows = this.liveDb.prepare(query).all(...params) as Record<string, unknown>[];\n return rows.map((r) => this.rowToProposal(r));\n });\n }\n\n /**\n * List proposals for a single `pattern_id` (newest `created_at` first).\n * Used when pattern-scoped scans must not be truncated by the global `list({ limit })` cap.\n */\n listByPatternId(patternId: string, limit = 10_000): CrystallizationProposal[] {\n return this.runWithDb(\"listByPatternId\", () => {\n const cap = limit > 0 ? limit : 10_000;\n const rows = this.liveDb\n .prepare(\n \"SELECT * FROM crystallization_proposals WHERE pattern_id = ? ORDER BY created_at DESC, rowid DESC LIMIT ?\",\n )\n .all(patternId, cap) as Record<string, unknown>[];\n return rows.map((r) => this.rowToProposal(r));\n });\n }\n\n // -------------------------------------------------------------------------\n // approve — transition drafted/validated → approved\n // -------------------------------------------------------------------------\n\n approve(\n id: string,\n opts?: {\n skillName?: string;\n skillContent?: string;\n category?: string;\n description?: string;\n recommendedOutput?: SkillProposalRecommendedOutput;\n proposalCardJson?: string;\n },\n ): CrystallizationProposal | null {\n return this.runWithDb(\"approve\", () => {\n const now = new Date().toISOString();\n const result = this.liveDb\n .prepare(\n `UPDATE crystallization_proposals\n SET status = 'approved',\n skill_name = COALESCE(?, skill_name),\n skill_content = COALESCE(?, skill_content),\n category = COALESCE(?, category),\n description = COALESCE(?, description),\n recommended_output = COALESCE(?, recommended_output),\n proposal_card_json = COALESCE(?, proposal_card_json),\n approved_at = COALESCE(approved_at, ?),\n updated_at = ?\n WHERE id = ? AND status IN ('drafted', 'validated')`,\n )\n .run(\n opts?.skillName ?? null,\n opts?.skillContent ?? null,\n opts?.category ?? null,\n opts?.description ?? null,\n opts?.recommendedOutput ?? null,\n opts?.proposalCardJson ?? null,\n now,\n now,\n id,\n );\n\n if (result.changes === 0) return null;\n return this.getByIdInternal(id);\n });\n }\n\n approveWithinCap(\n id: string,\n maxCrystallized: number,\n opts?: {\n skillName?: string;\n skillContent?: string;\n category?: string;\n description?: string;\n recommendedOutput?: SkillProposalRecommendedOutput;\n proposalCardJson?: string;\n },\n ): ApproveWithinCapResult {\n return this.runWithDb(\"approveWithinCap\", () => {\n const tx = createTransaction(\n this.liveDb,\n (): ApproveWithinCapResult => {\n const row = this.liveDb\n .prepare(\"SELECT COUNT(*) as n FROM crystallization_proposals WHERE status IN ('approved', 'installed')\")\n .get() as { n: number };\n if (row.n >= maxCrystallized) {\n return { kind: \"limit-reached\" };\n }\n\n const now = new Date().toISOString();\n const result = this.liveDb\n .prepare(\n `UPDATE crystallization_proposals\n SET status = 'approved',\n skill_name = COALESCE(?, skill_name),\n skill_content = COALESCE(?, skill_content),\n category = COALESCE(?, category),\n description = COALESCE(?, description),\n recommended_output = COALESCE(?, recommended_output),\n proposal_card_json = COALESCE(?, proposal_card_json),\n approved_at = COALESCE(approved_at, ?),\n updated_at = ?\n WHERE id = ? AND status IN ('drafted', 'validated')`,\n )\n .run(\n opts?.skillName ?? null,\n opts?.skillContent ?? null,\n opts?.category ?? null,\n opts?.description ?? null,\n opts?.recommendedOutput ?? null,\n opts?.proposalCardJson ?? null,\n now,\n now,\n id,\n );\n\n if (result.changes === 0) {\n return { kind: \"not-approvable\" };\n }\n const proposal = this.getByIdInternal(id);\n if (!proposal) {\n return { kind: \"not-approvable\" };\n }\n return { kind: \"approved\", proposal };\n },\n \"IMMEDIATE\",\n );\n return tx();\n });\n }\n\n saveValidationResult(id: string, validationResult: SkillProposalValidationResult): CrystallizationProposal | null {\n return this.runWithDb(\"saveValidationResult\", () => {\n const now = new Date().toISOString();\n const result = this.liveDb\n .prepare(\n `UPDATE crystallization_proposals\n SET validation_result = ?, updated_at = ?\n WHERE id = ?`,\n )\n .run(JSON.stringify(validationResult), now, id);\n if (result.changes === 0) return null;\n return this.getByIdInternal(id);\n });\n }\n\n // -------------------------------------------------------------------------\n // install — transition approved → installed + outputPath\n // -------------------------------------------------------------------------\n\n install(id: string, outputPath: string): CrystallizationProposal | null {\n return this.runWithDb(\"install\", () => {\n const tx = createTransaction(\n this.liveDb,\n (): CrystallizationProposal | null => {\n const proposal = this.liveDb\n .prepare(\"SELECT id, pattern_id, status FROM crystallization_proposals WHERE id = ?\")\n .get(id) as { id: string; pattern_id: string; status: CrystallizationStatus } | undefined;\n if (!proposal || proposal.status !== \"approved\") {\n return null;\n }\n\n const now = new Date().toISOString();\n this.liveDb\n .prepare(\n `UPDATE crystallization_proposals\n SET status = 'superseded',\n superseded_by = ?,\n superseded_at = COALESCE(superseded_at, ?),\n updated_at = ?\n WHERE pattern_id = ? AND id <> ? AND status = 'installed'`,\n )\n .run(id, now, now, proposal.pattern_id, id);\n\n const result = this.liveDb\n .prepare(\n `UPDATE crystallization_proposals\n SET status = 'installed', output_path = ?, installed_at = COALESCE(installed_at, ?), updated_at = ?\n WHERE id = ? AND status = 'approved'`,\n )\n .run(outputPath, now, now, id);\n if (result.changes === 0) return null;\n return this.getByIdInternal(id);\n },\n \"IMMEDIATE\",\n );\n return tx();\n });\n }\n\n // -------------------------------------------------------------------------\n // reject — transition drafted/validated/approved → rejected\n // -------------------------------------------------------------------------\n\n reject(id: string, reason?: string): CrystallizationProposal | null {\n return this.runWithDb(\"reject\", () => {\n const now = new Date().toISOString();\n const result = this.liveDb\n .prepare(\n `UPDATE crystallization_proposals\n SET status = 'rejected', rejection_reason = ?, updated_at = ?\n WHERE id = ? AND status IN ('drafted', 'validated', 'approved')`,\n )\n .run(reason ?? null, now, id);\n\n if (result.changes === 0) return null;\n return this.getByIdInternal(id);\n });\n }\n\n /** Mark an installed proposal as quarantined (failed re-validation of on-disk SKILL.md). */\n quarantine(id: string, reason?: string): CrystallizationProposal | null {\n return this.runWithDb(\"quarantine\", () => {\n const now = new Date().toISOString();\n const result = this.liveDb\n .prepare(\n `UPDATE crystallization_proposals\n SET status = 'quarantined', rejection_reason = ?, updated_at = ?\n WHERE id = ? AND status = 'installed'`,\n )\n .run(reason ?? null, now, id);\n if (result.changes === 0) return null;\n return this.getByIdInternal(id);\n });\n }\n\n // -------------------------------------------------------------------------\n // count\n // -------------------------------------------------------------------------\n\n count(status?: CrystallizationQueueStatusFilter): number {\n return this.runWithDb(\"count\", () => {\n assertCrystallizationQueueStatusFilter(status);\n if (status === \"ready\") {\n const row = this.liveDb\n .prepare(\n \"SELECT COUNT(*) as n FROM crystallization_proposals WHERE status = 'validated' AND COALESCE(json_extract(validation_result, '$.approvalDecision'), '') = 'allow'\",\n )\n .get() as { n: number };\n return row.n;\n }\n if (status === \"needs-override\") {\n const row = this.liveDb\n .prepare(\n \"SELECT COUNT(*) as n FROM crystallization_proposals WHERE status = 'validated' AND json_extract(validation_result, '$.approvalDecision') = 'allow-with-override'\",\n )\n .get() as { n: number };\n return row.n;\n }\n if (status) {\n const statuses = this.expandStatusFilter(status as CrystallizationStatusFilter);\n const row = this.liveDb\n .prepare(\n `SELECT COUNT(*) as n FROM crystallization_proposals WHERE status IN (${statuses.map(() => \"?\").join(\",\")})`,\n )\n .get(...statuses) as { n: number };\n return row.n;\n }\n const row = this.liveDb.prepare(\"SELECT COUNT(*) as n FROM crystallization_proposals\").get() as { n: number };\n return row.n;\n });\n }\n\n // -------------------------------------------------------------------------\n // hasPendingOrApprovedForPattern — prevent duplicate proposals (compat alias)\n // -------------------------------------------------------------------------\n\n hasPendingOrApprovedForPattern(patternId: string): boolean {\n return this.runWithDb(\"hasPendingOrApprovedForPattern\", () => {\n const row = this.liveDb\n .prepare(\n \"SELECT COUNT(*) as n FROM crystallization_proposals WHERE pattern_id = ? AND status IN ('candidate','drafted','validated','approved')\",\n )\n .get(patternId) as { n: number };\n return row.n > 0;\n });\n }\n\n /**\n * Rejection / quarantine guard: returns true if the latest proposal for this pattern was rejected\n * or quarantined with the same evidence hash (no meaningful new evidence since suppression).\n *\n * When `legacyEvidenceHash` is provided and the stored hash is legacy-format, suppression uses\n * milestone buckets from `pattern_snapshot` at rejection time so metric milestone crossings can\n * re-arm proposals without permanently blocking on legacy hash equality.\n */\n isRejectedWithSameEvidence(\n patternId: string,\n evidenceHash: string,\n opts?: { legacyEvidenceHash?: string; evidenceCountBucketSize?: number },\n ): boolean {\n return this.runWithDb(\"isRejectedWithSameEvidence\", () => {\n const row = this.liveDb\n .prepare(\n \"SELECT status, evidence_hash, pattern_snapshot FROM crystallization_proposals WHERE pattern_id = ? ORDER BY created_at DESC, rowid DESC LIMIT 1\",\n )\n .get(patternId) as { status?: string; evidence_hash?: string; pattern_snapshot?: string } | undefined;\n if (!row) return false;\n if (row.status !== \"rejected\" && row.status !== \"quarantined\") return false;\n\n const stored = row.evidence_hash ?? \"\";\n if (stored === evidenceHash) return true;\n\n const legacyEvidenceHash = opts?.legacyEvidenceHash;\n if (!legacyEvidenceHash || stored !== legacyEvidenceHash) return false;\n\n const bucketSize = opts.evidenceCountBucketSize ?? 5;\n try {\n const atRejection = JSON.parse(row.pattern_snapshot ?? \"{}\") as WorkflowPattern;\n const rejectionMilestoneHash = computeEvidenceHash(atRejection, {\n evidenceCountBucketSize: bucketSize,\n });\n return rejectionMilestoneHash === evidenceHash;\n } catch {\n return true;\n }\n });\n }\n\n supersede(id: string, supersededBy: string): CrystallizationProposal | null {\n return this.runWithDb(\"supersede\", () => {\n const now = new Date().toISOString();\n const result = this.liveDb\n .prepare(\n `UPDATE crystallization_proposals\n SET status = 'superseded', superseded_by = ?, superseded_at = ?, updated_at = ?\n WHERE id = ? AND status IN ('installed', 'approved', 'quarantined')`,\n )\n .run(supersededBy, now, now, id);\n if (result.changes === 0) return null;\n return this.getByIdInternal(id);\n });\n }\n\n // -------------------------------------------------------------------------\n // Private helpers\n // -------------------------------------------------------------------------\n\n private rowToProposal(row: Record<string, unknown>): CrystallizationProposal {\n return {\n id: row.id as string,\n patternId: row.pattern_id as string,\n evidenceHash: (row.evidence_hash as string | null | undefined) ?? (row.pattern_id as string),\n skillName: row.skill_name as string,\n skillContent: row.skill_content as string,\n status: row.status as string as CrystallizationStatus,\n patternSnapshot: row.pattern_snapshot as string,\n proposalCardJson: row.proposal_card_json ? (row.proposal_card_json as string) : undefined,\n category: row.category ? (row.category as string) : undefined,\n description: row.description ? (row.description as string) : undefined,\n confidence: row.confidence !== null && row.confidence !== undefined ? (row.confidence as number) : undefined,\n recommendedOutput: row.recommended_output\n ? (row.recommended_output as SkillProposalRecommendedOutput)\n : undefined,\n rejectionReason: row.rejection_reason ? (row.rejection_reason as string) : undefined,\n outputPath: row.output_path ? (row.output_path as string) : undefined,\n approvedAt: row.approved_at ? (row.approved_at as string) : undefined,\n installedAt: row.installed_at ? (row.installed_at as string) : undefined,\n supersededAt: row.superseded_at ? (row.superseded_at as string) : undefined,\n supersededBy: row.superseded_by ? (row.superseded_by as string) : undefined,\n validationResult: parseValidationResult(row.validation_result),\n createdAt: row.created_at as string,\n updatedAt: row.updated_at as string,\n };\n }\n}\n\nfunction migrateCrystallizationSchemaV1(db: DatabaseSync): void {\n const cols = db.prepare(\"PRAGMA table_info(crystallization_proposals)\").all() as Array<{ name: string }>;\n const has = (name: string) => cols.some((c) => c.name === name);\n\n if (!has(\"evidence_hash\")) {\n db.exec(\"ALTER TABLE crystallization_proposals ADD COLUMN evidence_hash TEXT\");\n }\n if (!has(\"proposal_card_json\")) {\n db.exec(\"ALTER TABLE crystallization_proposals ADD COLUMN proposal_card_json TEXT\");\n }\n if (!has(\"category\")) {\n db.exec(\"ALTER TABLE crystallization_proposals ADD COLUMN category TEXT\");\n }\n if (!has(\"description\")) {\n db.exec(\"ALTER TABLE crystallization_proposals ADD COLUMN description TEXT\");\n }\n if (!has(\"confidence\")) {\n db.exec(\"ALTER TABLE crystallization_proposals ADD COLUMN confidence REAL\");\n }\n if (!has(\"recommended_output\")) {\n db.exec(\"ALTER TABLE crystallization_proposals ADD COLUMN recommended_output TEXT\");\n }\n if (!has(\"approved_at\")) {\n db.exec(\"ALTER TABLE crystallization_proposals ADD COLUMN approved_at TEXT\");\n }\n if (!has(\"installed_at\")) {\n db.exec(\"ALTER TABLE crystallization_proposals ADD COLUMN installed_at TEXT\");\n }\n if (!has(\"superseded_at\")) {\n db.exec(\"ALTER TABLE crystallization_proposals ADD COLUMN superseded_at TEXT\");\n }\n if (!has(\"superseded_by\")) {\n db.exec(\"ALTER TABLE crystallization_proposals ADD COLUMN superseded_by TEXT\");\n }\n if (!has(\"validation_result\")) {\n db.exec(\"ALTER TABLE crystallization_proposals ADD COLUMN validation_result TEXT\");\n }\n\n db.exec(\"UPDATE crystallization_proposals SET status = 'validated' WHERE status = 'pending' AND status IS NOT NULL\");\n db.exec(\n \"UPDATE crystallization_proposals SET status = 'installed', installed_at = COALESCE(installed_at, datetime('now')) WHERE status = 'approved' AND output_path IS NOT NULL AND TRIM(output_path) <> ''\",\n );\n\n db.exec(\n \"UPDATE crystallization_proposals SET evidence_hash = pattern_id WHERE (evidence_hash IS NULL OR evidence_hash = '') AND pattern_id IS NOT NULL\",\n );\n\n db.exec(\"CREATE INDEX IF NOT EXISTS idx_cp_evidence_hash ON crystallization_proposals(evidence_hash)\");\n\n const duplicates = db\n .prepare(\n \"SELECT pattern_id FROM crystallization_proposals WHERE status = 'installed' GROUP BY pattern_id HAVING COUNT(*) > 1\",\n )\n .all() as Array<{ pattern_id: string }>;\n for (const row of duplicates) {\n const installedRows = db\n .prepare(\n `SELECT id\n FROM crystallization_proposals\n WHERE pattern_id = ? AND status = 'installed'\n ORDER BY normalizedCrystallizationTimestamp(installed_at, created_at) DESC,\n normalizedCrystallizationTimestamp(updated_at, created_at) DESC,\n normalizedCrystallizationTimestamp(created_at, NULL) DESC,\n id DESC`,\n )\n .all(row.pattern_id) as Array<{ id: string }>;\n const keepId = installedRows[0]?.id;\n if (!keepId) continue;\n db.prepare(\n `UPDATE crystallization_proposals\n SET status = 'superseded',\n superseded_by = ?,\n superseded_at = COALESCE(superseded_at, datetime('now')),\n updated_at = datetime('now')\n WHERE pattern_id = ? AND status = 'installed' AND id <> ?`,\n ).run(keepId, row.pattern_id, keepId);\n }\n db.exec(\n \"CREATE UNIQUE INDEX IF NOT EXISTS idx_cp_one_installed_per_pattern ON crystallization_proposals(pattern_id) WHERE status = 'installed'\",\n );\n}\n\nfunction migrateCrystallizationSchemaV2(db: DatabaseSync): void {\n db.exec(\"CREATE INDEX IF NOT EXISTS idx_cp_created_at ON crystallization_proposals(created_at)\");\n}\n\nfunction parseValidationResult(value: unknown): SkillProposalValidationResult | undefined {\n if (typeof value !== \"string\" || value.trim().length === 0) return undefined;\n try {\n return JSON.parse(value) as SkillProposalValidationResult;\n } catch {\n return undefined;\n }\n}\n\nfunction normalizeCrystallizationTimestamp(value: unknown): number | null {\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return value > 10_000_000_000 ? value / 1000 : value;\n }\n if (typeof value !== \"string\") return null;\n const trimmed = value.trim();\n if (!trimmed) return null;\n const numeric = Number(trimmed);\n if (Number.isFinite(numeric)) {\n return numeric > 10_000_000_000 ? numeric / 1000 : numeric;\n }\n const parsed = Date.parse(trimmed.includes(\"T\") ? trimmed : `${trimmed.replace(\" \", \"T\")}Z`);\n return Number.isFinite(parsed) ? parsed / 1000 : null;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"crystallization-store.js","names":[],"sources":["../../backends/crystallization-store.ts"],"sourcesContent":["/**\n * Crystallization Store — SQLite backend for workflow crystallization proposals (Issue #208).\n *\n * Stores pending/approved/rejected skill crystallization proposals derived from\n * workflow patterns. Human approval is required before any skill is written to disk.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport { mkdirSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { DatabaseSync } from \"node:sqlite\";\nimport type { SQLInputValue } from \"node:sqlite\";\n\nimport type { SkillProposalValidationResult } from \"../services/generated-skill-validation.js\";\nimport { computeEvidenceHash } from \"../services/pattern-detector-hash.js\";\nimport { createTransaction } from \"../utils/sqlite-transaction.js\";\nimport type { WorkflowPattern } from \"./workflow-store.js\";\nimport { BaseSqliteStore } from \"./base-sqlite-store.js\";\nimport { escapeLikeLiteralForBackslashEscape } from \"./facts-db/entity-layer.js\";\nimport { readSchemaVersion, runVersionedSchemaMigration } from \"./sqlite-schema-meta.js\";\n\n/** Increment when adding a new idempotent migration step in `runSchemaMigrations`. */\nexport const CRYSTALLIZATION_STORE_SCHEMA_VERSION = 2;\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Proposal lifecycle.\n *\n * Notes:\n * - We keep compatibility aliases in filtering/counting (\"pending\"/\"approved\")\n * to avoid breaking older CLI/digest code paths.\n */\nexport type CrystallizationStatus =\n | \"candidate\"\n | \"drafted\"\n | \"validated\"\n | \"approved\"\n | \"installed\"\n | \"quarantined\"\n | \"rejected\"\n | \"superseded\";\n\ntype CrystallizationStatusFilter = CrystallizationStatus | \"pending\" | \"approved\";\n\n/** Values accepted by `skills queue --status` and `CrystallizationStore.list({ status })`. */\nexport type CrystallizationQueueStatusFilter = CrystallizationStatusFilter | \"ready\" | \"needs-override\";\n\nexport const CRYSTALLIZATION_QUEUE_STATUS_FILTERS: ReadonlyArray<CrystallizationQueueStatusFilter> = [\n \"candidate\",\n \"drafted\",\n \"validated\",\n \"approved\",\n \"installed\",\n \"quarantined\",\n \"rejected\",\n \"superseded\",\n \"pending\",\n \"ready\",\n \"needs-override\",\n];\n\nexport function isCrystallizationQueueStatusFilter(s: string): s is CrystallizationQueueStatusFilter {\n return (CRYSTALLIZATION_QUEUE_STATUS_FILTERS as readonly string[]).includes(s);\n}\n\nexport function assertCrystallizationQueueStatusFilter(status: string | undefined): void {\n if (status === undefined) return;\n if (!isCrystallizationQueueStatusFilter(status)) {\n throw new Error(`Invalid status filter: \"${status}\". Allowed: ${CRYSTALLIZATION_QUEUE_STATUS_FILTERS.join(\", \")}`);\n }\n}\n\nexport type SkillProposalRecommendedOutput = \"SKILL.md only\";\n\nexport type SkillProposalCard = {\n name: string;\n category: string;\n description: string;\n observed_runs: number;\n successful_runs: number;\n failed_runs: number;\n captures: string[];\n why_useful: string;\n risks: string[];\n confidence: number;\n recommended_output: SkillProposalRecommendedOutput;\n provenance: {\n source: \"workflow-pattern\";\n pattern_id: string;\n evidence_hash: string;\n tool_sequence: string[];\n example_goals: string[];\n };\n};\n\nexport interface CrystallizationProposal {\n id: string;\n patternId: string;\n /** Stable hash of the non-metric evidence used to generate this proposal. */\n evidenceHash: string;\n skillName: string;\n skillContent: string;\n status: CrystallizationStatus;\n /** JSON-encoded WorkflowPattern for reference */\n patternSnapshot: string;\n /** JSON-encoded proposal card (see issue #208/#??? proposal lifecycle). */\n proposalCardJson?: string;\n category?: string;\n description?: string;\n confidence?: number;\n recommendedOutput?: SkillProposalRecommendedOutput;\n /** Reason provided when rejecting */\n rejectionReason?: string;\n /** Path where the skill was written on approval */\n outputPath?: string;\n approvedAt?: string;\n installedAt?: string;\n supersededAt?: string;\n supersededBy?: string;\n /** Stored generated-skill validation / eval snapshot (optional). */\n validationResult?: SkillProposalValidationResult;\n createdAt: string;\n updatedAt: string;\n}\n\ninterface CreateProposalInput {\n patternId: string;\n evidenceHash: string;\n skillName: string;\n skillContent: string;\n patternSnapshot: string;\n proposalCardJson?: string;\n category?: string;\n description?: string;\n confidence?: number;\n recommendedOutput?: SkillProposalRecommendedOutput;\n /** Initial state (default: drafted). */\n status?: CrystallizationStatus;\n /** Optional rejection reason when creating already-rejected records (validator gate). */\n rejectionReason?: string;\n validationResult?: SkillProposalValidationResult;\n}\n\ninterface ProposalFilter {\n status?: CrystallizationQueueStatusFilter;\n skillName?: string;\n limit?: number;\n}\n\ntype ApproveWithinCapResult =\n | { kind: \"approved\"; proposal: CrystallizationProposal }\n | { kind: \"limit-reached\" }\n | { kind: \"not-approvable\" };\n\ntype CreateApprovedWithinCapResult =\n | { kind: \"approved\"; proposal: CrystallizationProposal }\n | { kind: \"limit-reached\" };\n\n// ---------------------------------------------------------------------------\n// CrystallizationStore\n// ---------------------------------------------------------------------------\n\nexport class CrystallizationStore extends BaseSqliteStore {\n constructor(dbPath: string) {\n mkdirSync(dirname(dbPath), { recursive: true });\n const db = new DatabaseSync(dbPath);\n super(db, { deferClose: true });\n this.registerSqlFunctions();\n\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS crystallization_proposals (\n id TEXT PRIMARY KEY,\n pattern_id TEXT NOT NULL,\n evidence_hash TEXT,\n skill_name TEXT NOT NULL,\n skill_content TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n pattern_snapshot TEXT NOT NULL DEFAULT '{}',\n proposal_card_json TEXT,\n category TEXT,\n description TEXT,\n confidence REAL,\n recommended_output TEXT,\n rejection_reason TEXT,\n output_path TEXT,\n approved_at TEXT,\n installed_at TEXT,\n superseded_at TEXT,\n superseded_by TEXT,\n validation_result TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n );\n\n CREATE INDEX IF NOT EXISTS idx_cp_status ON crystallization_proposals(status);\n CREATE INDEX IF NOT EXISTS idx_cp_pattern_id ON crystallization_proposals(pattern_id);\n CREATE INDEX IF NOT EXISTS idx_cp_skill_name ON crystallization_proposals(skill_name);\n `);\n\n this.runSchemaMigrations();\n }\n\n protected getSubsystemName(): string {\n return \"crystallization-store\";\n }\n\n private registerSqlFunctions(): void {\n this.liveDb.function(\"normalizedCrystallizationTimestamp\", { deterministic: true }, (primary, fallback) => {\n return normalizeCrystallizationTimestamp(primary) ?? normalizeCrystallizationTimestamp(fallback) ?? 0;\n });\n }\n\n private runSchemaMigrations(): void {\n const namespace = \"crystallization\";\n let v = readSchemaVersion(this.liveDb, namespace);\n while (v < CRYSTALLIZATION_STORE_SCHEMA_VERSION) {\n const next = v + 1;\n if (next === 1) {\n runVersionedSchemaMigration(this.liveDb, namespace, next, () => migrateCrystallizationSchemaV1(this.liveDb));\n } else if (next === 2) {\n runVersionedSchemaMigration(this.liveDb, namespace, next, () => migrateCrystallizationSchemaV2(this.liveDb));\n } else {\n throw new Error(`crystallization-store: unsupported schema migration target ${next}`);\n }\n v = next;\n }\n }\n\n private expandStatusFilter(status: CrystallizationStatusFilter): CrystallizationStatus[] {\n if (status === \"pending\") return [\"drafted\", \"validated\"];\n if (status === \"approved\") return [\"approved\", \"installed\"];\n const canonical: CrystallizationStatus[] = [\n \"candidate\",\n \"drafted\",\n \"validated\",\n \"approved\",\n \"installed\",\n \"quarantined\",\n \"rejected\",\n \"superseded\",\n ];\n const single = status as CrystallizationStatus;\n if (!canonical.includes(single)) {\n throw new Error(\n `Invalid crystallization status filter: \"${status}\". Allowed: ${CRYSTALLIZATION_QUEUE_STATUS_FILTERS.join(\", \")}`,\n );\n }\n return [single];\n }\n\n // -------------------------------------------------------------------------\n // create\n // -------------------------------------------------------------------------\n\n create(input: CreateProposalInput): CrystallizationProposal {\n return this.runWithDb(\"create\", () => {\n const id = randomUUID();\n const now = new Date().toISOString();\n this.insertProposalInternal(id, input, input.status ?? \"drafted\", now);\n\n // biome-ignore lint/style/noNonNullAssertion: Known to exist\n return this.getByIdInternal(id)!;\n });\n }\n\n createApprovedWithinCap(input: CreateProposalInput, maxCrystallized: number): CreateApprovedWithinCapResult {\n return this.runWithDb(\"createApprovedWithinCap\", () => {\n const tx = createTransaction(\n this.liveDb,\n (): CreateApprovedWithinCapResult => {\n const row = this.liveDb\n .prepare(\"SELECT COUNT(*) as n FROM crystallization_proposals WHERE status IN ('approved', 'installed')\")\n .get() as { n: number };\n if (row.n >= maxCrystallized) {\n return { kind: \"limit-reached\" };\n }\n\n const id = randomUUID();\n const now = new Date().toISOString();\n this.insertProposalInternal(id, input, \"approved\", now, now);\n const proposal = this.getByIdInternal(id);\n if (!proposal) {\n throw new Error(\"Failed to read created crystallization proposal\");\n }\n return { kind: \"approved\", proposal };\n },\n \"IMMEDIATE\",\n );\n return tx();\n });\n }\n\n private insertProposalInternal(\n id: string,\n input: CreateProposalInput,\n status: CrystallizationStatus,\n now: string,\n approvedAt?: string,\n ): void {\n this.liveDb\n .prepare(\n `INSERT INTO crystallization_proposals\n (id, pattern_id, evidence_hash, skill_name, skill_content, status, pattern_snapshot, proposal_card_json, category, description, confidence, recommended_output, rejection_reason, validation_result, approved_at, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .run(\n id,\n input.patternId,\n input.evidenceHash,\n input.skillName,\n input.skillContent,\n status,\n input.patternSnapshot,\n input.proposalCardJson ?? null,\n input.category ?? null,\n input.description ?? null,\n input.confidence ?? null,\n input.recommendedOutput ?? null,\n input.rejectionReason ?? null,\n input.validationResult ? JSON.stringify(input.validationResult) : null,\n approvedAt ?? null,\n now,\n now,\n );\n }\n\n // -------------------------------------------------------------------------\n // getById\n // -------------------------------------------------------------------------\n\n getById(id: string): CrystallizationProposal | null {\n return this.runWithDb(\"getById\", () => this.getByIdInternal(id));\n }\n\n private getByIdInternal(id: string): CrystallizationProposal | null {\n const row = this.liveDb.prepare(\"SELECT * FROM crystallization_proposals WHERE id = ?\").get(id) as\n | Record<string, unknown>\n | undefined;\n if (!row) return null;\n return this.rowToProposal(row);\n }\n\n // -------------------------------------------------------------------------\n // getByPatternId — find proposal for a given pattern id\n // -------------------------------------------------------------------------\n\n getByPatternId(patternId: string): CrystallizationProposal | null {\n return this.runWithDb(\"getByPatternId\", () => {\n const row = this.liveDb\n .prepare(\n \"SELECT * FROM crystallization_proposals WHERE pattern_id = ? ORDER BY created_at DESC, rowid DESC LIMIT 1\",\n )\n .get(patternId) as Record<string, unknown> | undefined;\n if (!row) return null;\n return this.rowToProposal(row);\n });\n }\n\n // -------------------------------------------------------------------------\n // list\n // -------------------------------------------------------------------------\n\n list(filter?: ProposalFilter): CrystallizationProposal[] {\n return this.runWithDb(\"list\", () => {\n assertCrystallizationQueueStatusFilter(filter?.status);\n\n let query = \"SELECT * FROM crystallization_proposals WHERE 1=1\";\n const params: SQLInputValue[] = [];\n\n if (filter?.status === \"ready\") {\n query +=\n \" AND status = 'validated' AND COALESCE(json_extract(validation_result, '$.approvalDecision'), '') = 'allow'\";\n } else if (filter?.status === \"needs-override\") {\n query +=\n \" AND status = 'validated' AND json_extract(validation_result, '$.approvalDecision') = 'allow-with-override'\";\n } else if (filter?.status) {\n const statuses = this.expandStatusFilter(filter.status as CrystallizationStatusFilter);\n if (statuses.length > 0) {\n query += ` AND status IN (${statuses.map(() => \"?\").join(\",\")})`;\n params.push(...statuses);\n }\n }\n if (filter?.skillName) {\n query += \" AND skill_name LIKE ? ESCAPE '\\\\'\";\n params.push(`%${escapeLikeLiteralForBackslashEscape(filter.skillName)}%`);\n }\n\n query += \" ORDER BY created_at DESC\";\n\n if (filter?.limit && filter.limit > 0) {\n query += \" LIMIT ?\";\n params.push(filter.limit);\n }\n\n const rows = this.liveDb.prepare(query).all(...params) as Record<string, unknown>[];\n return rows.map((r) => this.rowToProposal(r));\n });\n }\n\n /**\n * List proposals for a single `pattern_id` (newest `created_at` first).\n * Used when pattern-scoped scans must not be truncated by the global `list({ limit })` cap.\n */\n listByPatternId(patternId: string, limit = 10_000): CrystallizationProposal[] {\n return this.runWithDb(\"listByPatternId\", () => {\n const cap = limit > 0 ? limit : 10_000;\n const rows = this.liveDb\n .prepare(\n \"SELECT * FROM crystallization_proposals WHERE pattern_id = ? ORDER BY created_at DESC, rowid DESC LIMIT ?\",\n )\n .all(patternId, cap) as Record<string, unknown>[];\n return rows.map((r) => this.rowToProposal(r));\n });\n }\n\n // -------------------------------------------------------------------------\n // approve — transition drafted/validated → approved\n // -------------------------------------------------------------------------\n\n approve(\n id: string,\n opts?: {\n skillName?: string;\n skillContent?: string;\n category?: string;\n description?: string;\n recommendedOutput?: SkillProposalRecommendedOutput;\n proposalCardJson?: string;\n },\n ): CrystallizationProposal | null {\n return this.runWithDb(\"approve\", () => {\n const now = new Date().toISOString();\n const result = this.liveDb\n .prepare(\n `UPDATE crystallization_proposals\n SET status = 'approved',\n skill_name = COALESCE(?, skill_name),\n skill_content = COALESCE(?, skill_content),\n category = COALESCE(?, category),\n description = COALESCE(?, description),\n recommended_output = COALESCE(?, recommended_output),\n proposal_card_json = COALESCE(?, proposal_card_json),\n approved_at = COALESCE(approved_at, ?),\n updated_at = ?\n WHERE id = ? AND status IN ('drafted', 'validated')`,\n )\n .run(\n opts?.skillName ?? null,\n opts?.skillContent ?? null,\n opts?.category ?? null,\n opts?.description ?? null,\n opts?.recommendedOutput ?? null,\n opts?.proposalCardJson ?? null,\n now,\n now,\n id,\n );\n\n if (result.changes === 0) return null;\n return this.getByIdInternal(id);\n });\n }\n\n approveWithinCap(\n id: string,\n maxCrystallized: number,\n opts?: {\n skillName?: string;\n skillContent?: string;\n category?: string;\n description?: string;\n recommendedOutput?: SkillProposalRecommendedOutput;\n proposalCardJson?: string;\n },\n ): ApproveWithinCapResult {\n return this.runWithDb(\"approveWithinCap\", () => {\n const tx = createTransaction(\n this.liveDb,\n (): ApproveWithinCapResult => {\n const row = this.liveDb\n .prepare(\"SELECT COUNT(*) as n FROM crystallization_proposals WHERE status IN ('approved', 'installed')\")\n .get() as { n: number };\n if (row.n >= maxCrystallized) {\n return { kind: \"limit-reached\" };\n }\n\n const now = new Date().toISOString();\n const result = this.liveDb\n .prepare(\n `UPDATE crystallization_proposals\n SET status = 'approved',\n skill_name = COALESCE(?, skill_name),\n skill_content = COALESCE(?, skill_content),\n category = COALESCE(?, category),\n description = COALESCE(?, description),\n recommended_output = COALESCE(?, recommended_output),\n proposal_card_json = COALESCE(?, proposal_card_json),\n approved_at = COALESCE(approved_at, ?),\n updated_at = ?\n WHERE id = ? AND status IN ('drafted', 'validated')`,\n )\n .run(\n opts?.skillName ?? null,\n opts?.skillContent ?? null,\n opts?.category ?? null,\n opts?.description ?? null,\n opts?.recommendedOutput ?? null,\n opts?.proposalCardJson ?? null,\n now,\n now,\n id,\n );\n\n if (result.changes === 0) {\n return { kind: \"not-approvable\" };\n }\n const proposal = this.getByIdInternal(id);\n if (!proposal) {\n return { kind: \"not-approvable\" };\n }\n return { kind: \"approved\", proposal };\n },\n \"IMMEDIATE\",\n );\n return tx();\n });\n }\n\n saveValidationResult(id: string, validationResult: SkillProposalValidationResult): CrystallizationProposal | null {\n return this.runWithDb(\"saveValidationResult\", () => {\n const now = new Date().toISOString();\n const result = this.liveDb\n .prepare(\n `UPDATE crystallization_proposals\n SET validation_result = ?, updated_at = ?\n WHERE id = ?`,\n )\n .run(JSON.stringify(validationResult), now, id);\n if (result.changes === 0) return null;\n return this.getByIdInternal(id);\n });\n }\n\n // -------------------------------------------------------------------------\n // install — transition approved → installed + outputPath\n // -------------------------------------------------------------------------\n\n install(id: string, outputPath: string): CrystallizationProposal | null {\n return this.runWithDb(\"install\", () => {\n const tx = createTransaction(\n this.liveDb,\n (): CrystallizationProposal | null => {\n const proposal = this.liveDb\n .prepare(\"SELECT id, pattern_id, status FROM crystallization_proposals WHERE id = ?\")\n .get(id) as { id: string; pattern_id: string; status: CrystallizationStatus } | undefined;\n if (!proposal || proposal.status !== \"approved\") {\n return null;\n }\n\n const now = new Date().toISOString();\n this.liveDb\n .prepare(\n `UPDATE crystallization_proposals\n SET status = 'superseded',\n superseded_by = ?,\n superseded_at = COALESCE(superseded_at, ?),\n updated_at = ?\n WHERE pattern_id = ? AND id <> ? AND status = 'installed'`,\n )\n .run(id, now, now, proposal.pattern_id, id);\n\n const result = this.liveDb\n .prepare(\n `UPDATE crystallization_proposals\n SET status = 'installed', output_path = ?, installed_at = COALESCE(installed_at, ?), updated_at = ?\n WHERE id = ? AND status = 'approved'`,\n )\n .run(outputPath, now, now, id);\n if (result.changes === 0) return null;\n return this.getByIdInternal(id);\n },\n \"IMMEDIATE\",\n );\n return tx();\n });\n }\n\n // -------------------------------------------------------------------------\n // reject — transition drafted/validated/approved → rejected\n // -------------------------------------------------------------------------\n\n reject(id: string, reason?: string): CrystallizationProposal | null {\n return this.runWithDb(\"reject\", () => {\n const now = new Date().toISOString();\n const result = this.liveDb\n .prepare(\n `UPDATE crystallization_proposals\n SET status = 'rejected', rejection_reason = ?, updated_at = ?\n WHERE id = ? AND status IN ('drafted', 'validated', 'approved')`,\n )\n .run(reason ?? null, now, id);\n\n if (result.changes === 0) return null;\n return this.getByIdInternal(id);\n });\n }\n\n /** Mark an installed proposal as quarantined (failed re-validation of on-disk SKILL.md). */\n quarantine(id: string, reason?: string): CrystallizationProposal | null {\n return this.runWithDb(\"quarantine\", () => {\n const now = new Date().toISOString();\n const result = this.liveDb\n .prepare(\n `UPDATE crystallization_proposals\n SET status = 'quarantined', rejection_reason = ?, updated_at = ?\n WHERE id = ? AND status = 'installed'`,\n )\n .run(reason ?? null, now, id);\n if (result.changes === 0) return null;\n return this.getByIdInternal(id);\n });\n }\n\n // -------------------------------------------------------------------------\n // count\n // -------------------------------------------------------------------------\n\n count(status?: CrystallizationQueueStatusFilter): number {\n return this.runWithDb(\"count\", () => {\n assertCrystallizationQueueStatusFilter(status);\n if (status === \"ready\") {\n const row = this.liveDb\n .prepare(\n \"SELECT COUNT(*) as n FROM crystallization_proposals WHERE status = 'validated' AND COALESCE(json_extract(validation_result, '$.approvalDecision'), '') = 'allow'\",\n )\n .get() as { n: number };\n return row.n;\n }\n if (status === \"needs-override\") {\n const row = this.liveDb\n .prepare(\n \"SELECT COUNT(*) as n FROM crystallization_proposals WHERE status = 'validated' AND json_extract(validation_result, '$.approvalDecision') = 'allow-with-override'\",\n )\n .get() as { n: number };\n return row.n;\n }\n if (status) {\n const statuses = this.expandStatusFilter(status as CrystallizationStatusFilter);\n const row = this.liveDb\n .prepare(\n `SELECT COUNT(*) as n FROM crystallization_proposals WHERE status IN (${statuses.map(() => \"?\").join(\",\")})`,\n )\n .get(...statuses) as { n: number };\n return row.n;\n }\n const row = this.liveDb.prepare(\"SELECT COUNT(*) as n FROM crystallization_proposals\").get() as { n: number };\n return row.n;\n });\n }\n\n // -------------------------------------------------------------------------\n // hasPendingOrApprovedForPattern — prevent duplicate proposals (compat alias)\n // -------------------------------------------------------------------------\n\n hasPendingOrApprovedForPattern(patternId: string): boolean {\n return this.runWithDb(\"hasPendingOrApprovedForPattern\", () => {\n const row = this.liveDb\n .prepare(\n \"SELECT COUNT(*) as n FROM crystallization_proposals WHERE pattern_id = ? AND status IN ('candidate','drafted','validated','approved')\",\n )\n .get(patternId) as { n: number };\n return row.n > 0;\n });\n }\n\n /**\n * Rejection / quarantine guard: returns true if the latest proposal for this pattern was rejected\n * or quarantined with the same evidence hash (no meaningful new evidence since suppression).\n *\n * When `legacyEvidenceHash` is provided and the stored hash is legacy-format, suppression uses\n * milestone buckets from `pattern_snapshot` at rejection time so metric milestone crossings can\n * re-arm proposals without permanently blocking on legacy hash equality.\n */\n isRejectedWithSameEvidence(\n patternId: string,\n evidenceHash: string,\n opts?: { legacyEvidenceHash?: string; evidenceCountBucketSize?: number },\n ): boolean {\n return this.runWithDb(\"isRejectedWithSameEvidence\", () => {\n const row = this.liveDb\n .prepare(\n \"SELECT status, evidence_hash, pattern_snapshot FROM crystallization_proposals WHERE pattern_id = ? ORDER BY created_at DESC, rowid DESC LIMIT 1\",\n )\n .get(patternId) as { status?: string; evidence_hash?: string; pattern_snapshot?: string } | undefined;\n if (!row) return false;\n if (row.status !== \"rejected\" && row.status !== \"quarantined\") return false;\n\n const stored = row.evidence_hash ?? \"\";\n if (stored === evidenceHash) return true;\n\n const legacyEvidenceHash = opts?.legacyEvidenceHash;\n if (!legacyEvidenceHash || stored !== legacyEvidenceHash) return false;\n\n const bucketSize = opts.evidenceCountBucketSize ?? 5;\n try {\n const atRejection = JSON.parse(row.pattern_snapshot ?? \"{}\") as WorkflowPattern;\n const rejectionMilestoneHash = computeEvidenceHash(atRejection, {\n evidenceCountBucketSize: bucketSize,\n });\n return rejectionMilestoneHash === evidenceHash;\n } catch {\n return true;\n }\n });\n }\n\n supersede(id: string, supersededBy: string): CrystallizationProposal | null {\n return this.runWithDb(\"supersede\", () => {\n const now = new Date().toISOString();\n const result = this.liveDb\n .prepare(\n `UPDATE crystallization_proposals\n SET status = 'superseded', superseded_by = ?, superseded_at = ?, updated_at = ?\n WHERE id = ? AND status IN ('installed', 'approved', 'quarantined')`,\n )\n .run(supersededBy, now, now, id);\n if (result.changes === 0) return null;\n return this.getByIdInternal(id);\n });\n }\n\n // -------------------------------------------------------------------------\n // Private helpers\n // -------------------------------------------------------------------------\n\n private rowToProposal(row: Record<string, unknown>): CrystallizationProposal {\n return {\n id: row.id as string,\n patternId: row.pattern_id as string,\n evidenceHash: (row.evidence_hash as string | null | undefined) ?? (row.pattern_id as string),\n skillName: row.skill_name as string,\n skillContent: row.skill_content as string,\n status: row.status as string as CrystallizationStatus,\n patternSnapshot: row.pattern_snapshot as string,\n proposalCardJson: row.proposal_card_json ? (row.proposal_card_json as string) : undefined,\n category: row.category ? (row.category as string) : undefined,\n description: row.description ? (row.description as string) : undefined,\n confidence: row.confidence !== null && row.confidence !== undefined ? (row.confidence as number) : undefined,\n recommendedOutput: row.recommended_output\n ? (row.recommended_output as SkillProposalRecommendedOutput)\n : undefined,\n rejectionReason: row.rejection_reason ? (row.rejection_reason as string) : undefined,\n outputPath: row.output_path ? (row.output_path as string) : undefined,\n approvedAt: row.approved_at ? (row.approved_at as string) : undefined,\n installedAt: row.installed_at ? (row.installed_at as string) : undefined,\n supersededAt: row.superseded_at ? (row.superseded_at as string) : undefined,\n supersededBy: row.superseded_by ? (row.superseded_by as string) : undefined,\n validationResult: parseValidationResult(row.validation_result),\n createdAt: row.created_at as string,\n updatedAt: row.updated_at as string,\n };\n }\n}\n\nfunction migrateCrystallizationSchemaV1(db: DatabaseSync): void {\n const cols = db.prepare(\"PRAGMA table_info(crystallization_proposals)\").all() as Array<{ name: string }>;\n const has = (name: string) => cols.some((c) => c.name === name);\n\n if (!has(\"evidence_hash\")) {\n db.exec(\"ALTER TABLE crystallization_proposals ADD COLUMN evidence_hash TEXT\");\n }\n if (!has(\"proposal_card_json\")) {\n db.exec(\"ALTER TABLE crystallization_proposals ADD COLUMN proposal_card_json TEXT\");\n }\n if (!has(\"category\")) {\n db.exec(\"ALTER TABLE crystallization_proposals ADD COLUMN category TEXT\");\n }\n if (!has(\"description\")) {\n db.exec(\"ALTER TABLE crystallization_proposals ADD COLUMN description TEXT\");\n }\n if (!has(\"confidence\")) {\n db.exec(\"ALTER TABLE crystallization_proposals ADD COLUMN confidence REAL\");\n }\n if (!has(\"recommended_output\")) {\n db.exec(\"ALTER TABLE crystallization_proposals ADD COLUMN recommended_output TEXT\");\n }\n if (!has(\"approved_at\")) {\n db.exec(\"ALTER TABLE crystallization_proposals ADD COLUMN approved_at TEXT\");\n }\n if (!has(\"installed_at\")) {\n db.exec(\"ALTER TABLE crystallization_proposals ADD COLUMN installed_at TEXT\");\n }\n if (!has(\"superseded_at\")) {\n db.exec(\"ALTER TABLE crystallization_proposals ADD COLUMN superseded_at TEXT\");\n }\n if (!has(\"superseded_by\")) {\n db.exec(\"ALTER TABLE crystallization_proposals ADD COLUMN superseded_by TEXT\");\n }\n if (!has(\"validation_result\")) {\n db.exec(\"ALTER TABLE crystallization_proposals ADD COLUMN validation_result TEXT\");\n }\n\n db.exec(\"UPDATE crystallization_proposals SET status = 'validated' WHERE status = 'pending' AND status IS NOT NULL\");\n db.exec(\n \"UPDATE crystallization_proposals SET status = 'installed', installed_at = COALESCE(installed_at, datetime('now')) WHERE status = 'approved' AND output_path IS NOT NULL AND TRIM(output_path) <> ''\",\n );\n\n db.exec(\n \"UPDATE crystallization_proposals SET evidence_hash = pattern_id WHERE (evidence_hash IS NULL OR evidence_hash = '') AND pattern_id IS NOT NULL\",\n );\n\n db.exec(\"CREATE INDEX IF NOT EXISTS idx_cp_evidence_hash ON crystallization_proposals(evidence_hash)\");\n\n const duplicates = db\n .prepare(\n \"SELECT pattern_id FROM crystallization_proposals WHERE status = 'installed' GROUP BY pattern_id HAVING COUNT(*) > 1\",\n )\n .all() as Array<{ pattern_id: string }>;\n for (const row of duplicates) {\n const installedRows = db\n .prepare(\n `SELECT id\n FROM crystallization_proposals\n WHERE pattern_id = ? AND status = 'installed'\n ORDER BY normalizedCrystallizationTimestamp(installed_at, created_at) DESC,\n normalizedCrystallizationTimestamp(updated_at, created_at) DESC,\n normalizedCrystallizationTimestamp(created_at, NULL) DESC,\n id DESC`,\n )\n .all(row.pattern_id) as Array<{ id: string }>;\n const keepId = installedRows[0]?.id;\n if (!keepId) continue;\n db.prepare(\n `UPDATE crystallization_proposals\n SET status = 'superseded',\n superseded_by = ?,\n superseded_at = COALESCE(superseded_at, datetime('now')),\n updated_at = datetime('now')\n WHERE pattern_id = ? AND status = 'installed' AND id <> ?`,\n ).run(keepId, row.pattern_id, keepId);\n }\n db.exec(\n \"CREATE UNIQUE INDEX IF NOT EXISTS idx_cp_one_installed_per_pattern ON crystallization_proposals(pattern_id) WHERE status = 'installed'\",\n );\n}\n\nfunction migrateCrystallizationSchemaV2(db: DatabaseSync): void {\n db.exec(\"CREATE INDEX IF NOT EXISTS idx_cp_created_at ON crystallization_proposals(created_at)\");\n}\n\nfunction parseValidationResult(value: unknown): SkillProposalValidationResult | undefined {\n if (typeof value !== \"string\" || value.trim().length === 0) return undefined;\n try {\n return JSON.parse(value) as SkillProposalValidationResult;\n } catch {\n return undefined;\n }\n}\n\nfunction normalizeCrystallizationTimestamp(value: unknown): number | null {\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return value > 10_000_000_000 ? value / 1000 : value;\n }\n if (typeof value !== \"string\") return null;\n const trimmed = value.trim();\n if (!trimmed) return null;\n const numeric = Number(trimmed);\n if (Number.isFinite(numeric)) {\n return numeric > 10_000_000_000 ? numeric / 1000 : numeric;\n }\n const parsed = Date.parse(trimmed.includes(\"T\") ? trimmed : `${trimmed.replace(\" \", \"T\")}Z`);\n return Number.isFinite(parsed) ? parsed / 1000 : null;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"edict-store.js","names":[],"sources":["../../backends/edict-store.ts"],"sourcesContent":["/**\n * EdictStore — SQLite-backed store for verified ground-truth facts.\n *\n * Edicts are facts that are:\n * 1. Verified — confidence is explicitly marked as verified by a human or trusted source\n * 2. Non-negotiable — the agent treats it as true without reasoning\n * 3. Forced-injection — always included in context regardless of token budget pressure\n * 4. Small and declarative — not a story, just a statement of fact\n *\n * Edicts are stored separately from facts (own table) to allow independent lifecycle\n * management and to ensure they are never pruned by normal memory decay.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport { mkdirSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { DatabaseSync, type SQLInputValue } from \"node:sqlite\";\nimport { capturePluginError } from \"../services/error-reporter.js\";\nimport { parseTags, serializeTags } from \"../utils/tags.js\";\nimport { BaseSqliteStore } from \"./base-sqlite-store.js\";\n\n/** TTL modes for edicts */\ntype EdictTtl = \"never\" | \"event\" | number;\n\nfunction normalizeEdictText(text: string): string {\n return text.trim().replace(/\\s+/g, \" \").toLowerCase();\n}\n\nfunction parseIsoDateToUnixSeconds(iso: string): number | null {\n const ms = Date.parse(iso);\n if (!Number.isFinite(ms)) return null;\n return Math.floor(ms / 1000);\n}\n\n/** An edict entry — verified ground-truth fact */\ninterface EdictEntry {\n id: string;\n /** The verified statement of fact */\n text: string;\n /** Optional source describing who or what verified this edict (e.g. \"human:markus\", \"ops:oncall-runbook\") */\n source?: string | null;\n /** Unix timestamp when this edict was verified */\n verifiedAt: number | null;\n /** ISO 8601 date or null. Edict expires after this date (for ttl=\"event\"). */\n expiresAt: string | null;\n /** Unix epoch seconds expiry (preferred for comparisons); derived from expiresAt. */\n expiresAtSec: number | null;\n /** TTL mode: \"never\" (permanent), \"event\" (expiresAt date), or seconds (ttl as number) */\n ttl: EdictTtl;\n /** Labels for filtering (e.g. [\"operations\", \"ssh\"]) */\n tags: string[];\n /** When this edict was created (Unix epoch seconds) */\n createdAt: number;\n /** When this edict was last updated (Unix epoch seconds) */\n updatedAt: number;\n}\n\n/** Input for creating a new edict */\ninterface AddEdictInput {\n text: string;\n source?: string;\n tags?: string[];\n ttl?: EdictTtl;\n expiresAt?: string;\n}\n\n/** Input for updating an existing edict */\ninterface UpdateEdictInput {\n id: string;\n text?: string;\n source?: string;\n tags?: string[];\n ttl?: EdictTtl;\n expiresAt?: string;\n}\n\n/** Options for listing/retrieving edicts */\ninterface ListEdictsOptions {\n tags?: string[];\n includeExpired?: boolean;\n limit?: number;\n}\n\n/** Options for getEdicts (extends ListEdictsOptions) */\ninterface GetEdictsOptions extends ListEdictsOptions {\n format?: \"full\" | \"prompt\";\n}\n\n/** Statistics about the edict store */\ninterface EdictStats {\n total: number;\n byTag: Record<string, number>;\n expired: number;\n expiringIn7Days: number;\n}\n\n/** Render an edict as a Markdown bullet line with tag prefix */\nfunction renderEdictLine(edict: EdictEntry): string {\n // Edicts are intended to be short and declarative, but cap pathological cases so\n // forced injection cannot explode prompt size.\n const MAX_EDICT_PROMPT_TEXT_CHARS = 1000;\n const tagStr = edict.tags.length > 0 ? `[${edict.tags[0]}] ` : \"\";\n const text =\n edict.text.length > MAX_EDICT_PROMPT_TEXT_CHARS\n ? `${edict.text.slice(0, MAX_EDICT_PROMPT_TEXT_CHARS)}…`\n : edict.text;\n return `- ${tagStr}${text}`;\n}\n\n/** Render a list of edicts as a Markdown block for system prompt injection */\nfunction renderEdictsForPrompt(edicts: EdictEntry[]): string {\n if (edicts.length === 0) return \"\";\n const header = \"## Verified Ground Truth\\n\";\n const lines = edicts.map((e) => renderEdictLine(e));\n return `${header + lines.join(\"\\n\")}\\n`;\n}\n\n/** Escape a string for safe use as a SQLite LIKE pattern */\nfunction escapeLikePattern(s: string): string {\n return s.replace(/[~%_]/g, \"~$&\");\n}\n\nexport class EdictStore extends BaseSqliteStore {\n /** Set true only after `runMigrations()` completes successfully (issue #964 / #953). */\n private _isReady = false;\n\n constructor(dbPath: string) {\n mkdirSync(dirname(dbPath), { recursive: true });\n const db = new DatabaseSync(dbPath);\n super(db, { deferClose: true });\n\n try {\n this.runMigrations();\n this._isReady = true;\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"edict-store\",\n operation: \"runMigrations\",\n });\n this._isReady = false;\n }\n }\n\n protected getSubsystemName(): string {\n return \"edict-store\";\n }\n\n /** Run all schema migrations. Idempotent — safe to call on existing databases. */\n private runMigrations(): void {\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS edicts (\n id TEXT PRIMARY KEY,\n text TEXT NOT NULL,\n normalized_text TEXT,\n source TEXT,\n verified_at INTEGER,\n expires_at TEXT,\n expires_at_sec INTEGER,\n ttl TEXT NOT NULL DEFAULT 'never',\n tags TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n )\n `);\n\n this.db.exec(`\n CREATE INDEX IF NOT EXISTS idx_edicts_tags ON edicts(tags)\n WHERE tags IS NOT NULL AND tags != ''\n `);\n\n this.db.exec(`\n CREATE INDEX IF NOT EXISTS idx_edicts_expires ON edicts(expires_at)\n WHERE expires_at IS NOT NULL\n `);\n\n // Backward-compat migrations for databases created before these columns existed.\n const tableInfo = this.db.prepare(\"PRAGMA table_info(edicts)\").all() as Array<{ name: string }>;\n const hasCol = (name: string) => tableInfo.some((c) => c.name === name);\n\n if (!hasCol(\"id\")) {\n // SQLite cannot add a PRIMARY KEY via ALTER TABLE; add a plain column + unique index instead.\n this.db.exec(\"ALTER TABLE edicts ADD COLUMN id TEXT\");\n }\n // Populate ids for any legacy/incomplete rows deterministically enough for our use-case.\n this.db.exec(\"UPDATE edicts SET id = ('e_' || lower(hex(randomblob(6)))) WHERE id IS NULL OR id = ''\");\n this.db.exec(\"CREATE UNIQUE INDEX IF NOT EXISTS idx_edicts_id_unique ON edicts(id)\");\n\n if (!hasCol(\"normalized_text\")) {\n this.db.exec(\"ALTER TABLE edicts ADD COLUMN normalized_text TEXT\");\n }\n\n if (!hasCol(\"expires_at_sec\")) {\n this.db.exec(\"ALTER TABLE edicts ADD COLUMN expires_at_sec INTEGER\");\n }\n this.db.exec(\"CREATE INDEX IF NOT EXISTS idx_edicts_expires_sec ON edicts(expires_at_sec)\");\n\n // Backfill derived columns best-effort (id may already exist).\n const rows = this.db.prepare(\"SELECT rowid, id, text, expires_at FROM edicts ORDER BY rowid ASC\").all() as Array<{\n rowid: number;\n id: string | null;\n text: string;\n expires_at: string | null;\n }>;\n\n const updateNorm = this.db.prepare(\"UPDATE edicts SET normalized_text = ? WHERE rowid = ?\");\n const updateExpires = this.db.prepare(\"UPDATE edicts SET expires_at_sec = ? WHERE rowid = ?\");\n for (const row of rows) {\n // Keep normalized_text in sync even for legacy rows.\n try {\n updateNorm.run(normalizeEdictText(row.text), row.rowid);\n } catch {\n // Best-effort only.\n }\n\n if (row.expires_at?.trim()) {\n const sec = parseIsoDateToUnixSeconds(row.expires_at.trim());\n if (sec != null) {\n try {\n updateExpires.run(sec, row.rowid);\n } catch {\n // Best-effort only.\n }\n }\n }\n }\n }\n\n /** Check if an edict is currently expired (based on ttl and expires_at) */\n isExpired(edict: EdictEntry): boolean {\n if (edict.ttl === \"never\") return false;\n if (edict.ttl === \"event\") {\n const expiresAt = edict.expiresAt?.trim();\n // Fail closed for malformed legacy rows: event TTL without a valid expiry is considered expired.\n if (!expiresAt) return true;\n const sec = edict.expiresAtSec ?? parseIsoDateToUnixSeconds(expiresAt);\n if (sec == null) return true;\n return Math.floor(Date.now() / 1000) >= sec;\n }\n // Numeric TTL: seconds since creation\n const ttlSec = typeof edict.ttl === \"number\" ? edict.ttl : 0;\n return Date.now() / 1000 > edict.createdAt + ttlSec;\n }\n\n /** Add a new edict. Returns the created edict. Throws on duplicate text (normalized). */\n add(input: AddEdictInput): EdictEntry {\n return this.runWithDb(\"add\", () => this.addInternal(input));\n }\n\n private addInternal(input: AddEdictInput): EdictEntry {\n const nowSec = Math.floor(Date.now() / 1000);\n const id = `e_${randomUUID().replace(/-/g, \"\").slice(0, 12)}`;\n const source = input.source ?? null;\n const tagsStr = input.tags && input.tags.length > 0 ? serializeTags(input.tags) : null;\n const ttl: EdictTtl = input.ttl ?? \"never\";\n\n const ttlStr = typeof ttl === \"number\" ? String(ttl) : ttl;\n\n if (typeof ttl === \"number\" && (!Number.isFinite(ttl) || ttl <= 0 || !Number.isInteger(ttl))) {\n throw new Error(\"Edict ttl must be a positive integer number of seconds, 'never', or 'event'\");\n }\n\n const expiresAt = input.expiresAt ?? null;\n const expiresAtSec = expiresAt ? parseIsoDateToUnixSeconds(expiresAt) : null;\n if (ttl === \"event\") {\n if (!expiresAt) {\n throw new Error(\"Edict ttl='event' requires expiresAt (ISO 8601)\");\n }\n if (expiresAtSec == null) {\n throw new Error(\"Edict expiresAt must be a valid ISO 8601 date\");\n }\n }\n\n const normalizedText = normalizeEdictText(input.text);\n const existing = this.findByNormalizedTextInternal(normalizedText);\n if (existing) {\n throw new Error(`Edict with similar text already exists: ${existing.id}`);\n }\n\n this.liveDb\n .prepare(\n `INSERT INTO edicts (id, text, normalized_text, source, verified_at, expires_at, expires_at_sec, ttl, tags, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .run(\n id,\n input.text.trim(),\n normalizedText,\n source,\n nowSec,\n expiresAt,\n expiresAtSec,\n ttlStr,\n tagsStr,\n nowSec,\n nowSec,\n );\n\n return {\n id,\n text: input.text.trim(),\n source,\n verifiedAt: nowSec,\n expiresAt,\n expiresAtSec,\n ttl,\n tags: input.tags ?? [],\n createdAt: nowSec,\n updatedAt: nowSec,\n };\n }\n\n /** Find an edict by normalized text (for duplicate detection) */\n private findByNormalizedTextInternal(normalized: string): EdictEntry | null {\n const rows = this.liveDb.prepare(\"SELECT * FROM edicts WHERE normalized_text = ? LIMIT 1\").all(normalized) as Array<\n Record<string, unknown>\n >;\n return rows.length > 0 ? this.rowToEntry(rows[0]) : null;\n }\n\n /** Get a single edict by id */\n getById(id: string): EdictEntry | null {\n return this.runWithDb(\"getById\", () => this.getByIdInternal(id));\n }\n\n private getByIdInternal(id: string): EdictEntry | null {\n const row = this.liveDb.prepare(\"SELECT * FROM edicts WHERE id = ?\").get(id) as Record<string, unknown> | undefined;\n return row ? this.rowToEntry(row) : null;\n }\n\n /** List edicts, optionally filtered by tags */\n list(options: ListEdictsOptions = {}): EdictEntry[] {\n if (!this._isReady) return [];\n try {\n return this.runWithDb(\"list\", () => this.listInternal(options));\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"edict-store\",\n operation: \"list\",\n });\n return [];\n }\n }\n\n private listInternal(options: ListEdictsOptions = {}): EdictEntry[] {\n const { tags, includeExpired = false, limit = 100 } = options;\n const nowSec = Math.floor(Date.now() / 1000);\n const parts: string[] = [];\n const params: SQLInputValue[] = [];\n\n if (!includeExpired) {\n parts.push(\n `((ttl = 'never') OR (ttl = 'event' AND expires_at_sec IS NOT NULL AND expires_at_sec > ?) OR (CAST(ttl AS INTEGER) > 0 AND created_at + CAST(ttl AS INTEGER) > ?))`,\n );\n params.push(nowSec, nowSec);\n }\n\n if (tags && tags.length > 0) {\n const tagParts: string[] = [];\n for (const tag of tags) {\n const t = tag.trim();\n if (!t) continue;\n tagParts.push(`(',' || LOWER(COALESCE(tags, '')) || ',') LIKE ? ESCAPE '~'`);\n params.push(`%,${escapeLikePattern(t.toLowerCase())},%`);\n }\n if (tagParts.length > 0) parts.push(`(${tagParts.join(\" OR \")})`);\n }\n\n const safeLimit = Math.max(1, Math.min(1000, Math.floor(limit)));\n const where = parts.length > 0 ? `WHERE ${parts.join(\" AND \")}` : \"\";\n params.push(safeLimit);\n\n const rows = this.liveDb\n .prepare(`SELECT * FROM edicts ${where} ORDER BY created_at DESC LIMIT ?`)\n .all(...params) as Array<Record<string, unknown>>;\n\n return rows.map((r) => this.rowToEntry(r));\n }\n\n /** Get all non-expired edicts, optionally filtered by tags */\n getEdicts(options: GetEdictsOptions = {}): {\n edicts: EdictEntry[];\n renderForPrompt: string;\n } {\n if (!this._isReady) {\n return { edicts: [], renderForPrompt: \"\" };\n }\n try {\n return this.runWithDb(\"getEdicts\", () => {\n const { tags, format = \"prompt\", limit = 100 } = options;\n const edicts = this.listInternal({\n tags,\n includeExpired: false,\n limit,\n });\n const renderForPrompt = format === \"prompt\" ? renderEdictsForPrompt(edicts) : \"\";\n return { edicts, renderForPrompt };\n });\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"edict-store\",\n operation: \"getEdicts\",\n });\n return { edicts: [], renderForPrompt: \"\" };\n }\n }\n\n /** Update an existing edict. Returns the updated edict or null if not found. */\n update(input: UpdateEdictInput): EdictEntry | null {\n return this.runWithDb(\"update\", () => this.updateInternal(input));\n }\n\n private updateInternal(input: UpdateEdictInput): EdictEntry | null {\n const existing = this.getByIdInternal(input.id);\n if (!existing) return null;\n\n const nowSec = Math.floor(Date.now() / 1000);\n const text = input.text !== undefined ? input.text.trim() : existing.text;\n const source = input.source !== undefined ? input.source : existing.source;\n const tags = input.tags !== undefined ? input.tags : existing.tags;\n const ttl = input.ttl !== undefined ? input.ttl : existing.ttl;\n const expiresAt = input.expiresAt !== undefined ? input.expiresAt : existing.expiresAt;\n const ttlStr = typeof ttl === \"number\" ? String(ttl) : ttl;\n const expiresAtSec = expiresAt ? parseIsoDateToUnixSeconds(expiresAt) : null;\n if (typeof ttl === \"number\" && (!Number.isFinite(ttl) || ttl <= 0 || !Number.isInteger(ttl))) {\n throw new Error(\"Edict ttl must be a positive integer number of seconds, 'never', or 'event'\");\n }\n if (ttl === \"event\") {\n if (!expiresAt) {\n throw new Error(\"Edict ttl='event' requires expiresAt (ISO 8601)\");\n }\n if (expiresAtSec == null) {\n throw new Error(\"Edict expiresAt must be a valid ISO 8601 date\");\n }\n }\n const tagsStr = tags.length > 0 ? serializeTags(tags) : null;\n const normalizedText = normalizeEdictText(text);\n\n this.liveDb\n .prepare(\n \"UPDATE edicts SET text = ?, normalized_text = ?, source = ?, expires_at = ?, expires_at_sec = ?, ttl = ?, tags = ?, updated_at = ? WHERE id = ?\",\n )\n .run(text, normalizedText, source ?? null, expiresAt ?? null, expiresAtSec, ttlStr, tagsStr, nowSec, input.id);\n\n return {\n ...existing,\n text,\n expiresAtSec,\n source,\n expiresAt,\n ttl,\n tags,\n updatedAt: nowSec,\n };\n }\n\n /** Remove an edict by id */\n remove(id: string): boolean {\n return this.runWithDb(\"remove\", () => {\n const result = this.liveDb.prepare(\"DELETE FROM edicts WHERE id = ?\").run(id);\n return result.changes > 0;\n });\n }\n\n /** Count total edicts */\n count(): number {\n return this.runWithDb(\"count\", () => {\n const row = this.liveDb.prepare(\"SELECT COUNT(*) as cnt FROM edicts\").get() as { cnt: number };\n return row.cnt;\n });\n }\n\n /** Get statistics about the edict store */\n stats(): EdictStats {\n return this.runWithDb(\"stats\", () => this.statsInternal());\n }\n\n private statsInternal(): EdictStats {\n const nowSec = Math.floor(Date.now() / 1000);\n const totalRow = this.liveDb.prepare(\"SELECT COUNT(*) as cnt FROM edicts\").get() as { cnt: number };\n const total = totalRow.cnt;\n\n const expiredRow = this.liveDb\n .prepare(\n `SELECT COUNT(*) as cnt FROM edicts WHERE\n (ttl = 'event' AND (expires_at_sec IS NULL OR expires_at_sec <= ?))\n OR (CAST(ttl AS INTEGER) > 0 AND created_at + CAST(ttl AS INTEGER) <= ?)`,\n )\n .get(nowSec, nowSec) as { cnt: number };\n const expired = expiredRow.cnt;\n\n const sevenDaysFromNowSec = nowSec + 7 * 24 * 3600;\n const expiringRow = this.liveDb\n .prepare(\n `SELECT COUNT(*) as cnt FROM edicts WHERE ttl = 'event' AND expires_at_sec IS NOT NULL AND expires_at_sec > ? AND expires_at_sec <= ?`,\n )\n .get(nowSec, sevenDaysFromNowSec) as { cnt: number };\n const expiringIn7Days = expiringRow.cnt;\n\n const allRows = this.liveDb.prepare(\"SELECT tags FROM edicts\").all() as Array<{ tags: string | null }>;\n const byTag: Record<string, number> = {};\n for (const row of allRows) {\n if (!row.tags) continue;\n const parsed = parseTags(row.tags);\n for (const tag of parsed) {\n byTag[tag] = (byTag[tag] ?? 0) + 1;\n }\n }\n\n return { total, byTag, expired, expiringIn7Days };\n }\n\n /** Prune all expired edicts. Returns count of deleted rows. */\n pruneExpired(): number {\n return this.runWithDb(\"pruneExpired\", () => {\n const nowSec = Math.floor(Date.now() / 1000);\n const result = this.liveDb\n .prepare(\n `DELETE FROM edicts WHERE\n (ttl = 'event' AND (expires_at_sec IS NULL OR expires_at_sec <= ?))\n OR (CAST(ttl AS INTEGER) > 0 AND created_at + CAST(ttl AS INTEGER) <= ?)`,\n )\n .run(nowSec, nowSec);\n return Number(result.changes ?? 0);\n });\n }\n\n /** Convert a raw SQLite row to an EdictEntry */\n private rowToEntry(row: Record<string, unknown>): EdictEntry {\n const ttlRaw = (row.ttl as string) ?? \"never\";\n let ttl: EdictTtl;\n if (ttlRaw === \"never\") ttl = \"never\";\n else if (ttlRaw === \"event\") ttl = \"event\";\n else {\n const parsed = Number(ttlRaw);\n ttl = Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0;\n }\n\n return {\n id: row.id as string,\n text: row.text as string,\n source: (row.source as string) ?? null,\n verifiedAt: (row.verified_at as number) ?? null,\n expiresAt: (row.expires_at as string) ?? null,\n expiresAtSec: (row.expires_at_sec as number) ?? null,\n ttl,\n tags: parseTags(row.tags as string | null),\n createdAt: row.created_at as number,\n updatedAt: row.updated_at as number,\n };\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"edict-store.js","names":[],"sources":["../../backends/edict-store.ts"],"sourcesContent":["/**\n * EdictStore — SQLite-backed store for verified ground-truth facts.\n *\n * Edicts are facts that are:\n * 1. Verified — confidence is explicitly marked as verified by a human or trusted source\n * 2. Non-negotiable — the agent treats it as true without reasoning\n * 3. Forced-injection — always included in context regardless of token budget pressure\n * 4. Small and declarative — not a story, just a statement of fact\n *\n * Edicts are stored separately from facts (own table) to allow independent lifecycle\n * management and to ensure they are never pruned by normal memory decay.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport { mkdirSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { DatabaseSync, type SQLInputValue } from \"node:sqlite\";\nimport { capturePluginError } from \"../services/error-reporter.js\";\nimport { parseTags, serializeTags } from \"../utils/tags.js\";\nimport { BaseSqliteStore } from \"./base-sqlite-store.js\";\n\n/** TTL modes for edicts */\ntype EdictTtl = \"never\" | \"event\" | number;\n\nfunction normalizeEdictText(text: string): string {\n return text.trim().replace(/\\s+/g, \" \").toLowerCase();\n}\n\nfunction parseIsoDateToUnixSeconds(iso: string): number | null {\n const ms = Date.parse(iso);\n if (!Number.isFinite(ms)) return null;\n return Math.floor(ms / 1000);\n}\n\n/** An edict entry — verified ground-truth fact */\ninterface EdictEntry {\n id: string;\n /** The verified statement of fact */\n text: string;\n /** Optional source describing who or what verified this edict (e.g. \"human:markus\", \"ops:oncall-runbook\") */\n source?: string | null;\n /** Unix timestamp when this edict was verified */\n verifiedAt: number | null;\n /** ISO 8601 date or null. Edict expires after this date (for ttl=\"event\"). */\n expiresAt: string | null;\n /** Unix epoch seconds expiry (preferred for comparisons); derived from expiresAt. */\n expiresAtSec: number | null;\n /** TTL mode: \"never\" (permanent), \"event\" (expiresAt date), or seconds (ttl as number) */\n ttl: EdictTtl;\n /** Labels for filtering (e.g. [\"operations\", \"ssh\"]) */\n tags: string[];\n /** When this edict was created (Unix epoch seconds) */\n createdAt: number;\n /** When this edict was last updated (Unix epoch seconds) */\n updatedAt: number;\n}\n\n/** Input for creating a new edict */\ninterface AddEdictInput {\n text: string;\n source?: string;\n tags?: string[];\n ttl?: EdictTtl;\n expiresAt?: string;\n}\n\n/** Input for updating an existing edict */\ninterface UpdateEdictInput {\n id: string;\n text?: string;\n source?: string;\n tags?: string[];\n ttl?: EdictTtl;\n expiresAt?: string;\n}\n\n/** Options for listing/retrieving edicts */\ninterface ListEdictsOptions {\n tags?: string[];\n includeExpired?: boolean;\n limit?: number;\n}\n\n/** Options for getEdicts (extends ListEdictsOptions) */\ninterface GetEdictsOptions extends ListEdictsOptions {\n format?: \"full\" | \"prompt\";\n}\n\n/** Statistics about the edict store */\ninterface EdictStats {\n total: number;\n byTag: Record<string, number>;\n expired: number;\n expiringIn7Days: number;\n}\n\n/** Render an edict as a Markdown bullet line with tag prefix */\nfunction renderEdictLine(edict: EdictEntry): string {\n // Edicts are intended to be short and declarative, but cap pathological cases so\n // forced injection cannot explode prompt size.\n const MAX_EDICT_PROMPT_TEXT_CHARS = 1000;\n const tagStr = edict.tags.length > 0 ? `[${edict.tags[0]}] ` : \"\";\n const text =\n edict.text.length > MAX_EDICT_PROMPT_TEXT_CHARS\n ? `${edict.text.slice(0, MAX_EDICT_PROMPT_TEXT_CHARS)}…`\n : edict.text;\n return `- ${tagStr}${text}`;\n}\n\n/** Render a list of edicts as a Markdown block for system prompt injection */\nfunction renderEdictsForPrompt(edicts: EdictEntry[]): string {\n if (edicts.length === 0) return \"\";\n const header = \"## Verified Ground Truth\\n\";\n const lines = edicts.map((e) => renderEdictLine(e));\n return `${header + lines.join(\"\\n\")}\\n`;\n}\n\n/** Escape a string for safe use as a SQLite LIKE pattern */\nfunction escapeLikePattern(s: string): string {\n return s.replace(/[~%_]/g, \"~$&\");\n}\n\nexport class EdictStore extends BaseSqliteStore {\n /** Set true only after `runMigrations()` completes successfully (issue #964 / #953). */\n private _isReady = false;\n\n constructor(dbPath: string) {\n mkdirSync(dirname(dbPath), { recursive: true });\n const db = new DatabaseSync(dbPath);\n super(db, { deferClose: true });\n\n try {\n this.runMigrations();\n this._isReady = true;\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"edict-store\",\n operation: \"runMigrations\",\n });\n this._isReady = false;\n }\n }\n\n protected getSubsystemName(): string {\n return \"edict-store\";\n }\n\n /** Run all schema migrations. Idempotent — safe to call on existing databases. */\n private runMigrations(): void {\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS edicts (\n id TEXT PRIMARY KEY,\n text TEXT NOT NULL,\n normalized_text TEXT,\n source TEXT,\n verified_at INTEGER,\n expires_at TEXT,\n expires_at_sec INTEGER,\n ttl TEXT NOT NULL DEFAULT 'never',\n tags TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n )\n `);\n\n this.db.exec(`\n CREATE INDEX IF NOT EXISTS idx_edicts_tags ON edicts(tags)\n WHERE tags IS NOT NULL AND tags != ''\n `);\n\n this.db.exec(`\n CREATE INDEX IF NOT EXISTS idx_edicts_expires ON edicts(expires_at)\n WHERE expires_at IS NOT NULL\n `);\n\n // Backward-compat migrations for databases created before these columns existed.\n const tableInfo = this.db.prepare(\"PRAGMA table_info(edicts)\").all() as Array<{ name: string }>;\n const hasCol = (name: string) => tableInfo.some((c) => c.name === name);\n\n if (!hasCol(\"id\")) {\n // SQLite cannot add a PRIMARY KEY via ALTER TABLE; add a plain column + unique index instead.\n this.db.exec(\"ALTER TABLE edicts ADD COLUMN id TEXT\");\n }\n // Populate ids for any legacy/incomplete rows deterministically enough for our use-case.\n this.db.exec(\"UPDATE edicts SET id = ('e_' || lower(hex(randomblob(6)))) WHERE id IS NULL OR id = ''\");\n this.db.exec(\"CREATE UNIQUE INDEX IF NOT EXISTS idx_edicts_id_unique ON edicts(id)\");\n\n if (!hasCol(\"normalized_text\")) {\n this.db.exec(\"ALTER TABLE edicts ADD COLUMN normalized_text TEXT\");\n }\n\n if (!hasCol(\"expires_at_sec\")) {\n this.db.exec(\"ALTER TABLE edicts ADD COLUMN expires_at_sec INTEGER\");\n }\n this.db.exec(\"CREATE INDEX IF NOT EXISTS idx_edicts_expires_sec ON edicts(expires_at_sec)\");\n\n // Backfill derived columns best-effort (id may already exist).\n const rows = this.db.prepare(\"SELECT rowid, id, text, expires_at FROM edicts ORDER BY rowid ASC\").all() as Array<{\n rowid: number;\n id: string | null;\n text: string;\n expires_at: string | null;\n }>;\n\n const updateNorm = this.db.prepare(\"UPDATE edicts SET normalized_text = ? WHERE rowid = ?\");\n const updateExpires = this.db.prepare(\"UPDATE edicts SET expires_at_sec = ? WHERE rowid = ?\");\n for (const row of rows) {\n // Keep normalized_text in sync even for legacy rows.\n try {\n updateNorm.run(normalizeEdictText(row.text), row.rowid);\n } catch {\n // Best-effort only.\n }\n\n if (row.expires_at?.trim()) {\n const sec = parseIsoDateToUnixSeconds(row.expires_at.trim());\n if (sec != null) {\n try {\n updateExpires.run(sec, row.rowid);\n } catch {\n // Best-effort only.\n }\n }\n }\n }\n }\n\n /** Check if an edict is currently expired (based on ttl and expires_at) */\n isExpired(edict: EdictEntry): boolean {\n if (edict.ttl === \"never\") return false;\n if (edict.ttl === \"event\") {\n const expiresAt = edict.expiresAt?.trim();\n // Fail closed for malformed legacy rows: event TTL without a valid expiry is considered expired.\n if (!expiresAt) return true;\n const sec = edict.expiresAtSec ?? parseIsoDateToUnixSeconds(expiresAt);\n if (sec == null) return true;\n return Math.floor(Date.now() / 1000) >= sec;\n }\n // Numeric TTL: seconds since creation\n const ttlSec = typeof edict.ttl === \"number\" ? edict.ttl : 0;\n return Date.now() / 1000 > edict.createdAt + ttlSec;\n }\n\n /** Add a new edict. Returns the created edict. Throws on duplicate text (normalized). */\n add(input: AddEdictInput): EdictEntry {\n return this.runWithDb(\"add\", () => this.addInternal(input));\n }\n\n private addInternal(input: AddEdictInput): EdictEntry {\n const nowSec = Math.floor(Date.now() / 1000);\n const id = `e_${randomUUID().replace(/-/g, \"\").slice(0, 12)}`;\n const source = input.source ?? null;\n const tagsStr = input.tags && input.tags.length > 0 ? serializeTags(input.tags) : null;\n const ttl: EdictTtl = input.ttl ?? \"never\";\n\n const ttlStr = typeof ttl === \"number\" ? String(ttl) : ttl;\n\n if (typeof ttl === \"number\" && (!Number.isFinite(ttl) || ttl <= 0 || !Number.isInteger(ttl))) {\n throw new Error(\"Edict ttl must be a positive integer number of seconds, 'never', or 'event'\");\n }\n\n const expiresAt = input.expiresAt ?? null;\n const expiresAtSec = expiresAt ? parseIsoDateToUnixSeconds(expiresAt) : null;\n if (ttl === \"event\") {\n if (!expiresAt) {\n throw new Error(\"Edict ttl='event' requires expiresAt (ISO 8601)\");\n }\n if (expiresAtSec == null) {\n throw new Error(\"Edict expiresAt must be a valid ISO 8601 date\");\n }\n }\n\n const normalizedText = normalizeEdictText(input.text);\n const existing = this.findByNormalizedTextInternal(normalizedText);\n if (existing) {\n throw new Error(`Edict with similar text already exists: ${existing.id}`);\n }\n\n this.liveDb\n .prepare(\n `INSERT INTO edicts (id, text, normalized_text, source, verified_at, expires_at, expires_at_sec, ttl, tags, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .run(\n id,\n input.text.trim(),\n normalizedText,\n source,\n nowSec,\n expiresAt,\n expiresAtSec,\n ttlStr,\n tagsStr,\n nowSec,\n nowSec,\n );\n\n return {\n id,\n text: input.text.trim(),\n source,\n verifiedAt: nowSec,\n expiresAt,\n expiresAtSec,\n ttl,\n tags: input.tags ?? [],\n createdAt: nowSec,\n updatedAt: nowSec,\n };\n }\n\n /** Find an edict by normalized text (for duplicate detection) */\n private findByNormalizedTextInternal(normalized: string): EdictEntry | null {\n const rows = this.liveDb.prepare(\"SELECT * FROM edicts WHERE normalized_text = ? LIMIT 1\").all(normalized) as Array<\n Record<string, unknown>\n >;\n return rows.length > 0 ? this.rowToEntry(rows[0]) : null;\n }\n\n /** Get a single edict by id */\n getById(id: string): EdictEntry | null {\n return this.runWithDb(\"getById\", () => this.getByIdInternal(id));\n }\n\n private getByIdInternal(id: string): EdictEntry | null {\n const row = this.liveDb.prepare(\"SELECT * FROM edicts WHERE id = ?\").get(id) as Record<string, unknown> | undefined;\n return row ? this.rowToEntry(row) : null;\n }\n\n /** List edicts, optionally filtered by tags */\n list(options: ListEdictsOptions = {}): EdictEntry[] {\n if (!this._isReady) return [];\n try {\n return this.runWithDb(\"list\", () => this.listInternal(options));\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"edict-store\",\n operation: \"list\",\n });\n return [];\n }\n }\n\n private listInternal(options: ListEdictsOptions = {}): EdictEntry[] {\n const { tags, includeExpired = false, limit = 100 } = options;\n const nowSec = Math.floor(Date.now() / 1000);\n const parts: string[] = [];\n const params: SQLInputValue[] = [];\n\n if (!includeExpired) {\n parts.push(\n `((ttl = 'never') OR (ttl = 'event' AND expires_at_sec IS NOT NULL AND expires_at_sec > ?) OR (CAST(ttl AS INTEGER) > 0 AND created_at + CAST(ttl AS INTEGER) > ?))`,\n );\n params.push(nowSec, nowSec);\n }\n\n if (tags && tags.length > 0) {\n const tagParts: string[] = [];\n for (const tag of tags) {\n const t = tag.trim();\n if (!t) continue;\n tagParts.push(`(',' || LOWER(COALESCE(tags, '')) || ',') LIKE ? ESCAPE '~'`);\n params.push(`%,${escapeLikePattern(t.toLowerCase())},%`);\n }\n if (tagParts.length > 0) parts.push(`(${tagParts.join(\" OR \")})`);\n }\n\n const safeLimit = Math.max(1, Math.min(1000, Math.floor(limit)));\n const where = parts.length > 0 ? `WHERE ${parts.join(\" AND \")}` : \"\";\n params.push(safeLimit);\n\n const rows = this.liveDb\n .prepare(`SELECT * FROM edicts ${where} ORDER BY created_at DESC LIMIT ?`)\n .all(...params) as Array<Record<string, unknown>>;\n\n return rows.map((r) => this.rowToEntry(r));\n }\n\n /** Get all non-expired edicts, optionally filtered by tags */\n getEdicts(options: GetEdictsOptions = {}): {\n edicts: EdictEntry[];\n renderForPrompt: string;\n } {\n if (!this._isReady) {\n return { edicts: [], renderForPrompt: \"\" };\n }\n try {\n return this.runWithDb(\"getEdicts\", () => {\n const { tags, format = \"prompt\", limit = 100 } = options;\n const edicts = this.listInternal({\n tags,\n includeExpired: false,\n limit,\n });\n const renderForPrompt = format === \"prompt\" ? renderEdictsForPrompt(edicts) : \"\";\n return { edicts, renderForPrompt };\n });\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n subsystem: \"edict-store\",\n operation: \"getEdicts\",\n });\n return { edicts: [], renderForPrompt: \"\" };\n }\n }\n\n /** Update an existing edict. Returns the updated edict or null if not found. */\n update(input: UpdateEdictInput): EdictEntry | null {\n return this.runWithDb(\"update\", () => this.updateInternal(input));\n }\n\n private updateInternal(input: UpdateEdictInput): EdictEntry | null {\n const existing = this.getByIdInternal(input.id);\n if (!existing) return null;\n\n const nowSec = Math.floor(Date.now() / 1000);\n const text = input.text !== undefined ? input.text.trim() : existing.text;\n const source = input.source !== undefined ? input.source : existing.source;\n const tags = input.tags !== undefined ? input.tags : existing.tags;\n const ttl = input.ttl !== undefined ? input.ttl : existing.ttl;\n const expiresAt = input.expiresAt !== undefined ? input.expiresAt : existing.expiresAt;\n const ttlStr = typeof ttl === \"number\" ? String(ttl) : ttl;\n const expiresAtSec = expiresAt ? parseIsoDateToUnixSeconds(expiresAt) : null;\n if (typeof ttl === \"number\" && (!Number.isFinite(ttl) || ttl <= 0 || !Number.isInteger(ttl))) {\n throw new Error(\"Edict ttl must be a positive integer number of seconds, 'never', or 'event'\");\n }\n if (ttl === \"event\") {\n if (!expiresAt) {\n throw new Error(\"Edict ttl='event' requires expiresAt (ISO 8601)\");\n }\n if (expiresAtSec == null) {\n throw new Error(\"Edict expiresAt must be a valid ISO 8601 date\");\n }\n }\n const tagsStr = tags.length > 0 ? serializeTags(tags) : null;\n const normalizedText = normalizeEdictText(text);\n\n this.liveDb\n .prepare(\n \"UPDATE edicts SET text = ?, normalized_text = ?, source = ?, expires_at = ?, expires_at_sec = ?, ttl = ?, tags = ?, updated_at = ? WHERE id = ?\",\n )\n .run(text, normalizedText, source ?? null, expiresAt ?? null, expiresAtSec, ttlStr, tagsStr, nowSec, input.id);\n\n return {\n ...existing,\n text,\n expiresAtSec,\n source,\n expiresAt,\n ttl,\n tags,\n updatedAt: nowSec,\n };\n }\n\n /** Remove an edict by id */\n remove(id: string): boolean {\n return this.runWithDb(\"remove\", () => {\n const result = this.liveDb.prepare(\"DELETE FROM edicts WHERE id = ?\").run(id);\n return result.changes > 0;\n });\n }\n\n /** Count total edicts */\n count(): number {\n return this.runWithDb(\"count\", () => {\n const row = this.liveDb.prepare(\"SELECT COUNT(*) as cnt FROM edicts\").get() as { cnt: number };\n return row.cnt;\n });\n }\n\n /** Get statistics about the edict store */\n stats(): EdictStats {\n return this.runWithDb(\"stats\", () => this.statsInternal());\n }\n\n private statsInternal(): EdictStats {\n const nowSec = Math.floor(Date.now() / 1000);\n const totalRow = this.liveDb.prepare(\"SELECT COUNT(*) as cnt FROM edicts\").get() as { cnt: number };\n const total = totalRow.cnt;\n\n const expiredRow = this.liveDb\n .prepare(\n `SELECT COUNT(*) as cnt FROM edicts WHERE\n (ttl = 'event' AND (expires_at_sec IS NULL OR expires_at_sec <= ?))\n OR (CAST(ttl AS INTEGER) > 0 AND created_at + CAST(ttl AS INTEGER) <= ?)`,\n )\n .get(nowSec, nowSec) as { cnt: number };\n const expired = expiredRow.cnt;\n\n const sevenDaysFromNowSec = nowSec + 7 * 24 * 3600;\n const expiringRow = this.liveDb\n .prepare(\n `SELECT COUNT(*) as cnt FROM edicts WHERE ttl = 'event' AND expires_at_sec IS NOT NULL AND expires_at_sec > ? AND expires_at_sec <= ?`,\n )\n .get(nowSec, sevenDaysFromNowSec) as { cnt: number };\n const expiringIn7Days = expiringRow.cnt;\n\n const allRows = this.liveDb.prepare(\"SELECT tags FROM edicts\").all() as Array<{ tags: string | null }>;\n const byTag: Record<string, number> = {};\n for (const row of allRows) {\n if (!row.tags) continue;\n const parsed = parseTags(row.tags);\n for (const tag of parsed) {\n byTag[tag] = (byTag[tag] ?? 0) + 1;\n }\n }\n\n return { total, byTag, expired, expiringIn7Days };\n }\n\n /** Prune all expired edicts. Returns count of deleted rows. */\n pruneExpired(): number {\n return this.runWithDb(\"pruneExpired\", () => {\n const nowSec = Math.floor(Date.now() / 1000);\n const result = this.liveDb\n .prepare(\n `DELETE FROM edicts WHERE\n (ttl = 'event' AND (expires_at_sec IS NULL OR expires_at_sec <= ?))\n OR (CAST(ttl AS INTEGER) > 0 AND created_at + CAST(ttl AS INTEGER) <= ?)`,\n )\n .run(nowSec, nowSec);\n return Number(result.changes ?? 0);\n });\n }\n\n /** Convert a raw SQLite row to an EdictEntry */\n private rowToEntry(row: Record<string, unknown>): EdictEntry {\n const ttlRaw = (row.ttl as string) ?? \"never\";\n let ttl: EdictTtl;\n if (ttlRaw === \"never\") ttl = \"never\";\n else if (ttlRaw === \"event\") ttl = \"event\";\n else {\n const parsed = Number(ttlRaw);\n ttl = Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0;\n }\n\n return {\n id: row.id as string,\n text: row.text as string,\n source: (row.source as string) ?? null,\n verifiedAt: (row.verified_at as number) ?? null,\n expiresAt: (row.expires_at as string) ?? null,\n expiresAtSec: (row.expires_at_sec as number) ?? null,\n ttl,\n tags: parseTags(row.tags as string | null),\n createdAt: row.created_at as number,\n updatedAt: row.updated_at as number,\n };\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"event-bus.js","names":[],"sources":["../../backends/event-bus.ts"],"sourcesContent":["/**\n * Event Bus — append-only SQLite table that all sensor sweeps write to and the\n * Rumination Engine reads from.\n *\n * Status lifecycle: raw → processed → surfaced → pushed → archived\n */\n\nimport { createHash } from \"node:crypto\";\nimport { mkdirSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { DatabaseSync } from \"node:sqlite\";\nimport type { SQLInputValue } from \"node:sqlite\";\nimport { capturePluginError } from \"../services/error-reporter.js\";\nimport { BaseSqliteStore } from \"./base-sqlite-store.js\";\n\ntype EventStatus = \"raw\" | \"processed\" | \"surfaced\" | \"pushed\" | \"archived\";\n\ninterface MemoryEvent {\n id: number;\n event_type: string;\n source: string;\n payload: Record<string, unknown>;\n importance: number;\n status: EventStatus;\n created_at: string;\n processed_at: string | null;\n fingerprint: string | null;\n}\n\ninterface QueryFilter {\n status?: EventStatus;\n type?: string;\n since?: string;\n limit?: number;\n}\n\n/**\n * Compute a SHA-256 fingerprint from a string.\n * Callers compose the input from type + entity_id + summary + ttl_bucket.\n */\nexport function computeFingerprint(input: string): string {\n return createHash(\"sha256\").update(input).digest(\"hex\");\n}\n\n/**\n * Manages the SQLite-backed Event Bus for the hybrid memory system.\n * Provides an append-only store for incoming telemetry, sensor data, and\n * unstructured events. Events transition through a defined lifecycle\n * (raw -> processed -> surfaced -> pushed -> archived) as they are consumed\n * by the Rumination Engine.\n */\nexport class EventBus extends BaseSqliteStore {\n protected readonly dbPath: string;\n /** Tracks terminal closed state separately from base class field. */\n private _terminallyClosed = false;\n\n /**\n * Initializes the Event Bus database, creating the file and schema if needed.\n * @param dbPath Absolute path to the SQLite database file.\n */\n constructor(dbPath: string) {\n mkdirSync(dirname(dbPath), { recursive: true });\n const db = new DatabaseSync(dbPath);\n super(db);\n this.dbPath = dbPath;\n this.migrate();\n }\n\n protected getSubsystemName(): string {\n return \"event-bus\";\n }\n\n /**\n * Returns the terminal closed state of this EventBus.\n * When true, all subsequent operations throw an Error.\n */\n public get closed(): boolean {\n return this._terminallyClosed;\n }\n\n protected get liveDb(): DatabaseSync {\n if (this._terminallyClosed) {\n throw new Error(\"EventBus is closed\");\n }\n if (!this._dbOpen) {\n this.db.open();\n this._dbOpen = true;\n this.applyPragmas();\n }\n return this.db;\n }\n\n /**\n * Closes the EventBus and sets the terminal closed state.\n * After this, all subsequent operations throw an Error.\n */\n public close(): void {\n if (this._terminallyClosed) return;\n this._terminallyClosed = true;\n super.close();\n }\n\n private migrate(): void {\n this.liveDb.exec(`\n CREATE TABLE IF NOT EXISTS memory_events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n event_type TEXT NOT NULL,\n source TEXT NOT NULL,\n payload TEXT NOT NULL,\n importance REAL NOT NULL DEFAULT 0.5 CHECK(importance >= 0.0 AND importance <= 1.0),\n status TEXT NOT NULL DEFAULT 'raw'\n CHECK(status IN ('raw','processed','surfaced','pushed','archived')),\n created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now', 'subsec')),\n processed_at TEXT,\n fingerprint TEXT\n );\n CREATE INDEX IF NOT EXISTS idx_events_status ON memory_events(status);\n CREATE INDEX IF NOT EXISTS idx_events_type ON memory_events(event_type);\n CREATE INDEX IF NOT EXISTS idx_events_created ON memory_events(created_at);\n CREATE INDEX IF NOT EXISTS idx_events_fingerprint ON memory_events(fingerprint);\n `);\n }\n\n /**\n * Append a new event and return its auto-generated id.\n */\n appendEvent(\n type: string,\n source: string,\n payload: Record<string, unknown>,\n importance = 0.5,\n fingerprint?: string,\n ): number {\n if (!(importance >= 0 && importance <= 1)) {\n throw new RangeError(`EventBus: importance must be between 0 and 1, got ${importance}`);\n }\n const result = this.liveDb\n .prepare(\n `INSERT INTO memory_events (event_type, source, payload, importance, fingerprint)\n VALUES (?, ?, ?, ?, ?)`,\n )\n .run(type, source, JSON.stringify(payload), importance, fingerprint ?? null);\n return result.lastInsertRowid as number;\n }\n\n /**\n * Query events with optional filters.\n */\n queryEvents(filter: QueryFilter = {}): MemoryEvent[] {\n const { status, type, since, limit = 100 } = filter;\n const conditions: string[] = [];\n const params: SQLInputValue[] = [];\n\n if (status !== undefined) {\n conditions.push(\"status = ?\");\n params.push(status);\n }\n if (type !== undefined) {\n conditions.push(\"event_type = ?\");\n params.push(type);\n }\n if (since !== undefined) {\n conditions.push(\"created_at >= ?\");\n params.push(since);\n }\n\n const where = conditions.length > 0 ? `WHERE ${conditions.join(\" AND \")}` : \"\";\n const sql = `SELECT * FROM memory_events ${where} ORDER BY id ASC LIMIT ?`;\n params.push(limit);\n\n const rows = this.liveDb.prepare(sql).all(...params) as Record<string, unknown>[];\n return rows.map((r) => this.rowToEvent(r));\n }\n\n /**\n * Update the status of an event. Sets processed_at when transitioning away from 'raw'.\n * Throws if no event with the given id exists.\n */\n updateStatus(id: number, newStatus: EventStatus): void {\n const processedAt = newStatus !== \"raw\" ? new Date().toISOString() : null;\n const result = this.liveDb\n .prepare(\"UPDATE memory_events SET status = ?, processed_at = COALESCE(processed_at, ?) WHERE id = ?\")\n .run(newStatus, processedAt, id);\n if (result.changes === 0) {\n throw new Error(`EventBus: no event found with id ${id}`);\n }\n }\n\n /**\n * Check if a recent duplicate exists for the given fingerprint within a cooldown window.\n * Returns true if a duplicate was found (caller should skip inserting).\n */\n dedup(fingerprint: string, cooldownHours = 3): boolean {\n const cutoff = new Date(Date.now() - cooldownHours * 3600 * 1000).toISOString();\n const row = this.liveDb\n .prepare(\n `SELECT 1 FROM memory_events\n WHERE fingerprint = ? AND created_at >= ?\n LIMIT 1`,\n )\n .get(fingerprint, cutoff);\n return row !== undefined;\n }\n\n /**\n * Delete archived events older than N days. Returns the number of rows deleted.\n */\n pruneArchived(olderThanDays = 30): number {\n const cutoff = new Date(Date.now() - olderThanDays * 24 * 3600 * 1000).toISOString();\n const result = this.liveDb\n .prepare(`DELETE FROM memory_events WHERE status = 'archived' AND created_at < ?`)\n .run(cutoff);\n return Number(result.changes);\n }\n\n /**\n * Deserializes a raw SQLite row into a strongly-typed MemoryEvent.\n * Gracefully falls back to an empty object if JSON parsing fails.\n */\n private rowToEvent(row: Record<string, unknown>): MemoryEvent {\n let payload: Record<string, unknown> = {};\n try {\n payload = JSON.parse(row.payload as string) as Record<string, unknown>;\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"json-parse-payload\",\n severity: \"info\",\n subsystem: \"event-bus\",\n });\n }\n\n return {\n id: row.id as number,\n event_type: row.event_type as string,\n source: row.source as string,\n payload,\n importance: row.importance as number,\n status: row.status as EventStatus,\n created_at: row.created_at as string,\n processed_at: (row.processed_at as string | null) ?? null,\n fingerprint: (row.fingerprint as string | null) ?? null,\n };\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"event-bus.js","names":[],"sources":["../../backends/event-bus.ts"],"sourcesContent":["/**\n * Event Bus — append-only SQLite table that all sensor sweeps write to and the\n * Rumination Engine reads from.\n *\n * Status lifecycle: raw → processed → surfaced → pushed → archived\n */\n\nimport { createHash } from \"node:crypto\";\nimport { mkdirSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { DatabaseSync } from \"node:sqlite\";\nimport type { SQLInputValue } from \"node:sqlite\";\nimport { capturePluginError } from \"../services/error-reporter.js\";\nimport { BaseSqliteStore } from \"./base-sqlite-store.js\";\n\ntype EventStatus = \"raw\" | \"processed\" | \"surfaced\" | \"pushed\" | \"archived\";\n\ninterface MemoryEvent {\n id: number;\n event_type: string;\n source: string;\n payload: Record<string, unknown>;\n importance: number;\n status: EventStatus;\n created_at: string;\n processed_at: string | null;\n fingerprint: string | null;\n}\n\ninterface QueryFilter {\n status?: EventStatus;\n type?: string;\n since?: string;\n limit?: number;\n}\n\n/**\n * Compute a SHA-256 fingerprint from a string.\n * Callers compose the input from type + entity_id + summary + ttl_bucket.\n */\nexport function computeFingerprint(input: string): string {\n return createHash(\"sha256\").update(input).digest(\"hex\");\n}\n\n/**\n * Manages the SQLite-backed Event Bus for the hybrid memory system.\n * Provides an append-only store for incoming telemetry, sensor data, and\n * unstructured events. Events transition through a defined lifecycle\n * (raw -> processed -> surfaced -> pushed -> archived) as they are consumed\n * by the Rumination Engine.\n */\nexport class EventBus extends BaseSqliteStore {\n protected readonly dbPath: string;\n /** Tracks terminal closed state separately from base class field. */\n private _terminallyClosed = false;\n\n /**\n * Initializes the Event Bus database, creating the file and schema if needed.\n * @param dbPath Absolute path to the SQLite database file.\n */\n constructor(dbPath: string) {\n mkdirSync(dirname(dbPath), { recursive: true });\n const db = new DatabaseSync(dbPath);\n super(db);\n this.dbPath = dbPath;\n this.migrate();\n }\n\n protected getSubsystemName(): string {\n return \"event-bus\";\n }\n\n /**\n * Returns the terminal closed state of this EventBus.\n * When true, all subsequent operations throw an Error.\n */\n public get closed(): boolean {\n return this._terminallyClosed;\n }\n\n protected get liveDb(): DatabaseSync {\n if (this._terminallyClosed) {\n throw new Error(\"EventBus is closed\");\n }\n if (!this._dbOpen) {\n this.db.open();\n this._dbOpen = true;\n this.applyPragmas();\n }\n return this.db;\n }\n\n /**\n * Closes the EventBus and sets the terminal closed state.\n * After this, all subsequent operations throw an Error.\n */\n public close(): void {\n if (this._terminallyClosed) return;\n this._terminallyClosed = true;\n super.close();\n }\n\n private migrate(): void {\n this.liveDb.exec(`\n CREATE TABLE IF NOT EXISTS memory_events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n event_type TEXT NOT NULL,\n source TEXT NOT NULL,\n payload TEXT NOT NULL,\n importance REAL NOT NULL DEFAULT 0.5 CHECK(importance >= 0.0 AND importance <= 1.0),\n status TEXT NOT NULL DEFAULT 'raw'\n CHECK(status IN ('raw','processed','surfaced','pushed','archived')),\n created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now', 'subsec')),\n processed_at TEXT,\n fingerprint TEXT\n );\n CREATE INDEX IF NOT EXISTS idx_events_status ON memory_events(status);\n CREATE INDEX IF NOT EXISTS idx_events_type ON memory_events(event_type);\n CREATE INDEX IF NOT EXISTS idx_events_created ON memory_events(created_at);\n CREATE INDEX IF NOT EXISTS idx_events_fingerprint ON memory_events(fingerprint);\n `);\n }\n\n /**\n * Append a new event and return its auto-generated id.\n */\n appendEvent(\n type: string,\n source: string,\n payload: Record<string, unknown>,\n importance = 0.5,\n fingerprint?: string,\n ): number {\n if (!(importance >= 0 && importance <= 1)) {\n throw new RangeError(`EventBus: importance must be between 0 and 1, got ${importance}`);\n }\n const result = this.liveDb\n .prepare(\n `INSERT INTO memory_events (event_type, source, payload, importance, fingerprint)\n VALUES (?, ?, ?, ?, ?)`,\n )\n .run(type, source, JSON.stringify(payload), importance, fingerprint ?? null);\n return result.lastInsertRowid as number;\n }\n\n /**\n * Query events with optional filters.\n */\n queryEvents(filter: QueryFilter = {}): MemoryEvent[] {\n const { status, type, since, limit = 100 } = filter;\n const conditions: string[] = [];\n const params: SQLInputValue[] = [];\n\n if (status !== undefined) {\n conditions.push(\"status = ?\");\n params.push(status);\n }\n if (type !== undefined) {\n conditions.push(\"event_type = ?\");\n params.push(type);\n }\n if (since !== undefined) {\n conditions.push(\"created_at >= ?\");\n params.push(since);\n }\n\n const where = conditions.length > 0 ? `WHERE ${conditions.join(\" AND \")}` : \"\";\n const sql = `SELECT * FROM memory_events ${where} ORDER BY id ASC LIMIT ?`;\n params.push(limit);\n\n const rows = this.liveDb.prepare(sql).all(...params) as Record<string, unknown>[];\n return rows.map((r) => this.rowToEvent(r));\n }\n\n /**\n * Update the status of an event. Sets processed_at when transitioning away from 'raw'.\n * Throws if no event with the given id exists.\n */\n updateStatus(id: number, newStatus: EventStatus): void {\n const processedAt = newStatus !== \"raw\" ? new Date().toISOString() : null;\n const result = this.liveDb\n .prepare(\"UPDATE memory_events SET status = ?, processed_at = COALESCE(processed_at, ?) WHERE id = ?\")\n .run(newStatus, processedAt, id);\n if (result.changes === 0) {\n throw new Error(`EventBus: no event found with id ${id}`);\n }\n }\n\n /**\n * Check if a recent duplicate exists for the given fingerprint within a cooldown window.\n * Returns true if a duplicate was found (caller should skip inserting).\n */\n dedup(fingerprint: string, cooldownHours = 3): boolean {\n const cutoff = new Date(Date.now() - cooldownHours * 3600 * 1000).toISOString();\n const row = this.liveDb\n .prepare(\n `SELECT 1 FROM memory_events\n WHERE fingerprint = ? AND created_at >= ?\n LIMIT 1`,\n )\n .get(fingerprint, cutoff);\n return row !== undefined;\n }\n\n /**\n * Delete archived events older than N days. Returns the number of rows deleted.\n */\n pruneArchived(olderThanDays = 30): number {\n const cutoff = new Date(Date.now() - olderThanDays * 24 * 3600 * 1000).toISOString();\n const result = this.liveDb\n .prepare(`DELETE FROM memory_events WHERE status = 'archived' AND created_at < ?`)\n .run(cutoff);\n return Number(result.changes);\n }\n\n /**\n * Deserializes a raw SQLite row into a strongly-typed MemoryEvent.\n * Gracefully falls back to an empty object if JSON parsing fails.\n */\n private rowToEvent(row: Record<string, unknown>): MemoryEvent {\n let payload: Record<string, unknown> = {};\n try {\n payload = JSON.parse(row.payload as string) as Record<string, unknown>;\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"json-parse-payload\",\n severity: \"info\",\n subsystem: \"event-bus\",\n });\n }\n\n return {\n id: row.id as number,\n event_type: row.event_type as string,\n source: row.source as string,\n payload,\n importance: row.importance as number,\n status: row.status as EventStatus,\n created_at: row.created_at as string,\n processed_at: (row.processed_at as string | null) ?? null,\n fingerprint: (row.fingerprint as string | null) ?? null,\n };\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"event-log.js","names":[],"sources":["../../backends/event-log.ts"],"sourcesContent":["/**\n * Episodic Event Log — Layer 1 of the three-layer memory architecture.\n *\n * Captures raw episodic events during a session before they are consolidated\n * into long-term facts (Layer 2) or archived (Layer 3). Acts as a high-fidelity\n * journal: what happened, when, in which session, and involving which entities.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport {\n copyFileSync,\n createReadStream,\n createWriteStream,\n existsSync,\n mkdirSync,\n renameSync,\n unlinkSync,\n} from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { createInterface } from \"node:readline\";\nimport { DatabaseSync } from \"node:sqlite\";\nimport type { SQLInputValue } from \"node:sqlite\";\nimport { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport { createGunzip, createGzip } from \"node:zlib\";\nimport { capturePluginError } from \"../services/error-reporter.js\";\nimport { expandTilde } from \"../utils/path.js\";\nimport { createTransaction } from \"../utils/sqlite-transaction.js\";\nimport { BaseSqliteStore } from \"./base-sqlite-store.js\";\n\nexport type EventType =\n | \"fact_learned\"\n | \"decision_made\"\n | \"action_taken\"\n | \"entity_mentioned\"\n | \"preference_expressed\"\n | \"correction\"\n /** Session / transport lifecycle — excluded from episodic consolidation by default (#1185). */\n | \"session_start\"\n | \"session_end\"\n | \"heartbeat\"\n | \"transport_connect\"\n | \"transport_disconnect\";\n\nexport function categoryToEventType(category: string): EventType {\n switch (category) {\n case \"preference\":\n return \"preference_expressed\";\n case \"decision\":\n return \"decision_made\";\n case \"action\":\n return \"action_taken\";\n case \"entity\":\n return \"entity_mentioned\";\n default:\n return \"fact_learned\";\n }\n}\n\nexport interface EventLogEntry {\n id: string;\n sessionId: string;\n timestamp: string;\n eventType: EventType;\n content: Record<string, unknown>;\n entities?: string[];\n consolidatedInto?: string;\n metadata?: Record<string, unknown>;\n createdAt: string;\n}\n\nfunction migrateEventLogRelaxEventTypeCheck(db: DatabaseSync): void {\n const row = db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='event_log'`).get() as\n | { sql: string }\n | undefined;\n if (!row?.sql?.includes(\"CHECK(event_type\")) return;\n try {\n db.exec(\"BEGIN\");\n db.exec(\"DROP TABLE IF EXISTS event_log__mig;\");\n db.exec(`\n CREATE TABLE event_log__mig (\n id TEXT PRIMARY KEY,\n session_id TEXT NOT NULL,\n timestamp TEXT NOT NULL,\n event_type TEXT NOT NULL,\n content TEXT NOT NULL,\n entities TEXT,\n consolidated_into TEXT,\n metadata TEXT,\n created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))\n );\n `);\n db.exec(`\n INSERT INTO event_log__mig (id, session_id, timestamp, event_type, content, entities, consolidated_into, metadata, created_at)\n SELECT id, session_id, timestamp, event_type, content, entities, consolidated_into, metadata, created_at FROM event_log;\n `);\n db.exec(\"DROP TABLE event_log;\");\n db.exec(\"ALTER TABLE event_log__mig RENAME TO event_log;\");\n db.exec(`\n CREATE INDEX IF NOT EXISTS idx_event_log_session ON event_log(session_id);\n CREATE INDEX IF NOT EXISTS idx_event_log_timestamp ON event_log(timestamp);\n CREATE INDEX IF NOT EXISTS idx_event_log_type ON event_log(event_type);\n CREATE INDEX IF NOT EXISTS idx_event_log_consolidated ON event_log(consolidated_into);\n `);\n db.exec(\"COMMIT\");\n } catch (err) {\n try {\n db.exec(\"ROLLBACK\");\n } catch {\n // ignore\n }\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"event-log-migrate-event-type-check\",\n subsystem: \"event-log\",\n });\n }\n}\n\nexport class EventLog extends BaseSqliteStore {\n protected readonly dbPath: string;\n\n constructor(dbPath: string) {\n mkdirSync(dirname(dbPath), { recursive: true });\n const db = new DatabaseSync(dbPath);\n super(db);\n this.dbPath = dbPath;\n\n this.liveDb.exec(`\n CREATE TABLE IF NOT EXISTS event_log (\n id TEXT PRIMARY KEY,\n session_id TEXT NOT NULL,\n timestamp TEXT NOT NULL,\n event_type TEXT NOT NULL,\n content TEXT NOT NULL,\n entities TEXT,\n consolidated_into TEXT,\n metadata TEXT,\n created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))\n );\n CREATE INDEX IF NOT EXISTS idx_event_log_session ON event_log(session_id);\n CREATE INDEX IF NOT EXISTS idx_event_log_timestamp ON event_log(timestamp);\n CREATE INDEX IF NOT EXISTS idx_event_log_type ON event_log(event_type);\n CREATE INDEX IF NOT EXISTS idx_event_log_consolidated ON event_log(consolidated_into);\n `);\n migrateEventLogRelaxEventTypeCheck(this.liveDb);\n }\n\n protected getSubsystemName(): string {\n return \"event-log\";\n }\n\n /** Append a single event and return its generated id. */\n append(entry: Omit<EventLogEntry, \"id\" | \"createdAt\">): string {\n const id = randomUUID();\n this.liveDb\n .prepare(\n `INSERT INTO event_log (id, session_id, timestamp, event_type, content, entities, consolidated_into, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .run(\n id,\n entry.sessionId,\n entry.timestamp,\n entry.eventType,\n JSON.stringify(entry.content),\n entry.entities != null ? JSON.stringify(entry.entities) : null,\n entry.consolidatedInto ?? null,\n entry.metadata != null ? JSON.stringify(entry.metadata) : null,\n );\n return id;\n }\n\n /** Append multiple events atomically. Returns array of generated ids in input order. */\n appendBatch(entries: Omit<EventLogEntry, \"id\" | \"createdAt\">[]): string[] {\n const ids: string[] = [];\n const stmt = this.liveDb.prepare(\n `INSERT INTO event_log (id, session_id, timestamp, event_type, content, entities, consolidated_into, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n );\n const insertAll = createTransaction(this.liveDb, (batch: Omit<EventLogEntry, \"id\" | \"createdAt\">[]) => {\n for (const entry of batch) {\n const id = randomUUID();\n ids.push(id);\n stmt.run(\n id,\n entry.sessionId,\n entry.timestamp,\n entry.eventType,\n JSON.stringify(entry.content),\n entry.entities != null ? JSON.stringify(entry.entities) : null,\n entry.consolidatedInto ?? null,\n entry.metadata != null ? JSON.stringify(entry.metadata) : null,\n );\n }\n });\n insertAll(entries);\n return ids;\n }\n\n /** Return events for a session in chronological order (oldest-first) with a safety limit. */\n getBySession(sessionId: string, limit = 1000): EventLogEntry[] {\n const rows = this.liveDb\n .prepare(\"SELECT * FROM event_log WHERE session_id = ? ORDER BY timestamp ASC LIMIT ?\")\n .all(sessionId, limit) as Record<string, unknown>[];\n return rows.map((r) => this.rowToEntry(r));\n }\n\n /** Return a single event by id (or null if not found). */\n getById(id: string): EventLogEntry | null {\n const row = this.liveDb.prepare(\"SELECT * FROM event_log WHERE id = ?\").get(id) as\n | Record<string, unknown>\n | undefined;\n return row ? this.rowToEntry(row) : null;\n }\n\n /** Return events whose timestamp falls within [from, to] (ISO strings), optionally filtered by eventType. */\n getByTimeRange(from: string, to: string, eventType?: string): EventLogEntry[] {\n const params: SQLInputValue[] = [from, to];\n let query = \"SELECT * FROM event_log WHERE timestamp >= ? AND timestamp <= ?\";\n if (eventType) {\n query += \" AND event_type = ?\";\n params.push(eventType);\n }\n query += \" ORDER BY timestamp ASC\";\n const rows = this.liveDb.prepare(query).all(...params) as Record<string, unknown>[];\n return rows.map((r) => this.rowToEntry(r));\n }\n\n /** Return events not yet consolidated into a fact. Optionally only events older than N days. */\n getUnconsolidated(olderThanDays?: number): EventLogEntry[] {\n const params: SQLInputValue[] = [];\n let query = \"SELECT * FROM event_log WHERE consolidated_into IS NULL\";\n if (olderThanDays !== undefined) {\n const cutoff = new Date(Date.now() - olderThanDays * 24 * 3600 * 1000).toISOString();\n query += \" AND timestamp < ?\";\n params.push(cutoff);\n }\n query += \" ORDER BY timestamp ASC\";\n const rows = this.liveDb.prepare(query).all(...params) as Record<string, unknown>[];\n return rows.map((r) => this.rowToEntry(r));\n }\n\n /** Return events that mention the given entity name (exact match within the entities JSON array). */\n getByEntity(entityName: string, limit = 1000): EventLogEntry[] {\n const rows = this.liveDb\n .prepare(\n `SELECT * FROM event_log\n WHERE entities IS NOT NULL\n AND EXISTS (SELECT 1 FROM json_each(entities) WHERE value = ?)\n ORDER BY timestamp ASC\n LIMIT ?`,\n )\n .all(entityName, limit) as Record<string, unknown>[];\n return rows.map((r) => this.rowToEntry(r));\n }\n\n /** Mark a set of events as consolidated into the given fact id. */\n markConsolidated(eventIds: string[], factId: string): void {\n const stmt = this.liveDb.prepare(\"UPDATE event_log SET consolidated_into = ? WHERE id = ?\");\n const updateAll = createTransaction(this.liveDb, (ids: string[]) => {\n for (const id of ids) {\n stmt.run(factId, id);\n }\n });\n updateAll(eventIds);\n }\n\n /**\n * Archive consolidated events older than N days to compressed JSONL files.\n * Returns the number of rows archived and the files written.\n */\n async archiveConsolidated(olderThanDays: number, archiveDir: string): Promise<{ archived: number; files: string[] }> {\n if (olderThanDays <= 0) return { archived: 0, files: [] };\n const cutoff = new Date(Date.now() - olderThanDays * 24 * 3600 * 1000).toISOString();\n const resolvedDir = expandTilde(archiveDir);\n mkdirSync(resolvedDir, { recursive: true });\n\n const months = this.liveDb\n .prepare(\n `SELECT DISTINCT strftime('%Y-%m', timestamp) AS ym\n FROM event_log\n WHERE timestamp < ? AND consolidated_into IS NOT NULL\n ORDER BY ym ASC`,\n )\n .all(cutoff) as { ym: string | null }[];\n\n const files: string[] = [];\n let archived = 0;\n for (const row of months) {\n const month = row.ym;\n if (!month) continue;\n const countRow = this.liveDb\n .prepare(\n `SELECT COUNT(*) AS count FROM event_log\n WHERE timestamp < ?\n AND consolidated_into IS NOT NULL\n AND strftime('%Y-%m', timestamp) = ?`,\n )\n .get(cutoff, month) as { count: number };\n if (!countRow?.count) continue;\n\n const ids: string[] = [];\n const stmt = this.liveDb.prepare(\n `SELECT * FROM event_log\n WHERE timestamp < ?\n AND consolidated_into IS NOT NULL\n AND strftime('%Y-%m', timestamp) = ?\n ORDER BY timestamp ASC`,\n );\n const filePath = join(resolvedDir, `${month}.jsonl.gz`);\n const tempPath = `${filePath}.tmp`;\n const rowToEntry = this.rowToEntry.bind(this);\n\n try {\n const lineStream = Readable.from(\n (async function* () {\n const seenIds = new Set<string>();\n\n if (existsSync(filePath)) {\n try {\n const input = createReadStream(filePath).pipe(createGunzip());\n const rl = createInterface({ input, crlfDelay: Number.POSITIVE_INFINITY });\n for await (const line of rl) {\n if (!line.trim()) continue;\n try {\n const o = JSON.parse(line) as { id?: string };\n if (o.id) seenIds.add(o.id);\n } catch {\n // Skip corrupt line but still yield to preserve archive\n }\n yield `${line}\\n`;\n }\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"read-existing-archive\",\n severity: \"info\",\n subsystem: \"event-log\",\n });\n const backupPath = `${filePath}.corrupted.${Date.now()}`;\n try {\n copyFileSync(filePath, backupPath);\n unlinkSync(filePath);\n } catch (renameErr) {\n capturePluginError(renameErr instanceof Error ? renameErr : new Error(String(renameErr)), {\n operation: \"backup-corrupted-archive\",\n severity: \"info\",\n subsystem: \"event-log\",\n });\n }\n }\n }\n\n for (const r of stmt.iterate(cutoff, month) as Iterable<Record<string, unknown>>) {\n const entry = rowToEntry(r);\n ids.push(entry.id);\n if (!seenIds.has(entry.id)) {\n seenIds.add(entry.id);\n yield `${JSON.stringify(entry)}\\n`;\n }\n }\n })(),\n );\n\n await pipeline(lineStream, createGzip(), createWriteStream(tempPath));\n\n // renameSync must succeed before we delete source rows.\n // If rename throws, we fall into the catch block and tempPath is cleaned up.\n renameSync(tempPath, filePath);\n\n // Only delete rows after both the write pipeline AND the atomic rename have\n // succeeded — ensures rows are never deleted from SQLite unless their data\n // is safely in the final archive file.\n const del = this.liveDb.prepare(\"DELETE FROM event_log WHERE id = ?\");\n const deleteBatch = createTransaction(this.liveDb, (batch: string[]) => {\n for (const id of batch) del.run(id);\n });\n deleteBatch(ids);\n files.push(filePath);\n archived += ids.length;\n } catch (err) {\n if (existsSync(tempPath)) {\n try {\n unlinkSync(tempPath);\n } catch {\n // Ignore cleanup errors\n }\n }\n throw err;\n }\n }\n return { archived, files };\n }\n\n /**\n * Delete events whose timestamp is older than N days.\n * By default, only deletes events that have already been consolidated\n * (consolidated_into IS NOT NULL) to prevent silent data loss of unprocessed\n * episodic events. When includeUnconsolidated is true, deletes all old events.\n * Returns the number of rows deleted.\n */\n archiveOld(olderThanDays: number, includeUnconsolidated = false): number {\n const cutoff = new Date(Date.now() - olderThanDays * 24 * 3600 * 1000).toISOString();\n const result = this.liveDb\n .prepare(\"DELETE FROM event_log WHERE timestamp < ? AND (consolidated_into IS NOT NULL OR ?)\")\n .run(cutoff, includeUnconsolidated ? 1 : 0);\n return Number(result.changes);\n }\n\n /** Return aggregate statistics about the event log. */\n getStats(): {\n total: number;\n unconsolidated: number;\n byType: Record<string, number>;\n oldestUnconsolidated: string | null;\n } {\n const totalRow = this.liveDb.prepare(\"SELECT COUNT(*) AS count FROM event_log\").get() as { count: number };\n const unconsolidatedRow = this.liveDb\n .prepare(\"SELECT COUNT(*) AS count FROM event_log WHERE consolidated_into IS NULL\")\n .get() as { count: number };\n const typeRows = this.liveDb\n .prepare(\"SELECT event_type, COUNT(*) AS count FROM event_log GROUP BY event_type\")\n .all() as { event_type: string; count: number }[];\n const oldestRow = this.liveDb\n .prepare(\"SELECT MIN(timestamp) AS oldest FROM event_log WHERE consolidated_into IS NULL\")\n .get() as { oldest: string | null };\n\n const byType: Record<string, number> = {};\n for (const row of typeRows) {\n byType[row.event_type] = row.count;\n }\n\n return {\n total: totalRow?.count ?? 0,\n unconsolidated: unconsolidatedRow?.count ?? 0,\n byType,\n oldestUnconsolidated: oldestRow?.oldest ?? null,\n };\n }\n\n private rowToEntry(row: Record<string, unknown>): EventLogEntry {\n let content: Record<string, unknown> = {};\n try {\n content = JSON.parse(row.content as string) as Record<string, unknown>;\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"json-parse-content\",\n severity: \"info\",\n subsystem: \"event-log\",\n });\n }\n\n let entities: string[] | undefined;\n if (row.entities != null) {\n try {\n entities = JSON.parse(row.entities as string) as string[];\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"json-parse-entities\",\n severity: \"info\",\n subsystem: \"event-log\",\n });\n }\n }\n\n let metadata: Record<string, unknown> | undefined;\n if (row.metadata != null) {\n try {\n metadata = JSON.parse(row.metadata as string) as Record<string, unknown>;\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"json-parse-metadata\",\n severity: \"info\",\n subsystem: \"event-log\",\n });\n }\n }\n\n return {\n id: row.id as string,\n sessionId: row.session_id as string,\n timestamp: row.timestamp as string,\n eventType: row.event_type as EventType,\n content,\n entities,\n consolidatedInto: row.consolidated_into != null ? (row.consolidated_into as string) : undefined,\n metadata,\n createdAt: row.created_at as string,\n };\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"event-log.js","names":[],"sources":["../../backends/event-log.ts"],"sourcesContent":["/**\n * Episodic Event Log — Layer 1 of the three-layer memory architecture.\n *\n * Captures raw episodic events during a session before they are consolidated\n * into long-term facts (Layer 2) or archived (Layer 3). Acts as a high-fidelity\n * journal: what happened, when, in which session, and involving which entities.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport {\n copyFileSync,\n createReadStream,\n createWriteStream,\n existsSync,\n mkdirSync,\n renameSync,\n unlinkSync,\n} from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { createInterface } from \"node:readline\";\nimport { DatabaseSync } from \"node:sqlite\";\nimport type { SQLInputValue } from \"node:sqlite\";\nimport { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport { createGunzip, createGzip } from \"node:zlib\";\nimport { capturePluginError } from \"../services/error-reporter.js\";\nimport { expandTilde } from \"../utils/path.js\";\nimport { createTransaction } from \"../utils/sqlite-transaction.js\";\nimport { BaseSqliteStore } from \"./base-sqlite-store.js\";\n\nexport type EventType =\n | \"fact_learned\"\n | \"decision_made\"\n | \"action_taken\"\n | \"entity_mentioned\"\n | \"preference_expressed\"\n | \"correction\"\n /** Session / transport lifecycle — excluded from episodic consolidation by default (#1185). */\n | \"session_start\"\n | \"session_end\"\n | \"heartbeat\"\n | \"transport_connect\"\n | \"transport_disconnect\";\n\nexport function categoryToEventType(category: string): EventType {\n switch (category) {\n case \"preference\":\n return \"preference_expressed\";\n case \"decision\":\n return \"decision_made\";\n case \"action\":\n return \"action_taken\";\n case \"entity\":\n return \"entity_mentioned\";\n default:\n return \"fact_learned\";\n }\n}\n\nexport interface EventLogEntry {\n id: string;\n sessionId: string;\n timestamp: string;\n eventType: EventType;\n content: Record<string, unknown>;\n entities?: string[];\n consolidatedInto?: string;\n metadata?: Record<string, unknown>;\n createdAt: string;\n}\n\nfunction migrateEventLogRelaxEventTypeCheck(db: DatabaseSync): void {\n const row = db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='event_log'`).get() as\n | { sql: string }\n | undefined;\n if (!row?.sql?.includes(\"CHECK(event_type\")) return;\n try {\n db.exec(\"BEGIN\");\n db.exec(\"DROP TABLE IF EXISTS event_log__mig;\");\n db.exec(`\n CREATE TABLE event_log__mig (\n id TEXT PRIMARY KEY,\n session_id TEXT NOT NULL,\n timestamp TEXT NOT NULL,\n event_type TEXT NOT NULL,\n content TEXT NOT NULL,\n entities TEXT,\n consolidated_into TEXT,\n metadata TEXT,\n created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))\n );\n `);\n db.exec(`\n INSERT INTO event_log__mig (id, session_id, timestamp, event_type, content, entities, consolidated_into, metadata, created_at)\n SELECT id, session_id, timestamp, event_type, content, entities, consolidated_into, metadata, created_at FROM event_log;\n `);\n db.exec(\"DROP TABLE event_log;\");\n db.exec(\"ALTER TABLE event_log__mig RENAME TO event_log;\");\n db.exec(`\n CREATE INDEX IF NOT EXISTS idx_event_log_session ON event_log(session_id);\n CREATE INDEX IF NOT EXISTS idx_event_log_timestamp ON event_log(timestamp);\n CREATE INDEX IF NOT EXISTS idx_event_log_type ON event_log(event_type);\n CREATE INDEX IF NOT EXISTS idx_event_log_consolidated ON event_log(consolidated_into);\n `);\n db.exec(\"COMMIT\");\n } catch (err) {\n try {\n db.exec(\"ROLLBACK\");\n } catch {\n // ignore\n }\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"event-log-migrate-event-type-check\",\n subsystem: \"event-log\",\n });\n }\n}\n\nexport class EventLog extends BaseSqliteStore {\n protected readonly dbPath: string;\n\n constructor(dbPath: string) {\n mkdirSync(dirname(dbPath), { recursive: true });\n const db = new DatabaseSync(dbPath);\n super(db);\n this.dbPath = dbPath;\n\n this.liveDb.exec(`\n CREATE TABLE IF NOT EXISTS event_log (\n id TEXT PRIMARY KEY,\n session_id TEXT NOT NULL,\n timestamp TEXT NOT NULL,\n event_type TEXT NOT NULL,\n content TEXT NOT NULL,\n entities TEXT,\n consolidated_into TEXT,\n metadata TEXT,\n created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))\n );\n CREATE INDEX IF NOT EXISTS idx_event_log_session ON event_log(session_id);\n CREATE INDEX IF NOT EXISTS idx_event_log_timestamp ON event_log(timestamp);\n CREATE INDEX IF NOT EXISTS idx_event_log_type ON event_log(event_type);\n CREATE INDEX IF NOT EXISTS idx_event_log_consolidated ON event_log(consolidated_into);\n `);\n migrateEventLogRelaxEventTypeCheck(this.liveDb);\n }\n\n protected getSubsystemName(): string {\n return \"event-log\";\n }\n\n /** Append a single event and return its generated id. */\n append(entry: Omit<EventLogEntry, \"id\" | \"createdAt\">): string {\n const id = randomUUID();\n this.liveDb\n .prepare(\n `INSERT INTO event_log (id, session_id, timestamp, event_type, content, entities, consolidated_into, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .run(\n id,\n entry.sessionId,\n entry.timestamp,\n entry.eventType,\n JSON.stringify(entry.content),\n entry.entities != null ? JSON.stringify(entry.entities) : null,\n entry.consolidatedInto ?? null,\n entry.metadata != null ? JSON.stringify(entry.metadata) : null,\n );\n return id;\n }\n\n /** Append multiple events atomically. Returns array of generated ids in input order. */\n appendBatch(entries: Omit<EventLogEntry, \"id\" | \"createdAt\">[]): string[] {\n const ids: string[] = [];\n const stmt = this.liveDb.prepare(\n `INSERT INTO event_log (id, session_id, timestamp, event_type, content, entities, consolidated_into, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n );\n const insertAll = createTransaction(this.liveDb, (batch: Omit<EventLogEntry, \"id\" | \"createdAt\">[]) => {\n for (const entry of batch) {\n const id = randomUUID();\n ids.push(id);\n stmt.run(\n id,\n entry.sessionId,\n entry.timestamp,\n entry.eventType,\n JSON.stringify(entry.content),\n entry.entities != null ? JSON.stringify(entry.entities) : null,\n entry.consolidatedInto ?? null,\n entry.metadata != null ? JSON.stringify(entry.metadata) : null,\n );\n }\n });\n insertAll(entries);\n return ids;\n }\n\n /** Return events for a session in chronological order (oldest-first) with a safety limit. */\n getBySession(sessionId: string, limit = 1000): EventLogEntry[] {\n const rows = this.liveDb\n .prepare(\"SELECT * FROM event_log WHERE session_id = ? ORDER BY timestamp ASC LIMIT ?\")\n .all(sessionId, limit) as Record<string, unknown>[];\n return rows.map((r) => this.rowToEntry(r));\n }\n\n /** Return a single event by id (or null if not found). */\n getById(id: string): EventLogEntry | null {\n const row = this.liveDb.prepare(\"SELECT * FROM event_log WHERE id = ?\").get(id) as\n | Record<string, unknown>\n | undefined;\n return row ? this.rowToEntry(row) : null;\n }\n\n /** Return events whose timestamp falls within [from, to] (ISO strings), optionally filtered by eventType. */\n getByTimeRange(from: string, to: string, eventType?: string): EventLogEntry[] {\n const params: SQLInputValue[] = [from, to];\n let query = \"SELECT * FROM event_log WHERE timestamp >= ? AND timestamp <= ?\";\n if (eventType) {\n query += \" AND event_type = ?\";\n params.push(eventType);\n }\n query += \" ORDER BY timestamp ASC\";\n const rows = this.liveDb.prepare(query).all(...params) as Record<string, unknown>[];\n return rows.map((r) => this.rowToEntry(r));\n }\n\n /** Return events not yet consolidated into a fact. Optionally only events older than N days. */\n getUnconsolidated(olderThanDays?: number): EventLogEntry[] {\n const params: SQLInputValue[] = [];\n let query = \"SELECT * FROM event_log WHERE consolidated_into IS NULL\";\n if (olderThanDays !== undefined) {\n const cutoff = new Date(Date.now() - olderThanDays * 24 * 3600 * 1000).toISOString();\n query += \" AND timestamp < ?\";\n params.push(cutoff);\n }\n query += \" ORDER BY timestamp ASC\";\n const rows = this.liveDb.prepare(query).all(...params) as Record<string, unknown>[];\n return rows.map((r) => this.rowToEntry(r));\n }\n\n /** Return events that mention the given entity name (exact match within the entities JSON array). */\n getByEntity(entityName: string, limit = 1000): EventLogEntry[] {\n const rows = this.liveDb\n .prepare(\n `SELECT * FROM event_log\n WHERE entities IS NOT NULL\n AND EXISTS (SELECT 1 FROM json_each(entities) WHERE value = ?)\n ORDER BY timestamp ASC\n LIMIT ?`,\n )\n .all(entityName, limit) as Record<string, unknown>[];\n return rows.map((r) => this.rowToEntry(r));\n }\n\n /** Mark a set of events as consolidated into the given fact id. */\n markConsolidated(eventIds: string[], factId: string): void {\n const stmt = this.liveDb.prepare(\"UPDATE event_log SET consolidated_into = ? WHERE id = ?\");\n const updateAll = createTransaction(this.liveDb, (ids: string[]) => {\n for (const id of ids) {\n stmt.run(factId, id);\n }\n });\n updateAll(eventIds);\n }\n\n /**\n * Archive consolidated events older than N days to compressed JSONL files.\n * Returns the number of rows archived and the files written.\n */\n async archiveConsolidated(olderThanDays: number, archiveDir: string): Promise<{ archived: number; files: string[] }> {\n if (olderThanDays <= 0) return { archived: 0, files: [] };\n const cutoff = new Date(Date.now() - olderThanDays * 24 * 3600 * 1000).toISOString();\n const resolvedDir = expandTilde(archiveDir);\n mkdirSync(resolvedDir, { recursive: true });\n\n const months = this.liveDb\n .prepare(\n `SELECT DISTINCT strftime('%Y-%m', timestamp) AS ym\n FROM event_log\n WHERE timestamp < ? AND consolidated_into IS NOT NULL\n ORDER BY ym ASC`,\n )\n .all(cutoff) as { ym: string | null }[];\n\n const files: string[] = [];\n let archived = 0;\n for (const row of months) {\n const month = row.ym;\n if (!month) continue;\n const countRow = this.liveDb\n .prepare(\n `SELECT COUNT(*) AS count FROM event_log\n WHERE timestamp < ?\n AND consolidated_into IS NOT NULL\n AND strftime('%Y-%m', timestamp) = ?`,\n )\n .get(cutoff, month) as { count: number };\n if (!countRow?.count) continue;\n\n const ids: string[] = [];\n const stmt = this.liveDb.prepare(\n `SELECT * FROM event_log\n WHERE timestamp < ?\n AND consolidated_into IS NOT NULL\n AND strftime('%Y-%m', timestamp) = ?\n ORDER BY timestamp ASC`,\n );\n const filePath = join(resolvedDir, `${month}.jsonl.gz`);\n const tempPath = `${filePath}.tmp`;\n const rowToEntry = this.rowToEntry.bind(this);\n\n try {\n const lineStream = Readable.from(\n (async function* () {\n const seenIds = new Set<string>();\n\n if (existsSync(filePath)) {\n try {\n const input = createReadStream(filePath).pipe(createGunzip());\n const rl = createInterface({ input, crlfDelay: Number.POSITIVE_INFINITY });\n for await (const line of rl) {\n if (!line.trim()) continue;\n try {\n const o = JSON.parse(line) as { id?: string };\n if (o.id) seenIds.add(o.id);\n } catch {\n // Skip corrupt line but still yield to preserve archive\n }\n yield `${line}\\n`;\n }\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"read-existing-archive\",\n severity: \"info\",\n subsystem: \"event-log\",\n });\n const backupPath = `${filePath}.corrupted.${Date.now()}`;\n try {\n copyFileSync(filePath, backupPath);\n unlinkSync(filePath);\n } catch (renameErr) {\n capturePluginError(renameErr instanceof Error ? renameErr : new Error(String(renameErr)), {\n operation: \"backup-corrupted-archive\",\n severity: \"info\",\n subsystem: \"event-log\",\n });\n }\n }\n }\n\n for (const r of stmt.iterate(cutoff, month) as Iterable<Record<string, unknown>>) {\n const entry = rowToEntry(r);\n ids.push(entry.id);\n if (!seenIds.has(entry.id)) {\n seenIds.add(entry.id);\n yield `${JSON.stringify(entry)}\\n`;\n }\n }\n })(),\n );\n\n await pipeline(lineStream, createGzip(), createWriteStream(tempPath));\n\n // renameSync must succeed before we delete source rows.\n // If rename throws, we fall into the catch block and tempPath is cleaned up.\n renameSync(tempPath, filePath);\n\n // Only delete rows after both the write pipeline AND the atomic rename have\n // succeeded — ensures rows are never deleted from SQLite unless their data\n // is safely in the final archive file.\n const del = this.liveDb.prepare(\"DELETE FROM event_log WHERE id = ?\");\n const deleteBatch = createTransaction(this.liveDb, (batch: string[]) => {\n for (const id of batch) del.run(id);\n });\n deleteBatch(ids);\n files.push(filePath);\n archived += ids.length;\n } catch (err) {\n if (existsSync(tempPath)) {\n try {\n unlinkSync(tempPath);\n } catch {\n // Ignore cleanup errors\n }\n }\n throw err;\n }\n }\n return { archived, files };\n }\n\n /**\n * Delete events whose timestamp is older than N days.\n * By default, only deletes events that have already been consolidated\n * (consolidated_into IS NOT NULL) to prevent silent data loss of unprocessed\n * episodic events. When includeUnconsolidated is true, deletes all old events.\n * Returns the number of rows deleted.\n */\n archiveOld(olderThanDays: number, includeUnconsolidated = false): number {\n const cutoff = new Date(Date.now() - olderThanDays * 24 * 3600 * 1000).toISOString();\n const result = this.liveDb\n .prepare(\"DELETE FROM event_log WHERE timestamp < ? AND (consolidated_into IS NOT NULL OR ?)\")\n .run(cutoff, includeUnconsolidated ? 1 : 0);\n return Number(result.changes);\n }\n\n /** Return aggregate statistics about the event log. */\n getStats(): {\n total: number;\n unconsolidated: number;\n byType: Record<string, number>;\n oldestUnconsolidated: string | null;\n } {\n const totalRow = this.liveDb.prepare(\"SELECT COUNT(*) AS count FROM event_log\").get() as { count: number };\n const unconsolidatedRow = this.liveDb\n .prepare(\"SELECT COUNT(*) AS count FROM event_log WHERE consolidated_into IS NULL\")\n .get() as { count: number };\n const typeRows = this.liveDb\n .prepare(\"SELECT event_type, COUNT(*) AS count FROM event_log GROUP BY event_type\")\n .all() as { event_type: string; count: number }[];\n const oldestRow = this.liveDb\n .prepare(\"SELECT MIN(timestamp) AS oldest FROM event_log WHERE consolidated_into IS NULL\")\n .get() as { oldest: string | null };\n\n const byType: Record<string, number> = {};\n for (const row of typeRows) {\n byType[row.event_type] = row.count;\n }\n\n return {\n total: totalRow?.count ?? 0,\n unconsolidated: unconsolidatedRow?.count ?? 0,\n byType,\n oldestUnconsolidated: oldestRow?.oldest ?? null,\n };\n }\n\n private rowToEntry(row: Record<string, unknown>): EventLogEntry {\n let content: Record<string, unknown> = {};\n try {\n content = JSON.parse(row.content as string) as Record<string, unknown>;\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"json-parse-content\",\n severity: \"info\",\n subsystem: \"event-log\",\n });\n }\n\n let entities: string[] | undefined;\n if (row.entities != null) {\n try {\n entities = JSON.parse(row.entities as string) as string[];\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"json-parse-entities\",\n severity: \"info\",\n subsystem: \"event-log\",\n });\n }\n }\n\n let metadata: Record<string, unknown> | undefined;\n if (row.metadata != null) {\n try {\n metadata = JSON.parse(row.metadata as string) as Record<string, unknown>;\n } catch (err) {\n capturePluginError(err instanceof Error ? err : new Error(String(err)), {\n operation: \"json-parse-metadata\",\n severity: \"info\",\n subsystem: \"event-log\",\n });\n }\n }\n\n return {\n id: row.id as string,\n sessionId: row.session_id as string,\n timestamp: row.timestamp as string,\n eventType: row.event_type as EventType,\n content,\n entities,\n consolidatedInto: row.consolidated_into != null ? (row.consolidated_into as string) : undefined,\n metadata,\n createdAt: row.created_at as string,\n };\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"cache-manager.js","names":[],"sources":["../../../backends/facts-db/cache-manager.ts"],"sourcesContent":["/**\n * In-memory cache for expensive FactsDB read paths (#870).\n */\n\n/** TTL-backed set cache with explicit invalidation (superseded fact texts). */\nexport class SupersededTextsCache {\n private data: Set<string> | null = null;\n private loadedAt = 0;\n\n constructor(private readonly ttlMs: number) {}\n\n getSnapshot(now: number, loader: () => string[]): Set<string> {\n if (this.data !== null && now - this.loadedAt < this.ttlMs) {\n return this.data;\n }\n const next = new Set(loader());\n this.data = next;\n this.loadedAt = now;\n return next;\n }\n\n invalidate(): void {\n this.data = null;\n }\n}\n"],"mappings":";;;;;AAKA,IAAa,uBAAb,MAAkC;CAIH;CAH7B,OAAmC;CACnC,WAAmB;CAEnB,YAAY,OAAgC;EAAf,KAAA,QAAA;;CAE7B,YAAY,KAAa,QAAqC;EAC5D,IAAI,KAAK,SAAS,QAAQ,MAAM,KAAK,WAAW,KAAK,OACnD,OAAO,KAAK;EAEd,MAAM,OAAO,IAAI,IAAI,QAAQ,CAAC;EAC9B,KAAK,OAAO;EACZ,KAAK,WAAW;EAChB,OAAO;;CAGT,aAAmB;EACjB,KAAK,OAAO"}
1
+ {"version":3,"file":"cache-manager.js","names":[],"sources":["../../../backends/facts-db/cache-manager.ts"],"sourcesContent":["/**\n * In-memory cache for expensive FactsDB read paths (#870).\n */\n\n/** TTL-backed set cache with explicit invalidation (superseded fact texts). */\nexport class SupersededTextsCache {\n private data: Set<string> | null = null;\n private loadedAt = 0;\n\n constructor(private readonly ttlMs: number) {}\n\n getSnapshot(now: number, loader: () => string[]): Set<string> {\n if (this.data !== null && now - this.loadedAt < this.ttlMs) {\n return this.data;\n }\n const next = new Set(loader());\n this.data = next;\n this.loadedAt = now;\n return next;\n }\n\n invalidate(): void {\n this.data = null;\n }\n}\n"],"mappings":";;;;;AAKA,IAAa,uBAAb,MAAkC;CAIH;CAH7B,OAAmC;CACnC,WAAmB;CAEnB,YAAY,OAAgC;EAAf,KAAA,QAAA;CAAgB;CAE7C,YAAY,KAAa,QAAqC;EAC5D,IAAI,KAAK,SAAS,QAAQ,MAAM,KAAK,WAAW,KAAK,OACnD,OAAO,KAAK;EAEd,MAAM,OAAO,IAAI,IAAI,OAAO,CAAC;EAC7B,KAAK,OAAO;EACZ,KAAK,WAAW;EAChB,OAAO;CACT;CAEA,aAAmB;EACjB,KAAK,OAAO;CACd;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"clusters.js","names":[],"sources":["../../../backends/facts-db/clusters.ts"],"sourcesContent":["/**\n * Topic cluster storage (Issue #146) (Issue #954 split).\n */\nimport type { DatabaseSync } from \"node:sqlite\";\n\nimport { createTransaction } from \"../../utils/sqlite-transaction.js\";\n\nexport function getAllLinkedFactIds(db: DatabaseSync): string[] {\n const rows = db\n .prepare(\n `SELECT DISTINCT id FROM (\n SELECT source_fact_id AS id FROM memory_links\n UNION\n SELECT target_fact_id AS id FROM memory_links\n )`,\n )\n .all() as Array<{ id: string }>;\n return rows.map((r) => r.id);\n}\n\nexport function getAllLinks(db: DatabaseSync): Array<{ sourceFactId: string; targetFactId: string }> {\n const rows = db.prepare(\"SELECT source_fact_id, target_fact_id FROM memory_links\").all() as Array<{\n source_fact_id: string;\n target_fact_id: string;\n }>;\n return rows.map((r) => ({\n sourceFactId: r.source_fact_id,\n targetFactId: r.target_fact_id,\n }));\n}\n\nexport function getAllEdges(\n db: DatabaseSync,\n limit = 5000,\n): Array<{\n source: string;\n target: string;\n linkType: string;\n strength: number;\n}> {\n const rows = db\n .prepare(\"SELECT source_fact_id, target_fact_id, link_type, strength FROM memory_links LIMIT ?\")\n .all(limit) as Array<{\n source_fact_id: string;\n target_fact_id: string;\n link_type: string;\n strength: number;\n }>;\n return rows.map((r) => ({\n source: r.source_fact_id,\n target: r.target_fact_id,\n linkType: r.link_type || \"RELATED_TO\",\n strength: r.strength ?? 0.8,\n }));\n}\n\nexport function saveClusters(\n db: DatabaseSync,\n clusters: Array<{\n id: string;\n label: string;\n factIds: string[];\n factCount: number;\n createdAt: number;\n updatedAt: number;\n }>,\n): void {\n const insertCluster = db.prepare(\n \"INSERT OR REPLACE INTO clusters (id, label, fact_count, created_at, updated_at) VALUES (?, ?, ?, ?, ?)\",\n );\n const insertMember = db.prepare(\"INSERT OR IGNORE INTO cluster_members (cluster_id, fact_id) VALUES (?, ?)\");\n\n createTransaction(db, () => {\n db.exec(\"DELETE FROM cluster_members\");\n db.exec(\"DELETE FROM clusters\");\n for (const cluster of clusters) {\n insertCluster.run(cluster.id, cluster.label, cluster.factCount, cluster.createdAt, cluster.updatedAt);\n for (const factId of cluster.factIds) {\n insertMember.run(cluster.id, factId);\n }\n }\n })();\n}\n\nexport function getClusters(db: DatabaseSync): Array<{\n id: string;\n label: string;\n factCount: number;\n createdAt: number;\n updatedAt: number;\n}> {\n const rows = db\n .prepare(\"SELECT id, label, fact_count, created_at, updated_at FROM clusters ORDER BY fact_count DESC\")\n .all() as Array<{\n id: string;\n label: string;\n fact_count: number;\n created_at: number;\n updated_at: number;\n }>;\n return rows.map((r) => ({\n id: r.id,\n label: r.label,\n factCount: r.fact_count,\n createdAt: r.created_at,\n updatedAt: r.updated_at,\n }));\n}\n\nexport function getClusterMembers(db: DatabaseSync, clusterId: string): string[] {\n const rows = db.prepare(\"SELECT fact_id FROM cluster_members WHERE cluster_id = ?\").all(clusterId) as Array<{\n fact_id: string;\n }>;\n return rows.map((r) => r.fact_id);\n}\n\nexport function getFactClusterId(db: DatabaseSync, factId: string): string | null {\n const row = db.prepare(\"SELECT cluster_id FROM cluster_members WHERE fact_id = ?\").get(factId) as\n | { cluster_id: string }\n | undefined;\n return row?.cluster_id ?? null;\n}\n"],"mappings":";;AAOA,SAAgB,oBAAoB,IAA4B;CAU9D,OATa,GACV,QACC;;;;WAKD,CACA,KACQ,CAAC,KAAK,MAAM,EAAE,GAAG;;AAG9B,SAAgB,YAAY,IAAyE;CAKnG,OAJa,GAAG,QAAQ,0DAA0D,CAAC,KAIxE,CAAC,KAAK,OAAO;EACtB,cAAc,EAAE;EAChB,cAAc,EAAE;EACjB,EAAE;;AAGL,SAAgB,YACd,IACA,QAAQ,KAMP;CASD,OARa,GACV,QAAQ,uFAAuF,CAC/F,IAAI,MAMI,CAAC,KAAK,OAAO;EACtB,QAAQ,EAAE;EACV,QAAQ,EAAE;EACV,UAAU,EAAE,aAAa;EACzB,UAAU,EAAE,YAAY;EACzB,EAAE;;AAGL,SAAgB,aACd,IACA,UAQM;CACN,MAAM,gBAAgB,GAAG,QACvB,yGACD;CACD,MAAM,eAAe,GAAG,QAAQ,4EAA4E;CAE5G,kBAAkB,UAAU;EAC1B,GAAG,KAAK,8BAA8B;EACtC,GAAG,KAAK,uBAAuB;EAC/B,KAAK,MAAM,WAAW,UAAU;GAC9B,cAAc,IAAI,QAAQ,IAAI,QAAQ,OAAO,QAAQ,WAAW,QAAQ,WAAW,QAAQ,UAAU;GACrG,KAAK,MAAM,UAAU,QAAQ,SAC3B,aAAa,IAAI,QAAQ,IAAI,OAAO;;GAGxC,EAAE;;AAGN,SAAgB,YAAY,IAMzB;CAUD,OATa,GACV,QAAQ,8FAA8F,CACtG,KAOQ,CAAC,KAAK,OAAO;EACtB,IAAI,EAAE;EACN,OAAO,EAAE;EACT,WAAW,EAAE;EACb,WAAW,EAAE;EACb,WAAW,EAAE;EACd,EAAE;;AAGL,SAAgB,kBAAkB,IAAkB,WAA6B;CAI/E,OAHa,GAAG,QAAQ,2DAA2D,CAAC,IAAI,UAG7E,CAAC,KAAK,MAAM,EAAE,QAAQ;;AAGnC,SAAgB,iBAAiB,IAAkB,QAA+B;CAIhF,OAHY,GAAG,QAAQ,2DAA2D,CAAC,IAAI,OAG7E,EAAE,cAAc"}
1
+ {"version":3,"file":"clusters.js","names":[],"sources":["../../../backends/facts-db/clusters.ts"],"sourcesContent":["/**\n * Topic cluster storage (Issue #146) (Issue #954 split).\n */\nimport type { DatabaseSync } from \"node:sqlite\";\n\nimport { createTransaction } from \"../../utils/sqlite-transaction.js\";\n\nexport function getAllLinkedFactIds(db: DatabaseSync): string[] {\n const rows = db\n .prepare(\n `SELECT DISTINCT id FROM (\n SELECT source_fact_id AS id FROM memory_links\n UNION\n SELECT target_fact_id AS id FROM memory_links\n )`,\n )\n .all() as Array<{ id: string }>;\n return rows.map((r) => r.id);\n}\n\nexport function getAllLinks(db: DatabaseSync): Array<{ sourceFactId: string; targetFactId: string }> {\n const rows = db.prepare(\"SELECT source_fact_id, target_fact_id FROM memory_links\").all() as Array<{\n source_fact_id: string;\n target_fact_id: string;\n }>;\n return rows.map((r) => ({\n sourceFactId: r.source_fact_id,\n targetFactId: r.target_fact_id,\n }));\n}\n\nexport function getAllEdges(\n db: DatabaseSync,\n limit = 5000,\n): Array<{\n source: string;\n target: string;\n linkType: string;\n strength: number;\n}> {\n const rows = db\n .prepare(\"SELECT source_fact_id, target_fact_id, link_type, strength FROM memory_links LIMIT ?\")\n .all(limit) as Array<{\n source_fact_id: string;\n target_fact_id: string;\n link_type: string;\n strength: number;\n }>;\n return rows.map((r) => ({\n source: r.source_fact_id,\n target: r.target_fact_id,\n linkType: r.link_type || \"RELATED_TO\",\n strength: r.strength ?? 0.8,\n }));\n}\n\nexport function saveClusters(\n db: DatabaseSync,\n clusters: Array<{\n id: string;\n label: string;\n factIds: string[];\n factCount: number;\n createdAt: number;\n updatedAt: number;\n }>,\n): void {\n const insertCluster = db.prepare(\n \"INSERT OR REPLACE INTO clusters (id, label, fact_count, created_at, updated_at) VALUES (?, ?, ?, ?, ?)\",\n );\n const insertMember = db.prepare(\"INSERT OR IGNORE INTO cluster_members (cluster_id, fact_id) VALUES (?, ?)\");\n\n createTransaction(db, () => {\n db.exec(\"DELETE FROM cluster_members\");\n db.exec(\"DELETE FROM clusters\");\n for (const cluster of clusters) {\n insertCluster.run(cluster.id, cluster.label, cluster.factCount, cluster.createdAt, cluster.updatedAt);\n for (const factId of cluster.factIds) {\n insertMember.run(cluster.id, factId);\n }\n }\n })();\n}\n\nexport function getClusters(db: DatabaseSync): Array<{\n id: string;\n label: string;\n factCount: number;\n createdAt: number;\n updatedAt: number;\n}> {\n const rows = db\n .prepare(\"SELECT id, label, fact_count, created_at, updated_at FROM clusters ORDER BY fact_count DESC\")\n .all() as Array<{\n id: string;\n label: string;\n fact_count: number;\n created_at: number;\n updated_at: number;\n }>;\n return rows.map((r) => ({\n id: r.id,\n label: r.label,\n factCount: r.fact_count,\n createdAt: r.created_at,\n updatedAt: r.updated_at,\n }));\n}\n\nexport function getClusterMembers(db: DatabaseSync, clusterId: string): string[] {\n const rows = db.prepare(\"SELECT fact_id FROM cluster_members WHERE cluster_id = ?\").all(clusterId) as Array<{\n fact_id: string;\n }>;\n return rows.map((r) => r.fact_id);\n}\n\nexport function getFactClusterId(db: DatabaseSync, factId: string): string | null {\n const row = db.prepare(\"SELECT cluster_id FROM cluster_members WHERE fact_id = ?\").get(factId) as\n | { cluster_id: string }\n | undefined;\n return row?.cluster_id ?? null;\n}\n"],"mappings":";;AAOA,SAAgB,oBAAoB,IAA4B;CAU9D,OATa,GACV,QACC;;;;UAKF,EACC,IACO,EAAE,KAAK,MAAM,EAAE,EAAE;AAC7B;AAEA,SAAgB,YAAY,IAAyE;CAKnG,OAJa,GAAG,QAAQ,yDAAyD,EAAE,IAIzE,EAAE,KAAK,OAAO;EACtB,cAAc,EAAE;EAChB,cAAc,EAAE;CAClB,EAAE;AACJ;AAEA,SAAgB,YACd,IACA,QAAQ,KAMP;CASD,OARa,GACV,QAAQ,sFAAsF,EAC9F,IAAI,KAMG,EAAE,KAAK,OAAO;EACtB,QAAQ,EAAE;EACV,QAAQ,EAAE;EACV,UAAU,EAAE,aAAa;EACzB,UAAU,EAAE,YAAY;CAC1B,EAAE;AACJ;AAEA,SAAgB,aACd,IACA,UAQM;CACN,MAAM,gBAAgB,GAAG,QACvB,wGACF;CACA,MAAM,eAAe,GAAG,QAAQ,2EAA2E;CAE3G,kBAAkB,UAAU;EAC1B,GAAG,KAAK,6BAA6B;EACrC,GAAG,KAAK,sBAAsB;EAC9B,KAAK,MAAM,WAAW,UAAU;GAC9B,cAAc,IAAI,QAAQ,IAAI,QAAQ,OAAO,QAAQ,WAAW,QAAQ,WAAW,QAAQ,SAAS;GACpG,KAAK,MAAM,UAAU,QAAQ,SAC3B,aAAa,IAAI,QAAQ,IAAI,MAAM;EAEvC;CACF,CAAC,EAAE;AACL;AAEA,SAAgB,YAAY,IAMzB;CAUD,OATa,GACV,QAAQ,6FAA6F,EACrG,IAOO,EAAE,KAAK,OAAO;EACtB,IAAI,EAAE;EACN,OAAO,EAAE;EACT,WAAW,EAAE;EACb,WAAW,EAAE;EACb,WAAW,EAAE;CACf,EAAE;AACJ;AAEA,SAAgB,kBAAkB,IAAkB,WAA6B;CAI/E,OAHa,GAAG,QAAQ,0DAA0D,EAAE,IAAI,SAG9E,EAAE,KAAK,MAAM,EAAE,OAAO;AAClC;AAEA,SAAgB,iBAAiB,IAAkB,QAA+B;CAIhF,OAHY,GAAG,QAAQ,0DAA0D,EAAE,IAAI,MAG9E,GAAG,cAAc;AAC5B"}
@@ -491,7 +491,7 @@ async function resolveContradictionsAutonomously(db, getById, supersede, options
491
491
  })) decisionsApplied++;
492
492
  }
493
493
  continue;
494
- } else if (lww.eligible && isFactVerified(db, contradiction.factIdOld)) reviewItem.suggestedReason = "Older fact is verified; leaving for manual review.";
494
+ } else if (isFactVerified(db, contradiction.factIdOld)) reviewItem.suggestedReason = "Older fact is verified; leaving for manual review.";
495
495
  if (llm && adjudicate) try {
496
496
  const llmDecision = await adjudicate(reviewItem);
497
497
  const confidence = clampConfidence(llmDecision.confidence);