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":"entity-layer.js","names":[],"sources":["../../../backends/facts-db/entity-layer.ts"],"sourcesContent":["/**\n * Organizations, contacts, and NER mention persistence (#985–#987).\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport type { DatabaseSync } from \"node:sqlite\";\nimport { canonicalizeEntityMention, countReason, makeEntityMentionKey } from \"../../utils/entity-mention-quality.js\";\nimport { isEntityStopWord, normalizeEntityStopWord } from \"../../utils/entity-stopwords.js\";\nimport { createTransaction } from \"../../utils/sqlite-transaction.js\";\nimport { readSchemaVersion, runVersionedSchemaMigration } from \"../sqlite-schema-meta.js\";\n\nexport type EntityMentionLabel =\n | \"PERSON\"\n | \"ORG\"\n | \"SERVICE\"\n | \"TOOL\"\n | \"MODEL\"\n | \"PROJECT\"\n | \"AGENT\"\n | \"ROLE\"\n | \"PRODUCT\"\n | \"LOCATION\";\n\ntype _FactEntityMentionRow = {\n id: string;\n factId: string;\n label: EntityMentionLabel;\n surfaceText: string;\n normalizedSurface: string;\n startOffset: number;\n endOffset: number;\n confidence: number;\n detectedLang: string | null;\n source: string;\n contactId: string | null;\n organizationId: string | null;\n};\n\nexport type OrganizationRow = {\n id: string;\n canonicalKey: string;\n displayName: string;\n aliasesJson: string | null;\n};\n\nexport type ContactRow = {\n id: string;\n normalizedKey: string;\n displayName: string;\n email: string | null;\n notes: string | null;\n aliasesJson: string | null;\n primaryOrgId: string | null;\n};\n\nexport type EntityEnrichmentBacklogByTier = {\n hot: number;\n warm: number;\n structural: number;\n cold: number;\n unknown: number;\n};\n\nexport type EntityEnrichmentBacklogSummary = {\n total: number;\n byTier: EntityEnrichmentBacklogByTier;\n};\n\nexport type ListFactsNeedingEnrichmentOptions = {\n /** Process full backlog (no LIMIT) for manual one-shot catch-up mode. */\n all?: boolean;\n};\n\nexport type EntityMentionsAuditSummary = {\n factsScanned: number;\n rowsScanned: number;\n accepted: number;\n rejected: number;\n duplicates: number;\n reclassified: number;\n rejectReasons: Record<string, number>;\n};\n\nexport type EntityMentionsCleanupSummary = EntityMentionsAuditSummary & {\n changedFacts: number;\n removedRows: number;\n};\n\nexport function normalizeEntityKey(name: string): string {\n return name.normalize(\"NFKD\").replace(/\\p{M}/gu, \"\").toLowerCase().replace(/\\s+/g, \" \").trim();\n}\n\nconst MIN_ENTITY_MENTION_LENGTH = 3;\nconst NON_ORG_SERVICE_TERMS = new Set([\"signal\", \"telegram\", \"whatsapp\"]);\nconst NON_ENTITY_ORG_TERMS = new Set([\"hybrid memory plugin\", \"the agent\", \"the system\"]);\nconst PERSON_ROLE_TITLES = new Set([\"architect\", \"developer\", \"director\", \"engineer\", \"manager\", \"surgeon\"]);\nconst KNOWN_TWO_CHAR_MENTIONS = new Set([\"gh\", \"jq\", \"ha\"]);\n\nfunction normalizeMentionSurfaceText(surfaceText: string): string {\n return surfaceText.trim();\n}\n\nfunction isValidTwoCharacterMention(mention: { label: EntityMentionLabel; surfaceText: string }): boolean {\n const surface = mention.surfaceText.trim();\n if (surface.length !== 2) {\n return false;\n }\n\n if (KNOWN_TWO_CHAR_MENTIONS.has(surface.toLowerCase())) {\n return true;\n }\n\n if (mention.label === \"PERSON\") {\n return /^\\p{L}{2}$/u.test(surface);\n }\n\n if (!/^[\\p{L}\\p{N}]{2}$/u.test(surface)) {\n return false;\n }\n\n if (/\\p{N}/u.test(surface) || /\\p{Lu}/u.test(surface)) {\n return true;\n }\n\n // Allow two-character org mentions in scripts without uppercase variants (e.g. Han).\n return /\\P{Script=Latin}/u.test(surface);\n}\n\nfunction shouldFilterEntityMention(mention: {\n label: EntityMentionLabel;\n surfaceText: string;\n normalizedSurface: string;\n}): boolean {\n if (\n mention.surfaceText.length < MIN_ENTITY_MENTION_LENGTH &&\n !isValidTwoCharacterMention({ label: mention.label, surfaceText: mention.surfaceText })\n ) {\n return true;\n }\n\n if (isEntityStopWord(mention.surfaceText)) {\n return true;\n }\n\n const stopWordKey = normalizeEntityStopWord(mention.surfaceText);\n if (mention.label === \"ORG\") {\n return NON_ORG_SERVICE_TERMS.has(stopWordKey) || NON_ENTITY_ORG_TERMS.has(stopWordKey);\n }\n\n if (!mention.normalizedSurface.includes(\" \") && PERSON_ROLE_TITLES.has(stopWordKey)) {\n return true;\n }\n\n return false;\n}\n\nfunction isContainedByLongerMention(\n mention: { label: EntityMentionLabel; normalizedSurface: string; startOffset: number; endOffset: number },\n other: { label: EntityMentionLabel; normalizedSurface: string; startOffset: number; endOffset: number },\n): boolean {\n if (mention.label !== other.label || mention.normalizedSurface === other.normalizedSurface) {\n return false;\n }\n\n if (other.normalizedSurface.length <= mention.normalizedSurface.length) {\n return false;\n }\n\n // Check if spans overlap: they must overlap for containment to apply\n const spansOverlap = mention.startOffset < other.endOffset && other.startOffset < mention.endOffset;\n if (!spansOverlap) {\n return false;\n }\n\n const isWordBoundary = (char: string): boolean => {\n return char === \" \" || /[,\\-.\\/()\\[\\]{}:;!?'\"&]/.test(char);\n };\n\n let searchStart = 0;\n while (searchStart <= other.normalizedSurface.length - mention.normalizedSurface.length) {\n const index = other.normalizedSurface.indexOf(mention.normalizedSurface, searchStart);\n if (index === -1) {\n return false;\n }\n\n const endIndex = index + mention.normalizedSurface.length;\n const beforeChar = index > 0 ? other.normalizedSurface[index - 1] : \" \";\n const afterChar = endIndex < other.normalizedSurface.length ? other.normalizedSurface[endIndex] : \" \";\n\n if (isWordBoundary(beforeChar) && (isWordBoundary(afterChar) || endIndex === other.normalizedSurface.length)) {\n return true;\n }\n\n searchStart = index + 1;\n }\n\n return false;\n}\n\nfunction preferEntityMention<\n T extends {\n confidence: number;\n surfaceText: string;\n startOffset: number;\n },\n>(left: T, right: T): T {\n if (right.confidence !== left.confidence) {\n return right.confidence > left.confidence ? right : left;\n }\n if (right.surfaceText.length !== left.surfaceText.length) {\n return right.surfaceText.length > left.surfaceText.length ? right : left;\n }\n return right.startOffset < left.startOffset ? right : left;\n}\n\nexport function normalizeFactEntityMentionsForPersistence<\n T extends {\n label: EntityMentionLabel;\n surfaceText: string;\n normalizedSurface: string;\n startOffset: number;\n endOffset: number;\n confidence: number;\n },\n>(mentions: readonly T[]): T[] {\n const prepared = mentions\n .map((mention) => {\n const surfaceText = normalizeMentionSurfaceText(mention.surfaceText);\n const normalizedSurface = normalizeEntityKey(mention.normalizedSurface) || normalizeEntityKey(surfaceText);\n // Adjust offsets for trimmed whitespace so stored offsets match the trimmed surfaceText in the original fact text\n const leadingTrim = mention.surfaceText.length - mention.surfaceText.trimStart().length;\n const trailingTrim = mention.surfaceText.length - mention.surfaceText.trimEnd().length;\n return {\n ...mention,\n surfaceText,\n normalizedSurface,\n startOffset: mention.startOffset + leadingTrim,\n endOffset: mention.endOffset - trailingTrim,\n };\n })\n .filter((mention) => mention.surfaceText.length > 0 && mention.normalizedSurface.length > 0)\n .filter((mention) => !shouldFilterEntityMention(mention))\n .filter(\n (mention, index, all) =>\n !all.some((other, otherIndex) => otherIndex !== index && isContainedByLongerMention(mention, other)),\n );\n\n const deduplicated = new Map<string, T>();\n for (const mention of prepared) {\n // Use surfaceText for the dedup key so it matches what upsertOrganization/upsertContact compute for canonical_key\n const key = `${mention.label}\\u0000${normalizeEntityKey(mention.surfaceText)}`;\n const existing = deduplicated.get(key);\n deduplicated.set(key, existing ? preferEntityMention(existing, mention) : mention);\n }\n\n return [...deduplicated.values()].sort((left, right) => left.startOffset - right.startOffset);\n}\n\n/** Escape `%`, `_`, and `\\` for SQLite `LIKE ... ESCAPE '\\'` literal matching. */\nexport function escapeLikeLiteralForBackslashEscape(s: string): string {\n return s.replace(/\\\\/g, \"\\\\\\\\\").replace(/%/g, \"\\\\%\").replace(/_/g, \"\\\\_\");\n}\n\nexport function migrateEntityLayerTables(db: DatabaseSync): void {\n db.exec(`\n CREATE TABLE IF NOT EXISTS organizations (\n id TEXT PRIMARY KEY,\n canonical_key TEXT NOT NULL UNIQUE,\n display_name TEXT NOT NULL,\n aliases_json TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n );\n CREATE INDEX IF NOT EXISTS idx_org_canonical ON organizations(canonical_key);\n `);\n\n db.exec(`\n CREATE TABLE IF NOT EXISTS contacts (\n id TEXT PRIMARY KEY,\n normalized_key TEXT NOT NULL,\n display_name TEXT NOT NULL,\n email TEXT,\n notes TEXT,\n aliases_json TEXT,\n primary_org_id TEXT REFERENCES organizations(id) ON DELETE SET NULL,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n );\n CREATE INDEX IF NOT EXISTS idx_contacts_org ON contacts(primary_org_id);\n `);\n // One row per normalized display key (upsertContact assumes uniqueness).\n db.exec(\"CREATE UNIQUE INDEX IF NOT EXISTS idx_contacts_normalized_key_unique ON contacts(normalized_key)\");\n\n db.exec(`\n CREATE TABLE IF NOT EXISTS fact_entity_mentions (\n id TEXT PRIMARY KEY,\n fact_id TEXT NOT NULL REFERENCES facts(id) ON DELETE CASCADE,\n label TEXT NOT NULL,\n surface_text TEXT NOT NULL,\n normalized_surface TEXT NOT NULL,\n start_offset INTEGER NOT NULL,\n end_offset INTEGER NOT NULL,\n confidence REAL NOT NULL DEFAULT 0.8,\n detected_lang TEXT,\n source TEXT NOT NULL DEFAULT 'llm',\n contact_id TEXT REFERENCES contacts(id) ON DELETE SET NULL,\n organization_id TEXT REFERENCES organizations(id) ON DELETE SET NULL,\n created_at INTEGER NOT NULL\n );\n CREATE INDEX IF NOT EXISTS idx_fem_fact ON fact_entity_mentions(fact_id);\n CREATE INDEX IF NOT EXISTS idx_fem_org ON fact_entity_mentions(organization_id);\n CREATE INDEX IF NOT EXISTS idx_fem_contact ON fact_entity_mentions(contact_id);\n CREATE INDEX IF NOT EXISTS idx_fem_label ON fact_entity_mentions(label);\n `);\n\n const version = readSchemaVersion(db, \"entity_layer\");\n if (version < 1) {\n runVersionedSchemaMigration(db, \"entity_layer\", 1, () => {\n const hasDuplicates = db\n .prepare(\n `SELECT 1\n FROM fact_entity_mentions\n GROUP BY fact_id, label, normalized_surface\n HAVING COUNT(*) > 1\n LIMIT 1`,\n )\n .get() as { 1: number } | undefined;\n if (hasDuplicates) {\n db.exec(`\n DELETE FROM fact_entity_mentions\n WHERE id IN (\n SELECT m1.id\n FROM fact_entity_mentions m1\n JOIN fact_entity_mentions m2\n ON m1.fact_id = m2.fact_id\n AND m1.label = m2.label\n AND m1.normalized_surface = m2.normalized_surface\n AND (\n m2.confidence > m1.confidence\n OR (m2.confidence = m1.confidence AND m2.created_at > m1.created_at)\n OR (m2.confidence = m1.confidence AND m2.created_at = m1.created_at AND m2.id > m1.id)\n )\n );\n `);\n }\n db.exec(`\n CREATE UNIQUE INDEX IF NOT EXISTS idx_fem_fact_label_norm\n ON fact_entity_mentions(fact_id, label, normalized_surface);\n `);\n });\n }\n\n db.exec(`\n CREATE TABLE IF NOT EXISTS org_fact_links (\n org_id TEXT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,\n fact_id TEXT NOT NULL REFERENCES facts(id) ON DELETE CASCADE,\n reason TEXT NOT NULL DEFAULT 'ner_mention',\n created_at INTEGER NOT NULL,\n PRIMARY KEY (org_id, fact_id, reason)\n );\n CREATE INDEX IF NOT EXISTS idx_org_fact_org ON org_fact_links(org_id);\n CREATE INDEX IF NOT EXISTS idx_org_fact_fact ON org_fact_links(fact_id);\n `);\n}\n\nfunction upsertOrganization(db: DatabaseSync, displayName: string): { id: string; created: boolean } | null {\n const canonicalKey = normalizeEntityKey(displayName);\n if (!canonicalKey) {\n return null;\n }\n const existing = db.prepare(\"SELECT id FROM organizations WHERE canonical_key = ?\").get(canonicalKey) as\n | { id: string }\n | undefined;\n if (existing) {\n const now = Math.floor(Date.now() / 1000);\n db.prepare(\"UPDATE organizations SET display_name = ?, updated_at = ? WHERE id = ?\").run(\n displayName.trim(),\n now,\n existing.id,\n );\n return { id: existing.id, created: false };\n }\n const id = randomUUID();\n const now = Math.floor(Date.now() / 1000);\n db.prepare(\n `INSERT INTO organizations (id, canonical_key, display_name, aliases_json, created_at, updated_at)\n VALUES (?, ?, ?, NULL, ?, ?)`,\n ).run(id, canonicalKey, displayName.trim(), now, now);\n return { id, created: true };\n}\n\nfunction upsertContact(\n db: DatabaseSync,\n displayName: string,\n primaryOrgId: string | null,\n): { id: string; created: boolean } | null {\n const nk = normalizeEntityKey(displayName);\n if (!nk) {\n return null;\n }\n const existing = db.prepare(\"SELECT id, primary_org_id FROM contacts WHERE normalized_key = ? LIMIT 1\").get(nk) as\n | { id: string; primary_org_id: string | null }\n | undefined;\n const now = Math.floor(Date.now() / 1000);\n if (existing) {\n if (primaryOrgId && !existing.primary_org_id) {\n db.prepare(\"UPDATE contacts SET primary_org_id = ?, updated_at = ?, display_name = ? WHERE id = ?\").run(\n primaryOrgId,\n now,\n displayName.trim(),\n existing.id,\n );\n } else {\n db.prepare(\"UPDATE contacts SET updated_at = ?, display_name = ? WHERE id = ?\").run(\n now,\n displayName.trim(),\n existing.id,\n );\n }\n return { id: existing.id, created: false };\n }\n const id = randomUUID();\n db.prepare(\n `INSERT INTO contacts (id, normalized_key, display_name, email, notes, aliases_json, primary_org_id, created_at, updated_at)\n VALUES (?, ?, ?, NULL, NULL, NULL, ?, ?, ?)`,\n ).run(id, nk, displayName.trim(), primaryOrgId, now, now);\n return { id, created: true };\n}\n\nexport function replaceFactEntityMentions(\n db: DatabaseSync,\n factId: string,\n mentions: Array<{\n label: EntityMentionLabel;\n surfaceText: string;\n normalizedSurface: string;\n startOffset: number;\n endOffset: number;\n confidence: number;\n detectedLang: string | null;\n source: string;\n }>,\n options: { preserveEnrichmentTimestamp?: boolean } = {},\n): void {\n const tx = createTransaction(db, () => {\n db.prepare(\"DELETE FROM fact_entity_mentions WHERE fact_id = ?\").run(factId);\n db.prepare(\"DELETE FROM org_fact_links WHERE fact_id = ? AND reason = 'ner_mention'\").run(factId);\n const normalizedMentions = normalizeFactEntityMentionsForPersistence(mentions);\n\n const now = Math.floor(Date.now() / 1000);\n // Intentional OR IGNORE: idx_fem_fact_label_norm enforces idempotent logical mention writes.\n const ins = db.prepare(\n `INSERT OR IGNORE INTO fact_entity_mentions (\n id, fact_id, label, surface_text, normalized_surface, start_offset, end_offset,\n confidence, detected_lang, source, contact_id, organization_id, created_at\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n );\n const insOrgLink = db.prepare(\n `INSERT OR IGNORE INTO org_fact_links (org_id, fact_id, reason, created_at) VALUES (?, ?, 'ner_mention', ?)`,\n );\n\n const orgIds: string[] = [];\n const personRows: Array<{ surface: string; contactId: string }> = [];\n\n for (const m of normalizedMentions) {\n let contactId: string | null = null;\n let organizationId: string | null = null;\n\n if (m.label === \"ORG\") {\n const org = upsertOrganization(db, m.surfaceText);\n if (org) {\n organizationId = org.id;\n orgIds.push(org.id);\n insOrgLink.run(org.id, factId, now);\n }\n } else if (m.label === \"PERSON\") {\n const con = upsertContact(db, m.surfaceText, null);\n if (con) {\n contactId = con.id;\n personRows.push({ surface: m.surfaceText, contactId: con.id });\n }\n }\n\n ins.run(\n randomUUID(),\n factId,\n m.label,\n m.surfaceText,\n m.normalizedSurface,\n m.startOffset,\n m.endOffset,\n m.confidence,\n m.detectedLang,\n m.source,\n contactId,\n organizationId,\n now,\n );\n }\n\n // If same fact mentions both a person and an org, set primary_org on contacts (weak v1 heuristic).\n if (orgIds.length > 0 && personRows.length > 0) {\n const primaryOrg = orgIds[0];\n for (const p of personRows) {\n db.prepare(\"UPDATE contacts SET primary_org_id = COALESCE(primary_org_id, ?), updated_at = ? WHERE id = ?\").run(\n primaryOrg,\n now,\n p.contactId,\n );\n }\n }\n\n if (!options.preserveEnrichmentTimestamp) {\n db.prepare(\"UPDATE facts SET entity_enrichment_at = ? WHERE id = ?\").run(now, factId);\n }\n });\n tx();\n}\n\nexport function getOrganizationByKeyOrName(db: DatabaseSync, query: string): OrganizationRow | null {\n const nk = normalizeEntityKey(query);\n if (!nk) return null;\n const byKey = db.prepare(\"SELECT * FROM organizations WHERE canonical_key = ?\").get(nk) as\n | Record<string, unknown>\n | undefined;\n if (byKey) return rowToOrg(byKey);\n const like = `%${escapeLikeLiteralForBackslashEscape(nk)}%`;\n const byName = db\n .prepare(\n \"SELECT * FROM organizations WHERE canonical_key LIKE ? ESCAPE '\\\\' ORDER BY length(display_name) ASC LIMIT 1\",\n )\n .get(like) as Record<string, unknown> | undefined;\n return byName ? rowToOrg(byName) : null;\n}\n\nfunction rowToOrg(row: Record<string, unknown>): OrganizationRow {\n return {\n id: row.id as string,\n canonicalKey: row.canonical_key as string,\n displayName: row.display_name as string,\n aliasesJson: (row.aliases_json as string | null) ?? null,\n };\n}\n\nexport function listContactsForOrg(db: DatabaseSync, orgId: string, limit: number): ContactRow[] {\n const rows = db\n .prepare(\n `SELECT * FROM contacts\n WHERE primary_org_id = ?\n ORDER BY display_name COLLATE NOCASE\n LIMIT ?`,\n )\n .all(orgId, limit) as Array<Record<string, unknown>>;\n return rows.map(rowToContact);\n}\n\nexport function listContactsByNamePrefix(db: DatabaseSync, prefix: string, limit: number): ContactRow[] {\n const p = prefix.trim().toLowerCase();\n if (!p) {\n const rows = db.prepare(\"SELECT * FROM contacts ORDER BY display_name COLLATE NOCASE LIMIT ?\").all(limit) as Array<\n Record<string, unknown>\n >;\n return rows.map(rowToContact);\n }\n const esc = escapeLikeLiteralForBackslashEscape(p);\n const pat = `${esc}%`;\n const normalizedPrefix = normalizeEntityKey(prefix);\n const escNormalized = escapeLikeLiteralForBackslashEscape(normalizedPrefix);\n const patNormalized = `${escNormalized}%`;\n const rows = db\n .prepare(\n `SELECT * FROM contacts\n WHERE lower(display_name) LIKE ? ESCAPE '\\\\' OR normalized_key LIKE ? ESCAPE '\\\\'\n ORDER BY display_name COLLATE NOCASE\n LIMIT ?`,\n )\n .all(pat, patNormalized, limit) as Array<Record<string, unknown>>;\n return rows.map(rowToContact);\n}\n\nfunction rowToContact(row: Record<string, unknown>): ContactRow {\n return {\n id: row.id as string,\n normalizedKey: row.normalized_key as string,\n displayName: row.display_name as string,\n email: (row.email as string | null) ?? null,\n notes: (row.notes as string | null) ?? null,\n aliasesJson: (row.aliases_json as string | null) ?? null,\n primaryOrgId: (row.primary_org_id as string | null) ?? null,\n };\n}\n\nexport function listFactIdsForOrg(db: DatabaseSync, orgId: string, limit: number): string[] {\n const rows = db\n .prepare(\"SELECT DISTINCT fact_id FROM org_fact_links WHERE org_id = ? ORDER BY created_at DESC LIMIT ?\")\n .all(orgId, limit) as Array<{ fact_id: string }>;\n return rows.map((r) => r.fact_id);\n}\n\nfunction buildEntityEnrichmentPendingBaseSql(): string {\n return `SELECT f.id FROM facts f\n WHERE f.superseded_at IS NULL\n AND (f.expires_at IS NULL OR f.expires_at > ?)\n AND length(f.text) >= ?\n AND f.entity_enrichment_at IS NULL\n ORDER BY\n CASE COALESCE(f.tier, 'warm')\n WHEN 'hot' THEN 0\n WHEN 'warm' THEN 1\n WHEN 'structural' THEN 2\n WHEN 'cold' THEN 3\n ELSE 4\n END ASC,\n COALESCE(f.last_accessed, f.last_confirmed_at, f.created_at) DESC,\n MAX(COALESCE(f.recall_count, 0), COALESCE(f.access_count, 0)) DESC,\n COALESCE(f.importance, 0) DESC,\n f.created_at DESC,\n f.id ASC`;\n}\n\nexport function listFactsNeedingEnrichment(\n db: DatabaseSync,\n limit: number,\n minTextLen: number,\n options?: ListFactsNeedingEnrichmentOptions,\n): string[] {\n const nowSec = Math.floor(Date.now() / 1000);\n const sql = options?.all\n ? buildEntityEnrichmentPendingBaseSql()\n : `${buildEntityEnrichmentPendingBaseSql()}\\n LIMIT ?`;\n const rows = (\n options?.all ? db.prepare(sql).all(nowSec, minTextLen) : db.prepare(sql).all(nowSec, minTextLen, limit)\n ) as Array<{ id: string }>;\n return rows.map((r) => r.id);\n}\n\nexport function getEntityEnrichmentBacklogSummary(\n db: DatabaseSync,\n minTextLen: number,\n): EntityEnrichmentBacklogSummary {\n const nowSec = Math.floor(Date.now() / 1000);\n const row = db\n .prepare(\n `SELECT\n COUNT(*) AS total,\n SUM(CASE WHEN COALESCE(f.tier, 'warm') = 'hot' THEN 1 ELSE 0 END) AS hot,\n SUM(CASE WHEN COALESCE(f.tier, 'warm') = 'warm' THEN 1 ELSE 0 END) AS warm,\n SUM(CASE WHEN COALESCE(f.tier, 'warm') = 'structural' THEN 1 ELSE 0 END) AS structural,\n SUM(CASE WHEN COALESCE(f.tier, 'warm') = 'cold' THEN 1 ELSE 0 END) AS cold,\n SUM(CASE WHEN COALESCE(f.tier, 'warm') NOT IN ('hot', 'warm', 'structural', 'cold') THEN 1 ELSE 0 END) AS unknown\n FROM facts f\n WHERE f.superseded_at IS NULL\n AND (f.expires_at IS NULL OR f.expires_at > ?)\n AND length(f.text) >= ?\n AND f.entity_enrichment_at IS NULL`,\n )\n .get(nowSec, minTextLen) as\n | {\n total: number | null;\n hot: number | null;\n warm: number | null;\n structural: number | null;\n cold: number | null;\n unknown: number | null;\n }\n | undefined;\n return {\n total: Number(row?.total ?? 0),\n byTier: {\n hot: Number(row?.hot ?? 0),\n warm: Number(row?.warm ?? 0),\n structural: Number(row?.structural ?? 0),\n cold: Number(row?.cold ?? 0),\n unknown: Number(row?.unknown ?? 0),\n },\n };\n}\n\ntype ProcessedEntityMention<T = void> = {\n label: EntityMentionLabel;\n surfaceText: string;\n normalizedSurface: string;\n confidence: number;\n sourceRow: T;\n};\n\ntype EntityMentionProcessingResult<T = void> = {\n accepted: Array<ProcessedEntityMention<T>>;\n counters: {\n rowsScanned: number;\n accepted: number;\n rejected: number;\n duplicates: number;\n reclassified: number;\n rejectReasons: Record<string, number>;\n };\n};\n\nfunction processEntityMentionsForFact<\n T extends {\n label: string;\n surface_text: string;\n normalized_surface: string;\n confidence: number;\n start_offset: number;\n end_offset: number;\n },\n>(rows: T[]): EntityMentionProcessingResult<T> {\n const acceptedByKey = new Map<string, ProcessedEntityMention<T>>();\n let rowsScanned = 0;\n let accepted = 0;\n let rejected = 0;\n let duplicates = 0;\n const rejectReasons: Record<string, number> = {};\n\n for (const row of rows) {\n rowsScanned++;\n const canonical = canonicalizeEntityMention({\n label: row.label,\n surfaceText: row.surface_text,\n normalizedSurface: row.normalized_surface,\n confidence: row.confidence,\n });\n if (!canonical.accepted) {\n rejected++;\n countReason(rejectReasons, canonical.reason);\n continue;\n }\n const key = makeEntityMentionKey(canonical.label, canonical.normalizedSurface);\n const existing = acceptedByKey.get(key);\n if (existing) {\n duplicates++;\n if (canonical.confidence > existing.confidence) {\n acceptedByKey.set(key, {\n label: canonical.label,\n surfaceText: canonical.surfaceText,\n normalizedSurface: canonical.normalizedSurface,\n confidence: canonical.confidence,\n sourceRow: row,\n });\n }\n } else {\n acceptedByKey.set(key, {\n label: canonical.label,\n surfaceText: canonical.surfaceText,\n normalizedSurface: canonical.normalizedSurface,\n confidence: canonical.confidence,\n sourceRow: row,\n });\n accepted++;\n }\n }\n\n const allAccepted = [...acceptedByKey.values()];\n let substringFilteredCount = 0;\n const filteredAccepted = allAccepted.filter((m) => {\n const isSubstring = allAccepted.some(\n (other) =>\n other !== m &&\n other.label === m.label &&\n other.normalizedSurface.length > m.normalizedSurface.length &&\n other.normalizedSurface.includes(m.normalizedSurface) &&\n other.sourceRow.start_offset <= m.sourceRow.start_offset &&\n other.sourceRow.end_offset >= m.sourceRow.end_offset,\n );\n if (isSubstring) {\n substringFilteredCount++;\n }\n return !isSubstring;\n });\n if (substringFilteredCount > 0) {\n rejected += substringFilteredCount;\n rejectReasons.substring = (rejectReasons.substring ?? 0) + substringFilteredCount;\n }\n accepted = filteredAccepted.length;\n const reclassified = filteredAccepted.reduce((count, mention) => {\n const sourceRow = mention.sourceRow;\n if (mention.label !== sourceRow.label || mention.normalizedSurface !== sourceRow.normalized_surface) {\n return count + 1;\n }\n return count;\n }, 0);\n\n return {\n accepted: filteredAccepted,\n counters: {\n rowsScanned,\n accepted,\n rejected,\n duplicates,\n reclassified,\n rejectReasons,\n },\n };\n}\n\nexport function auditEntityMentions(db: DatabaseSync, limit: number): EntityMentionsAuditSummary {\n const factIds = db\n .prepare(\n `SELECT fem.fact_id\n FROM fact_entity_mentions fem\n JOIN facts f ON fem.fact_id = f.id\n GROUP BY fem.fact_id\n ORDER BY f.created_at DESC\n LIMIT ?`,\n )\n .all(limit) as Array<{ fact_id: string }>;\n let rowsScanned = 0;\n let accepted = 0;\n let rejected = 0;\n let duplicates = 0;\n let reclassified = 0;\n const rejectReasons: Record<string, number> = {};\n for (const fact of factIds) {\n const rows = db\n .prepare(\n `SELECT id, label, surface_text, normalized_surface, start_offset, end_offset, confidence\n FROM fact_entity_mentions\n WHERE fact_id = ?`,\n )\n .all(fact.fact_id) as Array<{\n id: string;\n label: string;\n surface_text: string;\n normalized_surface: string;\n start_offset: number;\n end_offset: number;\n confidence: number;\n }>;\n const result = processEntityMentionsForFact(rows);\n rowsScanned += result.counters.rowsScanned;\n accepted += result.counters.accepted;\n rejected += result.counters.rejected;\n duplicates += result.counters.duplicates;\n reclassified += result.counters.reclassified;\n for (const [reason, count] of Object.entries(result.counters.rejectReasons)) {\n rejectReasons[reason] = (rejectReasons[reason] ?? 0) + count;\n }\n }\n return {\n factsScanned: factIds.length,\n rowsScanned,\n accepted,\n rejected,\n duplicates,\n reclassified,\n rejectReasons,\n };\n}\n\nfunction rowsSignature(\n rows: Array<{ label: string; normalizedSurface: string; surfaceText: string; confidence: number }>,\n): string {\n return JSON.stringify(\n rows\n .map((r) => ({\n label: r.label,\n normalizedSurface: r.normalizedSurface,\n surfaceText: r.surfaceText,\n confidence: Number(r.confidence.toFixed(4)),\n }))\n .sort((a, b) =>\n `${a.label}|${a.normalizedSurface}|${a.surfaceText}|${a.confidence}`.localeCompare(\n `${b.label}|${b.normalizedSurface}|${b.surfaceText}|${b.confidence}`,\n ),\n ),\n );\n}\n\nexport function cleanupEntityMentions(\n db: DatabaseSync,\n options: { limit: number; apply: boolean },\n): EntityMentionsCleanupSummary {\n const limit = Math.max(1, Math.floor(options.limit));\n const factIds = db\n .prepare(\n `SELECT fem.fact_id\n FROM fact_entity_mentions fem\n JOIN facts f ON fem.fact_id = f.id\n GROUP BY fem.fact_id\n ORDER BY f.created_at DESC\n LIMIT ?`,\n )\n .all(limit) as Array<{ fact_id: string }>;\n\n let rowsScanned = 0;\n let accepted = 0;\n let rejected = 0;\n let duplicates = 0;\n let reclassified = 0;\n let changedFacts = 0;\n let removedRows = 0;\n const rejectReasons: Record<string, number> = {};\n\n for (const fact of factIds) {\n const rows = db\n .prepare(\n `SELECT label, surface_text, normalized_surface, start_offset, end_offset, confidence, detected_lang, source\n FROM fact_entity_mentions\n WHERE fact_id = ?`,\n )\n .all(fact.fact_id) as Array<{\n label: string;\n surface_text: string;\n normalized_surface: string;\n start_offset: number;\n end_offset: number;\n confidence: number;\n detected_lang: string | null;\n source: string;\n }>;\n const before = rowsSignature(\n rows.map((row) => ({\n label: row.label,\n normalizedSurface: row.normalized_surface,\n surfaceText: row.surface_text,\n confidence: row.confidence,\n })),\n );\n\n const result = processEntityMentionsForFact(rows);\n rowsScanned += result.counters.rowsScanned;\n accepted += result.counters.accepted;\n rejected += result.counters.rejected;\n duplicates += result.counters.duplicates;\n reclassified += result.counters.reclassified;\n for (const [reason, count] of Object.entries(result.counters.rejectReasons)) {\n rejectReasons[reason] = (rejectReasons[reason] ?? 0) + count;\n }\n\n const nextRows = result.accepted.map((m) => ({\n label: m.label,\n surfaceText: m.surfaceText,\n normalizedSurface: m.normalizedSurface,\n startOffset: m.sourceRow.start_offset,\n endOffset: m.sourceRow.end_offset,\n confidence: m.confidence,\n detectedLang: m.sourceRow.detected_lang,\n source: m.sourceRow.source,\n }));\n\n const after = rowsSignature(\n nextRows.map((row) => ({\n label: row.label,\n normalizedSurface: row.normalizedSurface,\n surfaceText: row.surfaceText,\n confidence: row.confidence,\n })),\n );\n if (before !== after || rows.length !== nextRows.length) {\n changedFacts++;\n removedRows += Math.max(0, rows.length - nextRows.length);\n if (options.apply) {\n replaceFactEntityMentions(db, fact.fact_id, nextRows, { preserveEnrichmentTimestamp: true });\n if (nextRows.length === 0) {\n db.prepare(\"UPDATE facts SET entity_enrichment_at = NULL WHERE id = ?\").run(fact.fact_id);\n }\n }\n }\n }\n\n return {\n factsScanned: factIds.length,\n rowsScanned,\n accepted,\n rejected,\n duplicates,\n reclassified,\n rejectReasons,\n changedFacts,\n removedRows,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"entity-layer.js","names":[],"sources":["../../../backends/facts-db/entity-layer.ts"],"sourcesContent":["/**\n * Organizations, contacts, and NER mention persistence (#985–#987).\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport type { DatabaseSync } from \"node:sqlite\";\nimport { canonicalizeEntityMention, countReason, makeEntityMentionKey } from \"../../utils/entity-mention-quality.js\";\nimport { isEntityStopWord, normalizeEntityStopWord } from \"../../utils/entity-stopwords.js\";\nimport { createTransaction } from \"../../utils/sqlite-transaction.js\";\nimport { readSchemaVersion, runVersionedSchemaMigration } from \"../sqlite-schema-meta.js\";\n\nexport type EntityMentionLabel =\n | \"PERSON\"\n | \"ORG\"\n | \"SERVICE\"\n | \"TOOL\"\n | \"MODEL\"\n | \"PROJECT\"\n | \"AGENT\"\n | \"ROLE\"\n | \"PRODUCT\"\n | \"LOCATION\";\n\ntype _FactEntityMentionRow = {\n id: string;\n factId: string;\n label: EntityMentionLabel;\n surfaceText: string;\n normalizedSurface: string;\n startOffset: number;\n endOffset: number;\n confidence: number;\n detectedLang: string | null;\n source: string;\n contactId: string | null;\n organizationId: string | null;\n};\n\nexport type OrganizationRow = {\n id: string;\n canonicalKey: string;\n displayName: string;\n aliasesJson: string | null;\n};\n\nexport type ContactRow = {\n id: string;\n normalizedKey: string;\n displayName: string;\n email: string | null;\n notes: string | null;\n aliasesJson: string | null;\n primaryOrgId: string | null;\n};\n\nexport type EntityEnrichmentBacklogByTier = {\n hot: number;\n warm: number;\n structural: number;\n cold: number;\n unknown: number;\n};\n\nexport type EntityEnrichmentBacklogSummary = {\n total: number;\n byTier: EntityEnrichmentBacklogByTier;\n};\n\nexport type ListFactsNeedingEnrichmentOptions = {\n /** Process full backlog (no LIMIT) for manual one-shot catch-up mode. */\n all?: boolean;\n};\n\nexport type EntityMentionsAuditSummary = {\n factsScanned: number;\n rowsScanned: number;\n accepted: number;\n rejected: number;\n duplicates: number;\n reclassified: number;\n rejectReasons: Record<string, number>;\n};\n\nexport type EntityMentionsCleanupSummary = EntityMentionsAuditSummary & {\n changedFacts: number;\n removedRows: number;\n};\n\nexport function normalizeEntityKey(name: string): string {\n return name.normalize(\"NFKD\").replace(/\\p{M}/gu, \"\").toLowerCase().replace(/\\s+/g, \" \").trim();\n}\n\nconst MIN_ENTITY_MENTION_LENGTH = 3;\nconst NON_ORG_SERVICE_TERMS = new Set([\"signal\", \"telegram\", \"whatsapp\"]);\nconst NON_ENTITY_ORG_TERMS = new Set([\"hybrid memory plugin\", \"the agent\", \"the system\"]);\nconst PERSON_ROLE_TITLES = new Set([\"architect\", \"developer\", \"director\", \"engineer\", \"manager\", \"surgeon\"]);\nconst KNOWN_TWO_CHAR_MENTIONS = new Set([\"gh\", \"jq\", \"ha\"]);\n\nfunction normalizeMentionSurfaceText(surfaceText: string): string {\n return surfaceText.trim();\n}\n\nfunction isValidTwoCharacterMention(mention: { label: EntityMentionLabel; surfaceText: string }): boolean {\n const surface = mention.surfaceText.trim();\n if (surface.length !== 2) {\n return false;\n }\n\n if (KNOWN_TWO_CHAR_MENTIONS.has(surface.toLowerCase())) {\n return true;\n }\n\n if (mention.label === \"PERSON\") {\n return /^\\p{L}{2}$/u.test(surface);\n }\n\n if (!/^[\\p{L}\\p{N}]{2}$/u.test(surface)) {\n return false;\n }\n\n if (/\\p{N}/u.test(surface) || /\\p{Lu}/u.test(surface)) {\n return true;\n }\n\n // Allow two-character org mentions in scripts without uppercase variants (e.g. Han).\n return /\\P{Script=Latin}/u.test(surface);\n}\n\nfunction shouldFilterEntityMention(mention: {\n label: EntityMentionLabel;\n surfaceText: string;\n normalizedSurface: string;\n}): boolean {\n if (\n mention.surfaceText.length < MIN_ENTITY_MENTION_LENGTH &&\n !isValidTwoCharacterMention({ label: mention.label, surfaceText: mention.surfaceText })\n ) {\n return true;\n }\n\n if (isEntityStopWord(mention.surfaceText)) {\n return true;\n }\n\n const stopWordKey = normalizeEntityStopWord(mention.surfaceText);\n if (mention.label === \"ORG\") {\n return NON_ORG_SERVICE_TERMS.has(stopWordKey) || NON_ENTITY_ORG_TERMS.has(stopWordKey);\n }\n\n if (!mention.normalizedSurface.includes(\" \") && PERSON_ROLE_TITLES.has(stopWordKey)) {\n return true;\n }\n\n return false;\n}\n\nfunction isContainedByLongerMention(\n mention: { label: EntityMentionLabel; normalizedSurface: string; startOffset: number; endOffset: number },\n other: { label: EntityMentionLabel; normalizedSurface: string; startOffset: number; endOffset: number },\n): boolean {\n if (mention.label !== other.label || mention.normalizedSurface === other.normalizedSurface) {\n return false;\n }\n\n if (other.normalizedSurface.length <= mention.normalizedSurface.length) {\n return false;\n }\n\n // Check if spans overlap: they must overlap for containment to apply\n const spansOverlap = mention.startOffset < other.endOffset && other.startOffset < mention.endOffset;\n if (!spansOverlap) {\n return false;\n }\n\n const isWordBoundary = (char: string): boolean => {\n return char === \" \" || /[,\\-.\\/()\\[\\]{}:;!?'\"&]/.test(char);\n };\n\n let searchStart = 0;\n while (searchStart <= other.normalizedSurface.length - mention.normalizedSurface.length) {\n const index = other.normalizedSurface.indexOf(mention.normalizedSurface, searchStart);\n if (index === -1) {\n return false;\n }\n\n const endIndex = index + mention.normalizedSurface.length;\n const beforeChar = index > 0 ? other.normalizedSurface[index - 1] : \" \";\n const afterChar = endIndex < other.normalizedSurface.length ? other.normalizedSurface[endIndex] : \" \";\n\n if (isWordBoundary(beforeChar) && (isWordBoundary(afterChar) || endIndex === other.normalizedSurface.length)) {\n return true;\n }\n\n searchStart = index + 1;\n }\n\n return false;\n}\n\nfunction preferEntityMention<\n T extends {\n confidence: number;\n surfaceText: string;\n startOffset: number;\n },\n>(left: T, right: T): T {\n if (right.confidence !== left.confidence) {\n return right.confidence > left.confidence ? right : left;\n }\n if (right.surfaceText.length !== left.surfaceText.length) {\n return right.surfaceText.length > left.surfaceText.length ? right : left;\n }\n return right.startOffset < left.startOffset ? right : left;\n}\n\nexport function normalizeFactEntityMentionsForPersistence<\n T extends {\n label: EntityMentionLabel;\n surfaceText: string;\n normalizedSurface: string;\n startOffset: number;\n endOffset: number;\n confidence: number;\n },\n>(mentions: readonly T[]): T[] {\n const prepared = mentions\n .map((mention) => {\n const surfaceText = normalizeMentionSurfaceText(mention.surfaceText);\n const normalizedSurface = normalizeEntityKey(mention.normalizedSurface) || normalizeEntityKey(surfaceText);\n // Adjust offsets for trimmed whitespace so stored offsets match the trimmed surfaceText in the original fact text\n const leadingTrim = mention.surfaceText.length - mention.surfaceText.trimStart().length;\n const trailingTrim = mention.surfaceText.length - mention.surfaceText.trimEnd().length;\n return {\n ...mention,\n surfaceText,\n normalizedSurface,\n startOffset: mention.startOffset + leadingTrim,\n endOffset: mention.endOffset - trailingTrim,\n };\n })\n .filter((mention) => mention.surfaceText.length > 0 && mention.normalizedSurface.length > 0)\n .filter((mention) => !shouldFilterEntityMention(mention))\n .filter(\n (mention, index, all) =>\n !all.some((other, otherIndex) => otherIndex !== index && isContainedByLongerMention(mention, other)),\n );\n\n const deduplicated = new Map<string, T>();\n for (const mention of prepared) {\n // Use surfaceText for the dedup key so it matches what upsertOrganization/upsertContact compute for canonical_key\n const key = `${mention.label}\\u0000${normalizeEntityKey(mention.surfaceText)}`;\n const existing = deduplicated.get(key);\n deduplicated.set(key, existing ? preferEntityMention(existing, mention) : mention);\n }\n\n return [...deduplicated.values()].sort((left, right) => left.startOffset - right.startOffset);\n}\n\n/** Escape `%`, `_`, and `\\` for SQLite `LIKE ... ESCAPE '\\'` literal matching. */\nexport function escapeLikeLiteralForBackslashEscape(s: string): string {\n return s.replace(/\\\\/g, \"\\\\\\\\\").replace(/%/g, \"\\\\%\").replace(/_/g, \"\\\\_\");\n}\n\nexport function migrateEntityLayerTables(db: DatabaseSync): void {\n db.exec(`\n CREATE TABLE IF NOT EXISTS organizations (\n id TEXT PRIMARY KEY,\n canonical_key TEXT NOT NULL UNIQUE,\n display_name TEXT NOT NULL,\n aliases_json TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n );\n CREATE INDEX IF NOT EXISTS idx_org_canonical ON organizations(canonical_key);\n `);\n\n db.exec(`\n CREATE TABLE IF NOT EXISTS contacts (\n id TEXT PRIMARY KEY,\n normalized_key TEXT NOT NULL,\n display_name TEXT NOT NULL,\n email TEXT,\n notes TEXT,\n aliases_json TEXT,\n primary_org_id TEXT REFERENCES organizations(id) ON DELETE SET NULL,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n );\n CREATE INDEX IF NOT EXISTS idx_contacts_org ON contacts(primary_org_id);\n `);\n // One row per normalized display key (upsertContact assumes uniqueness).\n db.exec(\"CREATE UNIQUE INDEX IF NOT EXISTS idx_contacts_normalized_key_unique ON contacts(normalized_key)\");\n\n db.exec(`\n CREATE TABLE IF NOT EXISTS fact_entity_mentions (\n id TEXT PRIMARY KEY,\n fact_id TEXT NOT NULL REFERENCES facts(id) ON DELETE CASCADE,\n label TEXT NOT NULL,\n surface_text TEXT NOT NULL,\n normalized_surface TEXT NOT NULL,\n start_offset INTEGER NOT NULL,\n end_offset INTEGER NOT NULL,\n confidence REAL NOT NULL DEFAULT 0.8,\n detected_lang TEXT,\n source TEXT NOT NULL DEFAULT 'llm',\n contact_id TEXT REFERENCES contacts(id) ON DELETE SET NULL,\n organization_id TEXT REFERENCES organizations(id) ON DELETE SET NULL,\n created_at INTEGER NOT NULL\n );\n CREATE INDEX IF NOT EXISTS idx_fem_fact ON fact_entity_mentions(fact_id);\n CREATE INDEX IF NOT EXISTS idx_fem_org ON fact_entity_mentions(organization_id);\n CREATE INDEX IF NOT EXISTS idx_fem_contact ON fact_entity_mentions(contact_id);\n CREATE INDEX IF NOT EXISTS idx_fem_label ON fact_entity_mentions(label);\n `);\n\n const version = readSchemaVersion(db, \"entity_layer\");\n if (version < 1) {\n runVersionedSchemaMigration(db, \"entity_layer\", 1, () => {\n const hasDuplicates = db\n .prepare(\n `SELECT 1\n FROM fact_entity_mentions\n GROUP BY fact_id, label, normalized_surface\n HAVING COUNT(*) > 1\n LIMIT 1`,\n )\n .get() as { 1: number } | undefined;\n if (hasDuplicates) {\n db.exec(`\n DELETE FROM fact_entity_mentions\n WHERE id IN (\n SELECT m1.id\n FROM fact_entity_mentions m1\n JOIN fact_entity_mentions m2\n ON m1.fact_id = m2.fact_id\n AND m1.label = m2.label\n AND m1.normalized_surface = m2.normalized_surface\n AND (\n m2.confidence > m1.confidence\n OR (m2.confidence = m1.confidence AND m2.created_at > m1.created_at)\n OR (m2.confidence = m1.confidence AND m2.created_at = m1.created_at AND m2.id > m1.id)\n )\n );\n `);\n }\n db.exec(`\n CREATE UNIQUE INDEX IF NOT EXISTS idx_fem_fact_label_norm\n ON fact_entity_mentions(fact_id, label, normalized_surface);\n `);\n });\n }\n\n db.exec(`\n CREATE TABLE IF NOT EXISTS org_fact_links (\n org_id TEXT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,\n fact_id TEXT NOT NULL REFERENCES facts(id) ON DELETE CASCADE,\n reason TEXT NOT NULL DEFAULT 'ner_mention',\n created_at INTEGER NOT NULL,\n PRIMARY KEY (org_id, fact_id, reason)\n );\n CREATE INDEX IF NOT EXISTS idx_org_fact_org ON org_fact_links(org_id);\n CREATE INDEX IF NOT EXISTS idx_org_fact_fact ON org_fact_links(fact_id);\n `);\n}\n\nfunction upsertOrganization(db: DatabaseSync, displayName: string): { id: string; created: boolean } | null {\n const canonicalKey = normalizeEntityKey(displayName);\n if (!canonicalKey) {\n return null;\n }\n const existing = db.prepare(\"SELECT id FROM organizations WHERE canonical_key = ?\").get(canonicalKey) as\n | { id: string }\n | undefined;\n if (existing) {\n const now = Math.floor(Date.now() / 1000);\n db.prepare(\"UPDATE organizations SET display_name = ?, updated_at = ? WHERE id = ?\").run(\n displayName.trim(),\n now,\n existing.id,\n );\n return { id: existing.id, created: false };\n }\n const id = randomUUID();\n const now = Math.floor(Date.now() / 1000);\n db.prepare(\n `INSERT INTO organizations (id, canonical_key, display_name, aliases_json, created_at, updated_at)\n VALUES (?, ?, ?, NULL, ?, ?)`,\n ).run(id, canonicalKey, displayName.trim(), now, now);\n return { id, created: true };\n}\n\nfunction upsertContact(\n db: DatabaseSync,\n displayName: string,\n primaryOrgId: string | null,\n): { id: string; created: boolean } | null {\n const nk = normalizeEntityKey(displayName);\n if (!nk) {\n return null;\n }\n const existing = db.prepare(\"SELECT id, primary_org_id FROM contacts WHERE normalized_key = ? LIMIT 1\").get(nk) as\n | { id: string; primary_org_id: string | null }\n | undefined;\n const now = Math.floor(Date.now() / 1000);\n if (existing) {\n if (primaryOrgId && !existing.primary_org_id) {\n db.prepare(\"UPDATE contacts SET primary_org_id = ?, updated_at = ?, display_name = ? WHERE id = ?\").run(\n primaryOrgId,\n now,\n displayName.trim(),\n existing.id,\n );\n } else {\n db.prepare(\"UPDATE contacts SET updated_at = ?, display_name = ? WHERE id = ?\").run(\n now,\n displayName.trim(),\n existing.id,\n );\n }\n return { id: existing.id, created: false };\n }\n const id = randomUUID();\n db.prepare(\n `INSERT INTO contacts (id, normalized_key, display_name, email, notes, aliases_json, primary_org_id, created_at, updated_at)\n VALUES (?, ?, ?, NULL, NULL, NULL, ?, ?, ?)`,\n ).run(id, nk, displayName.trim(), primaryOrgId, now, now);\n return { id, created: true };\n}\n\nexport function replaceFactEntityMentions(\n db: DatabaseSync,\n factId: string,\n mentions: Array<{\n label: EntityMentionLabel;\n surfaceText: string;\n normalizedSurface: string;\n startOffset: number;\n endOffset: number;\n confidence: number;\n detectedLang: string | null;\n source: string;\n }>,\n options: { preserveEnrichmentTimestamp?: boolean } = {},\n): void {\n const tx = createTransaction(db, () => {\n db.prepare(\"DELETE FROM fact_entity_mentions WHERE fact_id = ?\").run(factId);\n db.prepare(\"DELETE FROM org_fact_links WHERE fact_id = ? AND reason = 'ner_mention'\").run(factId);\n const normalizedMentions = normalizeFactEntityMentionsForPersistence(mentions);\n\n const now = Math.floor(Date.now() / 1000);\n // Intentional OR IGNORE: idx_fem_fact_label_norm enforces idempotent logical mention writes.\n const ins = db.prepare(\n `INSERT OR IGNORE INTO fact_entity_mentions (\n id, fact_id, label, surface_text, normalized_surface, start_offset, end_offset,\n confidence, detected_lang, source, contact_id, organization_id, created_at\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n );\n const insOrgLink = db.prepare(\n `INSERT OR IGNORE INTO org_fact_links (org_id, fact_id, reason, created_at) VALUES (?, ?, 'ner_mention', ?)`,\n );\n\n const orgIds: string[] = [];\n const personRows: Array<{ surface: string; contactId: string }> = [];\n\n for (const m of normalizedMentions) {\n let contactId: string | null = null;\n let organizationId: string | null = null;\n\n if (m.label === \"ORG\") {\n const org = upsertOrganization(db, m.surfaceText);\n if (org) {\n organizationId = org.id;\n orgIds.push(org.id);\n insOrgLink.run(org.id, factId, now);\n }\n } else if (m.label === \"PERSON\") {\n const con = upsertContact(db, m.surfaceText, null);\n if (con) {\n contactId = con.id;\n personRows.push({ surface: m.surfaceText, contactId: con.id });\n }\n }\n\n ins.run(\n randomUUID(),\n factId,\n m.label,\n m.surfaceText,\n m.normalizedSurface,\n m.startOffset,\n m.endOffset,\n m.confidence,\n m.detectedLang,\n m.source,\n contactId,\n organizationId,\n now,\n );\n }\n\n // If same fact mentions both a person and an org, set primary_org on contacts (weak v1 heuristic).\n if (orgIds.length > 0 && personRows.length > 0) {\n const primaryOrg = orgIds[0];\n for (const p of personRows) {\n db.prepare(\"UPDATE contacts SET primary_org_id = COALESCE(primary_org_id, ?), updated_at = ? WHERE id = ?\").run(\n primaryOrg,\n now,\n p.contactId,\n );\n }\n }\n\n if (!options.preserveEnrichmentTimestamp) {\n db.prepare(\"UPDATE facts SET entity_enrichment_at = ? WHERE id = ?\").run(now, factId);\n }\n });\n tx();\n}\n\nexport function getOrganizationByKeyOrName(db: DatabaseSync, query: string): OrganizationRow | null {\n const nk = normalizeEntityKey(query);\n if (!nk) return null;\n const byKey = db.prepare(\"SELECT * FROM organizations WHERE canonical_key = ?\").get(nk) as\n | Record<string, unknown>\n | undefined;\n if (byKey) return rowToOrg(byKey);\n const like = `%${escapeLikeLiteralForBackslashEscape(nk)}%`;\n const byName = db\n .prepare(\n \"SELECT * FROM organizations WHERE canonical_key LIKE ? ESCAPE '\\\\' ORDER BY length(display_name) ASC LIMIT 1\",\n )\n .get(like) as Record<string, unknown> | undefined;\n return byName ? rowToOrg(byName) : null;\n}\n\nfunction rowToOrg(row: Record<string, unknown>): OrganizationRow {\n return {\n id: row.id as string,\n canonicalKey: row.canonical_key as string,\n displayName: row.display_name as string,\n aliasesJson: (row.aliases_json as string | null) ?? null,\n };\n}\n\nexport function listContactsForOrg(db: DatabaseSync, orgId: string, limit: number): ContactRow[] {\n const rows = db\n .prepare(\n `SELECT * FROM contacts\n WHERE primary_org_id = ?\n ORDER BY display_name COLLATE NOCASE\n LIMIT ?`,\n )\n .all(orgId, limit) as Array<Record<string, unknown>>;\n return rows.map(rowToContact);\n}\n\nexport function listContactsByNamePrefix(db: DatabaseSync, prefix: string, limit: number): ContactRow[] {\n const p = prefix.trim().toLowerCase();\n if (!p) {\n const rows = db.prepare(\"SELECT * FROM contacts ORDER BY display_name COLLATE NOCASE LIMIT ?\").all(limit) as Array<\n Record<string, unknown>\n >;\n return rows.map(rowToContact);\n }\n const esc = escapeLikeLiteralForBackslashEscape(p);\n const pat = `${esc}%`;\n const normalizedPrefix = normalizeEntityKey(prefix);\n const escNormalized = escapeLikeLiteralForBackslashEscape(normalizedPrefix);\n const patNormalized = `${escNormalized}%`;\n const rows = db\n .prepare(\n `SELECT * FROM contacts\n WHERE lower(display_name) LIKE ? ESCAPE '\\\\' OR normalized_key LIKE ? ESCAPE '\\\\'\n ORDER BY display_name COLLATE NOCASE\n LIMIT ?`,\n )\n .all(pat, patNormalized, limit) as Array<Record<string, unknown>>;\n return rows.map(rowToContact);\n}\n\nfunction rowToContact(row: Record<string, unknown>): ContactRow {\n return {\n id: row.id as string,\n normalizedKey: row.normalized_key as string,\n displayName: row.display_name as string,\n email: (row.email as string | null) ?? null,\n notes: (row.notes as string | null) ?? null,\n aliasesJson: (row.aliases_json as string | null) ?? null,\n primaryOrgId: (row.primary_org_id as string | null) ?? null,\n };\n}\n\nexport function listFactIdsForOrg(db: DatabaseSync, orgId: string, limit: number): string[] {\n const rows = db\n .prepare(\"SELECT DISTINCT fact_id FROM org_fact_links WHERE org_id = ? ORDER BY created_at DESC LIMIT ?\")\n .all(orgId, limit) as Array<{ fact_id: string }>;\n return rows.map((r) => r.fact_id);\n}\n\nfunction buildEntityEnrichmentPendingBaseSql(): string {\n return `SELECT f.id FROM facts f\n WHERE f.superseded_at IS NULL\n AND (f.expires_at IS NULL OR f.expires_at > ?)\n AND length(f.text) >= ?\n AND f.entity_enrichment_at IS NULL\n ORDER BY\n CASE COALESCE(f.tier, 'warm')\n WHEN 'hot' THEN 0\n WHEN 'warm' THEN 1\n WHEN 'structural' THEN 2\n WHEN 'cold' THEN 3\n ELSE 4\n END ASC,\n COALESCE(f.last_accessed, f.last_confirmed_at, f.created_at) DESC,\n MAX(COALESCE(f.recall_count, 0), COALESCE(f.access_count, 0)) DESC,\n COALESCE(f.importance, 0) DESC,\n f.created_at DESC,\n f.id ASC`;\n}\n\nexport function listFactsNeedingEnrichment(\n db: DatabaseSync,\n limit: number,\n minTextLen: number,\n options?: ListFactsNeedingEnrichmentOptions,\n): string[] {\n const nowSec = Math.floor(Date.now() / 1000);\n const sql = options?.all\n ? buildEntityEnrichmentPendingBaseSql()\n : `${buildEntityEnrichmentPendingBaseSql()}\\n LIMIT ?`;\n const rows = (\n options?.all ? db.prepare(sql).all(nowSec, minTextLen) : db.prepare(sql).all(nowSec, minTextLen, limit)\n ) as Array<{ id: string }>;\n return rows.map((r) => r.id);\n}\n\nexport function getEntityEnrichmentBacklogSummary(\n db: DatabaseSync,\n minTextLen: number,\n): EntityEnrichmentBacklogSummary {\n const nowSec = Math.floor(Date.now() / 1000);\n const row = db\n .prepare(\n `SELECT\n COUNT(*) AS total,\n SUM(CASE WHEN COALESCE(f.tier, 'warm') = 'hot' THEN 1 ELSE 0 END) AS hot,\n SUM(CASE WHEN COALESCE(f.tier, 'warm') = 'warm' THEN 1 ELSE 0 END) AS warm,\n SUM(CASE WHEN COALESCE(f.tier, 'warm') = 'structural' THEN 1 ELSE 0 END) AS structural,\n SUM(CASE WHEN COALESCE(f.tier, 'warm') = 'cold' THEN 1 ELSE 0 END) AS cold,\n SUM(CASE WHEN COALESCE(f.tier, 'warm') NOT IN ('hot', 'warm', 'structural', 'cold') THEN 1 ELSE 0 END) AS unknown\n FROM facts f\n WHERE f.superseded_at IS NULL\n AND (f.expires_at IS NULL OR f.expires_at > ?)\n AND length(f.text) >= ?\n AND f.entity_enrichment_at IS NULL`,\n )\n .get(nowSec, minTextLen) as\n | {\n total: number | null;\n hot: number | null;\n warm: number | null;\n structural: number | null;\n cold: number | null;\n unknown: number | null;\n }\n | undefined;\n return {\n total: Number(row?.total ?? 0),\n byTier: {\n hot: Number(row?.hot ?? 0),\n warm: Number(row?.warm ?? 0),\n structural: Number(row?.structural ?? 0),\n cold: Number(row?.cold ?? 0),\n unknown: Number(row?.unknown ?? 0),\n },\n };\n}\n\ntype ProcessedEntityMention<T = void> = {\n label: EntityMentionLabel;\n surfaceText: string;\n normalizedSurface: string;\n confidence: number;\n sourceRow: T;\n};\n\ntype EntityMentionProcessingResult<T = void> = {\n accepted: Array<ProcessedEntityMention<T>>;\n counters: {\n rowsScanned: number;\n accepted: number;\n rejected: number;\n duplicates: number;\n reclassified: number;\n rejectReasons: Record<string, number>;\n };\n};\n\nfunction processEntityMentionsForFact<\n T extends {\n label: string;\n surface_text: string;\n normalized_surface: string;\n confidence: number;\n start_offset: number;\n end_offset: number;\n },\n>(rows: T[]): EntityMentionProcessingResult<T> {\n const acceptedByKey = new Map<string, ProcessedEntityMention<T>>();\n let rowsScanned = 0;\n let accepted = 0;\n let rejected = 0;\n let duplicates = 0;\n const rejectReasons: Record<string, number> = {};\n\n for (const row of rows) {\n rowsScanned++;\n const canonical = canonicalizeEntityMention({\n label: row.label,\n surfaceText: row.surface_text,\n normalizedSurface: row.normalized_surface,\n confidence: row.confidence,\n });\n if (!canonical.accepted) {\n rejected++;\n countReason(rejectReasons, canonical.reason);\n continue;\n }\n const key = makeEntityMentionKey(canonical.label, canonical.normalizedSurface);\n const existing = acceptedByKey.get(key);\n if (existing) {\n duplicates++;\n if (canonical.confidence > existing.confidence) {\n acceptedByKey.set(key, {\n label: canonical.label,\n surfaceText: canonical.surfaceText,\n normalizedSurface: canonical.normalizedSurface,\n confidence: canonical.confidence,\n sourceRow: row,\n });\n }\n } else {\n acceptedByKey.set(key, {\n label: canonical.label,\n surfaceText: canonical.surfaceText,\n normalizedSurface: canonical.normalizedSurface,\n confidence: canonical.confidence,\n sourceRow: row,\n });\n accepted++;\n }\n }\n\n const allAccepted = [...acceptedByKey.values()];\n let substringFilteredCount = 0;\n const filteredAccepted = allAccepted.filter((m) => {\n const isSubstring = allAccepted.some(\n (other) =>\n other !== m &&\n other.label === m.label &&\n other.normalizedSurface.length > m.normalizedSurface.length &&\n other.normalizedSurface.includes(m.normalizedSurface) &&\n other.sourceRow.start_offset <= m.sourceRow.start_offset &&\n other.sourceRow.end_offset >= m.sourceRow.end_offset,\n );\n if (isSubstring) {\n substringFilteredCount++;\n }\n return !isSubstring;\n });\n if (substringFilteredCount > 0) {\n rejected += substringFilteredCount;\n rejectReasons.substring = (rejectReasons.substring ?? 0) + substringFilteredCount;\n }\n accepted = filteredAccepted.length;\n const reclassified = filteredAccepted.reduce((count, mention) => {\n const sourceRow = mention.sourceRow;\n if (mention.label !== sourceRow.label || mention.normalizedSurface !== sourceRow.normalized_surface) {\n return count + 1;\n }\n return count;\n }, 0);\n\n return {\n accepted: filteredAccepted,\n counters: {\n rowsScanned,\n accepted,\n rejected,\n duplicates,\n reclassified,\n rejectReasons,\n },\n };\n}\n\nexport function auditEntityMentions(db: DatabaseSync, limit: number): EntityMentionsAuditSummary {\n const factIds = db\n .prepare(\n `SELECT fem.fact_id\n FROM fact_entity_mentions fem\n JOIN facts f ON fem.fact_id = f.id\n GROUP BY fem.fact_id\n ORDER BY f.created_at DESC\n LIMIT ?`,\n )\n .all(limit) as Array<{ fact_id: string }>;\n let rowsScanned = 0;\n let accepted = 0;\n let rejected = 0;\n let duplicates = 0;\n let reclassified = 0;\n const rejectReasons: Record<string, number> = {};\n for (const fact of factIds) {\n const rows = db\n .prepare(\n `SELECT id, label, surface_text, normalized_surface, start_offset, end_offset, confidence\n FROM fact_entity_mentions\n WHERE fact_id = ?`,\n )\n .all(fact.fact_id) as Array<{\n id: string;\n label: string;\n surface_text: string;\n normalized_surface: string;\n start_offset: number;\n end_offset: number;\n confidence: number;\n }>;\n const result = processEntityMentionsForFact(rows);\n rowsScanned += result.counters.rowsScanned;\n accepted += result.counters.accepted;\n rejected += result.counters.rejected;\n duplicates += result.counters.duplicates;\n reclassified += result.counters.reclassified;\n for (const [reason, count] of Object.entries(result.counters.rejectReasons)) {\n rejectReasons[reason] = (rejectReasons[reason] ?? 0) + count;\n }\n }\n return {\n factsScanned: factIds.length,\n rowsScanned,\n accepted,\n rejected,\n duplicates,\n reclassified,\n rejectReasons,\n };\n}\n\nfunction rowsSignature(\n rows: Array<{ label: string; normalizedSurface: string; surfaceText: string; confidence: number }>,\n): string {\n return JSON.stringify(\n rows\n .map((r) => ({\n label: r.label,\n normalizedSurface: r.normalizedSurface,\n surfaceText: r.surfaceText,\n confidence: Number(r.confidence.toFixed(4)),\n }))\n .sort((a, b) =>\n `${a.label}|${a.normalizedSurface}|${a.surfaceText}|${a.confidence}`.localeCompare(\n `${b.label}|${b.normalizedSurface}|${b.surfaceText}|${b.confidence}`,\n ),\n ),\n );\n}\n\nexport function cleanupEntityMentions(\n db: DatabaseSync,\n options: { limit: number; apply: boolean },\n): EntityMentionsCleanupSummary {\n const limit = Math.max(1, Math.floor(options.limit));\n const factIds = db\n .prepare(\n `SELECT fem.fact_id\n FROM fact_entity_mentions fem\n JOIN facts f ON fem.fact_id = f.id\n GROUP BY fem.fact_id\n ORDER BY f.created_at DESC\n LIMIT ?`,\n )\n .all(limit) as Array<{ fact_id: string }>;\n\n let rowsScanned = 0;\n let accepted = 0;\n let rejected = 0;\n let duplicates = 0;\n let reclassified = 0;\n let changedFacts = 0;\n let removedRows = 0;\n const rejectReasons: Record<string, number> = {};\n\n for (const fact of factIds) {\n const rows = db\n .prepare(\n `SELECT label, surface_text, normalized_surface, start_offset, end_offset, confidence, detected_lang, source\n FROM fact_entity_mentions\n WHERE fact_id = ?`,\n )\n .all(fact.fact_id) as Array<{\n label: string;\n surface_text: string;\n normalized_surface: string;\n start_offset: number;\n end_offset: number;\n confidence: number;\n detected_lang: string | null;\n source: string;\n }>;\n const before = rowsSignature(\n rows.map((row) => ({\n label: row.label,\n normalizedSurface: row.normalized_surface,\n surfaceText: row.surface_text,\n confidence: row.confidence,\n })),\n );\n\n const result = processEntityMentionsForFact(rows);\n rowsScanned += result.counters.rowsScanned;\n accepted += result.counters.accepted;\n rejected += result.counters.rejected;\n duplicates += result.counters.duplicates;\n reclassified += result.counters.reclassified;\n for (const [reason, count] of Object.entries(result.counters.rejectReasons)) {\n rejectReasons[reason] = (rejectReasons[reason] ?? 0) + count;\n }\n\n const nextRows = result.accepted.map((m) => ({\n label: m.label,\n surfaceText: m.surfaceText,\n normalizedSurface: m.normalizedSurface,\n startOffset: m.sourceRow.start_offset,\n endOffset: m.sourceRow.end_offset,\n confidence: m.confidence,\n detectedLang: m.sourceRow.detected_lang,\n source: m.sourceRow.source,\n }));\n\n const after = rowsSignature(\n nextRows.map((row) => ({\n label: row.label,\n normalizedSurface: row.normalizedSurface,\n surfaceText: row.surfaceText,\n confidence: row.confidence,\n })),\n );\n if (before !== after || rows.length !== nextRows.length) {\n changedFacts++;\n removedRows += Math.max(0, rows.length - nextRows.length);\n if (options.apply) {\n replaceFactEntityMentions(db, fact.fact_id, nextRows, { preserveEnrichmentTimestamp: true });\n if (nextRows.length === 0) {\n db.prepare(\"UPDATE facts SET entity_enrichment_at = NULL WHERE id = ?\").run(fact.fact_id);\n }\n }\n }\n }\n\n return {\n factsScanned: factIds.length,\n rowsScanned,\n accepted,\n rejected,\n duplicates,\n reclassified,\n rejectReasons,\n changedFacts,\n removedRows,\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"episodes.js","names":[],"sources":["../../../backends/facts-db/episodes.ts"],"sourcesContent":["/**\n * Episodic memory: episodes table + FTS (#781) (Issue #954 split).\n */\nimport { randomUUID } from \"node:crypto\";\nimport type { SQLInputValue } from \"node:sqlite\";\nimport type { DatabaseSync } from \"node:sqlite\";\n\nimport type { DecayClass } from \"../../config.js\";\nimport type { Episode, EpisodeOutcome, ScopeFilter } from \"../../types/memory.js\";\nimport {\n episodeOutcomeForSqliteInsert,\n episodesTableIsLegacyFailedOutcomeCheck,\n storedEpisodeOutcomeToPublic,\n} from \"../../utils/sqlite-outcome-compat.js\";\nimport { createTransaction } from \"../../utils/sqlite-transaction.js\";\nimport { parseTags, serializeTags } from \"../../utils/tags.js\";\nimport { sanitizeFts5QueryForFacts } from \"./fts-text.js\";\nimport { scopeFilterClausePositional } from \"./scope-sql.js\";\n\nexport function rowToEpisode(row: Record<string, unknown>): Episode {\n const relatedFactIdsRaw = row.related_fact_ids as string | null;\n return {\n id: row.id as string,\n category: \"episode\",\n event: row.event as string,\n outcome: storedEpisodeOutcomeToPublic(String(row.outcome ?? \"\")),\n timestamp: row.timestamp as number,\n duration: (row.duration as number) ?? undefined,\n context: (row.context as string) ?? undefined,\n relatedFactIds: relatedFactIdsRaw ? (JSON.parse(relatedFactIdsRaw) as string[]) : undefined,\n procedureId: (row.procedure_id as string) ?? undefined,\n scope: (row.scope as \"global\" | \"user\" | \"agent\" | \"session\") ?? \"global\",\n scopeTarget: (row.scope_target as string) ?? undefined,\n agentId: (row.agent_id as string) ?? undefined,\n userId: (row.user_id as string) ?? undefined,\n sessionId: (row.session_id as string) ?? undefined,\n importance: row.importance as number,\n tags: parseTags(row.tags as string | null),\n decayClass: (row.decay_class as DecayClass) ?? \"normal\",\n createdAt: row.created_at as number,\n verifiedAt: (row.verified_at as number) ?? undefined,\n };\n}\n\nexport function recordEpisode(\n db: DatabaseSync,\n input: {\n event: string;\n outcome: EpisodeOutcome;\n timestamp?: number;\n duration?: number;\n context?: string;\n relatedFactIds?: string[];\n procedureId?: string;\n importance?: number;\n tags?: string[];\n decayClass?: DecayClass;\n scope?: \"global\" | \"user\" | \"agent\" | \"session\";\n scopeTarget?: string | null;\n agentId?: string;\n userId?: string;\n sessionId?: string;\n },\n): Episode {\n const id = randomUUID();\n const nowSec = Math.floor(Date.now() / 1000);\n const timestamp = input.timestamp ?? nowSec;\n\n const decayClass = input.decayClass ?? \"normal\";\n const scope = input.scope ?? \"global\";\n const scopeTarget = scope === \"global\" ? null : (input.scopeTarget ?? null);\n const tags = input.tags ?? [];\n const relatedFactIds = input.relatedFactIds ?? [];\n const legacyFailedOnly = episodesTableIsLegacyFailedOutcomeCheck(db);\n const outcomeForInsert = episodeOutcomeForSqliteInsert(db, input.outcome);\n const publicFromStored = storedEpisodeOutcomeToPublic(outcomeForInsert);\n // Legacy CHECK stores both `failure` and `unknown` as `failed`. Re-read maps\n // `failed` → `failure`, so callers cannot distinguish unknown after reload; keep\n // the immediate return aligned with the caller's intent for `unknown`.\n const returnedOutcome: EpisodeOutcome =\n legacyFailedOnly && input.outcome === \"unknown\" ? \"unknown\" : publicFromStored;\n\n let importance = input.importance ?? 0.5;\n if (input.outcome === \"failure\" && importance < 0.8) {\n importance = 0.8;\n }\n\n const tx = createTransaction(db, () => {\n db.prepare(\n `INSERT INTO episodes (id, event, outcome, timestamp, duration, context, related_fact_ids, procedure_id, scope, scope_target, agent_id, user_id, session_id, importance, tags, decay_class, created_at, verified_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n ).run(\n id,\n input.event,\n outcomeForInsert,\n timestamp,\n input.duration ?? null,\n input.context ?? null,\n relatedFactIds.length > 0 ? JSON.stringify(relatedFactIds) : null,\n input.procedureId ?? null,\n scope,\n scopeTarget,\n input.agentId ?? null,\n input.userId ?? null,\n input.sessionId ?? null,\n importance,\n serializeTags(tags),\n decayClass,\n nowSec,\n null,\n );\n\n for (const factId of relatedFactIds) {\n db.prepare(\n \"INSERT INTO episode_relations (id, episode_id, target_id, relation_type, strength, created_at) VALUES (?, ?, ?, ?, ?, ?)\",\n ).run(randomUUID(), id, factId, \"PART_OF\", 0.8, nowSec);\n }\n });\n tx();\n\n return {\n id,\n category: \"episode\",\n event: input.event,\n outcome: returnedOutcome,\n timestamp,\n duration: input.duration,\n context: input.context,\n relatedFactIds,\n procedureId: input.procedureId,\n scope,\n scopeTarget: scopeTarget ?? undefined,\n agentId: input.agentId,\n userId: input.userId,\n sessionId: input.sessionId,\n importance,\n tags,\n decayClass,\n createdAt: nowSec,\n };\n}\n\nexport function searchEpisodes(\n db: DatabaseSync,\n options: {\n query?: string;\n outcome?: EpisodeOutcome[];\n since?: number;\n until?: number;\n procedureId?: string;\n limit?: number;\n scopeFilter?: ScopeFilter | null;\n } = {},\n): Episode[] {\n const { query, outcome, since, until, procedureId, limit = 50, scopeFilter } = options;\n const params: unknown[] = [];\n const conditions: string[] = [];\n\n if (query?.trim()) {\n const sanitized = sanitizeFts5QueryForFacts(query.trim());\n const words = sanitized\n .split(/\\s+/)\n .filter((w) => w.length > 1)\n .slice(0, 8)\n .map((w) => `\"${w}\"`)\n .join(\" OR \");\n if (words) {\n conditions.push(\"e.rowid IN (SELECT rowid FROM episodes_fts WHERE episodes_fts MATCH ?)\");\n params.push(words);\n }\n }\n\n if (outcome && outcome.length > 0) {\n const legacyFailed = episodesTableIsLegacyFailedOutcomeCheck(db);\n const expanded = new Set<string>(outcome);\n // Map `failure` → stored `failed` on legacy CHECK. Do not map `unknown` →\n // `failed`: that would return rows that are semantically `failure` when the\n // caller asked for unknown-only results (both values share `failed` in DB).\n if (legacyFailed && outcome.includes(\"failure\")) {\n expanded.add(\"failed\");\n }\n const list = [...expanded];\n const placeholders = list.map(() => \"?\").join(\",\");\n conditions.push(`e.outcome IN (${placeholders})`);\n params.push(...list);\n }\n\n if (since !== undefined) {\n conditions.push(\"e.timestamp >= ?\");\n params.push(since);\n }\n if (until !== undefined) {\n conditions.push(\"e.timestamp <= ?\");\n params.push(until);\n }\n\n if (procedureId) {\n conditions.push(\"e.procedure_id = ?\");\n params.push(procedureId);\n }\n\n const scopeClause = scopeFilterClausePositional(scopeFilter);\n if (scopeClause.clause) {\n // Positional scope fragment is ` AND ( … )`; strip leading AND (not `^AND` — clause starts with spaces).\n const trimmed = scopeClause.clause.trimStart().replace(/^AND\\s+/i, \"\");\n if (trimmed) {\n conditions.push(trimmed);\n params.push(...scopeClause.params);\n }\n }\n\n const where = conditions.length > 0 ? `WHERE ${conditions.join(\" AND \")}` : \"\";\n const limitClause = \"ORDER BY e.timestamp DESC LIMIT ?\";\n params.push(limit);\n\n const sql = `SELECT e.* FROM episodes e ${where} ${limitClause}`;\n const rows = db.prepare(sql).all(...(params as SQLInputValue[])) as Array<Record<string, unknown>>;\n return rows.map((r) => rowToEpisode(r));\n}\n\nexport function getEpisode(db: DatabaseSync, id: string): Episode | null {\n const row = db.prepare(\"SELECT * FROM episodes WHERE id = ?\").get(id) as Record<string, unknown> | undefined;\n if (!row) return null;\n return rowToEpisode(row);\n}\n\nexport function deleteEpisode(db: DatabaseSync, id: string): boolean {\n const result = db.prepare(\"DELETE FROM episodes WHERE id = ?\").run(id);\n return result.changes > 0;\n}\n\nexport function episodesCount(db: DatabaseSync): number {\n try {\n const row = db.prepare(\"SELECT COUNT(*) as cnt FROM episodes\").get() as { cnt: number };\n return row?.cnt ?? 0;\n } catch {\n return 0;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"episodes.js","names":[],"sources":["../../../backends/facts-db/episodes.ts"],"sourcesContent":["/**\n * Episodic memory: episodes table + FTS (#781) (Issue #954 split).\n */\nimport { randomUUID } from \"node:crypto\";\nimport type { SQLInputValue } from \"node:sqlite\";\nimport type { DatabaseSync } from \"node:sqlite\";\n\nimport type { DecayClass } from \"../../config.js\";\nimport type { Episode, EpisodeOutcome, ScopeFilter } from \"../../types/memory.js\";\nimport {\n episodeOutcomeForSqliteInsert,\n episodesTableIsLegacyFailedOutcomeCheck,\n storedEpisodeOutcomeToPublic,\n} from \"../../utils/sqlite-outcome-compat.js\";\nimport { createTransaction } from \"../../utils/sqlite-transaction.js\";\nimport { parseTags, serializeTags } from \"../../utils/tags.js\";\nimport { sanitizeFts5QueryForFacts } from \"./fts-text.js\";\nimport { scopeFilterClausePositional } from \"./scope-sql.js\";\n\nexport function rowToEpisode(row: Record<string, unknown>): Episode {\n const relatedFactIdsRaw = row.related_fact_ids as string | null;\n return {\n id: row.id as string,\n category: \"episode\",\n event: row.event as string,\n outcome: storedEpisodeOutcomeToPublic(String(row.outcome ?? \"\")),\n timestamp: row.timestamp as number,\n duration: (row.duration as number) ?? undefined,\n context: (row.context as string) ?? undefined,\n relatedFactIds: relatedFactIdsRaw ? (JSON.parse(relatedFactIdsRaw) as string[]) : undefined,\n procedureId: (row.procedure_id as string) ?? undefined,\n scope: (row.scope as \"global\" | \"user\" | \"agent\" | \"session\") ?? \"global\",\n scopeTarget: (row.scope_target as string) ?? undefined,\n agentId: (row.agent_id as string) ?? undefined,\n userId: (row.user_id as string) ?? undefined,\n sessionId: (row.session_id as string) ?? undefined,\n importance: row.importance as number,\n tags: parseTags(row.tags as string | null),\n decayClass: (row.decay_class as DecayClass) ?? \"normal\",\n createdAt: row.created_at as number,\n verifiedAt: (row.verified_at as number) ?? undefined,\n };\n}\n\nexport function recordEpisode(\n db: DatabaseSync,\n input: {\n event: string;\n outcome: EpisodeOutcome;\n timestamp?: number;\n duration?: number;\n context?: string;\n relatedFactIds?: string[];\n procedureId?: string;\n importance?: number;\n tags?: string[];\n decayClass?: DecayClass;\n scope?: \"global\" | \"user\" | \"agent\" | \"session\";\n scopeTarget?: string | null;\n agentId?: string;\n userId?: string;\n sessionId?: string;\n },\n): Episode {\n const id = randomUUID();\n const nowSec = Math.floor(Date.now() / 1000);\n const timestamp = input.timestamp ?? nowSec;\n\n const decayClass = input.decayClass ?? \"normal\";\n const scope = input.scope ?? \"global\";\n const scopeTarget = scope === \"global\" ? null : (input.scopeTarget ?? null);\n const tags = input.tags ?? [];\n const relatedFactIds = input.relatedFactIds ?? [];\n const legacyFailedOnly = episodesTableIsLegacyFailedOutcomeCheck(db);\n const outcomeForInsert = episodeOutcomeForSqliteInsert(db, input.outcome);\n const publicFromStored = storedEpisodeOutcomeToPublic(outcomeForInsert);\n // Legacy CHECK stores both `failure` and `unknown` as `failed`. Re-read maps\n // `failed` → `failure`, so callers cannot distinguish unknown after reload; keep\n // the immediate return aligned with the caller's intent for `unknown`.\n const returnedOutcome: EpisodeOutcome =\n legacyFailedOnly && input.outcome === \"unknown\" ? \"unknown\" : publicFromStored;\n\n let importance = input.importance ?? 0.5;\n if (input.outcome === \"failure\" && importance < 0.8) {\n importance = 0.8;\n }\n\n const tx = createTransaction(db, () => {\n db.prepare(\n `INSERT INTO episodes (id, event, outcome, timestamp, duration, context, related_fact_ids, procedure_id, scope, scope_target, agent_id, user_id, session_id, importance, tags, decay_class, created_at, verified_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n ).run(\n id,\n input.event,\n outcomeForInsert,\n timestamp,\n input.duration ?? null,\n input.context ?? null,\n relatedFactIds.length > 0 ? JSON.stringify(relatedFactIds) : null,\n input.procedureId ?? null,\n scope,\n scopeTarget,\n input.agentId ?? null,\n input.userId ?? null,\n input.sessionId ?? null,\n importance,\n serializeTags(tags),\n decayClass,\n nowSec,\n null,\n );\n\n for (const factId of relatedFactIds) {\n db.prepare(\n \"INSERT INTO episode_relations (id, episode_id, target_id, relation_type, strength, created_at) VALUES (?, ?, ?, ?, ?, ?)\",\n ).run(randomUUID(), id, factId, \"PART_OF\", 0.8, nowSec);\n }\n });\n tx();\n\n return {\n id,\n category: \"episode\",\n event: input.event,\n outcome: returnedOutcome,\n timestamp,\n duration: input.duration,\n context: input.context,\n relatedFactIds,\n procedureId: input.procedureId,\n scope,\n scopeTarget: scopeTarget ?? undefined,\n agentId: input.agentId,\n userId: input.userId,\n sessionId: input.sessionId,\n importance,\n tags,\n decayClass,\n createdAt: nowSec,\n };\n}\n\nexport function searchEpisodes(\n db: DatabaseSync,\n options: {\n query?: string;\n outcome?: EpisodeOutcome[];\n since?: number;\n until?: number;\n procedureId?: string;\n limit?: number;\n scopeFilter?: ScopeFilter | null;\n } = {},\n): Episode[] {\n const { query, outcome, since, until, procedureId, limit = 50, scopeFilter } = options;\n const params: unknown[] = [];\n const conditions: string[] = [];\n\n if (query?.trim()) {\n const sanitized = sanitizeFts5QueryForFacts(query.trim());\n const words = sanitized\n .split(/\\s+/)\n .filter((w) => w.length > 1)\n .slice(0, 8)\n .map((w) => `\"${w}\"`)\n .join(\" OR \");\n if (words) {\n conditions.push(\"e.rowid IN (SELECT rowid FROM episodes_fts WHERE episodes_fts MATCH ?)\");\n params.push(words);\n }\n }\n\n if (outcome && outcome.length > 0) {\n const legacyFailed = episodesTableIsLegacyFailedOutcomeCheck(db);\n const expanded = new Set<string>(outcome);\n // Map `failure` → stored `failed` on legacy CHECK. Do not map `unknown` →\n // `failed`: that would return rows that are semantically `failure` when the\n // caller asked for unknown-only results (both values share `failed` in DB).\n if (legacyFailed && outcome.includes(\"failure\")) {\n expanded.add(\"failed\");\n }\n const list = [...expanded];\n const placeholders = list.map(() => \"?\").join(\",\");\n conditions.push(`e.outcome IN (${placeholders})`);\n params.push(...list);\n }\n\n if (since !== undefined) {\n conditions.push(\"e.timestamp >= ?\");\n params.push(since);\n }\n if (until !== undefined) {\n conditions.push(\"e.timestamp <= ?\");\n params.push(until);\n }\n\n if (procedureId) {\n conditions.push(\"e.procedure_id = ?\");\n params.push(procedureId);\n }\n\n const scopeClause = scopeFilterClausePositional(scopeFilter);\n if (scopeClause.clause) {\n // Positional scope fragment is ` AND ( … )`; strip leading AND (not `^AND` — clause starts with spaces).\n const trimmed = scopeClause.clause.trimStart().replace(/^AND\\s+/i, \"\");\n if (trimmed) {\n conditions.push(trimmed);\n params.push(...scopeClause.params);\n }\n }\n\n const where = conditions.length > 0 ? `WHERE ${conditions.join(\" AND \")}` : \"\";\n const limitClause = \"ORDER BY e.timestamp DESC LIMIT ?\";\n params.push(limit);\n\n const sql = `SELECT e.* FROM episodes e ${where} ${limitClause}`;\n const rows = db.prepare(sql).all(...(params as SQLInputValue[])) as Array<Record<string, unknown>>;\n return rows.map((r) => rowToEpisode(r));\n}\n\nexport function getEpisode(db: DatabaseSync, id: string): Episode | null {\n const row = db.prepare(\"SELECT * FROM episodes WHERE id = ?\").get(id) as Record<string, unknown> | undefined;\n if (!row) return null;\n return rowToEpisode(row);\n}\n\nexport function deleteEpisode(db: DatabaseSync, id: string): boolean {\n const result = db.prepare(\"DELETE FROM episodes WHERE id = ?\").run(id);\n return result.changes > 0;\n}\n\nexport function episodesCount(db: DatabaseSync): number {\n try {\n const row = db.prepare(\"SELECT COUNT(*) as cnt FROM episodes\").get() as { cnt: number };\n return row?.cnt ?? 0;\n } catch {\n return 0;\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"fact-queries.js","names":[],"sources":["../../../backends/facts-db/fact-queries.ts"],"sourcesContent":["/**\n * Pure SQL / FTS query fragments for FactsDB (#870, #888).\n */\n\nimport type { DatabaseSync } from \"node:sqlite\";\nimport { sanitizeFts5QueryForFacts } from \"./fts-text.js\";\n\n/** Load superseded fact texts (lowercased) for LanceDB overlap filtering. */\nexport function fetchSupersededFactTextsLower(db: DatabaseSync): string[] {\n const rows = db.prepare(\"SELECT text FROM facts WHERE superseded_at IS NOT NULL\").all() as Array<{ text: string }>;\n return rows.map((r) => r.text.toLowerCase());\n}\n\n/**\n * OR-joined quoted FTS terms (+ optional prefix terms) for classification-style lookups (#898 alignment with porter).\n */\n/** Broader OR clause for `FactsDB.search()` (min term length 1, all tokens, porter prefix). */\nexport function buildFactsSearchFtsOrClause(query: string, options?: { maxOrTerms?: number }): string | null {\n const sanitized = sanitizeFts5QueryForFacts(query);\n let terms = sanitized.split(/\\s+/).filter((w) => w.length > 1);\n const cap = options?.maxOrTerms;\n if (cap !== undefined && cap > 0 && terms.length > cap) {\n terms = terms.slice(0, cap);\n }\n if (terms.length === 0) return null;\n const parts = terms.map((w) => {\n if (/^[a-zA-Z0-9_]+$/.test(w) && w.length >= 3) {\n return `( \"${w}\" OR ${w}* )`;\n }\n return `\"${w}\"`;\n });\n return parts.join(\" OR \");\n}\n\n/**\n * True when `facts.preserve_tags` matches the trim-budget SQL exclusion\n * (`maintenance.ts` / `stats.ts`): non-null, non-empty after trim, not exactly `[]`.\n * Aligns token-tier stats with which rows the trim pass will never touch.\n */\nexport function preserveTagsColumnExcludesFromTrimSql(raw: string | null): boolean {\n return raw != null && String(raw).trim() !== \"\" && raw !== \"[]\";\n}\n\nexport function buildClassificationFtsOrClause(rawText: string): string | null {\n const sanitized = sanitizeFts5QueryForFacts(rawText);\n const parts = sanitized\n .split(/\\s+/)\n .filter((w) => w.length > 2)\n .slice(0, 5)\n .map((w) => {\n if (/^[a-zA-Z0-9_]+$/.test(w) && w.length >= 3) {\n return `( \"${w}\" OR ${w}* )`;\n }\n return `\"${w}\"`;\n });\n if (parts.length === 0) return null;\n return parts.join(\" OR \");\n}\n"],"mappings":";;;AAQA,SAAgB,8BAA8B,IAA4B;CAExE,OADa,GAAG,QAAQ,yDAAyD,CAAC,KACvE,CAAC,KAAK,MAAM,EAAE,KAAK,aAAa,CAAC;;;;;;AAO9C,SAAgB,4BAA4B,OAAe,SAAkD;CAE3G,IAAI,QADc,0BAA0B,MACvB,CAAC,MAAM,MAAM,CAAC,QAAQ,MAAM,EAAE,SAAS,EAAE;CAC9D,MAAM,MAAM,SAAS;CACrB,IAAI,QAAQ,KAAA,KAAa,MAAM,KAAK,MAAM,SAAS,KACjD,QAAQ,MAAM,MAAM,GAAG,IAAI;CAE7B,IAAI,MAAM,WAAW,GAAG,OAAO;CAO/B,OANc,MAAM,KAAK,MAAM;EAC7B,IAAI,kBAAkB,KAAK,EAAE,IAAI,EAAE,UAAU,GAC3C,OAAO,MAAM,EAAE,OAAO,EAAE;EAE1B,OAAO,IAAI,EAAE;GAEH,CAAC,KAAK,OAAO;;;;;;;AAQ3B,SAAgB,sCAAsC,KAA6B;CACjF,OAAO,OAAO,QAAQ,OAAO,IAAI,CAAC,MAAM,KAAK,MAAM,QAAQ;;AAG7D,SAAgB,+BAA+B,SAAgC;CAE7E,MAAM,QADY,0BAA0B,QACrB,CACpB,MAAM,MAAM,CACZ,QAAQ,MAAM,EAAE,SAAS,EAAE,CAC3B,MAAM,GAAG,EAAE,CACX,KAAK,MAAM;EACV,IAAI,kBAAkB,KAAK,EAAE,IAAI,EAAE,UAAU,GAC3C,OAAO,MAAM,EAAE,OAAO,EAAE;EAE1B,OAAO,IAAI,EAAE;GACb;CACJ,IAAI,MAAM,WAAW,GAAG,OAAO;CAC/B,OAAO,MAAM,KAAK,OAAO"}
1
+ {"version":3,"file":"fact-queries.js","names":[],"sources":["../../../backends/facts-db/fact-queries.ts"],"sourcesContent":["/**\n * Pure SQL / FTS query fragments for FactsDB (#870, #888).\n */\n\nimport type { DatabaseSync } from \"node:sqlite\";\nimport { sanitizeFts5QueryForFacts } from \"./fts-text.js\";\n\n/** Load superseded fact texts (lowercased) for LanceDB overlap filtering. */\nexport function fetchSupersededFactTextsLower(db: DatabaseSync): string[] {\n const rows = db.prepare(\"SELECT text FROM facts WHERE superseded_at IS NOT NULL\").all() as Array<{ text: string }>;\n return rows.map((r) => r.text.toLowerCase());\n}\n\n/**\n * OR-joined quoted FTS terms (+ optional prefix terms) for classification-style lookups (#898 alignment with porter).\n */\n/** Broader OR clause for `FactsDB.search()` (min term length 1, all tokens, porter prefix). */\nexport function buildFactsSearchFtsOrClause(query: string, options?: { maxOrTerms?: number }): string | null {\n const sanitized = sanitizeFts5QueryForFacts(query);\n let terms = sanitized.split(/\\s+/).filter((w) => w.length > 1);\n const cap = options?.maxOrTerms;\n if (cap !== undefined && cap > 0 && terms.length > cap) {\n terms = terms.slice(0, cap);\n }\n if (terms.length === 0) return null;\n const parts = terms.map((w) => {\n if (/^[a-zA-Z0-9_]+$/.test(w) && w.length >= 3) {\n return `( \"${w}\" OR ${w}* )`;\n }\n return `\"${w}\"`;\n });\n return parts.join(\" OR \");\n}\n\n/**\n * True when `facts.preserve_tags` matches the trim-budget SQL exclusion\n * (`maintenance.ts` / `stats.ts`): non-null, non-empty after trim, not exactly `[]`.\n * Aligns token-tier stats with which rows the trim pass will never touch.\n */\nexport function preserveTagsColumnExcludesFromTrimSql(raw: string | null): boolean {\n return raw != null && String(raw).trim() !== \"\" && raw !== \"[]\";\n}\n\nexport function buildClassificationFtsOrClause(rawText: string): string | null {\n const sanitized = sanitizeFts5QueryForFacts(rawText);\n const parts = sanitized\n .split(/\\s+/)\n .filter((w) => w.length > 2)\n .slice(0, 5)\n .map((w) => {\n if (/^[a-zA-Z0-9_]+$/.test(w) && w.length >= 3) {\n return `( \"${w}\" OR ${w}* )`;\n }\n return `\"${w}\"`;\n });\n if (parts.length === 0) return null;\n return parts.join(\" OR \");\n}\n"],"mappings":";;;AAQA,SAAgB,8BAA8B,IAA4B;CAExE,OADa,GAAG,QAAQ,wDAAwD,EAAE,IACxE,EAAE,KAAK,MAAM,EAAE,KAAK,YAAY,CAAC;AAC7C;;;;;AAMA,SAAgB,4BAA4B,OAAe,SAAkD;CAE3G,IAAI,QADc,0BAA0B,KACxB,EAAE,MAAM,KAAK,EAAE,QAAQ,MAAM,EAAE,SAAS,CAAC;CAC7D,MAAM,MAAM,SAAS;CACrB,IAAI,QAAQ,KAAA,KAAa,MAAM,KAAK,MAAM,SAAS,KACjD,QAAQ,MAAM,MAAM,GAAG,GAAG;CAE5B,IAAI,MAAM,WAAW,GAAG,OAAO;CAO/B,OANc,MAAM,KAAK,MAAM;EAC7B,IAAI,kBAAkB,KAAK,CAAC,KAAK,EAAE,UAAU,GAC3C,OAAO,MAAM,EAAE,OAAO,EAAE;EAE1B,OAAO,IAAI,EAAE;CACf,CACW,EAAE,KAAK,MAAM;AAC1B;;;;;;AAOA,SAAgB,sCAAsC,KAA6B;CACjF,OAAO,OAAO,QAAQ,OAAO,GAAG,EAAE,KAAK,MAAM,MAAM,QAAQ;AAC7D;AAEA,SAAgB,+BAA+B,SAAgC;CAE7E,MAAM,QADY,0BAA0B,OACtB,EACnB,MAAM,KAAK,EACX,QAAQ,MAAM,EAAE,SAAS,CAAC,EAC1B,MAAM,GAAG,CAAC,EACV,KAAK,MAAM;EACV,IAAI,kBAAkB,KAAK,CAAC,KAAK,EAAE,UAAU,GAC3C,OAAO,MAAM,EAAE,OAAO,EAAE;EAE1B,OAAO,IAAI,EAAE;CACf,CAAC;CACH,IAAI,MAAM,WAAW,GAAG,OAAO;CAC/B,OAAO,MAAM,KAAK,MAAM;AAC1B"}
@@ -1 +1 @@
1
- {"version":3,"file":"fact-read-queries.js","names":["factRows"],"sources":["../../../backends/facts-db/fact-read-queries.ts"],"sourcesContent":["/**\n * Fact reads: classification similarity, getById filters, list, getAll family (Issue #954 split).\n */\nimport type { SQLInputValue } from \"node:sqlite\";\nimport type { DatabaseSync } from \"node:sqlite\";\n\nimport { isValidCategory } from \"../../config.js\";\nimport { capturePluginError } from \"../../services/error-reporter.js\";\nimport type { MemoryEntry, MemoryScope, ScopeFilter, SearchResult } from \"../../types/memory.js\";\nimport { createTransaction } from \"../../utils/sqlite-transaction.js\";\nimport { estimateTokensForDisplay } from \"../../utils/text.js\";\nimport { buildClassificationFtsOrClause } from \"./fact-queries.js\";\nimport { rowToMemoryEntry } from \"./row-mapper.js\";\nimport { scopeFilterClausePositional } from \"./scope-sql.js\";\nimport { DASHBOARD_TIER_FILTER } from \"./stats.js\";\n\nexport function applyLookupFilters(\n entry: MemoryEntry,\n options?: { asOf?: number; scopeFilter?: ScopeFilter | null },\n): MemoryEntry | null {\n const asOf = options?.asOf;\n if (asOf != null) {\n const vf = entry.validFrom ?? entry.createdAt;\n const vu = entry.validUntil ?? null;\n if (vf > asOf || (vu != null && vu <= asOf)) return null;\n }\n const scopeFilter = options?.scopeFilter;\n if (scopeFilter && (scopeFilter.userId || scopeFilter.agentId || scopeFilter.sessionId)) {\n const scope = entry.scope ?? \"global\";\n if (scope === \"global\") return entry;\n const target = entry.scopeTarget ?? null;\n const matches =\n (scope === \"user\" && (scopeFilter.userId ?? null) === target) ||\n (scope === \"agent\" && (scopeFilter.agentId ?? null) === target) ||\n (scope === \"session\" && (scopeFilter.sessionId ?? null) === target);\n if (!matches) return null;\n }\n return entry;\n}\n\nexport function findSimilarForClassification(\n db: DatabaseSync,\n text: string,\n entity: string | null,\n key: string | null,\n limit = 5,\n scope: MemoryScope | null = null,\n scopeTarget: string | null = null,\n): MemoryEntry[] {\n const nowSec = Math.floor(Date.now() / 1000);\n const results: MemoryEntry[] = [];\n const safeScopeTarget = scope === \"global\" ? null : scopeTarget;\n const scopeSql =\n scope === null\n ? \"\"\n : scope === \"global\"\n ? \" AND COALESCE(scope, 'global') = 'global'\"\n : \" AND scope = ? AND scope_target = ?\";\n const scopeParams: SQLInputValue[] = scope === null || scope === \"global\" ? [] : [scope, safeScopeTarget];\n\n if (entity && key) {\n const rows = db\n .prepare(\n `SELECT * FROM facts WHERE lower(entity) = lower(?) AND lower(key) = lower(?) AND superseded_at IS NULL AND (expires_at IS NULL OR expires_at > ?)${scopeSql} ORDER BY created_at DESC LIMIT ?`,\n )\n .all(entity, key, nowSec, ...scopeParams, limit) as Array<Record<string, unknown>>;\n for (const row of rows) {\n results.push(rowToMemoryEntry(row));\n }\n }\n\n if (entity && results.length < limit) {\n const remaining = limit - results.length;\n const seenIds = new Set(results.map((r) => r.id));\n const rows = db\n .prepare(\n `SELECT * FROM facts WHERE lower(entity) = lower(?) AND superseded_at IS NULL AND (expires_at IS NULL OR expires_at > ?)${scopeSql} ORDER BY created_at DESC LIMIT ?`,\n )\n .all(entity, nowSec, ...scopeParams, remaining + results.length) as Array<Record<string, unknown>>;\n for (const row of rows) {\n const entry = rowToMemoryEntry(row);\n if (!seenIds.has(entry.id)) {\n results.push(entry);\n seenIds.add(entry.id);\n if (results.length >= limit) break;\n }\n }\n }\n\n if (results.length < limit) {\n const remaining = limit - results.length;\n const seenIds = new Set(results.map((r) => r.id));\n const words = buildClassificationFtsOrClause(text);\n if (words) {\n try {\n const ftsStmt = db.prepare(\"SELECT rowid FROM facts_fts WHERE facts_fts MATCH ? ORDER BY rank LIMIT ?\");\n let fetchLimit = Math.max((remaining + results.length) * 3, 50);\n const maxFetch = Math.min(10_000, Math.max(remaining * 200, 500));\n for (;;) {\n const ftsRows = ftsStmt.all(words, fetchLimit) as Array<{ rowid: number }>;\n if (ftsRows.length === 0) break;\n\n const CHUNK_SIZE = 500;\n const allRows: Array<Record<string, unknown>> = [];\n for (let i = 0; i < ftsRows.length; i += CHUNK_SIZE) {\n const chunk = ftsRows.slice(i, i + CHUNK_SIZE);\n const ph = chunk.map(() => \"?\").join(\",\");\n allRows.push(\n ...(db\n .prepare(\n `SELECT *, rowid AS _rowid FROM facts WHERE rowid IN (${ph}) AND superseded_at IS NULL AND (expires_at IS NULL OR expires_at > ?)${scopeSql}`,\n )\n .all(...chunk.map((r) => r.rowid), nowSec, ...scopeParams) as Array<Record<string, unknown>>),\n );\n }\n const factRows = allRows;\n const byRowid = new Map(factRows.map((r) => [r._rowid as number, r]));\n for (const { rowid } of ftsRows) {\n const row = byRowid.get(rowid);\n if (!row) continue;\n const entry = rowToMemoryEntry(row);\n if (!seenIds.has(entry.id)) {\n results.push(entry);\n seenIds.add(entry.id);\n if (results.length >= limit) break;\n }\n }\n\n if (results.length >= limit) break;\n if (ftsRows.length < fetchLimit) break;\n if (fetchLimit >= maxFetch) break;\n fetchLimit = Math.min(fetchLimit * 2, maxFetch);\n }\n } catch (err) {\n capturePluginError(err as Error, {\n operation: \"fts-query\",\n severity: \"info\",\n subsystem: \"facts\",\n });\n }\n }\n }\n\n return results.slice(0, limit);\n}\n\nexport function getFactsForConsolidation(\n db: DatabaseSync,\n limit: number,\n): Array<{\n id: string;\n text: string;\n category: string;\n entity: string | null;\n key: string | null;\n}> {\n const nowSec = Math.floor(Date.now() / 1000);\n const rows = db\n .prepare(\n `SELECT id, text, category, entity, key FROM facts\n WHERE (expires_at IS NULL OR expires_at > ?)\n AND superseded_at IS NULL\n AND lower(COALESCE(source, '')) NOT IN ('consolidation', 'dream-cycle')\n AND lower(COALESCE(key, '')) != 'consolidated'\n AND (',' || lower(COALESCE(tags, '')) || ',') NOT LIKE '%,consolidated,%'\n ORDER BY created_at DESC LIMIT ?`,\n )\n .all(nowSec, limit) as Array<Record<string, unknown>>;\n return rows.map((row) => ({\n id: row.id as string,\n text: row.text as string,\n category: row.category as string,\n entity: (row.entity as string) || null,\n key: (row.key as string) || null,\n }));\n}\n\nexport function getById(\n db: DatabaseSync,\n id: string,\n options?: { asOf?: number; scopeFilter?: ScopeFilter | null },\n): MemoryEntry | null {\n const row = db.prepare(\"SELECT * FROM facts WHERE id = ?\").get(id) as Record<string, unknown> | undefined;\n if (!row) return null;\n const entry = rowToMemoryEntry(row);\n return applyLookupFilters(entry, options);\n}\n\nexport function getByIds(\n db: DatabaseSync,\n ids: string[],\n options?: { asOf?: number; scopeFilter?: ScopeFilter | null },\n): Map<string, MemoryEntry> {\n const result = new Map<string, MemoryEntry>();\n if (ids.length === 0) return result;\n const uniqueIds = Array.from(new Set(ids));\n const CHUNK_SIZE = 500;\n for (let i = 0; i < uniqueIds.length; i += CHUNK_SIZE) {\n const chunk = uniqueIds.slice(i, i + CHUNK_SIZE);\n const placeholders = chunk.map(() => \"?\").join(\",\");\n const rows = db.prepare(`SELECT * FROM facts WHERE id IN (${placeholders})`).all(...chunk) as Array<\n Record<string, unknown>\n >;\n for (const row of rows) {\n const entry = rowToMemoryEntry(row);\n const filtered = applyLookupFilters(entry, options);\n if (filtered) result.set(filtered.id, filtered);\n }\n }\n return result;\n}\n\nexport function getRecentFacts(\n db: DatabaseSync,\n days: number,\n options?: { excludeCategories?: string[]; excludeTags?: string[] },\n): MemoryEntry[] {\n const nowSec = Math.floor(Date.now() / 1000);\n const windowStartSec = nowSec - Math.max(1, Math.min(90, days)) * 86400;\n const exclude = options?.excludeCategories ?? [\"pattern\", \"rule\"];\n // Implicit-feedback trajectory lessons are stored as `category=technical` with\n // `tags=[implicit-feedback,...]` (issue #1186) — exclude them from reflection\n // input so they don't dominate the LLM context with paraphrase noise.\n const excludeTags = options?.excludeTags ?? [\"implicit-feedback\"];\n const placeholders = exclude.map(() => \"?\").join(\",\");\n const tagPredicates = excludeTags.map(() => \"(',' || COALESCE(tags,'') || ',') NOT LIKE ?\").join(\" AND \");\n const tagClause = excludeTags.length > 0 ? ` AND ${tagPredicates}` : \"\";\n const tagParams = excludeTags.map((t) => `%,${t},%`);\n const rows = db\n .prepare(\n `SELECT * FROM facts WHERE (expires_at IS NULL OR expires_at > ?) AND superseded_at IS NULL\n AND (COALESCE(source_date, created_at) >= ?)\n AND category NOT IN (${placeholders})${tagClause}\n ORDER BY COALESCE(source_date, created_at) DESC`,\n )\n .all(nowSec, windowStartSec, ...exclude, ...tagParams) as Array<Record<string, unknown>>;\n return rows.map((row) => rowToMemoryEntry(row));\n}\n\nexport function getAll(\n db: DatabaseSync,\n options?: {\n includeSuperseded?: boolean;\n asOf?: number;\n scopeFilter?: ScopeFilter | null;\n },\n): MemoryEntry[] {\n const nowSec = Math.floor(Date.now() / 1000);\n const { includeSuperseded = false, asOf, scopeFilter } = options ?? {};\n const temporalFilter =\n asOf != null\n ? \" AND valid_from <= ? AND (valid_until IS NULL OR valid_until > ?)\"\n : includeSuperseded\n ? \"\"\n : \" AND superseded_at IS NULL\";\n const { clause: scopeClause, params: scopeParams } = scopeFilterClausePositional(scopeFilter);\n const params = asOf != null ? [...[nowSec, asOf, asOf], ...scopeParams] : [...[nowSec], ...scopeParams];\n const rows = db\n .prepare(\n `SELECT * FROM facts WHERE (expires_at IS NULL OR expires_at > ?)${temporalFilter}${scopeClause} ORDER BY created_at DESC`,\n )\n .all(...params) as Array<Record<string, unknown>>;\n return rows.map((row) => rowToMemoryEntry(row));\n}\n\nexport function getCount(db: DatabaseSync, options?: { includeSuperseded?: boolean }): number {\n const nowSec = Math.floor(Date.now() / 1000);\n const { includeSuperseded = false } = options ?? {};\n const temporalFilter = includeSuperseded ? \"\" : \" AND superseded_at IS NULL\";\n const row = db\n .prepare(`SELECT COUNT(*) AS count FROM facts WHERE (expires_at IS NULL OR expires_at > ?)${temporalFilter}`)\n .get(nowSec) as { count: number };\n return row?.count ?? 0;\n}\n\nexport function getAllIds(db: DatabaseSync): string[] {\n const nowSec = Math.floor(Date.now() / 1000);\n const rows = db\n .prepare(\"SELECT id FROM facts WHERE superseded_at IS NULL AND (expires_at IS NULL OR expires_at > ?)\")\n .all(nowSec) as Array<{ id: string }>;\n return rows.map((row) => row.id.toLowerCase());\n}\n\nexport function getBatch(\n db: DatabaseSync,\n offset: number,\n limit: number,\n options?: { includeSuperseded?: boolean },\n): MemoryEntry[] {\n const nowSec = Math.floor(Date.now() / 1000);\n const { includeSuperseded = false } = options ?? {};\n const temporalFilter = includeSuperseded ? \"\" : \" AND superseded_at IS NULL\";\n const rows = db\n .prepare(\n `SELECT * FROM facts WHERE (expires_at IS NULL OR expires_at > ?)${temporalFilter} ORDER BY created_at DESC LIMIT ? OFFSET ?`,\n )\n .all(nowSec, limit, offset) as Array<Record<string, unknown>>;\n return rows.map((row) => rowToMemoryEntry(row));\n}\n\nexport function listFacts(\n db: DatabaseSync,\n limit: number,\n filters?: {\n category?: string;\n entity?: string;\n key?: string;\n source?: string;\n tier?: string;\n },\n): MemoryEntry[] {\n const nowSec = Math.floor(Date.now() / 1000);\n if (\n (filters?.category != null && filters.category !== \"\" && !isValidCategory(filters.category)) ||\n (filters?.tier != null && filters.tier !== \"\" && !DASHBOARD_TIER_FILTER.has(filters.tier))\n ) {\n return [];\n }\n const parts: string[] = [\"(expires_at IS NULL OR expires_at > ?)\", \"superseded_at IS NULL\"];\n const params: SQLInputValue[] = [nowSec];\n if (filters?.category != null && isValidCategory(filters.category)) {\n parts.push(\"category = ?\");\n params.push(filters.category);\n }\n if (filters?.entity != null) {\n parts.push(\"lower(entity) = lower(?)\");\n params.push(filters.entity);\n }\n if (filters?.key != null) {\n parts.push(\"lower(key) = lower(?)\");\n params.push(filters.key);\n }\n if (filters?.source != null) {\n parts.push(\"source = ?\");\n params.push(filters.source);\n }\n if (filters?.tier != null && DASHBOARD_TIER_FILTER.has(filters.tier)) {\n parts.push(\"COALESCE(tier, 'warm') = ?\");\n params.push(filters.tier);\n }\n const where = parts.join(\" AND \");\n params.push(limit);\n const rows = db\n .prepare(`SELECT * FROM facts WHERE ${where} ORDER BY COALESCE(source_date, created_at) DESC LIMIT ?`)\n .all(...params) as Array<Record<string, unknown>>;\n return rows.map((row) => rowToMemoryEntry(row));\n}\n\n/**\n * Shared garbage detection utility for consistency across query-time, tiering, and SQL batch operations.\n * Returns true if the fact text looks like a reasoning trace or classifier artifact (#1559).\n */\nexport function isLikelyGarbage(input: {\n text: string;\n summary?: string | null;\n source?: string | null;\n category?: string | null;\n recall_count?: number;\n}): boolean {\n const text = input.text ?? \"\";\n const summary = input.summary ?? \"\";\n const combined = text + \"\\n\" + summary;\n\n // Reasoning traces embedded in fact content\n if (/<(?:redacted_)?think(?:ing)?>[\\s\\S]*?<\\/(?:redacted_)?think(?:ing)?>/i.test(combined)) return true;\n // Classifier / capability-hint artifacts\n if (/^Thinking Process:/im.test(combined)) return true;\n if (/^\\[(?:hot-memories|recall|hot\\/fact)\\]/im.test(combined)) return true;\n // Unhelpful source + long reasoning combo\n // Match reasoning trace patterns more precisely to avoid false positives with common English words\n if (input.source === \"auto-capture\" && combined.length > 3000) {\n // Reasoning-specific phrases that indicate LLM reasoning artifacts\n const reasoningPhrases =\n /\\b(?:let me think|my reasoning|step[- ]by[- ]step|thought process|reasoning process|analysis process)\\b/i;\n // Multiple paragraph breaks suggesting stream-of-consciousness\n const multipleBreaks = (combined.match(/\\n\\n/g) || []).length > 10;\n if (reasoningPhrases.test(combined) || multipleBreaks) {\n return true;\n }\n }\n // High recall counts for \"other\" category (self-reinforcing garbage loop)\n if (input.category === \"other\" && (input.recall_count ?? 0) > 500) {\n return true;\n }\n return false;\n}\n\n/** Quality score for HOT injection: 0–1. Garbage facts score 0 and are excluded. */\nfunction hotQualityScore(entry: MemoryEntry): number {\n if (\n isLikelyGarbage({\n text: entry.text,\n summary: entry.summary,\n source: entry.source,\n category: entry.category,\n recall_count: entry.recallCount,\n })\n )\n return 0;\n const text = entry.text ?? \"\";\n // Penalise very long unconstrained text (likely full conversation dumps)\n if (text.length > 8000) return 0.3;\n return 1.0;\n}\n\nexport function getHotFacts(db: DatabaseSync, maxTokens: number, scopeFilter?: ScopeFilter | null): SearchResult[] {\n const nowSec = Math.floor(Date.now() / 1000);\n const { clause: scopeClause, params: scopeParams } = scopeFilterClausePositional(scopeFilter);\n const rows = db\n .prepare(\n `SELECT * FROM facts\n WHERE tier = 'hot' AND superseded_at IS NULL AND (expires_at IS NULL OR expires_at > ?)\n ${scopeClause}\n ORDER BY COALESCE(last_accessed, last_confirmed_at, created_at) DESC`,\n )\n .all(nowSec, ...scopeParams) as Array<Record<string, unknown>>;\n const results: SearchResult[] = [];\n let usedTokens = 0;\n for (const row of rows) {\n if (usedTokens >= maxTokens) break;\n const entry = rowToMemoryEntry(row);\n // Apply quality filter: exclude reasoning traces, prompt artifacts, and garbage (#1559)\n const score = hotQualityScore(entry);\n if (score === 0) continue;\n const tokens = estimateTokensForDisplay(entry.summary || entry.text);\n if (usedTokens + tokens > maxTokens) {\n continue;\n }\n usedTokens += tokens;\n results.push({ entry, score, backend: \"sqlite\" as const });\n }\n return results;\n}\n\nexport function getByCategory(db: DatabaseSync, category: string): MemoryEntry[] {\n const rows = db.prepare(\"SELECT * FROM facts WHERE category = ? ORDER BY created_at DESC\").all(category) as Array<\n Record<string, unknown>\n >;\n return rows.map((row) => rowToMemoryEntry(row));\n}\n\n/**\n * Targeted project-fact query for active-task projection (#1553).\n * Only loads facts with category='project' and source='active-task', avoiding a full table scan.\n */\nexport function getProjectFacts(db: DatabaseSync, limit = 8000, scopeFilter?: ScopeFilter | null): MemoryEntry[] {\n const nowSec = Math.floor(Date.now() / 1000);\n const { clause: scopeClause, params: scopeParams } = scopeFilterClausePositional(scopeFilter);\n const rows = db\n .prepare(\n `SELECT * FROM facts\n WHERE category = 'project'\n AND source = 'active-task'\n AND (expires_at IS NULL OR expires_at > ?)\n AND superseded_at IS NULL\n ${scopeClause}\n ORDER BY created_at DESC\n LIMIT ?`,\n )\n .all(nowSec, ...scopeParams, limit) as Array<Record<string, unknown>>;\n return rows.map((row) => rowToMemoryEntry(row));\n}\n\nexport function listFactsByCategory(db: DatabaseSync, category: string, limit = 100): MemoryEntry[] {\n const rows = db\n .prepare(\n \"SELECT * FROM facts WHERE category = ? AND (superseded_at IS NULL) ORDER BY COALESCE(source_date, created_at) DESC LIMIT ?\",\n )\n .all(category, limit) as Array<Record<string, unknown>>;\n return rows.map((row) => rowToMemoryEntry(row));\n}\n\nexport function listDirectives(db: DatabaseSync, limit = 100): MemoryEntry[] {\n const rows = db\n .prepare(\n `SELECT * FROM facts WHERE source LIKE 'directive:%' AND (superseded_at IS NULL) ORDER BY created_at DESC LIMIT ?`,\n )\n .all(limit) as Array<Record<string, unknown>>;\n return rows.map((row) => rowToMemoryEntry(row));\n}\n\nexport function getMaxCreatedAtByCategory(db: DatabaseSync, category: string): number | null {\n const row = db\n .prepare(\"SELECT MAX(created_at) as max_created_at FROM facts WHERE category = ? AND (superseded_at IS NULL)\")\n .get(category) as Record<string, unknown> | undefined;\n if (!row) return null;\n const maxCreatedAt = row.max_created_at;\n if (typeof maxCreatedAt === \"number\" && Number.isFinite(maxCreatedAt)) {\n return maxCreatedAt;\n }\n return null;\n}\n\nexport function updateCategory(db: DatabaseSync, id: string, category: string): boolean {\n const result = db.prepare(\"UPDATE facts SET category = ? WHERE id = ?\").run(category, id);\n return result.changes > 0;\n}\n\n/**\n * Return \"other\" facts that have not been classify-attempted yet, or whose\n * created_at is newer than the last attempt (text was replaced / re-ingested).\n */\nexport function getUnattemptedOtherFacts(db: DatabaseSync): MemoryEntry[] {\n const rows = db\n .prepare(\n `SELECT * FROM facts\n WHERE category = 'other'\n AND (last_classify_attempt_at IS NULL OR created_at > last_classify_attempt_at)\n ORDER BY created_at DESC`,\n )\n .all() as Array<Record<string, unknown>>;\n return rows.map((row) => rowToMemoryEntry(row));\n}\n\n/** Mark facts as having been attempted by the auto-classifier (batch). */\nexport function markClassifyAttempt(db: DatabaseSync, ids: string[]): void {\n if (ids.length === 0) return;\n const now = Math.floor(Date.now() / 1000);\n const stmt = db.prepare(\"UPDATE facts SET last_classify_attempt_at = ? WHERE id = ?\");\n const run = (): void => {\n for (const id of ids) {\n stmt.run(now, id);\n }\n };\n createTransaction(db, run)();\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"fact-read-queries.js","names":["factRows"],"sources":["../../../backends/facts-db/fact-read-queries.ts"],"sourcesContent":["/**\n * Fact reads: classification similarity, getById filters, list, getAll family (Issue #954 split).\n */\nimport type { SQLInputValue } from \"node:sqlite\";\nimport type { DatabaseSync } from \"node:sqlite\";\n\nimport { isValidCategory } from \"../../config.js\";\nimport { capturePluginError } from \"../../services/error-reporter.js\";\nimport type { MemoryEntry, MemoryScope, ScopeFilter, SearchResult } from \"../../types/memory.js\";\nimport { createTransaction } from \"../../utils/sqlite-transaction.js\";\nimport { estimateTokensForDisplay } from \"../../utils/text.js\";\nimport { buildClassificationFtsOrClause } from \"./fact-queries.js\";\nimport { rowToMemoryEntry } from \"./row-mapper.js\";\nimport { scopeFilterClausePositional } from \"./scope-sql.js\";\nimport { DASHBOARD_TIER_FILTER } from \"./stats.js\";\n\nexport function applyLookupFilters(\n entry: MemoryEntry,\n options?: { asOf?: number; scopeFilter?: ScopeFilter | null },\n): MemoryEntry | null {\n const asOf = options?.asOf;\n if (asOf != null) {\n const vf = entry.validFrom ?? entry.createdAt;\n const vu = entry.validUntil ?? null;\n if (vf > asOf || (vu != null && vu <= asOf)) return null;\n }\n const scopeFilter = options?.scopeFilter;\n if (scopeFilter && (scopeFilter.userId || scopeFilter.agentId || scopeFilter.sessionId)) {\n const scope = entry.scope ?? \"global\";\n if (scope === \"global\") return entry;\n const target = entry.scopeTarget ?? null;\n const matches =\n (scope === \"user\" && (scopeFilter.userId ?? null) === target) ||\n (scope === \"agent\" && (scopeFilter.agentId ?? null) === target) ||\n (scope === \"session\" && (scopeFilter.sessionId ?? null) === target);\n if (!matches) return null;\n }\n return entry;\n}\n\nexport function findSimilarForClassification(\n db: DatabaseSync,\n text: string,\n entity: string | null,\n key: string | null,\n limit = 5,\n scope: MemoryScope | null = null,\n scopeTarget: string | null = null,\n): MemoryEntry[] {\n const nowSec = Math.floor(Date.now() / 1000);\n const results: MemoryEntry[] = [];\n const safeScopeTarget = scope === \"global\" ? null : scopeTarget;\n const scopeSql =\n scope === null\n ? \"\"\n : scope === \"global\"\n ? \" AND COALESCE(scope, 'global') = 'global'\"\n : \" AND scope = ? AND scope_target = ?\";\n const scopeParams: SQLInputValue[] = scope === null || scope === \"global\" ? [] : [scope, safeScopeTarget];\n\n if (entity && key) {\n const rows = db\n .prepare(\n `SELECT * FROM facts WHERE lower(entity) = lower(?) AND lower(key) = lower(?) AND superseded_at IS NULL AND (expires_at IS NULL OR expires_at > ?)${scopeSql} ORDER BY created_at DESC LIMIT ?`,\n )\n .all(entity, key, nowSec, ...scopeParams, limit) as Array<Record<string, unknown>>;\n for (const row of rows) {\n results.push(rowToMemoryEntry(row));\n }\n }\n\n if (entity && results.length < limit) {\n const remaining = limit - results.length;\n const seenIds = new Set(results.map((r) => r.id));\n const rows = db\n .prepare(\n `SELECT * FROM facts WHERE lower(entity) = lower(?) AND superseded_at IS NULL AND (expires_at IS NULL OR expires_at > ?)${scopeSql} ORDER BY created_at DESC LIMIT ?`,\n )\n .all(entity, nowSec, ...scopeParams, remaining + results.length) as Array<Record<string, unknown>>;\n for (const row of rows) {\n const entry = rowToMemoryEntry(row);\n if (!seenIds.has(entry.id)) {\n results.push(entry);\n seenIds.add(entry.id);\n if (results.length >= limit) break;\n }\n }\n }\n\n if (results.length < limit) {\n const remaining = limit - results.length;\n const seenIds = new Set(results.map((r) => r.id));\n const words = buildClassificationFtsOrClause(text);\n if (words) {\n try {\n const ftsStmt = db.prepare(\"SELECT rowid FROM facts_fts WHERE facts_fts MATCH ? ORDER BY rank LIMIT ?\");\n let fetchLimit = Math.max((remaining + results.length) * 3, 50);\n const maxFetch = Math.min(10_000, Math.max(remaining * 200, 500));\n for (;;) {\n const ftsRows = ftsStmt.all(words, fetchLimit) as Array<{ rowid: number }>;\n if (ftsRows.length === 0) break;\n\n const CHUNK_SIZE = 500;\n const allRows: Array<Record<string, unknown>> = [];\n for (let i = 0; i < ftsRows.length; i += CHUNK_SIZE) {\n const chunk = ftsRows.slice(i, i + CHUNK_SIZE);\n const ph = chunk.map(() => \"?\").join(\",\");\n allRows.push(\n ...(db\n .prepare(\n `SELECT *, rowid AS _rowid FROM facts WHERE rowid IN (${ph}) AND superseded_at IS NULL AND (expires_at IS NULL OR expires_at > ?)${scopeSql}`,\n )\n .all(...chunk.map((r) => r.rowid), nowSec, ...scopeParams) as Array<Record<string, unknown>>),\n );\n }\n const factRows = allRows;\n const byRowid = new Map(factRows.map((r) => [r._rowid as number, r]));\n for (const { rowid } of ftsRows) {\n const row = byRowid.get(rowid);\n if (!row) continue;\n const entry = rowToMemoryEntry(row);\n if (!seenIds.has(entry.id)) {\n results.push(entry);\n seenIds.add(entry.id);\n if (results.length >= limit) break;\n }\n }\n\n if (results.length >= limit) break;\n if (ftsRows.length < fetchLimit) break;\n if (fetchLimit >= maxFetch) break;\n fetchLimit = Math.min(fetchLimit * 2, maxFetch);\n }\n } catch (err) {\n capturePluginError(err as Error, {\n operation: \"fts-query\",\n severity: \"info\",\n subsystem: \"facts\",\n });\n }\n }\n }\n\n return results.slice(0, limit);\n}\n\nexport function getFactsForConsolidation(\n db: DatabaseSync,\n limit: number,\n): Array<{\n id: string;\n text: string;\n category: string;\n entity: string | null;\n key: string | null;\n}> {\n const nowSec = Math.floor(Date.now() / 1000);\n const rows = db\n .prepare(\n `SELECT id, text, category, entity, key FROM facts\n WHERE (expires_at IS NULL OR expires_at > ?)\n AND superseded_at IS NULL\n AND lower(COALESCE(source, '')) NOT IN ('consolidation', 'dream-cycle')\n AND lower(COALESCE(key, '')) != 'consolidated'\n AND (',' || lower(COALESCE(tags, '')) || ',') NOT LIKE '%,consolidated,%'\n ORDER BY created_at DESC LIMIT ?`,\n )\n .all(nowSec, limit) as Array<Record<string, unknown>>;\n return rows.map((row) => ({\n id: row.id as string,\n text: row.text as string,\n category: row.category as string,\n entity: (row.entity as string) || null,\n key: (row.key as string) || null,\n }));\n}\n\nexport function getById(\n db: DatabaseSync,\n id: string,\n options?: { asOf?: number; scopeFilter?: ScopeFilter | null },\n): MemoryEntry | null {\n const row = db.prepare(\"SELECT * FROM facts WHERE id = ?\").get(id) as Record<string, unknown> | undefined;\n if (!row) return null;\n const entry = rowToMemoryEntry(row);\n return applyLookupFilters(entry, options);\n}\n\nexport function getByIds(\n db: DatabaseSync,\n ids: string[],\n options?: { asOf?: number; scopeFilter?: ScopeFilter | null },\n): Map<string, MemoryEntry> {\n const result = new Map<string, MemoryEntry>();\n if (ids.length === 0) return result;\n const uniqueIds = Array.from(new Set(ids));\n const CHUNK_SIZE = 500;\n for (let i = 0; i < uniqueIds.length; i += CHUNK_SIZE) {\n const chunk = uniqueIds.slice(i, i + CHUNK_SIZE);\n const placeholders = chunk.map(() => \"?\").join(\",\");\n const rows = db.prepare(`SELECT * FROM facts WHERE id IN (${placeholders})`).all(...chunk) as Array<\n Record<string, unknown>\n >;\n for (const row of rows) {\n const entry = rowToMemoryEntry(row);\n const filtered = applyLookupFilters(entry, options);\n if (filtered) result.set(filtered.id, filtered);\n }\n }\n return result;\n}\n\nexport function getRecentFacts(\n db: DatabaseSync,\n days: number,\n options?: { excludeCategories?: string[]; excludeTags?: string[] },\n): MemoryEntry[] {\n const nowSec = Math.floor(Date.now() / 1000);\n const windowStartSec = nowSec - Math.max(1, Math.min(90, days)) * 86400;\n const exclude = options?.excludeCategories ?? [\"pattern\", \"rule\"];\n // Implicit-feedback trajectory lessons are stored as `category=technical` with\n // `tags=[implicit-feedback,...]` (issue #1186) — exclude them from reflection\n // input so they don't dominate the LLM context with paraphrase noise.\n const excludeTags = options?.excludeTags ?? [\"implicit-feedback\"];\n const placeholders = exclude.map(() => \"?\").join(\",\");\n const tagPredicates = excludeTags.map(() => \"(',' || COALESCE(tags,'') || ',') NOT LIKE ?\").join(\" AND \");\n const tagClause = excludeTags.length > 0 ? ` AND ${tagPredicates}` : \"\";\n const tagParams = excludeTags.map((t) => `%,${t},%`);\n const rows = db\n .prepare(\n `SELECT * FROM facts WHERE (expires_at IS NULL OR expires_at > ?) AND superseded_at IS NULL\n AND (COALESCE(source_date, created_at) >= ?)\n AND category NOT IN (${placeholders})${tagClause}\n ORDER BY COALESCE(source_date, created_at) DESC`,\n )\n .all(nowSec, windowStartSec, ...exclude, ...tagParams) as Array<Record<string, unknown>>;\n return rows.map((row) => rowToMemoryEntry(row));\n}\n\nexport function getAll(\n db: DatabaseSync,\n options?: {\n includeSuperseded?: boolean;\n asOf?: number;\n scopeFilter?: ScopeFilter | null;\n },\n): MemoryEntry[] {\n const nowSec = Math.floor(Date.now() / 1000);\n const { includeSuperseded = false, asOf, scopeFilter } = options ?? {};\n const temporalFilter =\n asOf != null\n ? \" AND valid_from <= ? AND (valid_until IS NULL OR valid_until > ?)\"\n : includeSuperseded\n ? \"\"\n : \" AND superseded_at IS NULL\";\n const { clause: scopeClause, params: scopeParams } = scopeFilterClausePositional(scopeFilter);\n const params = asOf != null ? [...[nowSec, asOf, asOf], ...scopeParams] : [...[nowSec], ...scopeParams];\n const rows = db\n .prepare(\n `SELECT * FROM facts WHERE (expires_at IS NULL OR expires_at > ?)${temporalFilter}${scopeClause} ORDER BY created_at DESC`,\n )\n .all(...params) as Array<Record<string, unknown>>;\n return rows.map((row) => rowToMemoryEntry(row));\n}\n\nexport function getCount(db: DatabaseSync, options?: { includeSuperseded?: boolean }): number {\n const nowSec = Math.floor(Date.now() / 1000);\n const { includeSuperseded = false } = options ?? {};\n const temporalFilter = includeSuperseded ? \"\" : \" AND superseded_at IS NULL\";\n const row = db\n .prepare(`SELECT COUNT(*) AS count FROM facts WHERE (expires_at IS NULL OR expires_at > ?)${temporalFilter}`)\n .get(nowSec) as { count: number };\n return row?.count ?? 0;\n}\n\nexport function getAllIds(db: DatabaseSync): string[] {\n const nowSec = Math.floor(Date.now() / 1000);\n const rows = db\n .prepare(\"SELECT id FROM facts WHERE superseded_at IS NULL AND (expires_at IS NULL OR expires_at > ?)\")\n .all(nowSec) as Array<{ id: string }>;\n return rows.map((row) => row.id.toLowerCase());\n}\n\nexport function getBatch(\n db: DatabaseSync,\n offset: number,\n limit: number,\n options?: { includeSuperseded?: boolean },\n): MemoryEntry[] {\n const nowSec = Math.floor(Date.now() / 1000);\n const { includeSuperseded = false } = options ?? {};\n const temporalFilter = includeSuperseded ? \"\" : \" AND superseded_at IS NULL\";\n const rows = db\n .prepare(\n `SELECT * FROM facts WHERE (expires_at IS NULL OR expires_at > ?)${temporalFilter} ORDER BY created_at DESC LIMIT ? OFFSET ?`,\n )\n .all(nowSec, limit, offset) as Array<Record<string, unknown>>;\n return rows.map((row) => rowToMemoryEntry(row));\n}\n\nexport function listFacts(\n db: DatabaseSync,\n limit: number,\n filters?: {\n category?: string;\n entity?: string;\n key?: string;\n source?: string;\n tier?: string;\n },\n): MemoryEntry[] {\n const nowSec = Math.floor(Date.now() / 1000);\n if (\n (filters?.category != null && filters.category !== \"\" && !isValidCategory(filters.category)) ||\n (filters?.tier != null && filters.tier !== \"\" && !DASHBOARD_TIER_FILTER.has(filters.tier))\n ) {\n return [];\n }\n const parts: string[] = [\"(expires_at IS NULL OR expires_at > ?)\", \"superseded_at IS NULL\"];\n const params: SQLInputValue[] = [nowSec];\n if (filters?.category != null && isValidCategory(filters.category)) {\n parts.push(\"category = ?\");\n params.push(filters.category);\n }\n if (filters?.entity != null) {\n parts.push(\"lower(entity) = lower(?)\");\n params.push(filters.entity);\n }\n if (filters?.key != null) {\n parts.push(\"lower(key) = lower(?)\");\n params.push(filters.key);\n }\n if (filters?.source != null) {\n parts.push(\"source = ?\");\n params.push(filters.source);\n }\n if (filters?.tier != null && DASHBOARD_TIER_FILTER.has(filters.tier)) {\n parts.push(\"COALESCE(tier, 'warm') = ?\");\n params.push(filters.tier);\n }\n const where = parts.join(\" AND \");\n params.push(limit);\n const rows = db\n .prepare(`SELECT * FROM facts WHERE ${where} ORDER BY COALESCE(source_date, created_at) DESC LIMIT ?`)\n .all(...params) as Array<Record<string, unknown>>;\n return rows.map((row) => rowToMemoryEntry(row));\n}\n\n/**\n * Shared garbage detection utility for consistency across query-time, tiering, and SQL batch operations.\n * Returns true if the fact text looks like a reasoning trace or classifier artifact (#1559).\n */\nexport function isLikelyGarbage(input: {\n text: string;\n summary?: string | null;\n source?: string | null;\n category?: string | null;\n recall_count?: number;\n}): boolean {\n const text = input.text ?? \"\";\n const summary = input.summary ?? \"\";\n const combined = text + \"\\n\" + summary;\n\n // Reasoning traces embedded in fact content\n if (/<(?:redacted_)?think(?:ing)?>[\\s\\S]*?<\\/(?:redacted_)?think(?:ing)?>/i.test(combined)) return true;\n // Classifier / capability-hint artifacts\n if (/^Thinking Process:/im.test(combined)) return true;\n if (/^\\[(?:hot-memories|recall|hot\\/fact)\\]/im.test(combined)) return true;\n // Unhelpful source + long reasoning combo\n // Match reasoning trace patterns more precisely to avoid false positives with common English words\n if (input.source === \"auto-capture\" && combined.length > 3000) {\n // Reasoning-specific phrases that indicate LLM reasoning artifacts\n const reasoningPhrases =\n /\\b(?:let me think|my reasoning|step[- ]by[- ]step|thought process|reasoning process|analysis process)\\b/i;\n // Multiple paragraph breaks suggesting stream-of-consciousness\n const multipleBreaks = (combined.match(/\\n\\n/g) || []).length > 10;\n if (reasoningPhrases.test(combined) || multipleBreaks) {\n return true;\n }\n }\n // High recall counts for \"other\" category (self-reinforcing garbage loop)\n if (input.category === \"other\" && (input.recall_count ?? 0) > 500) {\n return true;\n }\n return false;\n}\n\n/** Quality score for HOT injection: 0–1. Garbage facts score 0 and are excluded. */\nfunction hotQualityScore(entry: MemoryEntry): number {\n if (\n isLikelyGarbage({\n text: entry.text,\n summary: entry.summary,\n source: entry.source,\n category: entry.category,\n recall_count: entry.recallCount,\n })\n )\n return 0;\n const text = entry.text ?? \"\";\n // Penalise very long unconstrained text (likely full conversation dumps)\n if (text.length > 8000) return 0.3;\n return 1.0;\n}\n\nexport function getHotFacts(db: DatabaseSync, maxTokens: number, scopeFilter?: ScopeFilter | null): SearchResult[] {\n const nowSec = Math.floor(Date.now() / 1000);\n const { clause: scopeClause, params: scopeParams } = scopeFilterClausePositional(scopeFilter);\n const rows = db\n .prepare(\n `SELECT * FROM facts\n WHERE tier = 'hot' AND superseded_at IS NULL AND (expires_at IS NULL OR expires_at > ?)\n ${scopeClause}\n ORDER BY COALESCE(last_accessed, last_confirmed_at, created_at) DESC`,\n )\n .all(nowSec, ...scopeParams) as Array<Record<string, unknown>>;\n const results: SearchResult[] = [];\n let usedTokens = 0;\n for (const row of rows) {\n if (usedTokens >= maxTokens) break;\n const entry = rowToMemoryEntry(row);\n // Apply quality filter: exclude reasoning traces, prompt artifacts, and garbage (#1559)\n const score = hotQualityScore(entry);\n if (score === 0) continue;\n const tokens = estimateTokensForDisplay(entry.summary || entry.text);\n if (usedTokens + tokens > maxTokens) {\n continue;\n }\n usedTokens += tokens;\n results.push({ entry, score, backend: \"sqlite\" as const });\n }\n return results;\n}\n\nexport function getByCategory(db: DatabaseSync, category: string): MemoryEntry[] {\n const rows = db.prepare(\"SELECT * FROM facts WHERE category = ? ORDER BY created_at DESC\").all(category) as Array<\n Record<string, unknown>\n >;\n return rows.map((row) => rowToMemoryEntry(row));\n}\n\n/**\n * Targeted project-fact query for active-task projection (#1553).\n * Only loads facts with category='project' and source='active-task', avoiding a full table scan.\n */\nexport function getProjectFacts(db: DatabaseSync, limit = 8000, scopeFilter?: ScopeFilter | null): MemoryEntry[] {\n const nowSec = Math.floor(Date.now() / 1000);\n const { clause: scopeClause, params: scopeParams } = scopeFilterClausePositional(scopeFilter);\n const rows = db\n .prepare(\n `SELECT * FROM facts\n WHERE category = 'project'\n AND source = 'active-task'\n AND (expires_at IS NULL OR expires_at > ?)\n AND superseded_at IS NULL\n ${scopeClause}\n ORDER BY created_at DESC\n LIMIT ?`,\n )\n .all(nowSec, ...scopeParams, limit) as Array<Record<string, unknown>>;\n return rows.map((row) => rowToMemoryEntry(row));\n}\n\nexport function listFactsByCategory(db: DatabaseSync, category: string, limit = 100): MemoryEntry[] {\n const rows = db\n .prepare(\n \"SELECT * FROM facts WHERE category = ? AND (superseded_at IS NULL) ORDER BY COALESCE(source_date, created_at) DESC LIMIT ?\",\n )\n .all(category, limit) as Array<Record<string, unknown>>;\n return rows.map((row) => rowToMemoryEntry(row));\n}\n\nexport function listDirectives(db: DatabaseSync, limit = 100): MemoryEntry[] {\n const rows = db\n .prepare(\n `SELECT * FROM facts WHERE source LIKE 'directive:%' AND (superseded_at IS NULL) ORDER BY created_at DESC LIMIT ?`,\n )\n .all(limit) as Array<Record<string, unknown>>;\n return rows.map((row) => rowToMemoryEntry(row));\n}\n\nexport function getMaxCreatedAtByCategory(db: DatabaseSync, category: string): number | null {\n const row = db\n .prepare(\"SELECT MAX(created_at) as max_created_at FROM facts WHERE category = ? AND (superseded_at IS NULL)\")\n .get(category) as Record<string, unknown> | undefined;\n if (!row) return null;\n const maxCreatedAt = row.max_created_at;\n if (typeof maxCreatedAt === \"number\" && Number.isFinite(maxCreatedAt)) {\n return maxCreatedAt;\n }\n return null;\n}\n\nexport function updateCategory(db: DatabaseSync, id: string, category: string): boolean {\n const result = db.prepare(\"UPDATE facts SET category = ? WHERE id = ?\").run(category, id);\n return result.changes > 0;\n}\n\n/**\n * Return \"other\" facts that have not been classify-attempted yet, or whose\n * created_at is newer than the last attempt (text was replaced / re-ingested).\n */\nexport function getUnattemptedOtherFacts(db: DatabaseSync): MemoryEntry[] {\n const rows = db\n .prepare(\n `SELECT * FROM facts\n WHERE category = 'other'\n AND (last_classify_attempt_at IS NULL OR created_at > last_classify_attempt_at)\n ORDER BY created_at DESC`,\n )\n .all() as Array<Record<string, unknown>>;\n return rows.map((row) => rowToMemoryEntry(row));\n}\n\n/** Mark facts as having been attempted by the auto-classifier (batch). */\nexport function markClassifyAttempt(db: DatabaseSync, ids: string[]): void {\n if (ids.length === 0) return;\n const now = Math.floor(Date.now() / 1000);\n const stmt = db.prepare(\"UPDATE facts SET last_classify_attempt_at = ? WHERE id = ?\");\n const run = (): void => {\n for (const id of ids) {\n stmt.run(now, id);\n }\n };\n createTransaction(db, run)();\n}\n"],"mappings":"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"}