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":"contradictions.js","names":[],"sources":["../../../backends/facts-db/contradictions.ts"],"sourcesContent":["/**\n * Contradiction detection and resolution (Issue #157) (Issue #954 split).\n * Project-state latest-wins (LWW) policy added in Issue #1636.\n */\nimport { randomUUID } from \"node:crypto\";\nimport type { SQLInputValue } from \"node:sqlite\";\nimport type { DatabaseSync } from \"node:sqlite\";\n\nimport { capturePluginError } from \"../../services/error-reporter.js\";\nimport type { MemoryEntry } from \"../../types/memory.js\";\nimport { createTransaction } from \"../../utils/sqlite-transaction.js\";\nimport { parseTags, serializeTags } from \"../../utils/tags.js\";\nimport { rowToMemoryEntry } from \"./row-mapper.js\";\nimport type { MemoryLinkType } from \"./types.js\";\n\nexport interface ContradictionRecord {\n id: string;\n factIdNew: string;\n factIdOld: string;\n detectedAt: string;\n resolved: boolean;\n resolution: \"superseded\" | \"kept\" | \"merged\" | null;\n oldFactOriginalConfidence?: number;\n}\n\nexport function updateConfidence(db: DatabaseSync, id: string, delta: number): number | null {\n const row = db.prepare(\"SELECT confidence FROM facts WHERE id = ?\").get(id) as { confidence: number } | undefined;\n if (!row) return null;\n const current = row.confidence ?? 1.0;\n const updated = Math.max(0.1, Math.min(1.0, current + delta));\n db.prepare(\"UPDATE facts SET confidence = ? WHERE id = ?\").run(updated, id);\n return updated;\n}\n\nexport function setConfidenceTo(db: DatabaseSync, id: string, value: number): number | null {\n const row = db.prepare(\"SELECT confidence FROM facts WHERE id = ?\").get(id) as { confidence: number } | undefined;\n if (!row) return null;\n const updated = Math.max(0.1, Math.min(1, value));\n db.prepare(\"UPDATE facts SET confidence = ? WHERE id = ?\").run(updated, id);\n return updated;\n}\n\nexport function addTag(db: DatabaseSync, id: string, tag: string): void {\n const trimmed = tag.trim();\n const normalized = trimmed.toLowerCase();\n if (!normalized || normalized.includes(\",\")) return;\n const row = db.prepare(\"SELECT tags FROM facts WHERE id = ?\").get(id) as { tags: string | null } | undefined;\n if (!row) return;\n const tags = parseTags(row.tags);\n if (tags.some((t) => t.toLowerCase() === normalized)) return;\n tags.push(normalized);\n db.prepare(\"UPDATE facts SET tags = ? WHERE id = ?\").run(serializeTags(tags), id);\n}\n\nexport function findConflictingFacts(\n db: DatabaseSync,\n entity: string,\n key: string,\n value: string,\n excludeFactId: string,\n scope?: string | null,\n scopeTarget?: string | null,\n): MemoryEntry[] {\n const nowSec = Math.floor(Date.now() / 1000);\n const scopeClause = scope\n ? scopeTarget != null\n ? \"AND scope = ? AND scope_target = ?\"\n : \"AND scope = ? AND scope_target IS NULL\"\n : \"\";\n const baseParams: SQLInputValue[] = [entity, key, value, excludeFactId, nowSec];\n const scopeParams: SQLInputValue[] = scope ? (scopeTarget != null ? [scope, scopeTarget] : [scope]) : [];\n const rows = db\n .prepare(\n `SELECT * FROM facts\n WHERE lower(entity) = lower(?)\n AND lower(key) = lower(?)\n AND lower(value) != lower(?)\n AND id != ?\n AND superseded_at IS NULL\n AND (expires_at IS NULL OR expires_at > ?)\n ${scopeClause}\n ORDER BY created_at DESC`,\n )\n .all(...baseParams, ...scopeParams) as Array<Record<string, unknown>>;\n return rows.map((r) => rowToMemoryEntry(r));\n}\n\nexport function recordContradiction(\n db: DatabaseSync,\n factIdNew: string,\n factIdOld: string,\n createLink: (a: string, b: string, t: MemoryLinkType, s?: number) => string,\n): { id: string; oldFactOriginalConfidence: number } {\n const id = randomUUID();\n const detectedAt = new Date().toISOString();\n let oldFactOriginalConfidence = 1.0;\n\n const tx = createTransaction(db, () => {\n const oldFactRow = db.prepare(\"SELECT confidence FROM facts WHERE id = ?\").get(factIdOld) as\n | { confidence: number }\n | undefined;\n oldFactOriginalConfidence = oldFactRow?.confidence ?? 1.0;\n\n db.prepare(\n `INSERT INTO contradictions (id, fact_id_new, fact_id_old, detected_at, resolved, resolution, old_fact_original_confidence)\n VALUES (?, ?, ?, ?, 0, NULL, ?)`,\n ).run(id, factIdNew, factIdOld, detectedAt, oldFactOriginalConfidence);\n\n createLink(factIdNew, factIdOld, \"CONTRADICTS\", 1.0);\n\n updateConfidence(db, factIdOld, -0.2);\n });\n tx();\n return { id, oldFactOriginalConfidence };\n}\n\nexport function detectContradictions(\n db: DatabaseSync,\n newFactId: string,\n entity: string | null | undefined,\n key: string | null | undefined,\n value: string | null | undefined,\n scope: string | null | undefined,\n scopeTarget: string | null | undefined,\n createLink: (a: string, b: string, t: MemoryLinkType, s?: number) => string,\n): Array<{ contradictionId: string; oldFactId: string; oldFactOriginalConfidence: number }> {\n if (!entity?.trim() || !key?.trim() || !value?.trim()) return [];\n\n const conflicting = findConflictingFacts(db, entity.trim(), key.trim(), value.trim(), newFactId, scope, scopeTarget);\n const results: Array<{ contradictionId: string; oldFactId: string; oldFactOriginalConfidence: number }> = [];\n\n for (const old of conflicting) {\n if (old.value?.toLowerCase() === value.trim().toLowerCase()) continue;\n const contradiction = recordContradiction(db, newFactId, old.id, createLink);\n results.push({\n contradictionId: contradiction.id,\n oldFactId: old.id,\n oldFactOriginalConfidence: contradiction.oldFactOriginalConfidence,\n });\n }\n\n return results;\n}\n\nexport function getContradictions(db: DatabaseSync, factId?: string): ContradictionRecord[] {\n const rows = factId\n ? (db\n .prepare(\"SELECT * FROM contradictions WHERE fact_id_new = ? OR fact_id_old = ? ORDER BY detected_at DESC\")\n .all(factId, factId) as Array<Record<string, unknown>>)\n : (db.prepare(\"SELECT * FROM contradictions WHERE resolved = 0 ORDER BY detected_at DESC\").all() as Array<\n Record<string, unknown>\n >);\n return rows.map((r) => ({\n id: r.id as string,\n factIdNew: r.fact_id_new as string,\n factIdOld: r.fact_id_old as string,\n detectedAt: r.detected_at as string,\n resolved: (r.resolved as number) === 1,\n resolution: (r.resolution as \"superseded\" | \"kept\" | \"merged\" | null) ?? null,\n oldFactOriginalConfidence:\n r.old_fact_original_confidence == null ? undefined : (r.old_fact_original_confidence as number),\n }));\n}\n\nexport function resolveContradiction(\n db: DatabaseSync,\n contradictionId: string,\n resolution: \"superseded\" | \"kept\" | \"merged\",\n): boolean {\n const result = db\n .prepare(\"UPDATE contradictions SET resolved = 1, resolution = ? WHERE id = ? AND resolved = 0\")\n .run(resolution, contradictionId);\n return result.changes > 0;\n}\n\nexport function isContradicted(db: DatabaseSync, factId: string): boolean {\n const row = db\n .prepare(\"SELECT 1 FROM contradictions WHERE (fact_id_old = ? OR fact_id_new = ?) AND resolved = 0 LIMIT 1\")\n .get(factId, factId);\n return row != null;\n}\n\nexport function getContradictedIds(db: DatabaseSync, factIds: string[]): Set<string> {\n if (factIds.length === 0) return new Set();\n const result = new Set<string>();\n const CHUNK = 499;\n for (let i = 0; i < factIds.length; i += CHUNK) {\n const chunk = factIds.slice(i, i + CHUNK);\n const placeholders = chunk.map(() => \"?\").join(\",\");\n const rows = db\n .prepare(\n `SELECT fact_id_old AS id FROM contradictions WHERE fact_id_old IN (${placeholders}) AND resolved = 0\n UNION\n SELECT fact_id_new AS id FROM contradictions WHERE fact_id_new IN (${placeholders}) AND resolved = 0`,\n )\n .all(...chunk, ...chunk) as Array<{ id: string }>;\n for (const r of rows) result.add(r.id);\n }\n return result;\n}\n\nexport function isFactVerified(db: DatabaseSync, factId: string): boolean {\n const row = db.prepare(\"SELECT 1 FROM verified_facts WHERE fact_id = ? LIMIT 1\").get(factId);\n return row != null;\n}\n\nfunction isAutoResolvableContradiction(\n db: DatabaseSync,\n contradiction: ContradictionRecord,\n newFact: MemoryEntry,\n oldFact: MemoryEntry,\n): boolean {\n const newConf = newFact.confidence ?? 1.0;\n const oldConf = contradiction.oldFactOriginalConfidence ?? oldFact.confidence ?? 1.0;\n const newIsNewer = newFact.createdAt >= oldFact.createdAt;\n const newIsHigherConf = newConf > oldConf;\n const newIsFromUser = newFact.source === \"conversation\" || newFact.source === \"cli\";\n\n if (!(newIsNewer && newIsHigherConf && newIsFromUser)) return false;\n if (oldFact.supersededAt != null) return true;\n return !isFactVerified(db, contradiction.factIdOld);\n}\n\ntype AutoResolutionDecision =\n | { autoResolvable: false }\n | {\n autoResolvable: true;\n resolution: \"superseded\" | \"kept\";\n requiresSupersede: boolean;\n };\n\nfunction getAutoResolutionDecision(\n db: DatabaseSync,\n contradiction: ContradictionRecord,\n newFact: MemoryEntry | null,\n oldFact: MemoryEntry | null,\n): AutoResolutionDecision {\n if (!newFact && !oldFact) {\n return { autoResolvable: true, resolution: \"superseded\", requiresSupersede: false };\n }\n if (!newFact && oldFact) {\n return { autoResolvable: true, resolution: \"kept\", requiresSupersede: false };\n }\n if (newFact && !oldFact) {\n return { autoResolvable: true, resolution: \"superseded\", requiresSupersede: false };\n }\n if (!newFact || !oldFact) return { autoResolvable: false };\n if (!isAutoResolvableContradiction(db, contradiction, newFact, oldFact)) {\n return { autoResolvable: false };\n }\n return {\n autoResolvable: true,\n resolution: \"superseded\",\n requiresSupersede: oldFact.supersededAt == null,\n };\n}\n\nexport function previewResolveContradictionsAuto(\n db: DatabaseSync,\n getById: (id: string) => MemoryEntry | null,\n): {\n autoResolvable: Array<{\n contradictionId: string;\n factIdNew: string;\n factIdOld: string;\n }>;\n ambiguous: Array<{\n contradictionId: string;\n factIdNew: string;\n factIdOld: string;\n }>;\n} {\n const unresolved = getContradictions(db);\n const autoResolvable: Array<{\n contradictionId: string;\n factIdNew: string;\n factIdOld: string;\n }> = [];\n const ambiguous: Array<{\n contradictionId: string;\n factIdNew: string;\n factIdOld: string;\n }> = [];\n\n for (const c of unresolved) {\n const newFact = getById(c.factIdNew);\n const oldFact = getById(c.factIdOld);\n const decision = getAutoResolutionDecision(db, c, newFact, oldFact);\n if (decision.autoResolvable) {\n autoResolvable.push({\n contradictionId: c.id,\n factIdNew: c.factIdNew,\n factIdOld: c.factIdOld,\n });\n } else {\n ambiguous.push({\n contradictionId: c.id,\n factIdNew: c.factIdNew,\n factIdOld: c.factIdOld,\n });\n }\n }\n\n return { autoResolvable, ambiguous };\n}\n\nexport function resolveContradictionsAuto(\n db: DatabaseSync,\n getById: (id: string) => MemoryEntry | null,\n supersede: (oldId: string, newId: string | null) => boolean,\n): {\n autoResolved: Array<{\n contradictionId: string;\n factIdNew: string;\n factIdOld: string;\n }>;\n ambiguous: Array<{\n contradictionId: string;\n factIdNew: string;\n factIdOld: string;\n }>;\n} {\n const unresolved = getContradictions(db);\n const autoResolved: Array<{\n contradictionId: string;\n factIdNew: string;\n factIdOld: string;\n }> = [];\n const ambiguous: Array<{\n contradictionId: string;\n factIdNew: string;\n factIdOld: string;\n }> = [];\n\n for (const c of unresolved) {\n const newFact = getById(c.factIdNew);\n const oldFact = getById(c.factIdOld);\n const decision = getAutoResolutionDecision(db, c, newFact, oldFact);\n if (!decision.autoResolvable) {\n ambiguous.push({\n contradictionId: c.id,\n factIdNew: c.factIdNew,\n factIdOld: c.factIdOld,\n });\n continue;\n }\n\n if (decision.requiresSupersede) {\n const superseded = supersede(c.factIdOld, c.factIdNew);\n if (!superseded) {\n ambiguous.push({\n contradictionId: c.id,\n factIdNew: c.factIdNew,\n factIdOld: c.factIdOld,\n });\n continue;\n }\n }\n\n resolveContradiction(db, c.id, decision.resolution);\n if (decision.resolution === \"kept\" && c.oldFactOriginalConfidence != null) {\n db.prepare(\"UPDATE facts SET confidence = ? WHERE id = ?\").run(c.oldFactOriginalConfidence, c.factIdOld);\n }\n autoResolved.push({\n contradictionId: c.id,\n factIdNew: c.factIdNew,\n factIdOld: c.factIdOld,\n });\n }\n\n return { autoResolved, ambiguous };\n}\n\n// ---------------------------------------------------------------------------\n// Project-state latest-wins (LWW) policy (Issue #1636)\n// ---------------------------------------------------------------------------\n\n/**\n * Mutable project/task state keys eligible for the latest-wins policy.\n *\n * Each key represents a \"current-state snapshot\" — a single mutable fact whose\n * value is completely replaced when new information arrives (e.g. `status` moves\n * from `in_progress` to `done`; `next` is replaced with a new action item).\n * These differ from append-style project facts (e.g. decisions, milestones) where\n * both old and new values may independently hold — those must go through manual\n * contradiction review instead.\n */\nexport const PROJECT_STATE_LWW_KEYS: ReadonlySet<string> = new Set([\n \"status\",\n \"next\",\n \"task_updated\",\n \"related_session\",\n \"coverage\",\n \"last_live_verified_at\",\n \"live_state_hash\",\n \"last_actionable_blocker\",\n \"owner\",\n]);\n\n/**\n * Sources trusted for project-state LWW resolution.\n * `distillation` is excluded by default to keep automated distilled facts conservative.\n */\nexport const PROJECT_STATE_LWW_TRUSTED_SOURCES: ReadonlySet<string> = new Set([\n \"conversation\",\n \"cli\",\n \"active-task\",\n \"tool\",\n]);\n\n/** A single candidate contradiction eligible for project-state LWW resolution. */\nexport interface ProjectStateLwwCandidate {\n contradictionId: string;\n factIdNew: string;\n factIdOld: string;\n entity: string;\n key: string;\n scope: string | null;\n scopeTarget: string | null;\n /** epoch seconds */\n newFactDate: number;\n /** epoch seconds */\n oldFactDate: number;\n newSource: string;\n oldSource: string;\n newConf: number;\n oldConf: number;\n newValueExcerpt: string;\n oldValueExcerpt: string;\n /** `supersede`: LWW policy applies — newer trusted fact wins. `manual-review`: skipped by LWW. */\n action: \"supersede\" | \"manual-review\";\n /** True when the entity/key pair appears to have been reused for follow-up work. */\n possibleOverloadedEntity: boolean;\n}\n\n/** Grouped result of a project-state LWW resolution pass. */\nexport interface ProjectStateLwwResult {\n groups: Array<{\n entity: string;\n key: string;\n scope: string | null;\n scopeTarget: string | null;\n candidates: ProjectStateLwwCandidate[];\n }>;\n totalCandidates: number;\n wouldSupersede: number;\n wouldManualReview: number;\n /** Number of contradictions actually resolved (0 in dry-run). */\n applied: number;\n}\n\n/**\n * Distinct PR/issue reference count threshold above which an entity is considered\n * \"possibly overloaded\" (i.e. the same entity key was reused across follow-up work).\n * Pairs above this threshold are still reported but flagged for human review.\n */\nconst MAX_EXPECTED_REFS_PER_ENTITY = 2;\n\n/** Extract distinct PR/issue ref numbers like `#1234` from text. */\nfunction extractRefNumbers(text: string): Set<string> {\n const matches = text.match(/#\\d+/g);\n return new Set(matches ?? []);\n}\n\n/** Heuristic: flag if multiple distinct PR/issue numbers appear across the two facts. */\nfunction isPossiblyOverloadedEntity(newFact: MemoryEntry, oldFact: MemoryEntry): boolean {\n const combined = `${newFact.value ?? \"\"} ${newFact.text ?? \"\"} ${oldFact.value ?? \"\"} ${oldFact.text ?? \"\"}`;\n const refs = extractRefNumbers(combined);\n return refs.size > MAX_EXPECTED_REFS_PER_ENTITY;\n}\n\nfunction truncateExcerpt(s: string, maxLen: number): string {\n const t = s.replace(/\\s+/g, \" \").trim();\n return t.length <= maxLen ? t : `${t.slice(0, maxLen - 1)}…`;\n}\n\n/**\n * Resolve stale project-state contradictions using a latest-wins policy.\n *\n * Eligible pairs must satisfy all of:\n * - Both facts are category `project`\n * - The key (case-insensitive) is in {@link PROJECT_STATE_LWW_KEYS}\n * - The newer fact's source is in {@link PROJECT_STATE_LWW_TRUSTED_SOURCES}\n * - The newer fact is strictly newer (`createdAt >` older)\n * - The newer fact's confidence ≥ the older fact's original confidence\n *\n * In dry-run mode no mutations are made; results describe what would happen.\n * In apply mode, qualifying contradictions are resolved as `superseded` and\n * the older fact is marked superseded with provenance preserved.\n */\n\nexport type LwwEligibilityResult =\n | { eligible: false }\n | { eligible: true; qualifies: boolean; newConf: number; oldConf: number };\n\n/**\n * Evaluate whether a (newFact, oldFact) pair is eligible for project-state LWW\n * and, if so, whether the newer fact qualifies to supersede the older.\n *\n * Shared between the write-time path in `FactsDB.detectContradictions` and the\n * batch `resolveProjectStateLww` pass to keep eligibility logic in one place.\n *\n * @param oldFactOriginalConfidence The confidence captured at contradiction-detection\n * time (before the -0.2 penalty). Missing values are treated as non-qualifying:\n * LWW will require manual review instead of guessing from a possibly penalized score.\n */\nexport function evaluateLwwEligibility(\n newFact: MemoryEntry,\n oldFact: MemoryEntry,\n oldFactOriginalConfidence?: number,\n): LwwEligibilityResult {\n if (newFact.category !== \"project\" || oldFact.category !== \"project\") return { eligible: false };\n const keyLower = (newFact.key ?? oldFact.key ?? \"\").trim().toLowerCase();\n if (!PROJECT_STATE_LWW_KEYS.has(keyLower)) return { eligible: false };\n if (!PROJECT_STATE_LWW_TRUSTED_SOURCES.has(newFact.source ?? \"\")) return { eligible: false };\n const newConf = newFact.confidence ?? 1.0;\n const oldConf = oldFactOriginalConfidence ?? oldFact.confidence ?? 1.0;\n if (oldFactOriginalConfidence == null) {\n return { eligible: true, qualifies: false, newConf, oldConf };\n }\n const qualifies = newFact.createdAt > oldFact.createdAt && newConf >= oldConf;\n return { eligible: true, qualifies, newConf, oldConf };\n}\n\nexport function resolveProjectStateLww(\n db: DatabaseSync,\n getById: (id: string) => MemoryEntry | null,\n supersede: (oldId: string, newId: string | null) => boolean,\n options: { dryRun?: boolean } = {},\n): ProjectStateLwwResult {\n const { dryRun = false } = options;\n const unresolved = getContradictions(db);\n\n const groupMap = new Map<\n string,\n {\n entity: string;\n key: string;\n scope: string | null;\n scopeTarget: string | null;\n candidates: ProjectStateLwwCandidate[];\n }\n >();\n let applied = 0;\n\n for (const c of unresolved) {\n const newFact = getById(c.factIdNew);\n const oldFact = getById(c.factIdOld);\n if (!newFact || !oldFact) continue;\n\n const lww = evaluateLwwEligibility(newFact, oldFact, c.oldFactOriginalConfidence);\n if (!lww.eligible) continue;\n\n const { qualifies, newConf, oldConf } = lww;\n const overloaded = isPossiblyOverloadedEntity(newFact, oldFact);\n const action: \"supersede\" | \"manual-review\" =\n qualifies && !overloaded && !isFactVerified(db, c.factIdOld) ? \"supersede\" : \"manual-review\";\n\n const entity = newFact.entity ?? oldFact.entity ?? \"?\";\n const key = newFact.key ?? oldFact.key ?? \"?\";\n const scope = newFact.scope ?? null;\n const scopeTarget = newFact.scopeTarget ?? null;\n\n const groupKey = `${entity}\\0${key}\\0${scope ?? \"\"}\\0${scopeTarget ?? \"\"}`;\n let group = groupMap.get(groupKey);\n if (!group) {\n group = { entity, key, scope, scopeTarget, candidates: [] };\n groupMap.set(groupKey, group);\n }\n group.candidates.push({\n contradictionId: c.id,\n factIdNew: c.factIdNew,\n factIdOld: c.factIdOld,\n entity,\n key,\n scope,\n scopeTarget,\n newFactDate: newFact.createdAt,\n oldFactDate: oldFact.createdAt,\n newSource: newFact.source ?? \"unknown\",\n oldSource: oldFact.source ?? \"unknown\",\n newConf,\n oldConf,\n newValueExcerpt: truncateExcerpt(newFact.value ?? newFact.text ?? \"\", 80),\n oldValueExcerpt: truncateExcerpt(oldFact.value ?? oldFact.text ?? \"\", 80),\n action,\n possibleOverloadedEntity: overloaded,\n });\n\n if (!dryRun && action === \"supersede\") {\n if (oldFact.supersededAt != null) {\n resolveContradiction(db, c.id, \"superseded\");\n applied++;\n } else {\n const superseded = supersede(c.factIdOld, c.factIdNew);\n if (superseded) {\n resolveContradiction(db, c.id, \"superseded\");\n applied++;\n }\n }\n }\n }\n\n const groups = Array.from(groupMap.values());\n let totalCandidates = 0;\n let wouldSupersede = 0;\n let wouldManualReview = 0;\n for (const g of groups) {\n for (const cand of g.candidates) {\n totalCandidates++;\n if (cand.action === \"supersede\") wouldSupersede++;\n else wouldManualReview++;\n }\n }\n\n return { groups, totalCandidates, wouldSupersede, wouldManualReview, applied };\n}\n\nexport interface ContradictionReviewItem {\n contradictionId: string;\n factIdNew: string;\n factIdOld: string;\n entity: string;\n key: string;\n scope: string | null;\n scopeTarget: string | null;\n newFactDate: number;\n oldFactDate: number;\n newSource: string;\n oldSource: string;\n newConf: number;\n oldConf: number;\n newValueExcerpt: string;\n oldValueExcerpt: string;\n newTextExcerpt: string;\n oldTextExcerpt: string;\n possibleOverloadedEntity: boolean;\n suggestedDecision: \"keep_new\" | \"keep_old\" | \"manual_review\";\n suggestedStrategy: string;\n suggestedConfidence: number;\n suggestedReason: string;\n}\n\nexport interface LlmContradictionDecision {\n decision: \"keep_new\" | \"keep_old\" | \"merge\" | \"manual_review\";\n confidence: number;\n reason: string;\n mergedFactText?: string | null;\n}\n\nexport interface ResolveContradictionsAutoOptions {\n dryRun?: boolean;\n targetRate?: number;\n llm?: boolean;\n llmConfidenceThreshold?: number;\n adjudicate?: (item: ContradictionReviewItem) => Promise<LlmContradictionDecision>;\n actor?: string;\n toolVersion?: string;\n model?: string | null;\n}\n\nexport interface ResolveContradictionsAutoResult {\n total: number;\n deterministic: number;\n llm: number;\n merged: number;\n manualReview: number;\n applied: boolean;\n decisionsApplied: number;\n targetRate: number;\n achievedRate: number;\n targetMet: boolean;\n reviewItems: ContradictionReviewItem[];\n}\n\nexport interface ContradictionReviewDecision {\n contradictionId: string;\n decision: \"keep_new\" | \"keep_old\" | \"merge\" | \"manual_review\";\n reason?: string;\n confidence?: number;\n mergedFactText?: string | null;\n}\n\nexport interface ApplyContradictionReviewResult {\n applied: number;\n keptNew: number;\n keptOld: number;\n manualReview: number;\n rejected: number;\n errors: string[];\n}\n\nexport interface ContradictionResolutionAuditRow {\n id: string;\n contradictionId: string;\n keptFactId: string;\n supersededFactId: string;\n strategy: string;\n confidence: number;\n reason: string;\n decidedAt: number;\n actor: string;\n toolVersion: string | null;\n mode: \"auto\" | \"review\";\n model: string | null;\n}\n\nconst DEFAULT_CONTRADICTION_TARGET_RATE = 0.8;\nconst DEFAULT_LLM_AUTO_APPLY_CONFIDENCE = 0.9;\n\nfunction buildContradictionReviewItem(\n contradiction: ContradictionRecord,\n newFact: MemoryEntry,\n oldFact: MemoryEntry,\n): ContradictionReviewItem {\n const oldConf = contradiction.oldFactOriginalConfidence ?? oldFact.confidence ?? 1.0;\n const newConf = newFact.confidence ?? 1.0;\n return {\n contradictionId: contradiction.id,\n factIdNew: contradiction.factIdNew,\n factIdOld: contradiction.factIdOld,\n entity: newFact.entity ?? oldFact.entity ?? \"?\",\n key: newFact.key ?? oldFact.key ?? \"?\",\n scope: newFact.scope ?? oldFact.scope ?? null,\n scopeTarget: newFact.scopeTarget ?? oldFact.scopeTarget ?? null,\n newFactDate: newFact.createdAt,\n oldFactDate: oldFact.createdAt,\n newSource: newFact.source ?? \"unknown\",\n oldSource: oldFact.source ?? \"unknown\",\n newConf,\n oldConf,\n newValueExcerpt: truncateExcerpt(newFact.value ?? newFact.text ?? \"\", 80),\n oldValueExcerpt: truncateExcerpt(oldFact.value ?? oldFact.text ?? \"\", 80),\n newTextExcerpt: truncateExcerpt(newFact.text ?? newFact.value ?? \"\", 120),\n oldTextExcerpt: truncateExcerpt(oldFact.text ?? oldFact.value ?? \"\", 120),\n possibleOverloadedEntity: isPossiblyOverloadedEntity(newFact, oldFact),\n suggestedDecision: \"manual_review\",\n suggestedStrategy: \"manual-review\",\n suggestedConfidence: 0,\n suggestedReason: \"No safe deterministic resolution matched.\",\n };\n}\n\nfunction compareReviewItems(a: ContradictionReviewItem, b: ContradictionReviewItem): number {\n return (\n a.entity.localeCompare(b.entity) ||\n a.key.localeCompare(b.key) ||\n (a.scope ?? \"\").localeCompare(b.scope ?? \"\") ||\n (a.scopeTarget ?? \"\").localeCompare(b.scopeTarget ?? \"\") ||\n a.contradictionId.localeCompare(b.contradictionId)\n );\n}\n\nfunction clampConfidence(value: number | undefined): number {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return 0;\n return Math.max(0, Math.min(1, value));\n}\n\nfunction insertContradictionResolutionAudit(db: DatabaseSync, row: Omit<ContradictionResolutionAuditRow, \"id\">): void {\n db.prepare(\n `INSERT INTO contradiction_resolution_audit\n (id, contradiction_id, kept_fact_id, superseded_fact_id, strategy, confidence, reason, decided_at, actor, tool_version, mode, model)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n ).run(\n randomUUID(),\n row.contradictionId,\n row.keptFactId,\n row.supersededFactId,\n row.strategy,\n row.confidence,\n row.reason,\n row.decidedAt,\n row.actor,\n row.toolVersion,\n row.mode,\n row.model,\n );\n}\n\nexport function getContradictionResolutionAudit(\n db: DatabaseSync,\n contradictionId?: string,\n): ContradictionResolutionAuditRow[] {\n const query = contradictionId\n ? \"SELECT * FROM contradiction_resolution_audit WHERE contradiction_id = ? ORDER BY decided_at ASC, id ASC\"\n : \"SELECT * FROM contradiction_resolution_audit ORDER BY decided_at ASC, id ASC\";\n const rows = (contradictionId ? db.prepare(query).all(contradictionId) : db.prepare(query).all()) as Array<\n Record<string, unknown>\n >;\n return rows.map((row) => ({\n id: row.id as string,\n contradictionId: row.contradiction_id as string,\n keptFactId: row.kept_fact_id as string,\n supersededFactId: row.superseded_fact_id as string,\n strategy: row.strategy as string,\n confidence: Number(row.confidence ?? 0),\n reason: (row.reason as string) ?? \"\",\n decidedAt: Number(row.decided_at ?? 0),\n actor: (row.actor as string) ?? \"resolve-contradictions\",\n toolVersion: (row.tool_version as string | null) ?? null,\n mode: ((row.mode as \"auto\" | \"review\" | null) ?? \"auto\") === \"review\" ? \"review\" : \"auto\",\n model: (row.model as string | null) ?? null,\n }));\n}\n\ntype PersistedDecision = {\n contradictionId: string;\n keptFactId: string;\n supersededFactId: string;\n resolution: \"superseded\" | \"kept\";\n strategy: string;\n confidence: number;\n reason: string;\n actor: string;\n toolVersion?: string | null;\n mode: \"auto\" | \"review\";\n model?: string | null;\n};\n\nfunction persistContradictionDecision(\n db: DatabaseSync,\n getById: (id: string) => MemoryEntry | null,\n supersede: (oldId: string, newId: string | null) => boolean,\n decision: PersistedDecision,\n): boolean {\n const contradiction = db.prepare(\"SELECT resolved FROM contradictions WHERE id = ?\").get(decision.contradictionId) as\n | { resolved: number }\n | undefined;\n if (!contradiction || contradiction.resolved === 1) return false;\n\n let applied = false;\n const tx = createTransaction(db, () => {\n const supersededFact = getById(decision.supersededFactId);\n if (!supersededFact) {\n return;\n }\n\n if (supersededFact.supersededAt == null) {\n const didSupersede = supersede(decision.supersededFactId, decision.keptFactId);\n if (!didSupersede) {\n return;\n }\n }\n\n const resolved = resolveContradiction(db, decision.contradictionId, decision.resolution);\n if (!resolved) {\n return;\n }\n\n insertContradictionResolutionAudit(db, {\n contradictionId: decision.contradictionId,\n keptFactId: decision.keptFactId,\n supersededFactId: decision.supersededFactId,\n strategy: decision.strategy,\n confidence: decision.confidence,\n reason: decision.reason,\n decidedAt: Math.floor(Date.now() / 1000),\n actor: decision.actor,\n toolVersion: decision.toolVersion ?? null,\n mode: decision.mode,\n model: decision.model ?? null,\n });\n applied = true;\n });\n tx();\n return applied;\n}\n\nexport async function resolveContradictionsAutonomously(\n db: DatabaseSync,\n getById: (id: string) => MemoryEntry | null,\n supersede: (oldId: string, newId: string | null) => boolean,\n options: ResolveContradictionsAutoOptions = {},\n): Promise<ResolveContradictionsAutoResult> {\n const {\n dryRun = false,\n targetRate = DEFAULT_CONTRADICTION_TARGET_RATE,\n llm = false,\n llmConfidenceThreshold = DEFAULT_LLM_AUTO_APPLY_CONFIDENCE,\n adjudicate,\n actor = \"resolve-contradictions\",\n toolVersion,\n model,\n } = options;\n\n const unresolved = getContradictions(db);\n const reviewItems: ContradictionReviewItem[] = [];\n let total = 0;\n let deterministic = 0;\n let llmResolved = 0;\n const merged = 0;\n let decisionsApplied = 0;\n\n for (const contradiction of unresolved) {\n const newFact = getById(contradiction.factIdNew);\n const oldFact = getById(contradiction.factIdOld);\n if (!newFact || !oldFact) continue;\n total++;\n\n const reviewItem = buildContradictionReviewItem(contradiction, newFact, oldFact);\n const lww = evaluateLwwEligibility(newFact, oldFact, contradiction.oldFactOriginalConfidence);\n\n if (reviewItem.possibleOverloadedEntity) {\n reviewItem.suggestedReason = \"Possible entity reuse detected; leaving for manual review.\";\n } else if (lww.eligible && lww.qualifies && !isFactVerified(db, contradiction.factIdOld)) {\n reviewItem.suggestedDecision = \"keep_new\";\n reviewItem.suggestedStrategy = \"project-state-lww\";\n reviewItem.suggestedConfidence = 1;\n reviewItem.suggestedReason = \"Trusted newer project-state fact supersedes the stale value.\";\n deterministic++;\n if (!dryRun) {\n const applied = persistContradictionDecision(db, getById, supersede, {\n contradictionId: contradiction.id,\n keptFactId: contradiction.factIdNew,\n supersededFactId: contradiction.factIdOld,\n resolution: \"superseded\",\n strategy: reviewItem.suggestedStrategy,\n confidence: reviewItem.suggestedConfidence,\n reason: reviewItem.suggestedReason,\n actor,\n toolVersion,\n mode: \"auto\",\n model: null,\n });\n if (applied) decisionsApplied++;\n }\n continue;\n } else if (lww.eligible && isFactVerified(db, contradiction.factIdOld)) {\n reviewItem.suggestedReason = \"Older fact is verified; leaving for manual review.\";\n }\n\n if (llm && adjudicate) {\n try {\n const llmDecision = await adjudicate(reviewItem);\n const confidence = clampConfidence(llmDecision.confidence);\n if (\n (llmDecision.decision === \"keep_new\" || llmDecision.decision === \"keep_old\") &&\n confidence >= llmConfidenceThreshold\n ) {\n reviewItem.suggestedDecision = llmDecision.decision;\n reviewItem.suggestedStrategy = \"llm-adjudication\";\n reviewItem.suggestedConfidence = confidence;\n reviewItem.suggestedReason = llmDecision.reason?.trim() || \"LLM adjudication approved the resolution.\";\n llmResolved++;\n if (!dryRun) {\n const keepNew = llmDecision.decision === \"keep_new\";\n const applied = persistContradictionDecision(db, getById, supersede, {\n contradictionId: contradiction.id,\n keptFactId: keepNew ? contradiction.factIdNew : contradiction.factIdOld,\n supersededFactId: keepNew ? contradiction.factIdOld : contradiction.factIdNew,\n resolution: keepNew ? \"superseded\" : \"kept\",\n strategy: reviewItem.suggestedStrategy,\n confidence,\n reason: reviewItem.suggestedReason,\n actor,\n toolVersion,\n mode: \"auto\",\n model: model ?? null,\n });\n if (applied) decisionsApplied++;\n }\n continue;\n }\n\n if (llmDecision.decision === \"merge\") {\n reviewItem.suggestedReason =\n llmDecision.reason?.trim() || \"LLM requested merge; keep manual review for safety.\";\n } else if (confidence > 0) {\n reviewItem.suggestedReason =\n llmDecision.reason?.trim() || `LLM confidence ${confidence.toFixed(2)} below auto-apply threshold.`;\n }\n } catch (err) {\n reviewItem.suggestedReason = `LLM adjudication failed: ${err instanceof Error ? err.message : String(err)}`;\n }\n }\n\n reviewItems.push(reviewItem);\n }\n\n reviewItems.sort(compareReviewItems);\n const autoResolved = deterministic + llmResolved + merged;\n const achievedRate = total === 0 ? 1 : autoResolved / total;\n return {\n total,\n deterministic,\n llm: llmResolved,\n merged,\n manualReview: reviewItems.length,\n applied: !dryRun,\n decisionsApplied,\n targetRate,\n achievedRate,\n targetMet: achievedRate >= targetRate,\n reviewItems,\n };\n}\n\nexport function applyContradictionReviewDecisions(\n db: DatabaseSync,\n getById: (id: string) => MemoryEntry | null,\n supersede: (oldId: string, newId: string | null) => boolean,\n decisions: ContradictionReviewDecision[],\n options: { actor?: string; toolVersion?: string | null } = {},\n): ApplyContradictionReviewResult {\n const actor = options.actor ?? \"resolve-contradictions-review\";\n const toolVersion = options.toolVersion ?? null;\n let applied = 0;\n let keptNew = 0;\n let keptOld = 0;\n let manualReview = 0;\n let rejected = 0;\n const errors: string[] = [];\n\n for (const decision of decisions) {\n if (!decision.contradictionId?.trim()) {\n rejected++;\n errors.push(\"Decision is missing contradictionId.\");\n continue;\n }\n if (decision.decision === \"manual_review\") {\n manualReview++;\n continue;\n }\n if (decision.decision === \"merge\") {\n rejected++;\n errors.push(`Contradiction ${decision.contradictionId}: merge decisions must be handled manually.`);\n continue;\n }\n\n const contradiction = db\n .prepare(\"SELECT fact_id_new, fact_id_old, resolved FROM contradictions WHERE id = ?\")\n .get(decision.contradictionId) as { fact_id_new: string; fact_id_old: string; resolved: number } | undefined;\n if (!contradiction) {\n rejected++;\n errors.push(`Contradiction ${decision.contradictionId}: row not found.`);\n continue;\n }\n if (contradiction.resolved === 1) {\n rejected++;\n errors.push(`Contradiction ${decision.contradictionId}: already resolved.`);\n continue;\n }\n\n const keepNew = decision.decision === \"keep_new\";\n const didApply = persistContradictionDecision(db, getById, supersede, {\n contradictionId: decision.contradictionId,\n keptFactId: keepNew ? contradiction.fact_id_new : contradiction.fact_id_old,\n supersededFactId: keepNew ? contradiction.fact_id_old : contradiction.fact_id_new,\n resolution: keepNew ? \"superseded\" : \"kept\",\n strategy: \"manual-review-file\",\n confidence: clampConfidence(decision.confidence) || 1,\n reason: decision.reason?.trim() || \"Applied from contradiction review file.\",\n actor,\n toolVersion,\n mode: \"review\",\n model: null,\n });\n if (!didApply) {\n rejected++;\n errors.push(`Contradiction ${decision.contradictionId}: could not persist decision.`);\n continue;\n }\n applied++;\n if (keepNew) keptNew++;\n else keptOld++;\n }\n\n return { applied, keptNew, keptOld, manualReview, rejected, errors };\n}\n\nexport function contradictionsCount(db: DatabaseSync): number {\n try {\n const row = db.prepare(\"SELECT COUNT(*) as cnt FROM contradictions WHERE resolved = 0\").get() as {\n cnt: number;\n };\n return row?.cnt ?? 0;\n } catch (err) {\n capturePluginError(err as Error, {\n operation: \"count-contradictions\",\n severity: \"info\",\n subsystem: \"facts\",\n });\n return 0;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"contradictions.js","names":[],"sources":["../../../backends/facts-db/contradictions.ts"],"sourcesContent":["/**\n * Contradiction detection and resolution (Issue #157) (Issue #954 split).\n * Project-state latest-wins (LWW) policy added in Issue #1636.\n */\nimport { randomUUID } from \"node:crypto\";\nimport type { SQLInputValue } from \"node:sqlite\";\nimport type { DatabaseSync } from \"node:sqlite\";\n\nimport { capturePluginError } from \"../../services/error-reporter.js\";\nimport type { MemoryEntry } from \"../../types/memory.js\";\nimport { createTransaction } from \"../../utils/sqlite-transaction.js\";\nimport { parseTags, serializeTags } from \"../../utils/tags.js\";\nimport { rowToMemoryEntry } from \"./row-mapper.js\";\nimport type { MemoryLinkType } from \"./types.js\";\n\nexport interface ContradictionRecord {\n id: string;\n factIdNew: string;\n factIdOld: string;\n detectedAt: string;\n resolved: boolean;\n resolution: \"superseded\" | \"kept\" | \"merged\" | null;\n oldFactOriginalConfidence?: number;\n}\n\nexport function updateConfidence(db: DatabaseSync, id: string, delta: number): number | null {\n const row = db.prepare(\"SELECT confidence FROM facts WHERE id = ?\").get(id) as { confidence: number } | undefined;\n if (!row) return null;\n const current = row.confidence ?? 1.0;\n const updated = Math.max(0.1, Math.min(1.0, current + delta));\n db.prepare(\"UPDATE facts SET confidence = ? WHERE id = ?\").run(updated, id);\n return updated;\n}\n\nexport function setConfidenceTo(db: DatabaseSync, id: string, value: number): number | null {\n const row = db.prepare(\"SELECT confidence FROM facts WHERE id = ?\").get(id) as { confidence: number } | undefined;\n if (!row) return null;\n const updated = Math.max(0.1, Math.min(1, value));\n db.prepare(\"UPDATE facts SET confidence = ? WHERE id = ?\").run(updated, id);\n return updated;\n}\n\nexport function addTag(db: DatabaseSync, id: string, tag: string): void {\n const trimmed = tag.trim();\n const normalized = trimmed.toLowerCase();\n if (!normalized || normalized.includes(\",\")) return;\n const row = db.prepare(\"SELECT tags FROM facts WHERE id = ?\").get(id) as { tags: string | null } | undefined;\n if (!row) return;\n const tags = parseTags(row.tags);\n if (tags.some((t) => t.toLowerCase() === normalized)) return;\n tags.push(normalized);\n db.prepare(\"UPDATE facts SET tags = ? WHERE id = ?\").run(serializeTags(tags), id);\n}\n\nexport function findConflictingFacts(\n db: DatabaseSync,\n entity: string,\n key: string,\n value: string,\n excludeFactId: string,\n scope?: string | null,\n scopeTarget?: string | null,\n): MemoryEntry[] {\n const nowSec = Math.floor(Date.now() / 1000);\n const scopeClause = scope\n ? scopeTarget != null\n ? \"AND scope = ? AND scope_target = ?\"\n : \"AND scope = ? AND scope_target IS NULL\"\n : \"\";\n const baseParams: SQLInputValue[] = [entity, key, value, excludeFactId, nowSec];\n const scopeParams: SQLInputValue[] = scope ? (scopeTarget != null ? [scope, scopeTarget] : [scope]) : [];\n const rows = db\n .prepare(\n `SELECT * FROM facts\n WHERE lower(entity) = lower(?)\n AND lower(key) = lower(?)\n AND lower(value) != lower(?)\n AND id != ?\n AND superseded_at IS NULL\n AND (expires_at IS NULL OR expires_at > ?)\n ${scopeClause}\n ORDER BY created_at DESC`,\n )\n .all(...baseParams, ...scopeParams) as Array<Record<string, unknown>>;\n return rows.map((r) => rowToMemoryEntry(r));\n}\n\nexport function recordContradiction(\n db: DatabaseSync,\n factIdNew: string,\n factIdOld: string,\n createLink: (a: string, b: string, t: MemoryLinkType, s?: number) => string,\n): { id: string; oldFactOriginalConfidence: number } {\n const id = randomUUID();\n const detectedAt = new Date().toISOString();\n let oldFactOriginalConfidence = 1.0;\n\n const tx = createTransaction(db, () => {\n const oldFactRow = db.prepare(\"SELECT confidence FROM facts WHERE id = ?\").get(factIdOld) as\n | { confidence: number }\n | undefined;\n oldFactOriginalConfidence = oldFactRow?.confidence ?? 1.0;\n\n db.prepare(\n `INSERT INTO contradictions (id, fact_id_new, fact_id_old, detected_at, resolved, resolution, old_fact_original_confidence)\n VALUES (?, ?, ?, ?, 0, NULL, ?)`,\n ).run(id, factIdNew, factIdOld, detectedAt, oldFactOriginalConfidence);\n\n createLink(factIdNew, factIdOld, \"CONTRADICTS\", 1.0);\n\n updateConfidence(db, factIdOld, -0.2);\n });\n tx();\n return { id, oldFactOriginalConfidence };\n}\n\nexport function detectContradictions(\n db: DatabaseSync,\n newFactId: string,\n entity: string | null | undefined,\n key: string | null | undefined,\n value: string | null | undefined,\n scope: string | null | undefined,\n scopeTarget: string | null | undefined,\n createLink: (a: string, b: string, t: MemoryLinkType, s?: number) => string,\n): Array<{ contradictionId: string; oldFactId: string; oldFactOriginalConfidence: number }> {\n if (!entity?.trim() || !key?.trim() || !value?.trim()) return [];\n\n const conflicting = findConflictingFacts(db, entity.trim(), key.trim(), value.trim(), newFactId, scope, scopeTarget);\n const results: Array<{ contradictionId: string; oldFactId: string; oldFactOriginalConfidence: number }> = [];\n\n for (const old of conflicting) {\n if (old.value?.toLowerCase() === value.trim().toLowerCase()) continue;\n const contradiction = recordContradiction(db, newFactId, old.id, createLink);\n results.push({\n contradictionId: contradiction.id,\n oldFactId: old.id,\n oldFactOriginalConfidence: contradiction.oldFactOriginalConfidence,\n });\n }\n\n return results;\n}\n\nexport function getContradictions(db: DatabaseSync, factId?: string): ContradictionRecord[] {\n const rows = factId\n ? (db\n .prepare(\"SELECT * FROM contradictions WHERE fact_id_new = ? OR fact_id_old = ? ORDER BY detected_at DESC\")\n .all(factId, factId) as Array<Record<string, unknown>>)\n : (db.prepare(\"SELECT * FROM contradictions WHERE resolved = 0 ORDER BY detected_at DESC\").all() as Array<\n Record<string, unknown>\n >);\n return rows.map((r) => ({\n id: r.id as string,\n factIdNew: r.fact_id_new as string,\n factIdOld: r.fact_id_old as string,\n detectedAt: r.detected_at as string,\n resolved: (r.resolved as number) === 1,\n resolution: (r.resolution as \"superseded\" | \"kept\" | \"merged\" | null) ?? null,\n oldFactOriginalConfidence:\n r.old_fact_original_confidence == null ? undefined : (r.old_fact_original_confidence as number),\n }));\n}\n\nexport function resolveContradiction(\n db: DatabaseSync,\n contradictionId: string,\n resolution: \"superseded\" | \"kept\" | \"merged\",\n): boolean {\n const result = db\n .prepare(\"UPDATE contradictions SET resolved = 1, resolution = ? WHERE id = ? AND resolved = 0\")\n .run(resolution, contradictionId);\n return result.changes > 0;\n}\n\nexport function isContradicted(db: DatabaseSync, factId: string): boolean {\n const row = db\n .prepare(\"SELECT 1 FROM contradictions WHERE (fact_id_old = ? OR fact_id_new = ?) AND resolved = 0 LIMIT 1\")\n .get(factId, factId);\n return row != null;\n}\n\nexport function getContradictedIds(db: DatabaseSync, factIds: string[]): Set<string> {\n if (factIds.length === 0) return new Set();\n const result = new Set<string>();\n const CHUNK = 499;\n for (let i = 0; i < factIds.length; i += CHUNK) {\n const chunk = factIds.slice(i, i + CHUNK);\n const placeholders = chunk.map(() => \"?\").join(\",\");\n const rows = db\n .prepare(\n `SELECT fact_id_old AS id FROM contradictions WHERE fact_id_old IN (${placeholders}) AND resolved = 0\n UNION\n SELECT fact_id_new AS id FROM contradictions WHERE fact_id_new IN (${placeholders}) AND resolved = 0`,\n )\n .all(...chunk, ...chunk) as Array<{ id: string }>;\n for (const r of rows) result.add(r.id);\n }\n return result;\n}\n\nexport function isFactVerified(db: DatabaseSync, factId: string): boolean {\n const row = db.prepare(\"SELECT 1 FROM verified_facts WHERE fact_id = ? LIMIT 1\").get(factId);\n return row != null;\n}\n\nfunction isAutoResolvableContradiction(\n db: DatabaseSync,\n contradiction: ContradictionRecord,\n newFact: MemoryEntry,\n oldFact: MemoryEntry,\n): boolean {\n const newConf = newFact.confidence ?? 1.0;\n const oldConf = contradiction.oldFactOriginalConfidence ?? oldFact.confidence ?? 1.0;\n const newIsNewer = newFact.createdAt >= oldFact.createdAt;\n const newIsHigherConf = newConf > oldConf;\n const newIsFromUser = newFact.source === \"conversation\" || newFact.source === \"cli\";\n\n if (!(newIsNewer && newIsHigherConf && newIsFromUser)) return false;\n if (oldFact.supersededAt != null) return true;\n return !isFactVerified(db, contradiction.factIdOld);\n}\n\ntype AutoResolutionDecision =\n | { autoResolvable: false }\n | {\n autoResolvable: true;\n resolution: \"superseded\" | \"kept\";\n requiresSupersede: boolean;\n };\n\nfunction getAutoResolutionDecision(\n db: DatabaseSync,\n contradiction: ContradictionRecord,\n newFact: MemoryEntry | null,\n oldFact: MemoryEntry | null,\n): AutoResolutionDecision {\n if (!newFact && !oldFact) {\n return { autoResolvable: true, resolution: \"superseded\", requiresSupersede: false };\n }\n if (!newFact && oldFact) {\n return { autoResolvable: true, resolution: \"kept\", requiresSupersede: false };\n }\n if (newFact && !oldFact) {\n return { autoResolvable: true, resolution: \"superseded\", requiresSupersede: false };\n }\n if (!newFact || !oldFact) return { autoResolvable: false };\n if (!isAutoResolvableContradiction(db, contradiction, newFact, oldFact)) {\n return { autoResolvable: false };\n }\n return {\n autoResolvable: true,\n resolution: \"superseded\",\n requiresSupersede: oldFact.supersededAt == null,\n };\n}\n\nexport function previewResolveContradictionsAuto(\n db: DatabaseSync,\n getById: (id: string) => MemoryEntry | null,\n): {\n autoResolvable: Array<{\n contradictionId: string;\n factIdNew: string;\n factIdOld: string;\n }>;\n ambiguous: Array<{\n contradictionId: string;\n factIdNew: string;\n factIdOld: string;\n }>;\n} {\n const unresolved = getContradictions(db);\n const autoResolvable: Array<{\n contradictionId: string;\n factIdNew: string;\n factIdOld: string;\n }> = [];\n const ambiguous: Array<{\n contradictionId: string;\n factIdNew: string;\n factIdOld: string;\n }> = [];\n\n for (const c of unresolved) {\n const newFact = getById(c.factIdNew);\n const oldFact = getById(c.factIdOld);\n const decision = getAutoResolutionDecision(db, c, newFact, oldFact);\n if (decision.autoResolvable) {\n autoResolvable.push({\n contradictionId: c.id,\n factIdNew: c.factIdNew,\n factIdOld: c.factIdOld,\n });\n } else {\n ambiguous.push({\n contradictionId: c.id,\n factIdNew: c.factIdNew,\n factIdOld: c.factIdOld,\n });\n }\n }\n\n return { autoResolvable, ambiguous };\n}\n\nexport function resolveContradictionsAuto(\n db: DatabaseSync,\n getById: (id: string) => MemoryEntry | null,\n supersede: (oldId: string, newId: string | null) => boolean,\n): {\n autoResolved: Array<{\n contradictionId: string;\n factIdNew: string;\n factIdOld: string;\n }>;\n ambiguous: Array<{\n contradictionId: string;\n factIdNew: string;\n factIdOld: string;\n }>;\n} {\n const unresolved = getContradictions(db);\n const autoResolved: Array<{\n contradictionId: string;\n factIdNew: string;\n factIdOld: string;\n }> = [];\n const ambiguous: Array<{\n contradictionId: string;\n factIdNew: string;\n factIdOld: string;\n }> = [];\n\n for (const c of unresolved) {\n const newFact = getById(c.factIdNew);\n const oldFact = getById(c.factIdOld);\n const decision = getAutoResolutionDecision(db, c, newFact, oldFact);\n if (!decision.autoResolvable) {\n ambiguous.push({\n contradictionId: c.id,\n factIdNew: c.factIdNew,\n factIdOld: c.factIdOld,\n });\n continue;\n }\n\n if (decision.requiresSupersede) {\n const superseded = supersede(c.factIdOld, c.factIdNew);\n if (!superseded) {\n ambiguous.push({\n contradictionId: c.id,\n factIdNew: c.factIdNew,\n factIdOld: c.factIdOld,\n });\n continue;\n }\n }\n\n resolveContradiction(db, c.id, decision.resolution);\n if (decision.resolution === \"kept\" && c.oldFactOriginalConfidence != null) {\n db.prepare(\"UPDATE facts SET confidence = ? WHERE id = ?\").run(c.oldFactOriginalConfidence, c.factIdOld);\n }\n autoResolved.push({\n contradictionId: c.id,\n factIdNew: c.factIdNew,\n factIdOld: c.factIdOld,\n });\n }\n\n return { autoResolved, ambiguous };\n}\n\n// ---------------------------------------------------------------------------\n// Project-state latest-wins (LWW) policy (Issue #1636)\n// ---------------------------------------------------------------------------\n\n/**\n * Mutable project/task state keys eligible for the latest-wins policy.\n *\n * Each key represents a \"current-state snapshot\" — a single mutable fact whose\n * value is completely replaced when new information arrives (e.g. `status` moves\n * from `in_progress` to `done`; `next` is replaced with a new action item).\n * These differ from append-style project facts (e.g. decisions, milestones) where\n * both old and new values may independently hold — those must go through manual\n * contradiction review instead.\n */\nexport const PROJECT_STATE_LWW_KEYS: ReadonlySet<string> = new Set([\n \"status\",\n \"next\",\n \"task_updated\",\n \"related_session\",\n \"coverage\",\n \"last_live_verified_at\",\n \"live_state_hash\",\n \"last_actionable_blocker\",\n \"owner\",\n]);\n\n/**\n * Sources trusted for project-state LWW resolution.\n * `distillation` is excluded by default to keep automated distilled facts conservative.\n */\nexport const PROJECT_STATE_LWW_TRUSTED_SOURCES: ReadonlySet<string> = new Set([\n \"conversation\",\n \"cli\",\n \"active-task\",\n \"tool\",\n]);\n\n/** A single candidate contradiction eligible for project-state LWW resolution. */\nexport interface ProjectStateLwwCandidate {\n contradictionId: string;\n factIdNew: string;\n factIdOld: string;\n entity: string;\n key: string;\n scope: string | null;\n scopeTarget: string | null;\n /** epoch seconds */\n newFactDate: number;\n /** epoch seconds */\n oldFactDate: number;\n newSource: string;\n oldSource: string;\n newConf: number;\n oldConf: number;\n newValueExcerpt: string;\n oldValueExcerpt: string;\n /** `supersede`: LWW policy applies — newer trusted fact wins. `manual-review`: skipped by LWW. */\n action: \"supersede\" | \"manual-review\";\n /** True when the entity/key pair appears to have been reused for follow-up work. */\n possibleOverloadedEntity: boolean;\n}\n\n/** Grouped result of a project-state LWW resolution pass. */\nexport interface ProjectStateLwwResult {\n groups: Array<{\n entity: string;\n key: string;\n scope: string | null;\n scopeTarget: string | null;\n candidates: ProjectStateLwwCandidate[];\n }>;\n totalCandidates: number;\n wouldSupersede: number;\n wouldManualReview: number;\n /** Number of contradictions actually resolved (0 in dry-run). */\n applied: number;\n}\n\n/**\n * Distinct PR/issue reference count threshold above which an entity is considered\n * \"possibly overloaded\" (i.e. the same entity key was reused across follow-up work).\n * Pairs above this threshold are still reported but flagged for human review.\n */\nconst MAX_EXPECTED_REFS_PER_ENTITY = 2;\n\n/** Extract distinct PR/issue ref numbers like `#1234` from text. */\nfunction extractRefNumbers(text: string): Set<string> {\n const matches = text.match(/#\\d+/g);\n return new Set(matches ?? []);\n}\n\n/** Heuristic: flag if multiple distinct PR/issue numbers appear across the two facts. */\nfunction isPossiblyOverloadedEntity(newFact: MemoryEntry, oldFact: MemoryEntry): boolean {\n const combined = `${newFact.value ?? \"\"} ${newFact.text ?? \"\"} ${oldFact.value ?? \"\"} ${oldFact.text ?? \"\"}`;\n const refs = extractRefNumbers(combined);\n return refs.size > MAX_EXPECTED_REFS_PER_ENTITY;\n}\n\nfunction truncateExcerpt(s: string, maxLen: number): string {\n const t = s.replace(/\\s+/g, \" \").trim();\n return t.length <= maxLen ? t : `${t.slice(0, maxLen - 1)}…`;\n}\n\n/**\n * Resolve stale project-state contradictions using a latest-wins policy.\n *\n * Eligible pairs must satisfy all of:\n * - Both facts are category `project`\n * - The key (case-insensitive) is in {@link PROJECT_STATE_LWW_KEYS}\n * - The newer fact's source is in {@link PROJECT_STATE_LWW_TRUSTED_SOURCES}\n * - The newer fact is strictly newer (`createdAt >` older)\n * - The newer fact's confidence ≥ the older fact's original confidence\n *\n * In dry-run mode no mutations are made; results describe what would happen.\n * In apply mode, qualifying contradictions are resolved as `superseded` and\n * the older fact is marked superseded with provenance preserved.\n */\n\nexport type LwwEligibilityResult =\n | { eligible: false }\n | { eligible: true; qualifies: boolean; newConf: number; oldConf: number };\n\n/**\n * Evaluate whether a (newFact, oldFact) pair is eligible for project-state LWW\n * and, if so, whether the newer fact qualifies to supersede the older.\n *\n * Shared between the write-time path in `FactsDB.detectContradictions` and the\n * batch `resolveProjectStateLww` pass to keep eligibility logic in one place.\n *\n * @param oldFactOriginalConfidence The confidence captured at contradiction-detection\n * time (before the -0.2 penalty). Missing values are treated as non-qualifying:\n * LWW will require manual review instead of guessing from a possibly penalized score.\n */\nexport function evaluateLwwEligibility(\n newFact: MemoryEntry,\n oldFact: MemoryEntry,\n oldFactOriginalConfidence?: number,\n): LwwEligibilityResult {\n if (newFact.category !== \"project\" || oldFact.category !== \"project\") return { eligible: false };\n const keyLower = (newFact.key ?? oldFact.key ?? \"\").trim().toLowerCase();\n if (!PROJECT_STATE_LWW_KEYS.has(keyLower)) return { eligible: false };\n if (!PROJECT_STATE_LWW_TRUSTED_SOURCES.has(newFact.source ?? \"\")) return { eligible: false };\n const newConf = newFact.confidence ?? 1.0;\n const oldConf = oldFactOriginalConfidence ?? oldFact.confidence ?? 1.0;\n if (oldFactOriginalConfidence == null) {\n return { eligible: true, qualifies: false, newConf, oldConf };\n }\n const qualifies = newFact.createdAt > oldFact.createdAt && newConf >= oldConf;\n return { eligible: true, qualifies, newConf, oldConf };\n}\n\nexport function resolveProjectStateLww(\n db: DatabaseSync,\n getById: (id: string) => MemoryEntry | null,\n supersede: (oldId: string, newId: string | null) => boolean,\n options: { dryRun?: boolean } = {},\n): ProjectStateLwwResult {\n const { dryRun = false } = options;\n const unresolved = getContradictions(db);\n\n const groupMap = new Map<\n string,\n {\n entity: string;\n key: string;\n scope: string | null;\n scopeTarget: string | null;\n candidates: ProjectStateLwwCandidate[];\n }\n >();\n let applied = 0;\n\n for (const c of unresolved) {\n const newFact = getById(c.factIdNew);\n const oldFact = getById(c.factIdOld);\n if (!newFact || !oldFact) continue;\n\n const lww = evaluateLwwEligibility(newFact, oldFact, c.oldFactOriginalConfidence);\n if (!lww.eligible) continue;\n\n const { qualifies, newConf, oldConf } = lww;\n const overloaded = isPossiblyOverloadedEntity(newFact, oldFact);\n const action: \"supersede\" | \"manual-review\" =\n qualifies && !overloaded && !isFactVerified(db, c.factIdOld) ? \"supersede\" : \"manual-review\";\n\n const entity = newFact.entity ?? oldFact.entity ?? \"?\";\n const key = newFact.key ?? oldFact.key ?? \"?\";\n const scope = newFact.scope ?? null;\n const scopeTarget = newFact.scopeTarget ?? null;\n\n const groupKey = `${entity}\\0${key}\\0${scope ?? \"\"}\\0${scopeTarget ?? \"\"}`;\n let group = groupMap.get(groupKey);\n if (!group) {\n group = { entity, key, scope, scopeTarget, candidates: [] };\n groupMap.set(groupKey, group);\n }\n group.candidates.push({\n contradictionId: c.id,\n factIdNew: c.factIdNew,\n factIdOld: c.factIdOld,\n entity,\n key,\n scope,\n scopeTarget,\n newFactDate: newFact.createdAt,\n oldFactDate: oldFact.createdAt,\n newSource: newFact.source ?? \"unknown\",\n oldSource: oldFact.source ?? \"unknown\",\n newConf,\n oldConf,\n newValueExcerpt: truncateExcerpt(newFact.value ?? newFact.text ?? \"\", 80),\n oldValueExcerpt: truncateExcerpt(oldFact.value ?? oldFact.text ?? \"\", 80),\n action,\n possibleOverloadedEntity: overloaded,\n });\n\n if (!dryRun && action === \"supersede\") {\n if (oldFact.supersededAt != null) {\n resolveContradiction(db, c.id, \"superseded\");\n applied++;\n } else {\n const superseded = supersede(c.factIdOld, c.factIdNew);\n if (superseded) {\n resolveContradiction(db, c.id, \"superseded\");\n applied++;\n }\n }\n }\n }\n\n const groups = Array.from(groupMap.values());\n let totalCandidates = 0;\n let wouldSupersede = 0;\n let wouldManualReview = 0;\n for (const g of groups) {\n for (const cand of g.candidates) {\n totalCandidates++;\n if (cand.action === \"supersede\") wouldSupersede++;\n else wouldManualReview++;\n }\n }\n\n return { groups, totalCandidates, wouldSupersede, wouldManualReview, applied };\n}\n\nexport interface ContradictionReviewItem {\n contradictionId: string;\n factIdNew: string;\n factIdOld: string;\n entity: string;\n key: string;\n scope: string | null;\n scopeTarget: string | null;\n newFactDate: number;\n oldFactDate: number;\n newSource: string;\n oldSource: string;\n newConf: number;\n oldConf: number;\n newValueExcerpt: string;\n oldValueExcerpt: string;\n newTextExcerpt: string;\n oldTextExcerpt: string;\n possibleOverloadedEntity: boolean;\n suggestedDecision: \"keep_new\" | \"keep_old\" | \"manual_review\";\n suggestedStrategy: string;\n suggestedConfidence: number;\n suggestedReason: string;\n}\n\nexport interface LlmContradictionDecision {\n decision: \"keep_new\" | \"keep_old\" | \"merge\" | \"manual_review\";\n confidence: number;\n reason: string;\n mergedFactText?: string | null;\n}\n\nexport interface ResolveContradictionsAutoOptions {\n dryRun?: boolean;\n targetRate?: number;\n llm?: boolean;\n llmConfidenceThreshold?: number;\n adjudicate?: (item: ContradictionReviewItem) => Promise<LlmContradictionDecision>;\n actor?: string;\n toolVersion?: string;\n model?: string | null;\n}\n\nexport interface ResolveContradictionsAutoResult {\n total: number;\n deterministic: number;\n llm: number;\n merged: number;\n manualReview: number;\n applied: boolean;\n decisionsApplied: number;\n targetRate: number;\n achievedRate: number;\n targetMet: boolean;\n reviewItems: ContradictionReviewItem[];\n}\n\nexport interface ContradictionReviewDecision {\n contradictionId: string;\n decision: \"keep_new\" | \"keep_old\" | \"merge\" | \"manual_review\";\n reason?: string;\n confidence?: number;\n mergedFactText?: string | null;\n}\n\nexport interface ApplyContradictionReviewResult {\n applied: number;\n keptNew: number;\n keptOld: number;\n manualReview: number;\n rejected: number;\n errors: string[];\n}\n\nexport interface ContradictionResolutionAuditRow {\n id: string;\n contradictionId: string;\n keptFactId: string;\n supersededFactId: string;\n strategy: string;\n confidence: number;\n reason: string;\n decidedAt: number;\n actor: string;\n toolVersion: string | null;\n mode: \"auto\" | \"review\";\n model: string | null;\n}\n\nconst DEFAULT_CONTRADICTION_TARGET_RATE = 0.8;\nconst DEFAULT_LLM_AUTO_APPLY_CONFIDENCE = 0.9;\n\nfunction buildContradictionReviewItem(\n contradiction: ContradictionRecord,\n newFact: MemoryEntry,\n oldFact: MemoryEntry,\n): ContradictionReviewItem {\n const oldConf = contradiction.oldFactOriginalConfidence ?? oldFact.confidence ?? 1.0;\n const newConf = newFact.confidence ?? 1.0;\n return {\n contradictionId: contradiction.id,\n factIdNew: contradiction.factIdNew,\n factIdOld: contradiction.factIdOld,\n entity: newFact.entity ?? oldFact.entity ?? \"?\",\n key: newFact.key ?? oldFact.key ?? \"?\",\n scope: newFact.scope ?? oldFact.scope ?? null,\n scopeTarget: newFact.scopeTarget ?? oldFact.scopeTarget ?? null,\n newFactDate: newFact.createdAt,\n oldFactDate: oldFact.createdAt,\n newSource: newFact.source ?? \"unknown\",\n oldSource: oldFact.source ?? \"unknown\",\n newConf,\n oldConf,\n newValueExcerpt: truncateExcerpt(newFact.value ?? newFact.text ?? \"\", 80),\n oldValueExcerpt: truncateExcerpt(oldFact.value ?? oldFact.text ?? \"\", 80),\n newTextExcerpt: truncateExcerpt(newFact.text ?? newFact.value ?? \"\", 120),\n oldTextExcerpt: truncateExcerpt(oldFact.text ?? oldFact.value ?? \"\", 120),\n possibleOverloadedEntity: isPossiblyOverloadedEntity(newFact, oldFact),\n suggestedDecision: \"manual_review\",\n suggestedStrategy: \"manual-review\",\n suggestedConfidence: 0,\n suggestedReason: \"No safe deterministic resolution matched.\",\n };\n}\n\nfunction compareReviewItems(a: ContradictionReviewItem, b: ContradictionReviewItem): number {\n return (\n a.entity.localeCompare(b.entity) ||\n a.key.localeCompare(b.key) ||\n (a.scope ?? \"\").localeCompare(b.scope ?? \"\") ||\n (a.scopeTarget ?? \"\").localeCompare(b.scopeTarget ?? \"\") ||\n a.contradictionId.localeCompare(b.contradictionId)\n );\n}\n\nfunction clampConfidence(value: number | undefined): number {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return 0;\n return Math.max(0, Math.min(1, value));\n}\n\nfunction insertContradictionResolutionAudit(db: DatabaseSync, row: Omit<ContradictionResolutionAuditRow, \"id\">): void {\n db.prepare(\n `INSERT INTO contradiction_resolution_audit\n (id, contradiction_id, kept_fact_id, superseded_fact_id, strategy, confidence, reason, decided_at, actor, tool_version, mode, model)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n ).run(\n randomUUID(),\n row.contradictionId,\n row.keptFactId,\n row.supersededFactId,\n row.strategy,\n row.confidence,\n row.reason,\n row.decidedAt,\n row.actor,\n row.toolVersion,\n row.mode,\n row.model,\n );\n}\n\nexport function getContradictionResolutionAudit(\n db: DatabaseSync,\n contradictionId?: string,\n): ContradictionResolutionAuditRow[] {\n const query = contradictionId\n ? \"SELECT * FROM contradiction_resolution_audit WHERE contradiction_id = ? ORDER BY decided_at ASC, id ASC\"\n : \"SELECT * FROM contradiction_resolution_audit ORDER BY decided_at ASC, id ASC\";\n const rows = (contradictionId ? db.prepare(query).all(contradictionId) : db.prepare(query).all()) as Array<\n Record<string, unknown>\n >;\n return rows.map((row) => ({\n id: row.id as string,\n contradictionId: row.contradiction_id as string,\n keptFactId: row.kept_fact_id as string,\n supersededFactId: row.superseded_fact_id as string,\n strategy: row.strategy as string,\n confidence: Number(row.confidence ?? 0),\n reason: (row.reason as string) ?? \"\",\n decidedAt: Number(row.decided_at ?? 0),\n actor: (row.actor as string) ?? \"resolve-contradictions\",\n toolVersion: (row.tool_version as string | null) ?? null,\n mode: ((row.mode as \"auto\" | \"review\" | null) ?? \"auto\") === \"review\" ? \"review\" : \"auto\",\n model: (row.model as string | null) ?? null,\n }));\n}\n\ntype PersistedDecision = {\n contradictionId: string;\n keptFactId: string;\n supersededFactId: string;\n resolution: \"superseded\" | \"kept\";\n strategy: string;\n confidence: number;\n reason: string;\n actor: string;\n toolVersion?: string | null;\n mode: \"auto\" | \"review\";\n model?: string | null;\n};\n\nfunction persistContradictionDecision(\n db: DatabaseSync,\n getById: (id: string) => MemoryEntry | null,\n supersede: (oldId: string, newId: string | null) => boolean,\n decision: PersistedDecision,\n): boolean {\n const contradiction = db.prepare(\"SELECT resolved FROM contradictions WHERE id = ?\").get(decision.contradictionId) as\n | { resolved: number }\n | undefined;\n if (!contradiction || contradiction.resolved === 1) return false;\n\n let applied = false;\n const tx = createTransaction(db, () => {\n const supersededFact = getById(decision.supersededFactId);\n if (!supersededFact) {\n return;\n }\n\n if (supersededFact.supersededAt == null) {\n const didSupersede = supersede(decision.supersededFactId, decision.keptFactId);\n if (!didSupersede) {\n return;\n }\n }\n\n const resolved = resolveContradiction(db, decision.contradictionId, decision.resolution);\n if (!resolved) {\n return;\n }\n\n insertContradictionResolutionAudit(db, {\n contradictionId: decision.contradictionId,\n keptFactId: decision.keptFactId,\n supersededFactId: decision.supersededFactId,\n strategy: decision.strategy,\n confidence: decision.confidence,\n reason: decision.reason,\n decidedAt: Math.floor(Date.now() / 1000),\n actor: decision.actor,\n toolVersion: decision.toolVersion ?? null,\n mode: decision.mode,\n model: decision.model ?? null,\n });\n applied = true;\n });\n tx();\n return applied;\n}\n\nexport async function resolveContradictionsAutonomously(\n db: DatabaseSync,\n getById: (id: string) => MemoryEntry | null,\n supersede: (oldId: string, newId: string | null) => boolean,\n options: ResolveContradictionsAutoOptions = {},\n): Promise<ResolveContradictionsAutoResult> {\n const {\n dryRun = false,\n targetRate = DEFAULT_CONTRADICTION_TARGET_RATE,\n llm = false,\n llmConfidenceThreshold = DEFAULT_LLM_AUTO_APPLY_CONFIDENCE,\n adjudicate,\n actor = \"resolve-contradictions\",\n toolVersion,\n model,\n } = options;\n\n const unresolved = getContradictions(db);\n const reviewItems: ContradictionReviewItem[] = [];\n let total = 0;\n let deterministic = 0;\n let llmResolved = 0;\n const merged = 0;\n let decisionsApplied = 0;\n\n for (const contradiction of unresolved) {\n const newFact = getById(contradiction.factIdNew);\n const oldFact = getById(contradiction.factIdOld);\n if (!newFact || !oldFact) continue;\n total++;\n\n const reviewItem = buildContradictionReviewItem(contradiction, newFact, oldFact);\n const lww = evaluateLwwEligibility(newFact, oldFact, contradiction.oldFactOriginalConfidence);\n\n if (reviewItem.possibleOverloadedEntity) {\n reviewItem.suggestedReason = \"Possible entity reuse detected; leaving for manual review.\";\n } else if (lww.eligible && lww.qualifies && !isFactVerified(db, contradiction.factIdOld)) {\n reviewItem.suggestedDecision = \"keep_new\";\n reviewItem.suggestedStrategy = \"project-state-lww\";\n reviewItem.suggestedConfidence = 1;\n reviewItem.suggestedReason = \"Trusted newer project-state fact supersedes the stale value.\";\n deterministic++;\n if (!dryRun) {\n const applied = persistContradictionDecision(db, getById, supersede, {\n contradictionId: contradiction.id,\n keptFactId: contradiction.factIdNew,\n supersededFactId: contradiction.factIdOld,\n resolution: \"superseded\",\n strategy: reviewItem.suggestedStrategy,\n confidence: reviewItem.suggestedConfidence,\n reason: reviewItem.suggestedReason,\n actor,\n toolVersion,\n mode: \"auto\",\n model: null,\n });\n if (applied) decisionsApplied++;\n }\n continue;\n } else if (isFactVerified(db, contradiction.factIdOld)) {\n reviewItem.suggestedReason = \"Older fact is verified; leaving for manual review.\";\n }\n\n if (llm && adjudicate) {\n try {\n const llmDecision = await adjudicate(reviewItem);\n const confidence = clampConfidence(llmDecision.confidence);\n if (\n (llmDecision.decision === \"keep_new\" || llmDecision.decision === \"keep_old\") &&\n confidence >= llmConfidenceThreshold\n ) {\n reviewItem.suggestedDecision = llmDecision.decision;\n reviewItem.suggestedStrategy = \"llm-adjudication\";\n reviewItem.suggestedConfidence = confidence;\n reviewItem.suggestedReason = llmDecision.reason?.trim() || \"LLM adjudication approved the resolution.\";\n llmResolved++;\n if (!dryRun) {\n const keepNew = llmDecision.decision === \"keep_new\";\n const applied = persistContradictionDecision(db, getById, supersede, {\n contradictionId: contradiction.id,\n keptFactId: keepNew ? contradiction.factIdNew : contradiction.factIdOld,\n supersededFactId: keepNew ? contradiction.factIdOld : contradiction.factIdNew,\n resolution: keepNew ? \"superseded\" : \"kept\",\n strategy: reviewItem.suggestedStrategy,\n confidence,\n reason: reviewItem.suggestedReason,\n actor,\n toolVersion,\n mode: \"auto\",\n model: model ?? null,\n });\n if (applied) decisionsApplied++;\n }\n continue;\n }\n\n if (llmDecision.decision === \"merge\") {\n reviewItem.suggestedReason =\n llmDecision.reason?.trim() || \"LLM requested merge; keep manual review for safety.\";\n } else if (confidence > 0) {\n reviewItem.suggestedReason =\n llmDecision.reason?.trim() || `LLM confidence ${confidence.toFixed(2)} below auto-apply threshold.`;\n }\n } catch (err) {\n reviewItem.suggestedReason = `LLM adjudication failed: ${err instanceof Error ? err.message : String(err)}`;\n }\n }\n\n reviewItems.push(reviewItem);\n }\n\n reviewItems.sort(compareReviewItems);\n const autoResolved = deterministic + llmResolved + merged;\n const achievedRate = total === 0 ? 1 : autoResolved / total;\n return {\n total,\n deterministic,\n llm: llmResolved,\n merged,\n manualReview: reviewItems.length,\n applied: !dryRun,\n decisionsApplied,\n targetRate,\n achievedRate,\n targetMet: achievedRate >= targetRate,\n reviewItems,\n };\n}\n\nexport function applyContradictionReviewDecisions(\n db: DatabaseSync,\n getById: (id: string) => MemoryEntry | null,\n supersede: (oldId: string, newId: string | null) => boolean,\n decisions: ContradictionReviewDecision[],\n options: { actor?: string; toolVersion?: string | null } = {},\n): ApplyContradictionReviewResult {\n const actor = options.actor ?? \"resolve-contradictions-review\";\n const toolVersion = options.toolVersion ?? null;\n let applied = 0;\n let keptNew = 0;\n let keptOld = 0;\n let manualReview = 0;\n let rejected = 0;\n const errors: string[] = [];\n\n for (const decision of decisions) {\n if (!decision.contradictionId?.trim()) {\n rejected++;\n errors.push(\"Decision is missing contradictionId.\");\n continue;\n }\n if (decision.decision === \"manual_review\") {\n manualReview++;\n continue;\n }\n if (decision.decision === \"merge\") {\n rejected++;\n errors.push(`Contradiction ${decision.contradictionId}: merge decisions must be handled manually.`);\n continue;\n }\n\n const contradiction = db\n .prepare(\"SELECT fact_id_new, fact_id_old, resolved FROM contradictions WHERE id = ?\")\n .get(decision.contradictionId) as { fact_id_new: string; fact_id_old: string; resolved: number } | undefined;\n if (!contradiction) {\n rejected++;\n errors.push(`Contradiction ${decision.contradictionId}: row not found.`);\n continue;\n }\n if (contradiction.resolved === 1) {\n rejected++;\n errors.push(`Contradiction ${decision.contradictionId}: already resolved.`);\n continue;\n }\n\n const keepNew = decision.decision === \"keep_new\";\n const didApply = persistContradictionDecision(db, getById, supersede, {\n contradictionId: decision.contradictionId,\n keptFactId: keepNew ? contradiction.fact_id_new : contradiction.fact_id_old,\n supersededFactId: keepNew ? contradiction.fact_id_old : contradiction.fact_id_new,\n resolution: keepNew ? \"superseded\" : \"kept\",\n strategy: \"manual-review-file\",\n confidence: clampConfidence(decision.confidence) || 1,\n reason: decision.reason?.trim() || \"Applied from contradiction review file.\",\n actor,\n toolVersion,\n mode: \"review\",\n model: null,\n });\n if (!didApply) {\n rejected++;\n errors.push(`Contradiction ${decision.contradictionId}: could not persist decision.`);\n continue;\n }\n applied++;\n if (keepNew) keptNew++;\n else keptOld++;\n }\n\n return { applied, keptNew, keptOld, manualReview, rejected, errors };\n}\n\nexport function contradictionsCount(db: DatabaseSync): number {\n try {\n const row = db.prepare(\"SELECT COUNT(*) as cnt FROM contradictions WHERE resolved = 0\").get() as {\n cnt: number;\n };\n return row?.cnt ?? 0;\n } catch (err) {\n capturePluginError(err as Error, {\n operation: \"count-contradictions\",\n severity: \"info\",\n subsystem: \"facts\",\n });\n return 0;\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"crud.js","names":[],"sources":["../../../backends/facts-db/crud.ts"],"sourcesContent":["/**\n * Fact lifecycle CRUD: store, access refresh, delete, dedupe (Issue #954).\n */\nimport { randomUUID } from \"node:crypto\";\nimport type { DatabaseSync } from \"node:sqlite\";\n\nimport { type DecayClass, type MemoryCategory, type StoreConfig, TTL_DEFAULTS } from \"../../config.js\";\nimport { applyDedupe, hasGlobalDuplicateProbe, resolveDedupeProfile } from \"../../services/dedupe-policy.js\";\nimport { isPromptArtifactOrReasoningTrace } from \"../../services/capture-utils.js\";\nimport type { MemoryEntry, MemoryTier } from \"../../types/memory.js\";\nimport { SQLITE_BUSY_TIMEOUT_MS } from \"../../utils/constants.js\";\nimport { calculateExpiry, classifyDecay } from \"../../utils/decay.js\";\nimport { createTransaction, type SqliteTransactionBeginMode } from \"../../utils/sqlite-transaction.js\";\nimport { normalizedHash, serializeTags } from \"../../utils/tags.js\";\n\nconst SQLITE_BUSY_STORE_MAX_RETRIES = 3;\nconst SQLITE_BUSY_STORE_BACKOFF_BASE_MS = 50;\nconst SQLITE_BUSY_STORE_BACKOFF_MAX_MS = 500;\nconst SQLITE_BUSY_STORE_SLEEP = new Int32Array(new SharedArrayBuffer(4));\n\nfunction isSqliteBusyError(err: unknown): boolean {\n const message = err instanceof Error ? err.message : String(err);\n const code =\n typeof err === \"object\" && err !== null && \"code\" in err ? String((err as { code?: unknown }).code ?? \"\") : \"\";\n return /SQLITE_BUSY|database is locked/i.test(message) || /SQLITE_BUSY/i.test(code);\n}\n\nfunction sleepSync(ms: number): void {\n if (ms <= 0) return;\n Atomics.wait(SQLITE_BUSY_STORE_SLEEP, 0, 0, ms);\n}\n\nfunction runWithSqliteBusyRetry(db: DatabaseSync, run: () => void): void {\n for (let attempt = 0; attempt <= SQLITE_BUSY_STORE_MAX_RETRIES; attempt += 1) {\n try {\n run();\n return;\n } catch (err) {\n if (!isSqliteBusyError(err) || attempt === SQLITE_BUSY_STORE_MAX_RETRIES) {\n throw err;\n }\n // Re-apply timeout before retry in case lock contention happened after reconnect/reopen.\n db.exec(`PRAGMA busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS}`);\n const delayMs = Math.min(SQLITE_BUSY_STORE_BACKOFF_BASE_MS * 2 ** attempt, SQLITE_BUSY_STORE_BACKOFF_MAX_MS);\n sleepSync(delayMs);\n }\n }\n}\n\n// Pre-store guard constants: filter internal artifacts (#1560, #1561).\n// These categories and sources are not user-relevant memories.\nconst BLOCKED_CATEGORIES = new Set([\"noop\", \"classification\", \"artifact\", \"chain-of-thought\", \"prompt\"]);\nconst BLOCKED_SOURCES = new Set([\"think\", \"classify\", \"remember\", \"noop\", \"compact\", \"derive\"]);\n\n/** Check if an entry would be blocked by the pre-store guard. */\nexport function isPreStoreGuardBlocked(entry: { category?: string; source?: string; text: string }): boolean {\n const entryCategory = entry.category ?? \"\";\n const entrySource = entry.source ?? \"\";\n return (\n BLOCKED_CATEGORIES.has(entryCategory) ||\n BLOCKED_SOURCES.has(entrySource) ||\n isPromptArtifactOrReasoningTrace(entry.text)\n );\n}\n\n/** Input shape for `FactsDB.store` / `storeFact`. */\nexport type StoreFactInput = Omit<\n MemoryEntry,\n \"id\" | \"createdAt\" | \"decayClass\" | \"expiresAt\" | \"lastConfirmedAt\" | \"confidence\"\n> & {\n decayClass?: DecayClass;\n expiresAt?: number | null;\n confidence?: number;\n summary?: string | null;\n sourceDate?: number | null;\n tags?: string[] | null;\n validFrom?: number | null;\n validUntil?: number | null;\n supersedesId?: string | null;\n procedureType?: \"positive\" | \"negative\" | null;\n successCount?: number;\n lastValidated?: number | null;\n sourceSessions?: string | null;\n embeddingModel?: string | null;\n scope?: \"global\" | \"user\" | \"agent\" | \"session\";\n scopeTarget?: string | null;\n decayFreezeUntil?: number | null;\n provenanceSession?: string | null;\n sourceTurn?: number | null;\n extractionMethod?: string | null;\n extractionConfidence?: number | null;\n preserveUntil?: number | null;\n preserveTags?: string[] | null;\n};\n\nexport function validateStoreEntryInput(\n entry: Omit<MemoryEntry, \"id\" | \"createdAt\" | \"decayClass\" | \"expiresAt\" | \"lastConfirmedAt\" | \"confidence\"> & {\n category?: MemoryCategory;\n importance?: number;\n },\n): void {\n const text = (entry.text ?? \"\").trim();\n if (text.length === 0) {\n throw new Error(\"memory-hybrid: cannot store empty fact text\");\n }\n const imp = entry.importance ?? 0.5;\n if (!Number.isFinite(imp) || imp < 0 || imp > 1) {\n throw new Error(\"memory-hybrid: importance must be a number in [0, 1]\");\n }\n}\n\nexport function getDuplicateIdByNormalizedHash(db: DatabaseSync, text: string): string | null {\n const hash = normalizedHash(text);\n const row = db\n .prepare(\n \"SELECT id FROM facts WHERE normalized_hash = ? AND superseded_at IS NULL ORDER BY created_at DESC LIMIT 1\",\n )\n .get(hash) as { id: string } | undefined;\n return row?.id ?? null;\n}\n\nexport type StoreFactContext = {\n db: DatabaseSync;\n fuzzyDedupe: boolean;\n storeConfig?: StoreConfig;\n getById: (id: string) => MemoryEntry | null;\n invalidateSupersededCache: () => void;\n warnOnce?: (key: string, message: string) => void;\n warnOnceKey?: string;\n suppressVectorFallbackWarning?: boolean;\n /**\n * Allow trusted edit paths to re-store an already persisted fact whose legacy\n * source/category is now blocked by the artifact guard (#1560/#1561).\n */\n allowPreStoreGuardBypass?: boolean;\n /**\n * Pre-computed vector neighbour candidates for the new fact's embedding (#1186, #1194).\n * Caller is expected to populate this when the embedding is known and the policy has\n * `vectorThreshold` configured.\n */\n vectorCandidates?: ReadonlyArray<{ id: string; score: number }>;\n};\n\nexport type StoredFactResult = {\n /** The stored fact entry */\n entry: MemoryEntry;\n /** Stored results are never legacy-rejected. */\n rejected?: false;\n /**\n * CRITICAL (#2): ID of fact evicted during onOverflow=evict-lowest-confidence.\n * Caller MUST delete the vector from VectorDB to prevent orphaned vectors.\n */\n evictedFactId?: string | null;\n /**\n * True when the stored fact's text was updated in-place by a dedupe merge, so the\n * existing LanceDB vector now encodes stale content. Caller MUST re-embed\n * `entry.text` and replace the vector for `entry.id` in VectorDB.\n */\n embeddingStale?: boolean;\n /**\n * True only when this call inserted a brand-new fact row.\n * False when the returned entry points to a pre-existing row (skip/boost/merge).\n */\n newlyStored?: boolean;\n /**\n * Original text for dedupe-merge updates. Callers can use this for reliable rollback\n * when post-merge embed/vector persistence fails.\n */\n preMergeText?: string | null;\n /**\n * True when the pre-store guard filtered this entry as an internal artifact (#1560, #1561).\n * Callers must skip post-store operations (vector upsert, supersession, logging) when true.\n */\n skipped?: false;\n};\n\nexport type SkippedStoreFactResult = {\n /**\n * True when the pre-store guard filtered this entry as an internal artifact (#1560, #1561).\n * No fact was written, so callers must skip all post-store operations.\n */\n skipped: true;\n /** Non-addressable placeholder retained for legacy callers; do not use for vector/supersession/provenance side effects. */\n entry: MemoryEntry;\n /** Skipped results never evict persisted facts. */\n evictedFactId: null;\n /** Skipped results never modify embeddings. */\n embeddingStale: false;\n /** Skipped results never insert a persisted row. */\n newlyStored: false;\n /** Skipped results never mutate an existing row. */\n preMergeText: null;\n /** Legacy alias for skipped. */\n rejected: true;\n};\n\nexport type StoreFactResult = StoredFactResult | SkippedStoreFactResult;\n\nfunction createSkippedStorePlaceholder(entry: StoreFactInput): MemoryEntry {\n const nowSec = Math.floor(Date.now() / 1000);\n const decayClass =\n entry.decayClass ??\n classifyDecay(entry.entity ?? null, entry.key ?? null, entry.value ?? null, entry.text, {\n source: entry.source ?? null,\n category: entry.category ?? null,\n importance: entry.importance ?? 0.5,\n });\n return {\n id: \"\",\n text: entry.text,\n why: entry.why ?? null,\n category: entry.category ?? \"other\",\n importance: entry.importance ?? 0.5,\n entity: entry.entity ?? null,\n key: entry.key ?? null,\n value: entry.value ?? null,\n source: entry.source ?? \"conversation\",\n createdAt: nowSec,\n sourceDate: entry.sourceDate ?? null,\n decayClass,\n expiresAt: entry.expiresAt ?? calculateExpiry(decayClass, nowSec),\n lastConfirmedAt: nowSec,\n confidence: entry.confidence ?? 1,\n summary: entry.summary ?? null,\n tags: entry.tags ?? null,\n validFrom: entry.validFrom ?? null,\n validUntil: entry.validUntil ?? null,\n supersedesId: entry.supersedesId ?? null,\n scope: entry.scope ?? \"global\",\n scopeTarget: entry.scopeTarget ?? null,\n procedureType: entry.procedureType ?? null,\n successCount: entry.successCount,\n lastValidated: entry.lastValidated ?? null,\n sourceSessions: entry.sourceSessions ?? null,\n embeddingModel: entry.embeddingModel ?? null,\n provenanceSession: entry.provenanceSession ?? null,\n sourceTurn: entry.sourceTurn ?? null,\n extractionMethod: entry.extractionMethod ?? null,\n extractionConfidence: entry.extractionConfidence ?? null,\n decayFreezeUntil: entry.decayFreezeUntil ?? null,\n preserveUntil: entry.preserveUntil ?? null,\n preserveTags: entry.preserveTags ?? null,\n };\n}\n\nexport function storeFact(ctx: StoreFactContext, entry: StoreFactInput): StoreFactResult {\n validateStoreEntryInput(entry);\n\n const entryCategory = entry.category ?? \"\";\n const entrySource = entry.source ?? \"\";\n if (\n !ctx.allowPreStoreGuardBypass &&\n (BLOCKED_CATEGORIES.has(entryCategory) ||\n BLOCKED_SOURCES.has(entrySource) ||\n isPromptArtifactOrReasoningTrace(entry.text))\n ) {\n return {\n skipped: true,\n rejected: true,\n evictedFactId: null,\n embeddingStale: false,\n newlyStored: false,\n preMergeText: null,\n entry: createSkippedStorePlaceholder(entry),\n };\n }\n\n const sourceForPolicy = entry.source ?? \"conversation\";\n const profile = resolveDedupeProfile(sourceForPolicy, ctx.storeConfig ?? { fuzzyDedupe: ctx.fuzzyDedupe });\n const nowSec = Math.floor(Date.now() / 1000);\n const day = new Date(nowSec * 1000).toISOString().slice(0, 10);\n\n // Normalized-hash + lexical Jaccard dedupe (per-source profiles) before daily quota.\n const dedupe = applyDedupe(\n profile,\n {\n text: entry.text,\n source: sourceForPolicy,\n scope: entry.scope ?? \"global\",\n scopeTarget: entry.scopeTarget ?? null,\n category: entry.category ?? null,\n entity: entry.entity ?? null,\n key: entry.key ?? null,\n value: entry.value ?? null,\n },\n {\n db: ctx.db,\n nowSec,\n fuzzyDedupe: ctx.fuzzyDedupe,\n vectorCandidates: ctx.vectorCandidates,\n warnOnce: ctx.warnOnce,\n warnOnceKey: ctx.warnOnceKey,\n suppressVectorFallbackWarning: ctx.suppressVectorFallbackWarning,\n warn: (_m) => {},\n },\n );\n\n if (dedupe.action === \"skip\") {\n // #1186 acceptance (\"cosine ≥ 0.85 → skip + recall_count++\"): when we skipped because\n // of a near-duplicate, bump recall on the existing winner so the dedup acts as a\n // reinforcement signal instead of silently dropping the new evidence.\n if (dedupe.reason === \"vector\" || dedupe.reason === \"lexical\" || dedupe.reason === \"hash\") {\n runWithSqliteBusyRetry(ctx.db, () => {\n ctx.db\n .prepare(\n \"UPDATE facts SET recall_count = recall_count + 1, access_count = access_count + 1, last_confirmed_at = ? WHERE id = ?\",\n )\n .run(nowSec, dedupe.existingId);\n });\n }\n const existing = ctx.getById(dedupe.existingId);\n if (existing)\n return { entry: existing, evictedFactId: null, embeddingStale: false, newlyStored: false, preMergeText: null };\n throw new Error(\n `memory-hybrid: dedupe existing fact ${dedupe.existingId} not found (may have been deleted concurrently)`,\n );\n }\n\n if (dedupe.action === \"boost\") {\n runWithSqliteBusyRetry(ctx.db, () => {\n ctx.db\n .prepare(\n \"UPDATE facts SET recall_count = recall_count + 1, access_count = access_count + 1, importance = min(1.0, importance + ?) WHERE id = ?\",\n )\n .run(dedupe.boostBy, dedupe.existingId);\n });\n const boosted = ctx.getById(dedupe.existingId);\n if (boosted)\n return { entry: boosted, evictedFactId: null, embeddingStale: false, newlyStored: false, preMergeText: null };\n throw new Error(\n `memory-hybrid: dedupe existing fact ${dedupe.existingId} not found (may have been deleted concurrently)`,\n );\n }\n\n if (dedupe.action === \"merge\") {\n const existing = ctx.getById(dedupe.existingId);\n if (existing) {\n const alreadyContained = existing.text.includes(entry.text);\n if (alreadyContained) {\n return { entry: existing, evictedFactId: null, embeddingStale: false, newlyStored: false, preMergeText: null };\n }\n\n const rawMergedText = `${existing.text}\\n${entry.text}`;\n if (rawMergedText.length > 4000) {\n process.stderr.write(\n `memory-hybrid: dedupe merge for fact ${existing.id} truncated to 4000 chars (combined length=${rawMergedText.length}); some content may be lost\\n`,\n );\n }\n const mergedText = rawMergedText.slice(0, 4000);\n const mergedHash = normalizedHash(mergedText);\n // Wrap the merge UPDATE in a transaction so it is atomic and can be\n // rolled back on failure. Without this, an interrupted write leaves\n // SQLite with new merged text while LanceDB still encodes the pre-merge\n // content (split-brain). createTransaction() uses a SAVEPOINT when a\n // transaction is already active, so nesting is safe.\n const mergeTx = createTransaction(ctx.db, () => {\n ctx.db\n .prepare(\"UPDATE facts SET text = ?, normalized_hash = ? WHERE id = ?\")\n .run(mergedText, mergedHash, existing.id);\n });\n runWithSqliteBusyRetry(ctx.db, () => {\n mergeTx();\n });\n // Signal callers to re-embed only when the persisted text changed. This handles edge\n // cases where append text is truncated and the final merged text remains unchanged.\n const embeddingStale = mergedText !== existing.text;\n return {\n entry: ctx.getById(existing.id) ?? existing,\n evictedFactId: null,\n embeddingStale,\n newlyStored: false,\n preMergeText: existing.text,\n };\n }\n throw new Error(\n `memory-hybrid: dedupe existing fact ${dedupe.existingId} not found (may have been deleted concurrently)`,\n );\n }\n\n const id = randomUUID();\n\n const importance = entry.importance ?? 0.5;\n const why = entry.why ?? null;\n const entity = entry.entity?.trim() || null;\n const key = entry.key?.trim() || null;\n const value = entry.value ?? null;\n const source = entry.source ?? \"conversation\";\n const category = (entry.category?.trim() || \"other\").toLowerCase();\n const decayClass =\n entry.decayClass || classifyDecay(entity, key, value, entry.text, { source, category, importance });\n const expiresAt = entry.expiresAt !== undefined ? entry.expiresAt : calculateExpiry(decayClass, nowSec);\n const confidence = entry.confidence ?? 1.0;\n const summary = entry.summary ?? null;\n const embeddingModel = entry.embeddingModel ?? null;\n const normHash = normalizedHash(entry.text);\n const sourceDate = entry.sourceDate ?? null;\n const tags = entry.tags ?? null;\n const tagsStr = tags ? serializeTags(tags) : null;\n const validFrom = entry.validFrom ?? sourceDate ?? nowSec;\n const validUntil = entry.validUntil ?? null;\n const supersedesId = entry.supersedesId ?? null;\n const scope = entry.scope ?? \"global\";\n const scopeTarget = scope === \"global\" ? null : (entry.scopeTarget ?? null);\n if (scope !== \"global\" && !scopeTarget) {\n throw new Error(`scopeTarget required for non-global scope: ${scope}`);\n }\n const procedureType = entry.procedureType ?? null;\n const successCount = entry.successCount ?? 0;\n const lastValidated = entry.lastValidated ?? null;\n const sourceSessionsRaw = entry.sourceSessions ?? null;\n const sourceSessionsStr =\n sourceSessionsRaw == null\n ? null\n : typeof sourceSessionsRaw === \"string\"\n ? sourceSessionsRaw\n : JSON.stringify(sourceSessionsRaw);\n const provenanceSession = entry.provenanceSession ?? null;\n const sourceTurn = entry.sourceTurn ?? null;\n const extractionMethod = entry.extractionMethod ?? null;\n const extractionConfidence = entry.extractionConfidence !== undefined ? entry.extractionConfidence : null;\n const preserveUntil = entry.preserveUntil ?? null;\n const preserveTags = entry.preserveTags ?? null;\n const preserveTagsStr = preserveTags ? JSON.stringify(preserveTags) : null;\n const provenanceJson = entry.provenanceJson ?? null;\n\n const tier: MemoryTier = (entry as { tier?: MemoryTier }).tier ?? \"warm\";\n const rawFreeze = (entry as { decayFreezeUntil?: number | null }).decayFreezeUntil ?? null;\n const decayFreezeUntil = rawFreeze !== null && Number.isFinite(rawFreeze) ? rawFreeze : null;\n const adjustedExpiresAt =\n decayFreezeUntil !== null && expiresAt !== null && expiresAt < decayFreezeUntil ? decayFreezeUntil : expiresAt;\n const beginMode: SqliteTransactionBeginMode = profile.maxPerDay != null ? \"IMMEDIATE\" : \"DEFERRED\";\n // Quota \"drop\" path records `dropped` in this transaction, commits, then throws below so\n // observability survives the error. Retrying the same write increments `dropped` again (each\n // attempt is counted), which is intentional for operational metrics.\n let quotaExceededSource: string | null = null;\n let evictedFactId: string | null = null;\n const tx = createTransaction(\n ctx.db,\n () => {\n if (profile.maxPerDay != null) {\n const quotaRow = ctx.db\n .prepare(\"SELECT count FROM daily_writes WHERE source = ? AND day = ?\")\n .get(sourceForPolicy, day) as { count: number } | undefined;\n if ((quotaRow?.count ?? 0) >= profile.maxPerDay) {\n // #1194: legacy behaviour was throw-on-overflow + bump `dropped`. With\n // `onOverflow=evict-lowest-confidence`, we instead supersede the lowest-confidence\n // active fact for this source and let the new write through, which prevents the\n // quota from acting as a \"freeze the noise\" gate when noisy sources accumulate stale\n // low-confidence rows.\n if (profile.onOverflow === \"evict-lowest-confidence\") {\n const victim = ctx.db\n .prepare(\n `SELECT id FROM facts\n WHERE source = ? AND superseded_at IS NULL\n ORDER BY confidence ASC, COALESCE(recall_count, 0) ASC, created_at ASC\n LIMIT 1`,\n )\n .get(sourceForPolicy) as { id: string } | undefined;\n if (victim) {\n ctx.db\n .prepare(\"UPDATE facts SET superseded_at = ? WHERE id = ? AND superseded_at IS NULL\")\n .run(nowSec, victim.id);\n evictedFactId = victim.id;\n ctx.db\n .prepare(\n `INSERT INTO daily_writes (source, day, count, dropped, evicted) VALUES (?, ?, 0, 0, 1)\n ON CONFLICT(source, day) DO UPDATE SET evicted = evicted + 1`,\n )\n .run(sourceForPolicy, day);\n // Fall through to the INSERT path below (do not return).\n } else {\n quotaExceededSource = sourceForPolicy;\n ctx.db\n .prepare(\n `INSERT INTO daily_writes (source, day, count, dropped) VALUES (?, ?, 0, 1)\n ON CONFLICT(source, day) DO UPDATE SET dropped = dropped + 1`,\n )\n .run(sourceForPolicy, day);\n return;\n }\n } else {\n quotaExceededSource = sourceForPolicy;\n ctx.db\n .prepare(\n `INSERT INTO daily_writes (source, day, count, dropped) VALUES (?, ?, 0, 1)\n ON CONFLICT(source, day) DO UPDATE SET dropped = dropped + 1`,\n )\n .run(sourceForPolicy, day);\n return;\n }\n }\n }\n ctx.db\n .prepare(\n `INSERT INTO facts (id, text, why, category, importance, entity, key, value, source, created_at, decay_class, expires_at, last_confirmed_at, confidence, summary, embedding_model, normalized_hash, source_date, tags, valid_from, valid_until, supersedes_id, tier, scope, scope_target, procedure_type, success_count, last_validated, source_sessions, decay_freeze_until, provenance_session, source_turn, extraction_method, extraction_confidence, preserve_until, preserve_tags, provenance_json)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .run(\n id,\n entry.text,\n why,\n category,\n importance,\n entity,\n key,\n value,\n source,\n nowSec,\n decayClass,\n adjustedExpiresAt,\n nowSec,\n confidence,\n summary,\n embeddingModel,\n normHash,\n sourceDate,\n tagsStr,\n validFrom,\n validUntil,\n supersedesId,\n tier,\n scope,\n scopeTarget,\n procedureType,\n successCount,\n lastValidated,\n sourceSessionsStr,\n decayFreezeUntil,\n provenanceSession,\n sourceTurn,\n extractionMethod,\n extractionConfidence,\n preserveUntil,\n preserveTagsStr,\n provenanceJson,\n );\n // Count bump is in the same IMMEDIATE transaction as the facts INSERT when maxPerDay is set.\n if (profile.maxPerDay != null) {\n ctx.db\n .prepare(\n `INSERT INTO daily_writes (source, day, count, dropped) VALUES (?, ?, 1, 0)\n ON CONFLICT(source, day) DO UPDATE SET count = count + 1`,\n )\n .run(sourceForPolicy, day);\n }\n },\n beginMode,\n );\n runWithSqliteBusyRetry(ctx.db, () => {\n quotaExceededSource = null;\n evictedFactId = null;\n tx();\n });\n if (quotaExceededSource) {\n throw new Error(`memory-hybrid: daily write quota exceeded for source ${quotaExceededSource}`);\n }\n if (supersedesId || evictedFactId) {\n ctx.invalidateSupersededCache();\n }\n const loaded = ctx.getById(id);\n if (!loaded) {\n throw new Error(`memory-hybrid: store() failed to read back inserted fact ${id}`);\n }\n return { entry: loaded, evictedFactId, embeddingStale: false, newlyStored: true, preMergeText: null };\n}\n\n/**\n * Update recall_count and last_accessed for facts (bulk UPDATE).\n * MUST only be called when full content is injected or explicitly recalled by user.\n * Index-only exposures must use refreshIndexedFacts() instead (#1559).\n */\nexport function refreshAccessedFacts(db: DatabaseSync, ids: string[]): void {\n if (ids.length === 0) return;\n const nowSec = Math.floor(Date.now() / 1000);\n const BATCH_SIZE = 500;\n\n const tx = createTransaction(db, () => {\n for (let i = 0; i < ids.length; i += BATCH_SIZE) {\n const batch = ids.slice(i, i + BATCH_SIZE);\n const placeholders = batch.map(() => \"?\").join(\",\");\n\n db.prepare(\n `UPDATE facts SET last_confirmed_at = ?, expires_at = CASE decay_class WHEN 'stable' THEN ? + ? WHEN 'active' THEN ? + ? WHEN 'durable' THEN ? + ? WHEN 'normal' THEN ? + ? ELSE expires_at END WHERE id IN (${placeholders}) AND decay_class IN ('stable', 'active', 'durable', 'normal')`,\n ).run(\n nowSec,\n nowSec,\n TTL_DEFAULTS.stable,\n nowSec,\n TTL_DEFAULTS.active,\n nowSec,\n TTL_DEFAULTS.durable,\n nowSec,\n TTL_DEFAULTS.normal,\n ...batch,\n );\n\n db.prepare(\n `UPDATE facts SET recall_count = recall_count + 1, last_accessed = ?, access_count = access_count + 1, last_accessed_at = strftime('%Y-%m-%dT%H:%M:%SZ', ?, 'unixepoch') WHERE id IN (${placeholders})`,\n ).run(nowSec, nowSec, ...batch);\n }\n });\n tx();\n}\n\n/**\n * Update indexed_count and last_indexed for index-only exposures (#1559).\n * Does NOT inflate recall_count or last_accessed — these are separate signals.\n */\nexport function refreshIndexedFacts(db: DatabaseSync, ids: string[]): void {\n if (ids.length === 0) return;\n const nowSec = Math.floor(Date.now() / 1000);\n const BATCH_SIZE = 500;\n\n const tx = createTransaction(db, () => {\n for (let i = 0; i < ids.length; i += BATCH_SIZE) {\n const batch = ids.slice(i, i + BATCH_SIZE);\n const placeholders = batch.map(() => \"?\").join(\",\");\n db.prepare(\n `UPDATE facts SET indexed_count = indexed_count + 1, last_indexed = ? WHERE id IN (${placeholders})`,\n ).run(nowSec, ...batch);\n }\n });\n tx();\n}\n\nexport function deleteFact(db: DatabaseSync, id: string): boolean {\n const tx = createTransaction(db, () => {\n db.prepare(\"DELETE FROM contradictions WHERE fact_id_new = ? OR fact_id_old = ?\").run(id, id);\n db.prepare(\"DELETE FROM memory_links WHERE source_fact_id = ? OR target_fact_id = ?\").run(id, id);\n const result = db.prepare(\"DELETE FROM facts WHERE id = ?\").run(id);\n return result.changes > 0;\n });\n return tx();\n}\n\n/**\n * Exact match or write-time dedupe policy would not insert a new row.\n * When `source` is omitted, uses a global probe (any source) for idempotency / CLI alignment (#1202).\n */\nexport function hasDuplicateText(\n db: DatabaseSync,\n fuzzyDedupe: boolean,\n text: string,\n storeConfig?: StoreConfig,\n source?: string,\n structured?: { category?: MemoryCategory | null; entity?: string | null; key?: string | null; value?: string | null },\n): boolean {\n const nowSec = Math.floor(Date.now() / 1000);\n if (source === undefined) {\n return hasGlobalDuplicateProbe(db, text, { nowSec, fuzzyDedupe, storeConfig });\n }\n const profile = resolveDedupeProfile(source, storeConfig ?? { fuzzyDedupe });\n const r = applyDedupe(\n profile,\n {\n text,\n source,\n category: structured?.category ?? null,\n entity: structured?.entity ?? null,\n key: structured?.key ?? null,\n value: structured?.value ?? null,\n },\n { db, nowSec, fuzzyDedupe },\n );\n return r.action !== \"store\";\n}\n\nexport function statsDailyWrites(\n db: DatabaseSync,\n): Array<{ source: string; day: string; count: number; dropped: number; evicted: number }> {\n return db\n .prepare(\n \"SELECT source, day, count, dropped, COALESCE(evicted, 0) AS evicted FROM daily_writes ORDER BY day DESC, source ASC LIMIT 100\",\n )\n .all() as Array<{ source: string; day: string; count: number; dropped: number; evicted: number }>;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"crud.js","names":[],"sources":["../../../backends/facts-db/crud.ts"],"sourcesContent":["/**\n * Fact lifecycle CRUD: store, access refresh, delete, dedupe (Issue #954).\n */\nimport { randomUUID } from \"node:crypto\";\nimport type { DatabaseSync } from \"node:sqlite\";\n\nimport { type DecayClass, type MemoryCategory, type StoreConfig, TTL_DEFAULTS } from \"../../config.js\";\nimport { applyDedupe, hasGlobalDuplicateProbe, resolveDedupeProfile } from \"../../services/dedupe-policy.js\";\nimport { isPromptArtifactOrReasoningTrace } from \"../../services/capture-utils.js\";\nimport type { MemoryEntry, MemoryTier } from \"../../types/memory.js\";\nimport { SQLITE_BUSY_TIMEOUT_MS } from \"../../utils/constants.js\";\nimport { calculateExpiry, classifyDecay } from \"../../utils/decay.js\";\nimport { createTransaction, type SqliteTransactionBeginMode } from \"../../utils/sqlite-transaction.js\";\nimport { normalizedHash, serializeTags } from \"../../utils/tags.js\";\n\nconst SQLITE_BUSY_STORE_MAX_RETRIES = 3;\nconst SQLITE_BUSY_STORE_BACKOFF_BASE_MS = 50;\nconst SQLITE_BUSY_STORE_BACKOFF_MAX_MS = 500;\nconst SQLITE_BUSY_STORE_SLEEP = new Int32Array(new SharedArrayBuffer(4));\n\nfunction isSqliteBusyError(err: unknown): boolean {\n const message = err instanceof Error ? err.message : String(err);\n const code =\n typeof err === \"object\" && err !== null && \"code\" in err ? String((err as { code?: unknown }).code ?? \"\") : \"\";\n return /SQLITE_BUSY|database is locked/i.test(message) || /SQLITE_BUSY/i.test(code);\n}\n\nfunction sleepSync(ms: number): void {\n if (ms <= 0) return;\n Atomics.wait(SQLITE_BUSY_STORE_SLEEP, 0, 0, ms);\n}\n\nfunction runWithSqliteBusyRetry(db: DatabaseSync, run: () => void): void {\n for (let attempt = 0; attempt <= SQLITE_BUSY_STORE_MAX_RETRIES; attempt += 1) {\n try {\n run();\n return;\n } catch (err) {\n if (!isSqliteBusyError(err) || attempt === SQLITE_BUSY_STORE_MAX_RETRIES) {\n throw err;\n }\n // Re-apply timeout before retry in case lock contention happened after reconnect/reopen.\n db.exec(`PRAGMA busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS}`);\n const delayMs = Math.min(SQLITE_BUSY_STORE_BACKOFF_BASE_MS * 2 ** attempt, SQLITE_BUSY_STORE_BACKOFF_MAX_MS);\n sleepSync(delayMs);\n }\n }\n}\n\n// Pre-store guard constants: filter internal artifacts (#1560, #1561).\n// These categories and sources are not user-relevant memories.\nconst BLOCKED_CATEGORIES = new Set([\"noop\", \"classification\", \"artifact\", \"chain-of-thought\", \"prompt\"]);\nconst BLOCKED_SOURCES = new Set([\"think\", \"classify\", \"remember\", \"noop\", \"compact\", \"derive\"]);\n\n/** Check if an entry would be blocked by the pre-store guard. */\nexport function isPreStoreGuardBlocked(entry: { category?: string; source?: string; text: string }): boolean {\n const entryCategory = entry.category ?? \"\";\n const entrySource = entry.source ?? \"\";\n return (\n BLOCKED_CATEGORIES.has(entryCategory) ||\n BLOCKED_SOURCES.has(entrySource) ||\n isPromptArtifactOrReasoningTrace(entry.text)\n );\n}\n\n/** Input shape for `FactsDB.store` / `storeFact`. */\nexport type StoreFactInput = Omit<\n MemoryEntry,\n \"id\" | \"createdAt\" | \"decayClass\" | \"expiresAt\" | \"lastConfirmedAt\" | \"confidence\"\n> & {\n decayClass?: DecayClass;\n expiresAt?: number | null;\n confidence?: number;\n summary?: string | null;\n sourceDate?: number | null;\n tags?: string[] | null;\n validFrom?: number | null;\n validUntil?: number | null;\n supersedesId?: string | null;\n procedureType?: \"positive\" | \"negative\" | null;\n successCount?: number;\n lastValidated?: number | null;\n sourceSessions?: string | null;\n embeddingModel?: string | null;\n scope?: \"global\" | \"user\" | \"agent\" | \"session\";\n scopeTarget?: string | null;\n decayFreezeUntil?: number | null;\n provenanceSession?: string | null;\n sourceTurn?: number | null;\n extractionMethod?: string | null;\n extractionConfidence?: number | null;\n preserveUntil?: number | null;\n preserveTags?: string[] | null;\n};\n\nexport function validateStoreEntryInput(\n entry: Omit<MemoryEntry, \"id\" | \"createdAt\" | \"decayClass\" | \"expiresAt\" | \"lastConfirmedAt\" | \"confidence\"> & {\n category?: MemoryCategory;\n importance?: number;\n },\n): void {\n const text = (entry.text ?? \"\").trim();\n if (text.length === 0) {\n throw new Error(\"memory-hybrid: cannot store empty fact text\");\n }\n const imp = entry.importance ?? 0.5;\n if (!Number.isFinite(imp) || imp < 0 || imp > 1) {\n throw new Error(\"memory-hybrid: importance must be a number in [0, 1]\");\n }\n}\n\nexport function getDuplicateIdByNormalizedHash(db: DatabaseSync, text: string): string | null {\n const hash = normalizedHash(text);\n const row = db\n .prepare(\n \"SELECT id FROM facts WHERE normalized_hash = ? AND superseded_at IS NULL ORDER BY created_at DESC LIMIT 1\",\n )\n .get(hash) as { id: string } | undefined;\n return row?.id ?? null;\n}\n\nexport type StoreFactContext = {\n db: DatabaseSync;\n fuzzyDedupe: boolean;\n storeConfig?: StoreConfig;\n getById: (id: string) => MemoryEntry | null;\n invalidateSupersededCache: () => void;\n warnOnce?: (key: string, message: string) => void;\n warnOnceKey?: string;\n suppressVectorFallbackWarning?: boolean;\n /**\n * Allow trusted edit paths to re-store an already persisted fact whose legacy\n * source/category is now blocked by the artifact guard (#1560/#1561).\n */\n allowPreStoreGuardBypass?: boolean;\n /**\n * Pre-computed vector neighbour candidates for the new fact's embedding (#1186, #1194).\n * Caller is expected to populate this when the embedding is known and the policy has\n * `vectorThreshold` configured.\n */\n vectorCandidates?: ReadonlyArray<{ id: string; score: number }>;\n};\n\nexport type StoredFactResult = {\n /** The stored fact entry */\n entry: MemoryEntry;\n /** Stored results are never legacy-rejected. */\n rejected?: false;\n /**\n * CRITICAL (#2): ID of fact evicted during onOverflow=evict-lowest-confidence.\n * Caller MUST delete the vector from VectorDB to prevent orphaned vectors.\n */\n evictedFactId?: string | null;\n /**\n * True when the stored fact's text was updated in-place by a dedupe merge, so the\n * existing LanceDB vector now encodes stale content. Caller MUST re-embed\n * `entry.text` and replace the vector for `entry.id` in VectorDB.\n */\n embeddingStale?: boolean;\n /**\n * True only when this call inserted a brand-new fact row.\n * False when the returned entry points to a pre-existing row (skip/boost/merge).\n */\n newlyStored?: boolean;\n /**\n * Original text for dedupe-merge updates. Callers can use this for reliable rollback\n * when post-merge embed/vector persistence fails.\n */\n preMergeText?: string | null;\n /**\n * True when the pre-store guard filtered this entry as an internal artifact (#1560, #1561).\n * Callers must skip post-store operations (vector upsert, supersession, logging) when true.\n */\n skipped?: false;\n};\n\nexport type SkippedStoreFactResult = {\n /**\n * True when the pre-store guard filtered this entry as an internal artifact (#1560, #1561).\n * No fact was written, so callers must skip all post-store operations.\n */\n skipped: true;\n /** Non-addressable placeholder retained for legacy callers; do not use for vector/supersession/provenance side effects. */\n entry: MemoryEntry;\n /** Skipped results never evict persisted facts. */\n evictedFactId: null;\n /** Skipped results never modify embeddings. */\n embeddingStale: false;\n /** Skipped results never insert a persisted row. */\n newlyStored: false;\n /** Skipped results never mutate an existing row. */\n preMergeText: null;\n /** Legacy alias for skipped. */\n rejected: true;\n};\n\nexport type StoreFactResult = StoredFactResult | SkippedStoreFactResult;\n\nfunction createSkippedStorePlaceholder(entry: StoreFactInput): MemoryEntry {\n const nowSec = Math.floor(Date.now() / 1000);\n const decayClass =\n entry.decayClass ??\n classifyDecay(entry.entity ?? null, entry.key ?? null, entry.value ?? null, entry.text, {\n source: entry.source ?? null,\n category: entry.category ?? null,\n importance: entry.importance ?? 0.5,\n });\n return {\n id: \"\",\n text: entry.text,\n why: entry.why ?? null,\n category: entry.category ?? \"other\",\n importance: entry.importance ?? 0.5,\n entity: entry.entity ?? null,\n key: entry.key ?? null,\n value: entry.value ?? null,\n source: entry.source ?? \"conversation\",\n createdAt: nowSec,\n sourceDate: entry.sourceDate ?? null,\n decayClass,\n expiresAt: entry.expiresAt ?? calculateExpiry(decayClass, nowSec),\n lastConfirmedAt: nowSec,\n confidence: entry.confidence ?? 1,\n summary: entry.summary ?? null,\n tags: entry.tags ?? null,\n validFrom: entry.validFrom ?? null,\n validUntil: entry.validUntil ?? null,\n supersedesId: entry.supersedesId ?? null,\n scope: entry.scope ?? \"global\",\n scopeTarget: entry.scopeTarget ?? null,\n procedureType: entry.procedureType ?? null,\n successCount: entry.successCount,\n lastValidated: entry.lastValidated ?? null,\n sourceSessions: entry.sourceSessions ?? null,\n embeddingModel: entry.embeddingModel ?? null,\n provenanceSession: entry.provenanceSession ?? null,\n sourceTurn: entry.sourceTurn ?? null,\n extractionMethod: entry.extractionMethod ?? null,\n extractionConfidence: entry.extractionConfidence ?? null,\n decayFreezeUntil: entry.decayFreezeUntil ?? null,\n preserveUntil: entry.preserveUntil ?? null,\n preserveTags: entry.preserveTags ?? null,\n };\n}\n\nexport function storeFact(ctx: StoreFactContext, entry: StoreFactInput): StoreFactResult {\n validateStoreEntryInput(entry);\n\n const entryCategory = entry.category ?? \"\";\n const entrySource = entry.source ?? \"\";\n if (\n !ctx.allowPreStoreGuardBypass &&\n (BLOCKED_CATEGORIES.has(entryCategory) ||\n BLOCKED_SOURCES.has(entrySource) ||\n isPromptArtifactOrReasoningTrace(entry.text))\n ) {\n return {\n skipped: true,\n rejected: true,\n evictedFactId: null,\n embeddingStale: false,\n newlyStored: false,\n preMergeText: null,\n entry: createSkippedStorePlaceholder(entry),\n };\n }\n\n const sourceForPolicy = entry.source ?? \"conversation\";\n const profile = resolveDedupeProfile(sourceForPolicy, ctx.storeConfig ?? { fuzzyDedupe: ctx.fuzzyDedupe });\n const nowSec = Math.floor(Date.now() / 1000);\n const day = new Date(nowSec * 1000).toISOString().slice(0, 10);\n\n // Normalized-hash + lexical Jaccard dedupe (per-source profiles) before daily quota.\n const dedupe = applyDedupe(\n profile,\n {\n text: entry.text,\n source: sourceForPolicy,\n scope: entry.scope ?? \"global\",\n scopeTarget: entry.scopeTarget ?? null,\n category: entry.category ?? null,\n entity: entry.entity ?? null,\n key: entry.key ?? null,\n value: entry.value ?? null,\n },\n {\n db: ctx.db,\n nowSec,\n fuzzyDedupe: ctx.fuzzyDedupe,\n vectorCandidates: ctx.vectorCandidates,\n warnOnce: ctx.warnOnce,\n warnOnceKey: ctx.warnOnceKey,\n suppressVectorFallbackWarning: ctx.suppressVectorFallbackWarning,\n warn: (_m) => {},\n },\n );\n\n if (dedupe.action === \"skip\") {\n // #1186 acceptance (\"cosine ≥ 0.85 → skip + recall_count++\"): when we skipped because\n // of a near-duplicate, bump recall on the existing winner so the dedup acts as a\n // reinforcement signal instead of silently dropping the new evidence.\n if (dedupe.reason === \"vector\" || dedupe.reason === \"lexical\" || dedupe.reason === \"hash\") {\n runWithSqliteBusyRetry(ctx.db, () => {\n ctx.db\n .prepare(\n \"UPDATE facts SET recall_count = recall_count + 1, access_count = access_count + 1, last_confirmed_at = ? WHERE id = ?\",\n )\n .run(nowSec, dedupe.existingId);\n });\n }\n const existing = ctx.getById(dedupe.existingId);\n if (existing)\n return { entry: existing, evictedFactId: null, embeddingStale: false, newlyStored: false, preMergeText: null };\n throw new Error(\n `memory-hybrid: dedupe existing fact ${dedupe.existingId} not found (may have been deleted concurrently)`,\n );\n }\n\n if (dedupe.action === \"boost\") {\n runWithSqliteBusyRetry(ctx.db, () => {\n ctx.db\n .prepare(\n \"UPDATE facts SET recall_count = recall_count + 1, access_count = access_count + 1, importance = min(1.0, importance + ?) WHERE id = ?\",\n )\n .run(dedupe.boostBy, dedupe.existingId);\n });\n const boosted = ctx.getById(dedupe.existingId);\n if (boosted)\n return { entry: boosted, evictedFactId: null, embeddingStale: false, newlyStored: false, preMergeText: null };\n throw new Error(\n `memory-hybrid: dedupe existing fact ${dedupe.existingId} not found (may have been deleted concurrently)`,\n );\n }\n\n if (dedupe.action === \"merge\") {\n const existing = ctx.getById(dedupe.existingId);\n if (existing) {\n const alreadyContained = existing.text.includes(entry.text);\n if (alreadyContained) {\n return { entry: existing, evictedFactId: null, embeddingStale: false, newlyStored: false, preMergeText: null };\n }\n\n const rawMergedText = `${existing.text}\\n${entry.text}`;\n if (rawMergedText.length > 4000) {\n process.stderr.write(\n `memory-hybrid: dedupe merge for fact ${existing.id} truncated to 4000 chars (combined length=${rawMergedText.length}); some content may be lost\\n`,\n );\n }\n const mergedText = rawMergedText.slice(0, 4000);\n const mergedHash = normalizedHash(mergedText);\n // Wrap the merge UPDATE in a transaction so it is atomic and can be\n // rolled back on failure. Without this, an interrupted write leaves\n // SQLite with new merged text while LanceDB still encodes the pre-merge\n // content (split-brain). createTransaction() uses a SAVEPOINT when a\n // transaction is already active, so nesting is safe.\n const mergeTx = createTransaction(ctx.db, () => {\n ctx.db\n .prepare(\"UPDATE facts SET text = ?, normalized_hash = ? WHERE id = ?\")\n .run(mergedText, mergedHash, existing.id);\n });\n runWithSqliteBusyRetry(ctx.db, () => {\n mergeTx();\n });\n // Signal callers to re-embed only when the persisted text changed. This handles edge\n // cases where append text is truncated and the final merged text remains unchanged.\n const embeddingStale = mergedText !== existing.text;\n return {\n entry: ctx.getById(existing.id) ?? existing,\n evictedFactId: null,\n embeddingStale,\n newlyStored: false,\n preMergeText: existing.text,\n };\n }\n throw new Error(\n `memory-hybrid: dedupe existing fact ${dedupe.existingId} not found (may have been deleted concurrently)`,\n );\n }\n\n const id = randomUUID();\n\n const importance = entry.importance ?? 0.5;\n const why = entry.why ?? null;\n const entity = entry.entity?.trim() || null;\n const key = entry.key?.trim() || null;\n const value = entry.value ?? null;\n const source = entry.source ?? \"conversation\";\n const category = (entry.category?.trim() || \"other\").toLowerCase();\n const decayClass =\n entry.decayClass || classifyDecay(entity, key, value, entry.text, { source, category, importance });\n const expiresAt = entry.expiresAt !== undefined ? entry.expiresAt : calculateExpiry(decayClass, nowSec);\n const confidence = entry.confidence ?? 1.0;\n const summary = entry.summary ?? null;\n const embeddingModel = entry.embeddingModel ?? null;\n const normHash = normalizedHash(entry.text);\n const sourceDate = entry.sourceDate ?? null;\n const tags = entry.tags ?? null;\n const tagsStr = tags ? serializeTags(tags) : null;\n const validFrom = entry.validFrom ?? sourceDate ?? nowSec;\n const validUntil = entry.validUntil ?? null;\n const supersedesId = entry.supersedesId ?? null;\n const scope = entry.scope ?? \"global\";\n const scopeTarget = scope === \"global\" ? null : (entry.scopeTarget ?? null);\n if (scope !== \"global\" && !scopeTarget) {\n throw new Error(`scopeTarget required for non-global scope: ${scope}`);\n }\n const procedureType = entry.procedureType ?? null;\n const successCount = entry.successCount ?? 0;\n const lastValidated = entry.lastValidated ?? null;\n const sourceSessionsRaw = entry.sourceSessions ?? null;\n const sourceSessionsStr =\n sourceSessionsRaw == null\n ? null\n : typeof sourceSessionsRaw === \"string\"\n ? sourceSessionsRaw\n : JSON.stringify(sourceSessionsRaw);\n const provenanceSession = entry.provenanceSession ?? null;\n const sourceTurn = entry.sourceTurn ?? null;\n const extractionMethod = entry.extractionMethod ?? null;\n const extractionConfidence = entry.extractionConfidence !== undefined ? entry.extractionConfidence : null;\n const preserveUntil = entry.preserveUntil ?? null;\n const preserveTags = entry.preserveTags ?? null;\n const preserveTagsStr = preserveTags ? JSON.stringify(preserveTags) : null;\n const provenanceJson = entry.provenanceJson ?? null;\n\n const tier: MemoryTier = (entry as { tier?: MemoryTier }).tier ?? \"warm\";\n const rawFreeze = (entry as { decayFreezeUntil?: number | null }).decayFreezeUntil ?? null;\n const decayFreezeUntil = rawFreeze !== null && Number.isFinite(rawFreeze) ? rawFreeze : null;\n const adjustedExpiresAt =\n decayFreezeUntil !== null && expiresAt !== null && expiresAt < decayFreezeUntil ? decayFreezeUntil : expiresAt;\n const beginMode: SqliteTransactionBeginMode = profile.maxPerDay != null ? \"IMMEDIATE\" : \"DEFERRED\";\n // Quota \"drop\" path records `dropped` in this transaction, commits, then throws below so\n // observability survives the error. Retrying the same write increments `dropped` again (each\n // attempt is counted), which is intentional for operational metrics.\n let quotaExceededSource: string | null = null;\n let evictedFactId: string | null = null;\n const tx = createTransaction(\n ctx.db,\n () => {\n if (profile.maxPerDay != null) {\n const quotaRow = ctx.db\n .prepare(\"SELECT count FROM daily_writes WHERE source = ? AND day = ?\")\n .get(sourceForPolicy, day) as { count: number } | undefined;\n if ((quotaRow?.count ?? 0) >= profile.maxPerDay) {\n // #1194: legacy behaviour was throw-on-overflow + bump `dropped`. With\n // `onOverflow=evict-lowest-confidence`, we instead supersede the lowest-confidence\n // active fact for this source and let the new write through, which prevents the\n // quota from acting as a \"freeze the noise\" gate when noisy sources accumulate stale\n // low-confidence rows.\n if (profile.onOverflow === \"evict-lowest-confidence\") {\n const victim = ctx.db\n .prepare(\n `SELECT id FROM facts\n WHERE source = ? AND superseded_at IS NULL\n ORDER BY confidence ASC, COALESCE(recall_count, 0) ASC, created_at ASC\n LIMIT 1`,\n )\n .get(sourceForPolicy) as { id: string } | undefined;\n if (victim) {\n ctx.db\n .prepare(\"UPDATE facts SET superseded_at = ? WHERE id = ? AND superseded_at IS NULL\")\n .run(nowSec, victim.id);\n evictedFactId = victim.id;\n ctx.db\n .prepare(\n `INSERT INTO daily_writes (source, day, count, dropped, evicted) VALUES (?, ?, 0, 0, 1)\n ON CONFLICT(source, day) DO UPDATE SET evicted = evicted + 1`,\n )\n .run(sourceForPolicy, day);\n // Fall through to the INSERT path below (do not return).\n } else {\n quotaExceededSource = sourceForPolicy;\n ctx.db\n .prepare(\n `INSERT INTO daily_writes (source, day, count, dropped) VALUES (?, ?, 0, 1)\n ON CONFLICT(source, day) DO UPDATE SET dropped = dropped + 1`,\n )\n .run(sourceForPolicy, day);\n return;\n }\n } else {\n quotaExceededSource = sourceForPolicy;\n ctx.db\n .prepare(\n `INSERT INTO daily_writes (source, day, count, dropped) VALUES (?, ?, 0, 1)\n ON CONFLICT(source, day) DO UPDATE SET dropped = dropped + 1`,\n )\n .run(sourceForPolicy, day);\n return;\n }\n }\n }\n ctx.db\n .prepare(\n `INSERT INTO facts (id, text, why, category, importance, entity, key, value, source, created_at, decay_class, expires_at, last_confirmed_at, confidence, summary, embedding_model, normalized_hash, source_date, tags, valid_from, valid_until, supersedes_id, tier, scope, scope_target, procedure_type, success_count, last_validated, source_sessions, decay_freeze_until, provenance_session, source_turn, extraction_method, extraction_confidence, preserve_until, preserve_tags, provenance_json)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .run(\n id,\n entry.text,\n why,\n category,\n importance,\n entity,\n key,\n value,\n source,\n nowSec,\n decayClass,\n adjustedExpiresAt,\n nowSec,\n confidence,\n summary,\n embeddingModel,\n normHash,\n sourceDate,\n tagsStr,\n validFrom,\n validUntil,\n supersedesId,\n tier,\n scope,\n scopeTarget,\n procedureType,\n successCount,\n lastValidated,\n sourceSessionsStr,\n decayFreezeUntil,\n provenanceSession,\n sourceTurn,\n extractionMethod,\n extractionConfidence,\n preserveUntil,\n preserveTagsStr,\n provenanceJson,\n );\n // Count bump is in the same IMMEDIATE transaction as the facts INSERT when maxPerDay is set.\n if (profile.maxPerDay != null) {\n ctx.db\n .prepare(\n `INSERT INTO daily_writes (source, day, count, dropped) VALUES (?, ?, 1, 0)\n ON CONFLICT(source, day) DO UPDATE SET count = count + 1`,\n )\n .run(sourceForPolicy, day);\n }\n },\n beginMode,\n );\n runWithSqliteBusyRetry(ctx.db, () => {\n quotaExceededSource = null;\n evictedFactId = null;\n tx();\n });\n if (quotaExceededSource) {\n throw new Error(`memory-hybrid: daily write quota exceeded for source ${quotaExceededSource}`);\n }\n if (supersedesId || evictedFactId) {\n ctx.invalidateSupersededCache();\n }\n const loaded = ctx.getById(id);\n if (!loaded) {\n throw new Error(`memory-hybrid: store() failed to read back inserted fact ${id}`);\n }\n return { entry: loaded, evictedFactId, embeddingStale: false, newlyStored: true, preMergeText: null };\n}\n\n/**\n * Update recall_count and last_accessed for facts (bulk UPDATE).\n * MUST only be called when full content is injected or explicitly recalled by user.\n * Index-only exposures must use refreshIndexedFacts() instead (#1559).\n */\nexport function refreshAccessedFacts(db: DatabaseSync, ids: string[]): void {\n if (ids.length === 0) return;\n const nowSec = Math.floor(Date.now() / 1000);\n const BATCH_SIZE = 500;\n\n const tx = createTransaction(db, () => {\n for (let i = 0; i < ids.length; i += BATCH_SIZE) {\n const batch = ids.slice(i, i + BATCH_SIZE);\n const placeholders = batch.map(() => \"?\").join(\",\");\n\n db.prepare(\n `UPDATE facts SET last_confirmed_at = ?, expires_at = CASE decay_class WHEN 'stable' THEN ? + ? WHEN 'active' THEN ? + ? WHEN 'durable' THEN ? + ? WHEN 'normal' THEN ? + ? ELSE expires_at END WHERE id IN (${placeholders}) AND decay_class IN ('stable', 'active', 'durable', 'normal')`,\n ).run(\n nowSec,\n nowSec,\n TTL_DEFAULTS.stable,\n nowSec,\n TTL_DEFAULTS.active,\n nowSec,\n TTL_DEFAULTS.durable,\n nowSec,\n TTL_DEFAULTS.normal,\n ...batch,\n );\n\n db.prepare(\n `UPDATE facts SET recall_count = recall_count + 1, last_accessed = ?, access_count = access_count + 1, last_accessed_at = strftime('%Y-%m-%dT%H:%M:%SZ', ?, 'unixepoch') WHERE id IN (${placeholders})`,\n ).run(nowSec, nowSec, ...batch);\n }\n });\n tx();\n}\n\n/**\n * Update indexed_count and last_indexed for index-only exposures (#1559).\n * Does NOT inflate recall_count or last_accessed — these are separate signals.\n */\nexport function refreshIndexedFacts(db: DatabaseSync, ids: string[]): void {\n if (ids.length === 0) return;\n const nowSec = Math.floor(Date.now() / 1000);\n const BATCH_SIZE = 500;\n\n const tx = createTransaction(db, () => {\n for (let i = 0; i < ids.length; i += BATCH_SIZE) {\n const batch = ids.slice(i, i + BATCH_SIZE);\n const placeholders = batch.map(() => \"?\").join(\",\");\n db.prepare(\n `UPDATE facts SET indexed_count = indexed_count + 1, last_indexed = ? WHERE id IN (${placeholders})`,\n ).run(nowSec, ...batch);\n }\n });\n tx();\n}\n\nexport function deleteFact(db: DatabaseSync, id: string): boolean {\n const tx = createTransaction(db, () => {\n db.prepare(\"DELETE FROM contradictions WHERE fact_id_new = ? OR fact_id_old = ?\").run(id, id);\n db.prepare(\"DELETE FROM memory_links WHERE source_fact_id = ? OR target_fact_id = ?\").run(id, id);\n const result = db.prepare(\"DELETE FROM facts WHERE id = ?\").run(id);\n return result.changes > 0;\n });\n return tx();\n}\n\n/**\n * Exact match or write-time dedupe policy would not insert a new row.\n * When `source` is omitted, uses a global probe (any source) for idempotency / CLI alignment (#1202).\n */\nexport function hasDuplicateText(\n db: DatabaseSync,\n fuzzyDedupe: boolean,\n text: string,\n storeConfig?: StoreConfig,\n source?: string,\n structured?: { category?: MemoryCategory | null; entity?: string | null; key?: string | null; value?: string | null },\n): boolean {\n const nowSec = Math.floor(Date.now() / 1000);\n if (source === undefined) {\n return hasGlobalDuplicateProbe(db, text, { nowSec, fuzzyDedupe, storeConfig });\n }\n const profile = resolveDedupeProfile(source, storeConfig ?? { fuzzyDedupe });\n const r = applyDedupe(\n profile,\n {\n text,\n source,\n category: structured?.category ?? null,\n entity: structured?.entity ?? null,\n key: structured?.key ?? null,\n value: structured?.value ?? null,\n },\n { db, nowSec, fuzzyDedupe },\n );\n return r.action !== \"store\";\n}\n\nexport function statsDailyWrites(\n db: DatabaseSync,\n): Array<{ source: string; day: string; count: number; dropped: number; evicted: number }> {\n return db\n .prepare(\n \"SELECT source, day, count, dropped, COALESCE(evicted, 0) AS evicted FROM daily_writes ORDER BY day DESC, source ASC LIMIT 100\",\n )\n .all() as Array<{ source: string; day: string; count: number; dropped: number; evicted: number }>;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"db-connection.js","names":[],"sources":["../../../backends/facts-db/db-connection.ts"],"sourcesContent":["/**\n * SQLite FTS5 capability probe used at FactsDB construction.\n * Extracted from facts-db.ts for maintainability (#870).\n */\n\nimport type { DatabaseSync } from \"node:sqlite\";\nimport { capturePluginError } from \"../../services/error-reporter.js\";\n\n/**\n * Hard-startup guard: fail fast if FTS5 is unavailable (hybrid search would degrade silently).\n */\nexport function verifyFts5Support(db: DatabaseSync): void {\n let fts5CompileOption = false;\n try {\n const row = db.prepare(\"SELECT sqlite_compileoption_used('ENABLE_FTS5') as fts5\").get() as { fts5: number };\n fts5CompileOption = row.fts5 === 1;\n } catch {\n // Best-effort only\n }\n\n const probeTable = \"temp.memory_hybrid_fts5_probe\";\n try {\n db.exec(`DROP TABLE IF EXISTS ${probeTable}`);\n db.exec(`CREATE VIRTUAL TABLE ${probeTable} USING fts5(content)`);\n db.exec(`DROP TABLE ${probeTable}`);\n } catch (err) {\n try {\n db.exec(`DROP TABLE IF EXISTS ${probeTable}`);\n } catch {\n // Best-effort cleanup only.\n }\n const reason = err instanceof Error ? err.message : String(err);\n const finalError = new Error(\n `memory-hybrid: SQLite FTS5 capability check failed during startup. Hybrid search would silently degrade to vector-only, so plugin initialization is aborted. Use a Node.js/SQLite runtime with FTS5 enabled. Original error: ${reason}`,\n );\n\n capturePluginError(finalError, {\n operation: \"startup-fts5-probe\",\n severity: \"error\",\n subsystem: \"facts\",\n context: {\n fts5_compileoption: String(fts5CompileOption),\n fts5_available: \"false\",\n },\n });\n\n throw finalError;\n }\n}\n"],"mappings":";;;;;AAWA,SAAgB,kBAAkB,IAAwB;CACxD,IAAI,oBAAoB;CACxB,IAAI;EAEF,oBADY,GAAG,QAAQ,0DAA0D,CAAC,KAC3D,CAAC,SAAS;SAC3B;CAIR,MAAM,aAAa;CACnB,IAAI;EACF,GAAG,KAAK,wBAAwB,aAAa;EAC7C,GAAG,KAAK,wBAAwB,WAAW,sBAAsB;EACjE,GAAG,KAAK,cAAc,aAAa;UAC5B,KAAK;EACZ,IAAI;GACF,GAAG,KAAK,wBAAwB,aAAa;UACvC;EAGR,MAAM,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;EAC/D,MAAM,6BAAa,IAAI,MACrB,gOAAgO,SACjO;EAED,mBAAmB,YAAY;GAC7B,WAAW;GACX,UAAU;GACV,WAAW;GACX,SAAS;IACP,oBAAoB,OAAO,kBAAkB;IAC7C,gBAAgB;IACjB;GACF,CAAC;EAEF,MAAM"}
1
+ {"version":3,"file":"db-connection.js","names":[],"sources":["../../../backends/facts-db/db-connection.ts"],"sourcesContent":["/**\n * SQLite FTS5 capability probe used at FactsDB construction.\n * Extracted from facts-db.ts for maintainability (#870).\n */\n\nimport type { DatabaseSync } from \"node:sqlite\";\nimport { capturePluginError } from \"../../services/error-reporter.js\";\n\n/**\n * Hard-startup guard: fail fast if FTS5 is unavailable (hybrid search would degrade silently).\n */\nexport function verifyFts5Support(db: DatabaseSync): void {\n let fts5CompileOption = false;\n try {\n const row = db.prepare(\"SELECT sqlite_compileoption_used('ENABLE_FTS5') as fts5\").get() as { fts5: number };\n fts5CompileOption = row.fts5 === 1;\n } catch {\n // Best-effort only\n }\n\n const probeTable = \"temp.memory_hybrid_fts5_probe\";\n try {\n db.exec(`DROP TABLE IF EXISTS ${probeTable}`);\n db.exec(`CREATE VIRTUAL TABLE ${probeTable} USING fts5(content)`);\n db.exec(`DROP TABLE ${probeTable}`);\n } catch (err) {\n try {\n db.exec(`DROP TABLE IF EXISTS ${probeTable}`);\n } catch {\n // Best-effort cleanup only.\n }\n const reason = err instanceof Error ? err.message : String(err);\n const finalError = new Error(\n `memory-hybrid: SQLite FTS5 capability check failed during startup. Hybrid search would silently degrade to vector-only, so plugin initialization is aborted. Use a Node.js/SQLite runtime with FTS5 enabled. Original error: ${reason}`,\n );\n\n capturePluginError(finalError, {\n operation: \"startup-fts5-probe\",\n severity: \"error\",\n subsystem: \"facts\",\n context: {\n fts5_compileoption: String(fts5CompileOption),\n fts5_available: \"false\",\n },\n });\n\n throw finalError;\n }\n}\n"],"mappings":";;;;;AAWA,SAAgB,kBAAkB,IAAwB;CACxD,IAAI,oBAAoB;CACxB,IAAI;EAEF,oBADY,GAAG,QAAQ,yDAAyD,EAAE,IAC5D,EAAE,SAAS;CACnC,QAAQ,CAER;CAEA,MAAM,aAAa;CACnB,IAAI;EACF,GAAG,KAAK,wBAAwB,YAAY;EAC5C,GAAG,KAAK,wBAAwB,WAAW,qBAAqB;EAChE,GAAG,KAAK,cAAc,YAAY;CACpC,SAAS,KAAK;EACZ,IAAI;GACF,GAAG,KAAK,wBAAwB,YAAY;EAC9C,QAAQ,CAER;EACA,MAAM,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;EAC9D,MAAM,6BAAa,IAAI,MACrB,gOAAgO,QAClO;EAEA,mBAAmB,YAAY;GAC7B,WAAW;GACX,UAAU;GACV,WAAW;GACX,SAAS;IACP,oBAAoB,OAAO,iBAAiB;IAC5C,gBAAgB;GAClB;EACF,CAAC;EAED,MAAM;CACR;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"entity-autolink.js","names":[],"sources":["../../../backends/facts-db/entity-autolink.ts"],"sourcesContent":["/**\n * Auto-linking helpers (Issue #154) (Issue #954 split).\n */\nimport type { SQLInputValue } from \"node:sqlite\";\nimport type { DatabaseSync } from \"node:sqlite\";\n\nimport type { MemoryEntry } from \"../../types/memory.js\";\nimport { updateConfidence } from \"./contradictions.js\";\nimport { filterEntityStopWords } from \"../../utils/entity-stopwords.js\";\nimport { rowToMemoryEntry } from \"./row-mapper.js\";\nimport type { MemoryLinkType } from \"./types.js\";\n\nconst KNOWN_ENTITIES_CACHE_TTL_MS = 30_000;\nconst knownEntitiesCacheByDb = new WeakMap<DatabaseSync, { list: string[]; time: number }>();\n\nexport function getKnownEntities(db: DatabaseSync): string[] {\n const now = Date.now();\n const hit = knownEntitiesCacheByDb.get(db);\n if (hit && now - hit.time < KNOWN_ENTITIES_CACHE_TTL_MS) {\n return hit.list;\n }\n const rows = db\n .prepare(\"SELECT DISTINCT entity FROM facts WHERE entity IS NOT NULL AND superseded_at IS NULL\")\n .all() as Array<{ entity: string }>;\n const list = filterEntityStopWords(rows.map((r) => r.entity));\n knownEntitiesCacheByDb.set(db, { list, time: now });\n return list;\n}\n\nexport function extractEntitiesFromText(\n text: string,\n knownEntities: string[],\n): Array<{ entity: string; weight: number }> {\n const seen = new Map<string, number>();\n const lowerText = text.toLowerCase();\n\n for (const entity of knownEntities) {\n if (!entity) continue;\n const lowerEntity = entity.toLowerCase();\n\n if (!lowerText.includes(lowerEntity)) continue;\n\n const escapedForRegex = lowerEntity.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const wordBoundaryRe = new RegExp(`\\\\b${escapedForRegex}\\\\b`);\n if (wordBoundaryRe.test(lowerText)) {\n const current = seen.get(entity) ?? 0;\n if (current < 1.0) seen.set(entity, 1.0);\n continue;\n }\n\n const current = seen.get(entity) ?? 0;\n if (current < 0.7) seen.set(entity, 0.7);\n }\n\n const ipRe = /\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b/g;\n let m: RegExpExecArray | null;\n while ((m = ipRe.exec(text)) !== null) {\n const ip = m[0];\n if (!seen.has(ip)) seen.set(ip, 0.5);\n }\n\n return Array.from(seen.entries())\n .map(([entity, weight]) => ({ entity, weight }))\n .sort((a, b) => b.weight - a.weight);\n}\n\nexport function findEntityAnchor(db: DatabaseSync, entity: string, excludeId?: string): MemoryEntry | null {\n const nowSec = Math.floor(Date.now() / 1000);\n const excludeClause = excludeId ? \"AND id != ?\" : \"\";\n const params = excludeId ? [entity, nowSec, excludeId] : [entity, nowSec];\n const row = db\n .prepare(\n `SELECT * FROM facts\n WHERE lower(entity) = lower(?)\n AND superseded_at IS NULL\n AND (expires_at IS NULL OR expires_at > ?)\n ${excludeClause}\n ORDER BY created_at DESC, rowid DESC\n LIMIT 1`,\n )\n .get(...params) as Record<string, unknown> | undefined;\n return row ? rowToMemoryEntry(row) : null;\n}\n\nexport function autoDetectInstanceOf(\n db: DatabaseSync,\n newFactId: string,\n text: string,\n knownEntitiesList: string[] | undefined,\n createLink: (a: string, b: string, t: MemoryLinkType, s?: number) => string,\n getKnown: (d: DatabaseSync) => string[],\n): number {\n const patterns = [\n /\\bis\\s+an?\\s+([a-zA-Z][a-zA-Z0-9 _-]{1,40}?)(?:\\s*[,;.!?]|$)/gi,\n /\\btype\\s+of\\s+([a-zA-Z][a-zA-Z0-9 _-]{1,40}?)(?:\\s*[,;.!?]|$)/gi,\n /\\bkind\\s+of\\s+([a-zA-Z][a-zA-Z0-9 _-]{1,40}?)(?:\\s*[,;.!?]|$)/gi,\n ];\n\n const candidates = new Set<string>();\n for (const pattern of patterns) {\n let match: RegExpExecArray | null;\n pattern.lastIndex = 0;\n while ((match = pattern.exec(text)) !== null) {\n const typeName = match[1].trim().toLowerCase();\n if (typeName.length >= 2) candidates.add(typeName);\n }\n }\n\n if (candidates.size === 0) return 0;\n\n const entities = knownEntitiesList ?? getKnown(db);\n const knownEntitiesSet = new Set(entities.map((e) => e.toLowerCase()));\n let linked = 0;\n\n for (const typeName of candidates) {\n if (!knownEntitiesSet.has(typeName)) continue;\n const anchor = findEntityAnchor(db, typeName, newFactId);\n if (!anchor) continue;\n const existing = db\n .prepare(\n `SELECT id FROM memory_links\n WHERE source_fact_id = ? AND target_fact_id = ? AND link_type = 'INSTANCE_OF'`,\n )\n .get(newFactId, anchor.id);\n if (!existing) {\n createLink(newFactId, anchor.id, \"INSTANCE_OF\", 1.0);\n linked++;\n }\n }\n\n return linked;\n}\n\nexport function autoLinkEntities(\n db: DatabaseSync,\n newFactId: string,\n text: string,\n entity: string | null,\n key: string | null,\n sessionId: string | null,\n cfg: { coOccurrenceWeight: number; autoSupersede: boolean },\n scope: string | null | undefined,\n scopeTarget: string | null | undefined,\n createLink: (a: string, b: string, t: MemoryLinkType, s?: number) => string,\n supersede: (oldId: string, newId: string | null) => boolean,\n): { linkedCount: number; supersededIds: string[] } {\n let linkedCount = 0;\n const supersededIds: string[] = [];\n\n const knownEntities = getKnownEntities(db);\n const mentions = extractEntitiesFromText(text, knownEntities);\n\n for (const { entity: mentionedEntity, weight } of mentions) {\n const anchor = findEntityAnchor(db, mentionedEntity, newFactId);\n if (!anchor) continue;\n const existing = db\n .prepare(\n `SELECT id FROM memory_links\n WHERE source_fact_id = ? AND target_fact_id = ? AND link_type = 'RELATED_TO'`,\n )\n .get(newFactId, anchor.id);\n if (!existing) {\n createLink(newFactId, anchor.id, \"RELATED_TO\", weight);\n linkedCount++;\n }\n }\n\n if (sessionId) {\n const nowSec = Math.floor(Date.now() / 1000);\n const escapedSessionId = sessionId.replace(/[\\\\%_]/g, \"\\\\$&\");\n const recentRows = db\n .prepare(\n `SELECT * FROM facts\n WHERE id != ?\n AND superseded_at IS NULL\n AND (expires_at IS NULL OR expires_at > ?)\n AND source_sessions IS NOT NULL\n AND ((',' || source_sessions || ',' LIKE ? ESCAPE '\\\\') OR source_sessions LIKE ? ESCAPE '\\\\')\n ORDER BY created_at DESC\n LIMIT 20`,\n )\n .all(newFactId, nowSec, `%,${escapedSessionId},%`, `%\"${escapedSessionId}\"%`) as Array<Record<string, unknown>>;\n\n for (const row of recentRows) {\n const coEntry = rowToMemoryEntry(row);\n const existing = db\n .prepare(\n `SELECT id FROM memory_links\n WHERE source_fact_id = ? AND target_fact_id = ? AND link_type = 'RELATED_TO'`,\n )\n .get(newFactId, coEntry.id);\n if (!existing) {\n createLink(newFactId, coEntry.id, \"RELATED_TO\", cfg.coOccurrenceWeight);\n linkedCount++;\n }\n }\n }\n\n if (entity?.trim() && key?.trim()) {\n const nowSec = Math.floor(Date.now() / 1000);\n const scopeClause = scope\n ? scopeTarget != null\n ? \"AND scope = ? AND scope_target = ?\"\n : \"AND scope = ? AND scope_target IS NULL\"\n : \"\";\n const baseParams: SQLInputValue[] = [entity.trim(), key.trim(), newFactId, nowSec];\n const scopeParams: SQLInputValue[] = scope ? (scopeTarget != null ? [scope, scopeTarget] : [scope]) : [];\n const conflicting = db\n .prepare(\n `SELECT * FROM facts\n WHERE lower(entity) = lower(?)\n AND lower(key) = lower(?)\n AND id != ?\n AND superseded_at IS NULL\n AND (expires_at IS NULL OR expires_at > ?)\n ${scopeClause}\n ORDER BY created_at DESC`,\n )\n .all(...baseParams, ...scopeParams) as Array<Record<string, unknown>>;\n\n const newVal =\n ((db.prepare(\"SELECT value FROM facts WHERE id = ?\").get(newFactId) as { value: string | null } | undefined)\n ?.value as string) ?? null;\n\n if (newVal !== null) {\n for (const row of conflicting) {\n const oldFact = rowToMemoryEntry(row);\n if (oldFact.value !== null && newVal.toLowerCase() === oldFact.value.toLowerCase()) continue;\n\n const alreadyLinked = db\n .prepare(\n `SELECT id FROM memory_links\n WHERE source_fact_id = ? AND target_fact_id = ? AND link_type = 'SUPERSEDES'`,\n )\n .get(newFactId, oldFact.id);\n if (!alreadyLinked) {\n createLink(newFactId, oldFact.id, \"SUPERSEDES\", 1.0);\n\n if (cfg.autoSupersede) {\n supersede(oldFact.id, newFactId);\n\n const existingContradiction = db\n .prepare(\n `SELECT id FROM contradictions\n WHERE fact_id_new = ? AND fact_id_old = ?`,\n )\n .get(newFactId, oldFact.id);\n if (!existingContradiction) {\n updateConfidence(db, oldFact.id, -0.2);\n }\n supersededIds.push(oldFact.id);\n }\n }\n }\n }\n }\n\n linkedCount += autoDetectInstanceOf(db, newFactId, text, knownEntities, createLink, getKnownEntities);\n\n return { linkedCount, supersededIds };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"entity-autolink.js","names":[],"sources":["../../../backends/facts-db/entity-autolink.ts"],"sourcesContent":["/**\n * Auto-linking helpers (Issue #154) (Issue #954 split).\n */\nimport type { SQLInputValue } from \"node:sqlite\";\nimport type { DatabaseSync } from \"node:sqlite\";\n\nimport type { MemoryEntry } from \"../../types/memory.js\";\nimport { updateConfidence } from \"./contradictions.js\";\nimport { filterEntityStopWords } from \"../../utils/entity-stopwords.js\";\nimport { rowToMemoryEntry } from \"./row-mapper.js\";\nimport type { MemoryLinkType } from \"./types.js\";\n\nconst KNOWN_ENTITIES_CACHE_TTL_MS = 30_000;\nconst knownEntitiesCacheByDb = new WeakMap<DatabaseSync, { list: string[]; time: number }>();\n\nexport function getKnownEntities(db: DatabaseSync): string[] {\n const now = Date.now();\n const hit = knownEntitiesCacheByDb.get(db);\n if (hit && now - hit.time < KNOWN_ENTITIES_CACHE_TTL_MS) {\n return hit.list;\n }\n const rows = db\n .prepare(\"SELECT DISTINCT entity FROM facts WHERE entity IS NOT NULL AND superseded_at IS NULL\")\n .all() as Array<{ entity: string }>;\n const list = filterEntityStopWords(rows.map((r) => r.entity));\n knownEntitiesCacheByDb.set(db, { list, time: now });\n return list;\n}\n\nexport function extractEntitiesFromText(\n text: string,\n knownEntities: string[],\n): Array<{ entity: string; weight: number }> {\n const seen = new Map<string, number>();\n const lowerText = text.toLowerCase();\n\n for (const entity of knownEntities) {\n if (!entity) continue;\n const lowerEntity = entity.toLowerCase();\n\n if (!lowerText.includes(lowerEntity)) continue;\n\n const escapedForRegex = lowerEntity.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const wordBoundaryRe = new RegExp(`\\\\b${escapedForRegex}\\\\b`);\n if (wordBoundaryRe.test(lowerText)) {\n const current = seen.get(entity) ?? 0;\n if (current < 1.0) seen.set(entity, 1.0);\n continue;\n }\n\n const current = seen.get(entity) ?? 0;\n if (current < 0.7) seen.set(entity, 0.7);\n }\n\n const ipRe = /\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b/g;\n let m: RegExpExecArray | null;\n while ((m = ipRe.exec(text)) !== null) {\n const ip = m[0];\n if (!seen.has(ip)) seen.set(ip, 0.5);\n }\n\n return Array.from(seen.entries())\n .map(([entity, weight]) => ({ entity, weight }))\n .sort((a, b) => b.weight - a.weight);\n}\n\nexport function findEntityAnchor(db: DatabaseSync, entity: string, excludeId?: string): MemoryEntry | null {\n const nowSec = Math.floor(Date.now() / 1000);\n const excludeClause = excludeId ? \"AND id != ?\" : \"\";\n const params = excludeId ? [entity, nowSec, excludeId] : [entity, nowSec];\n const row = db\n .prepare(\n `SELECT * FROM facts\n WHERE lower(entity) = lower(?)\n AND superseded_at IS NULL\n AND (expires_at IS NULL OR expires_at > ?)\n ${excludeClause}\n ORDER BY created_at DESC, rowid DESC\n LIMIT 1`,\n )\n .get(...params) as Record<string, unknown> | undefined;\n return row ? rowToMemoryEntry(row) : null;\n}\n\nexport function autoDetectInstanceOf(\n db: DatabaseSync,\n newFactId: string,\n text: string,\n knownEntitiesList: string[] | undefined,\n createLink: (a: string, b: string, t: MemoryLinkType, s?: number) => string,\n getKnown: (d: DatabaseSync) => string[],\n): number {\n const patterns = [\n /\\bis\\s+an?\\s+([a-zA-Z][a-zA-Z0-9 _-]{1,40}?)(?:\\s*[,;.!?]|$)/gi,\n /\\btype\\s+of\\s+([a-zA-Z][a-zA-Z0-9 _-]{1,40}?)(?:\\s*[,;.!?]|$)/gi,\n /\\bkind\\s+of\\s+([a-zA-Z][a-zA-Z0-9 _-]{1,40}?)(?:\\s*[,;.!?]|$)/gi,\n ];\n\n const candidates = new Set<string>();\n for (const pattern of patterns) {\n let match: RegExpExecArray | null;\n pattern.lastIndex = 0;\n while ((match = pattern.exec(text)) !== null) {\n const typeName = match[1].trim().toLowerCase();\n if (typeName.length >= 2) candidates.add(typeName);\n }\n }\n\n if (candidates.size === 0) return 0;\n\n const entities = knownEntitiesList ?? getKnown(db);\n const knownEntitiesSet = new Set(entities.map((e) => e.toLowerCase()));\n let linked = 0;\n\n for (const typeName of candidates) {\n if (!knownEntitiesSet.has(typeName)) continue;\n const anchor = findEntityAnchor(db, typeName, newFactId);\n if (!anchor) continue;\n const existing = db\n .prepare(\n `SELECT id FROM memory_links\n WHERE source_fact_id = ? AND target_fact_id = ? AND link_type = 'INSTANCE_OF'`,\n )\n .get(newFactId, anchor.id);\n if (!existing) {\n createLink(newFactId, anchor.id, \"INSTANCE_OF\", 1.0);\n linked++;\n }\n }\n\n return linked;\n}\n\nexport function autoLinkEntities(\n db: DatabaseSync,\n newFactId: string,\n text: string,\n entity: string | null,\n key: string | null,\n sessionId: string | null,\n cfg: { coOccurrenceWeight: number; autoSupersede: boolean },\n scope: string | null | undefined,\n scopeTarget: string | null | undefined,\n createLink: (a: string, b: string, t: MemoryLinkType, s?: number) => string,\n supersede: (oldId: string, newId: string | null) => boolean,\n): { linkedCount: number; supersededIds: string[] } {\n let linkedCount = 0;\n const supersededIds: string[] = [];\n\n const knownEntities = getKnownEntities(db);\n const mentions = extractEntitiesFromText(text, knownEntities);\n\n for (const { entity: mentionedEntity, weight } of mentions) {\n const anchor = findEntityAnchor(db, mentionedEntity, newFactId);\n if (!anchor) continue;\n const existing = db\n .prepare(\n `SELECT id FROM memory_links\n WHERE source_fact_id = ? AND target_fact_id = ? AND link_type = 'RELATED_TO'`,\n )\n .get(newFactId, anchor.id);\n if (!existing) {\n createLink(newFactId, anchor.id, \"RELATED_TO\", weight);\n linkedCount++;\n }\n }\n\n if (sessionId) {\n const nowSec = Math.floor(Date.now() / 1000);\n const escapedSessionId = sessionId.replace(/[\\\\%_]/g, \"\\\\$&\");\n const recentRows = db\n .prepare(\n `SELECT * FROM facts\n WHERE id != ?\n AND superseded_at IS NULL\n AND (expires_at IS NULL OR expires_at > ?)\n AND source_sessions IS NOT NULL\n AND ((',' || source_sessions || ',' LIKE ? ESCAPE '\\\\') OR source_sessions LIKE ? ESCAPE '\\\\')\n ORDER BY created_at DESC\n LIMIT 20`,\n )\n .all(newFactId, nowSec, `%,${escapedSessionId},%`, `%\"${escapedSessionId}\"%`) as Array<Record<string, unknown>>;\n\n for (const row of recentRows) {\n const coEntry = rowToMemoryEntry(row);\n const existing = db\n .prepare(\n `SELECT id FROM memory_links\n WHERE source_fact_id = ? AND target_fact_id = ? AND link_type = 'RELATED_TO'`,\n )\n .get(newFactId, coEntry.id);\n if (!existing) {\n createLink(newFactId, coEntry.id, \"RELATED_TO\", cfg.coOccurrenceWeight);\n linkedCount++;\n }\n }\n }\n\n if (entity?.trim() && key?.trim()) {\n const nowSec = Math.floor(Date.now() / 1000);\n const scopeClause = scope\n ? scopeTarget != null\n ? \"AND scope = ? AND scope_target = ?\"\n : \"AND scope = ? AND scope_target IS NULL\"\n : \"\";\n const baseParams: SQLInputValue[] = [entity.trim(), key.trim(), newFactId, nowSec];\n const scopeParams: SQLInputValue[] = scope ? (scopeTarget != null ? [scope, scopeTarget] : [scope]) : [];\n const conflicting = db\n .prepare(\n `SELECT * FROM facts\n WHERE lower(entity) = lower(?)\n AND lower(key) = lower(?)\n AND id != ?\n AND superseded_at IS NULL\n AND (expires_at IS NULL OR expires_at > ?)\n ${scopeClause}\n ORDER BY created_at DESC`,\n )\n .all(...baseParams, ...scopeParams) as Array<Record<string, unknown>>;\n\n const newVal =\n ((db.prepare(\"SELECT value FROM facts WHERE id = ?\").get(newFactId) as { value: string | null } | undefined)\n ?.value as string) ?? null;\n\n if (newVal !== null) {\n for (const row of conflicting) {\n const oldFact = rowToMemoryEntry(row);\n if (oldFact.value !== null && newVal.toLowerCase() === oldFact.value.toLowerCase()) continue;\n\n const alreadyLinked = db\n .prepare(\n `SELECT id FROM memory_links\n WHERE source_fact_id = ? AND target_fact_id = ? AND link_type = 'SUPERSEDES'`,\n )\n .get(newFactId, oldFact.id);\n if (!alreadyLinked) {\n createLink(newFactId, oldFact.id, \"SUPERSEDES\", 1.0);\n\n if (cfg.autoSupersede) {\n supersede(oldFact.id, newFactId);\n\n const existingContradiction = db\n .prepare(\n `SELECT id FROM contradictions\n WHERE fact_id_new = ? AND fact_id_old = ?`,\n )\n .get(newFactId, oldFact.id);\n if (!existingContradiction) {\n updateConfidence(db, oldFact.id, -0.2);\n }\n supersededIds.push(oldFact.id);\n }\n }\n }\n }\n }\n\n linkedCount += autoDetectInstanceOf(db, newFactId, text, knownEntities, createLink, getKnownEntities);\n\n return { linkedCount, supersededIds };\n}\n"],"mappings":"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"}