@remnic/core 9.3.719 → 9.3.721

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 (387) hide show
  1. package/dist/access-boundary.d.ts +10 -7
  2. package/dist/access-boundary.js +37 -34
  3. package/dist/access-cli.js +71 -66
  4. package/dist/access-cli.js.map +1 -1
  5. package/dist/access-http.d.ts +11 -8
  6. package/dist/access-http.js +44 -41
  7. package/dist/access-mcp.d.ts +11 -8
  8. package/dist/access-mcp.js +40 -37
  9. package/dist/access-operations-batch.js +38 -35
  10. package/dist/access-operations.d.ts +11 -8
  11. package/dist/access-operations.js +39 -36
  12. package/dist/access-schema.d.ts +19 -19
  13. package/dist/access-schema.js +3 -3
  14. package/dist/{access-service-CZbnt0t2.d.ts → access-service-Dj3I-VBS.d.ts} +3 -3
  15. package/dist/access-service.d.ts +11 -8
  16. package/dist/access-service.js +36 -33
  17. package/dist/access-surface-catalog.d.ts +10 -7
  18. package/dist/action-confidence.d.ts +1 -1
  19. package/dist/active-memory-bridge.d.ts +1 -1
  20. package/dist/active-recall.d.ts +1 -1
  21. package/dist/active-recall.js +4 -2
  22. package/dist/active-recall.js.map +1 -1
  23. package/dist/adapters/index.js +7 -7
  24. package/dist/adapters/registry.js +3 -3
  25. package/dist/{auto-sync-24NNS3GU.js → auto-sync-F3KCSQFJ.js} +5 -5
  26. package/dist/behavior-learner.d.ts +1 -1
  27. package/dist/behavior-signals.d.ts +1 -1
  28. package/dist/bootstrap.d.ts +11 -8
  29. package/dist/briefing.d.ts +1 -1
  30. package/dist/briefing.js +12 -10
  31. package/dist/buffer-surprise-report.d.ts +1 -1
  32. package/dist/buffer.d.ts +1 -1
  33. package/dist/calibration.d.ts +1 -1
  34. package/dist/calibration.js +2 -2
  35. package/dist/capabilities.d.ts +30 -2
  36. package/dist/capabilities.js +3 -1
  37. package/dist/{catalog-CEYlMs9X.d.ts → catalog-F-OEVbRQ.d.ts} +1 -1
  38. package/dist/causal-behavior.d.ts +1 -1
  39. package/dist/causal-behavior.js +2 -2
  40. package/dist/causal-chain.js +2 -2
  41. package/dist/causal-consolidation.d.ts +1 -1
  42. package/dist/causal-consolidation.js +15 -13
  43. package/dist/causal-consolidation.js.map +1 -1
  44. package/dist/causal-retrieval.js +2 -2
  45. package/dist/causal-trajectory-graph.d.ts +4 -0
  46. package/dist/causal-trajectory-graph.js +2 -2
  47. package/dist/causal-trajectory.js +1 -1
  48. package/dist/{chunk-TH7WMHVK.js → chunk-3NN73CM7.js} +2 -2
  49. package/dist/{chunk-A5TUPZ3N.js → chunk-4SF232E6.js} +60 -30
  50. package/dist/chunk-4SF232E6.js.map +1 -0
  51. package/dist/{chunk-DGTDRIUO.js → chunk-4T3BMHKD.js} +2 -2
  52. package/dist/{chunk-EH27LHML.js → chunk-5O42UKXI.js} +2 -2
  53. package/dist/{chunk-KRGC4EDX.js → chunk-6FAMMBQ3.js} +2 -2
  54. package/dist/{chunk-FEB7W5JG.js → chunk-6OIDDKAA.js} +27 -6
  55. package/dist/chunk-6OIDDKAA.js.map +1 -0
  56. package/dist/{chunk-67HG7KFE.js → chunk-6YKIYQU7.js} +2 -2
  57. package/dist/{chunk-LXSG4L55.js → chunk-7CTNUVHZ.js} +2 -2
  58. package/dist/{chunk-UCJ5CDTK.js → chunk-A5XNBK2D.js} +22 -14
  59. package/dist/chunk-A5XNBK2D.js.map +1 -0
  60. package/dist/{chunk-7EEVXW7X.js → chunk-A7TJZLPK.js} +2 -2
  61. package/dist/{chunk-P5IPFZVH.js → chunk-B7WISIAJ.js} +5 -5
  62. package/dist/chunk-BCDKBDSY.js +173 -0
  63. package/dist/chunk-BCDKBDSY.js.map +1 -0
  64. package/dist/{chunk-JAKYZGV4.js → chunk-BXEOHV7K.js} +3 -3
  65. package/dist/chunk-C4X4QGIO.js +60 -0
  66. package/dist/chunk-C4X4QGIO.js.map +1 -0
  67. package/dist/{chunk-S3OPGGZB.js → chunk-D7EBTQ24.js} +2 -2
  68. package/dist/{chunk-IO5NQEGZ.js → chunk-DKT3X5C3.js} +2 -2
  69. package/dist/{chunk-PBH4JUAB.js → chunk-DVRHLN2O.js} +18 -3
  70. package/dist/chunk-DVRHLN2O.js.map +1 -0
  71. package/dist/{chunk-S35FX6MU.js → chunk-EBDA5TYA.js} +2 -2
  72. package/dist/{chunk-2PTBLS2S.js → chunk-EDOQM4E4.js} +2 -2
  73. package/dist/{chunk-YTZLBBEB.js → chunk-EEJMD2L2.js} +3 -3
  74. package/dist/{chunk-YGKUAX2B.js → chunk-EHQ5HO5T.js} +11 -11
  75. package/dist/chunk-EHQ5HO5T.js.map +1 -0
  76. package/dist/{chunk-JIZGYQYH.js → chunk-EM2LDXJ2.js} +4 -4
  77. package/dist/{chunk-YYOK4GVF.js → chunk-EVPNWN7Z.js} +10 -10
  78. package/dist/{chunk-HLIR3RJ7.js → chunk-EXUCUPV2.js} +2 -2
  79. package/dist/{chunk-F3PVXKHP.js → chunk-GDUOCHCD.js} +27 -21
  80. package/dist/chunk-GDUOCHCD.js.map +1 -0
  81. package/dist/{chunk-L5ASZA2F.js → chunk-GJSL4WG2.js} +17 -2
  82. package/dist/chunk-GJSL4WG2.js.map +1 -0
  83. package/dist/{chunk-2SXMCQEQ.js → chunk-GZQXOQQ2.js} +3 -3
  84. package/dist/{chunk-2KAYTPPT.js → chunk-HCVWF657.js} +12 -12
  85. package/dist/{chunk-N3DUH6OO.js → chunk-ILFSEIRT.js} +1 -1
  86. package/dist/chunk-ILFSEIRT.js.map +1 -0
  87. package/dist/{chunk-4KZGLDJJ.js → chunk-J3JPB7ZI.js} +2 -2
  88. package/dist/{chunk-3JJWNZTT.js → chunk-L2B3EE7B.js} +102 -42
  89. package/dist/chunk-L2B3EE7B.js.map +1 -0
  90. package/dist/{chunk-NFRBADNR.js → chunk-LA3HZKES.js} +610 -369
  91. package/dist/chunk-LA3HZKES.js.map +1 -0
  92. package/dist/{chunk-LQ6JI4VH.js → chunk-LD53WPMU.js} +4 -4
  93. package/dist/{chunk-URM3HW2R.js → chunk-LLUGAZ3B.js} +2 -2
  94. package/dist/{chunk-RVLB4E6N.js → chunk-MDBZTH7N.js} +5 -32
  95. package/dist/chunk-MDBZTH7N.js.map +1 -0
  96. package/dist/{chunk-U5H7WHTS.js → chunk-N77ELJ2D.js} +4 -4
  97. package/dist/{chunk-2VHMVWPU.js → chunk-NXRUUDUF.js} +2 -2
  98. package/dist/{chunk-DIFU33GU.js → chunk-OHTOPFRF.js} +2 -2
  99. package/dist/{chunk-74MYJSKB.js → chunk-ORAK3LNZ.js} +2 -2
  100. package/dist/{chunk-REWQAFSA.js → chunk-PD42EYQN.js} +2 -2
  101. package/dist/chunk-PHK3HARR.js +32 -0
  102. package/dist/chunk-PHK3HARR.js.map +1 -0
  103. package/dist/{chunk-773DPI6T.js → chunk-PKMQK2WY.js} +4 -4
  104. package/dist/{chunk-72VDBALG.js → chunk-QD6QP5P6.js} +63 -54
  105. package/dist/chunk-QD6QP5P6.js.map +1 -0
  106. package/dist/{chunk-CTOQEZSN.js → chunk-QR67FU5G.js} +2 -2
  107. package/dist/{chunk-AU7Q3LSC.js → chunk-RKNJBZ55.js} +7 -7
  108. package/dist/{chunk-YI7RZO7Y.js → chunk-SQNED75R.js} +1 -1
  109. package/dist/{chunk-5CEJH5ZN.js → chunk-TAPSNIMA.js} +4 -4
  110. package/dist/{chunk-IF362TF6.js → chunk-TQT3USCG.js} +2 -2
  111. package/dist/chunk-TQT3USCG.js.map +1 -0
  112. package/dist/{chunk-5RHM43BJ.js → chunk-TXQCG6SE.js} +3 -3
  113. package/dist/{chunk-ANI5DUAE.js → chunk-UIXXE6NI.js} +7 -7
  114. package/dist/{chunk-2COL35PK.js → chunk-UO5ZHIIH.js} +2 -2
  115. package/dist/{chunk-OJU73BUX.js → chunk-VCLGUIFM.js} +2 -2
  116. package/dist/{chunk-5J4OL7TJ.js → chunk-VUA4S7P6.js} +4 -4
  117. package/dist/{chunk-A66YRSOS.js → chunk-WQYSDPMA.js} +2 -2
  118. package/dist/chunk-XGTBNVIU.js +79 -0
  119. package/dist/chunk-XGTBNVIU.js.map +1 -0
  120. package/dist/{chunk-OZ2KQ7HF.js → chunk-YEE5KJQQ.js} +2 -2
  121. package/dist/chunk-YN2UN7N3.js +217 -0
  122. package/dist/chunk-YN2UN7N3.js.map +1 -0
  123. package/dist/{chunk-WRFKZEO6.js → chunk-YQGHW5ML.js} +4 -4
  124. package/dist/{chunk-APUJIMDM.js → chunk-Z4RU5VPM.js} +2 -2
  125. package/dist/{chunk-DCWIQFNA.js → chunk-ZUDM75KG.js} +4 -4
  126. package/dist/{cli-1orcdXOY.d.ts → cli-P_GaYcCY.d.ts} +3 -3
  127. package/dist/cli.d.ts +9 -6
  128. package/dist/cli.js +67 -63
  129. package/dist/compounding/engine.d.ts +1 -1
  130. package/dist/compounding/engine.js +12 -10
  131. package/dist/compounding/preference-consolidator.d.ts +1 -1
  132. package/dist/compression-optimizer.d.ts +1 -1
  133. package/dist/config.d.ts +1 -1
  134. package/dist/config.js +4 -2
  135. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  136. package/dist/connectors/codex-materialize-runner.js +12 -10
  137. package/dist/connectors/codex-materialize.d.ts +1 -1
  138. package/dist/connectors/index.d.ts +1 -1
  139. package/dist/connectors/index.js +13 -11
  140. package/dist/consolidation-provenance-check.d.ts +1 -1
  141. package/dist/consolidation-undo.d.ts +1 -1
  142. package/dist/contradiction/index.d.ts +1 -1
  143. package/dist/conversation-index/backend.d.ts +1 -1
  144. package/dist/conversation-index/backend.js +2 -2
  145. package/dist/conversation-index/chunker.d.ts +1 -1
  146. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  147. package/dist/conversation-index/indexer.d.ts +1 -1
  148. package/dist/conversation-index/search.d.ts +1 -1
  149. package/dist/day-summary.d.ts +1 -1
  150. package/dist/delinearize.d.ts +1 -1
  151. package/dist/direct-answer-wiring.d.ts +1 -1
  152. package/dist/direct-answer.d.ts +1 -1
  153. package/dist/embedding-fallback.d.ts +1 -1
  154. package/dist/enrichment/index.d.ts +1 -1
  155. package/dist/entity-retrieval.d.ts +1 -1
  156. package/dist/entity-retrieval.js +12 -10
  157. package/dist/entity-schema.d.ts +1 -1
  158. package/dist/explicit-capture.d.ts +11 -8
  159. package/dist/extraction-faithfulness.d.ts +8 -2
  160. package/dist/extraction-faithfulness.js +4 -3
  161. package/dist/extraction-judge-telemetry.d.ts +1 -1
  162. package/dist/extraction-judge-training.d.ts +1 -1
  163. package/dist/extraction-judge.d.ts +1 -1
  164. package/dist/extraction-judge.js +3 -3
  165. package/dist/extraction.d.ts +1 -1
  166. package/dist/extraction.js +5 -4
  167. package/dist/faithfulness-config.d.ts +55 -0
  168. package/dist/faithfulness-config.js +11 -0
  169. package/dist/faithfulness-config.js.map +1 -0
  170. package/dist/fallback-llm.d.ts +1 -1
  171. package/dist/fallback-llm.js +2 -2
  172. package/dist/{forget-U66KPDPY.js → forget-OEE5OELK.js} +2 -2
  173. package/dist/graph-dashboard-diff.d.ts +4 -0
  174. package/dist/graph-dashboard-key.d.ts +4 -0
  175. package/dist/graph-dashboard-parser.d.ts +4 -0
  176. package/dist/{graph-edge-decay-JLOF3YW4.js → graph-edge-decay-QPRJQ7DL.js} +3 -3
  177. package/dist/graph-edge-reinforcement.d.ts +4 -0
  178. package/dist/graph-snapshot.d.ts +7 -3
  179. package/dist/graph-snapshot.js +3 -3
  180. package/dist/graph.d.ts +7 -1
  181. package/dist/graph.js +2 -2
  182. package/dist/identity-continuity.d.ts +1 -1
  183. package/dist/importance.d.ts +1 -1
  184. package/dist/index.d.ts +571 -570
  185. package/dist/index.js +104 -99
  186. package/dist/index.js.map +1 -1
  187. package/dist/intent.d.ts +1 -1
  188. package/dist/lcm/engine.d.ts +1 -1
  189. package/dist/lcm/engine.js +2 -2
  190. package/dist/lcm/index.d.ts +1 -1
  191. package/dist/lcm/index.js +4 -4
  192. package/dist/lcm/tools.d.ts +1 -1
  193. package/dist/lifecycle.d.ts +1 -1
  194. package/dist/live-connectors-runner.d.ts +1 -1
  195. package/dist/local-llm.d.ts +1 -1
  196. package/dist/local-model-endpoint.d.ts +96 -0
  197. package/dist/local-model-endpoint.js +12 -0
  198. package/dist/local-model-endpoint.js.map +1 -0
  199. package/dist/maintenance/memory-governance.d.ts +1 -1
  200. package/dist/maintenance/memory-governance.js +12 -10
  201. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +12 -10
  202. package/dist/maintenance/rebuild-memory-projection.js +13 -11
  203. package/dist/mcp-memory-inspector-app.d.ts +11 -8
  204. package/dist/memory-action-policy.d.ts +1 -1
  205. package/dist/memory-cache.d.ts +1 -1
  206. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  207. package/dist/memory-projection-store.d.ts +1 -1
  208. package/dist/memory-provenance.d.ts +1 -1
  209. package/dist/memory-worth-outcomes.d.ts +1 -1
  210. package/dist/models-json.d.ts +1 -1
  211. package/dist/namespaces/migrate.d.ts +2 -2
  212. package/dist/namespaces/migrate.js +22 -20
  213. package/dist/namespaces/principal.d.ts +1 -1
  214. package/dist/namespaces/search.d.ts +1 -1
  215. package/dist/namespaces/search.js +9 -9
  216. package/dist/namespaces/storage.d.ts +2 -2
  217. package/dist/namespaces/storage.js +12 -10
  218. package/dist/native-knowledge.d.ts +1 -1
  219. package/dist/operator-toolkit.d.ts +3 -2
  220. package/dist/operator-toolkit.js +32 -28
  221. package/dist/orchestration/maintenance.d.ts +2 -2
  222. package/dist/orchestration/maintenance.js +14 -12
  223. package/dist/{orchestrator-kOxredSr.d.ts → orchestrator-f5xv19kp.d.ts} +178 -81
  224. package/dist/orchestrator.d.ts +8 -5
  225. package/dist/orchestrator.js +56 -52
  226. package/dist/patterns-cli.d.ts +1 -1
  227. package/dist/policy-runtime.d.ts +1 -1
  228. package/dist/provenance.d.ts +1 -1
  229. package/dist/provenance.js +2 -1
  230. package/dist/qmd-recall-cache.d.ts +1 -1
  231. package/dist/qmd.d.ts +1 -1
  232. package/dist/recall-disclosure-escalation.d.ts +1 -1
  233. package/dist/recall-explain-renderer.d.ts +1 -1
  234. package/dist/recall-explain-renderer.js +3 -3
  235. package/dist/recall-planner-llm.d.ts +1 -1
  236. package/dist/recall-planner-llm.js +2 -2
  237. package/dist/recall-state.d.ts +1 -1
  238. package/dist/recall-tag-filter.d.ts +1 -1
  239. package/dist/recall-xray-cli.d.ts +1 -1
  240. package/dist/recall-xray-cli.js +4 -4
  241. package/dist/recall-xray-renderer.d.ts +1 -1
  242. package/dist/recall-xray-renderer.js +3 -3
  243. package/dist/recall-xray.d.ts +18 -1
  244. package/dist/recall-xray.js +2 -2
  245. package/dist/resolve-auth-token.d.ts +1 -1
  246. package/dist/resume-bundles.js +5 -3
  247. package/dist/retrieval-agents.d.ts +1 -1
  248. package/dist/retrieval-tiers.d.ts +1 -1
  249. package/dist/routing/engine.d.ts +1 -1
  250. package/dist/routing/store.d.ts +1 -1
  251. package/dist/search/embed-helper.d.ts +1 -1
  252. package/dist/search/factory.d.ts +1 -1
  253. package/dist/search/factory.js +8 -8
  254. package/dist/search/index.d.ts +1 -1
  255. package/dist/search/index.js +14 -14
  256. package/dist/search/lancedb-backend.d.ts +1 -1
  257. package/dist/search/lancedb-backend.js +2 -2
  258. package/dist/search/meilisearch-backend.d.ts +1 -1
  259. package/dist/search/meilisearch-backend.js +2 -2
  260. package/dist/search/noop-backend.d.ts +1 -1
  261. package/dist/search/orama-backend.d.ts +1 -1
  262. package/dist/search/orama-backend.js +2 -2
  263. package/dist/search/port.d.ts +1 -1
  264. package/dist/search/remote-backend.d.ts +1 -1
  265. package/dist/{semantic-consolidation-B58v3S-a.d.ts → semantic-consolidation-BQm1e1Pn.d.ts} +1 -1
  266. package/dist/semantic-consolidation.d.ts +2 -2
  267. package/dist/semantic-consolidation.js +13 -11
  268. package/dist/semantic-rule-promotion.js +12 -10
  269. package/dist/semantic-rule-verifier.d.ts +1 -1
  270. package/dist/semantic-rule-verifier.js +12 -10
  271. package/dist/session-observer-bands.d.ts +1 -1
  272. package/dist/session-observer-state.d.ts +1 -1
  273. package/dist/shared-context/manager.d.ts +1 -1
  274. package/dist/signal.d.ts +1 -1
  275. package/dist/storage.d.ts +45 -1
  276. package/dist/storage.js +11 -9
  277. package/dist/summarizer.d.ts +1 -1
  278. package/dist/summarizer.js +3 -3
  279. package/dist/summary-snapshot.d.ts +1 -1
  280. package/dist/temporal-supersession.d.ts +1 -1
  281. package/dist/temporal-validity.d.ts +1 -1
  282. package/dist/threading.d.ts +1 -1
  283. package/dist/tier-migration.d.ts +1 -1
  284. package/dist/tier-routing.d.ts +1 -1
  285. package/dist/topics.d.ts +1 -1
  286. package/dist/transcript.d.ts +1 -1
  287. package/dist/transfer/backup.js +2 -2
  288. package/dist/transfer/capsule-export.js +2 -2
  289. package/dist/transfer/capsule-import.js +1 -1
  290. package/dist/trust-score-stage.d.ts +178 -0
  291. package/dist/trust-score-stage.js +18 -0
  292. package/dist/trust-score-stage.js.map +1 -0
  293. package/dist/trust-score.d.ts +164 -0
  294. package/dist/trust-score.js +22 -0
  295. package/dist/trust-score.js.map +1 -0
  296. package/dist/{types-ACmDOScJ.d.ts → types-DwO1Ot4n.d.ts} +56 -2
  297. package/dist/types.d.ts +1 -1
  298. package/dist/types.js +1 -1
  299. package/dist/utility-runtime.d.ts +1 -1
  300. package/dist/verified-recall.js +12 -10
  301. package/package.json +2 -2
  302. package/src/access-service.ts +8 -5
  303. package/src/capabilities-cluster-b.test.ts +78 -0
  304. package/src/capabilities.test.ts +11 -1
  305. package/src/capabilities.ts +58 -0
  306. package/src/cli/research-status-commands.ts +7 -3
  307. package/src/cli.ts +11 -6
  308. package/src/config.ts +60 -30
  309. package/src/extraction-faithfulness.test.ts +175 -0
  310. package/src/extraction-faithfulness.ts +53 -1
  311. package/src/faithfulness-config.test.ts +96 -0
  312. package/src/faithfulness-config.ts +138 -0
  313. package/src/graph-snapshot.ts +3 -3
  314. package/src/graph.ts +8 -7
  315. package/src/lifecycle/tombstones.test.ts +242 -1
  316. package/src/lifecycle/tombstones.ts +157 -70
  317. package/src/local-model-endpoint.test.ts +155 -0
  318. package/src/local-model-endpoint.ts +189 -0
  319. package/src/operator-toolkit.ts +24 -7
  320. package/src/orchestration/tier-migration-coordinator.ts +286 -0
  321. package/src/orchestrator.ts +331 -222
  322. package/src/recall-xray.test.ts +62 -0
  323. package/src/recall-xray.ts +34 -0
  324. package/src/trust-score-recall-paths.test.ts +157 -0
  325. package/src/trust-score-stage.test.ts +350 -0
  326. package/src/trust-score-stage.ts +398 -0
  327. package/src/trust-score.test.ts +282 -0
  328. package/src/trust-score.ts +449 -0
  329. package/src/types.ts +61 -1
  330. package/dist/chunk-3JJWNZTT.js.map +0 -1
  331. package/dist/chunk-72VDBALG.js.map +0 -1
  332. package/dist/chunk-A5TUPZ3N.js.map +0 -1
  333. package/dist/chunk-F3PVXKHP.js.map +0 -1
  334. package/dist/chunk-FEB7W5JG.js.map +0 -1
  335. package/dist/chunk-IF362TF6.js.map +0 -1
  336. package/dist/chunk-L5ASZA2F.js.map +0 -1
  337. package/dist/chunk-N3DUH6OO.js.map +0 -1
  338. package/dist/chunk-NFRBADNR.js.map +0 -1
  339. package/dist/chunk-PBH4JUAB.js.map +0 -1
  340. package/dist/chunk-RVLB4E6N.js.map +0 -1
  341. package/dist/chunk-UCJ5CDTK.js.map +0 -1
  342. package/dist/chunk-YGKUAX2B.js.map +0 -1
  343. /package/dist/{auto-sync-24NNS3GU.js.map → auto-sync-F3KCSQFJ.js.map} +0 -0
  344. /package/dist/{chunk-TH7WMHVK.js.map → chunk-3NN73CM7.js.map} +0 -0
  345. /package/dist/{chunk-DGTDRIUO.js.map → chunk-4T3BMHKD.js.map} +0 -0
  346. /package/dist/{chunk-EH27LHML.js.map → chunk-5O42UKXI.js.map} +0 -0
  347. /package/dist/{chunk-KRGC4EDX.js.map → chunk-6FAMMBQ3.js.map} +0 -0
  348. /package/dist/{chunk-67HG7KFE.js.map → chunk-6YKIYQU7.js.map} +0 -0
  349. /package/dist/{chunk-LXSG4L55.js.map → chunk-7CTNUVHZ.js.map} +0 -0
  350. /package/dist/{chunk-7EEVXW7X.js.map → chunk-A7TJZLPK.js.map} +0 -0
  351. /package/dist/{chunk-P5IPFZVH.js.map → chunk-B7WISIAJ.js.map} +0 -0
  352. /package/dist/{chunk-JAKYZGV4.js.map → chunk-BXEOHV7K.js.map} +0 -0
  353. /package/dist/{chunk-S3OPGGZB.js.map → chunk-D7EBTQ24.js.map} +0 -0
  354. /package/dist/{chunk-IO5NQEGZ.js.map → chunk-DKT3X5C3.js.map} +0 -0
  355. /package/dist/{chunk-S35FX6MU.js.map → chunk-EBDA5TYA.js.map} +0 -0
  356. /package/dist/{chunk-2PTBLS2S.js.map → chunk-EDOQM4E4.js.map} +0 -0
  357. /package/dist/{chunk-YTZLBBEB.js.map → chunk-EEJMD2L2.js.map} +0 -0
  358. /package/dist/{chunk-JIZGYQYH.js.map → chunk-EM2LDXJ2.js.map} +0 -0
  359. /package/dist/{chunk-YYOK4GVF.js.map → chunk-EVPNWN7Z.js.map} +0 -0
  360. /package/dist/{chunk-HLIR3RJ7.js.map → chunk-EXUCUPV2.js.map} +0 -0
  361. /package/dist/{chunk-2SXMCQEQ.js.map → chunk-GZQXOQQ2.js.map} +0 -0
  362. /package/dist/{chunk-2KAYTPPT.js.map → chunk-HCVWF657.js.map} +0 -0
  363. /package/dist/{chunk-4KZGLDJJ.js.map → chunk-J3JPB7ZI.js.map} +0 -0
  364. /package/dist/{chunk-LQ6JI4VH.js.map → chunk-LD53WPMU.js.map} +0 -0
  365. /package/dist/{chunk-URM3HW2R.js.map → chunk-LLUGAZ3B.js.map} +0 -0
  366. /package/dist/{chunk-U5H7WHTS.js.map → chunk-N77ELJ2D.js.map} +0 -0
  367. /package/dist/{chunk-2VHMVWPU.js.map → chunk-NXRUUDUF.js.map} +0 -0
  368. /package/dist/{chunk-DIFU33GU.js.map → chunk-OHTOPFRF.js.map} +0 -0
  369. /package/dist/{chunk-74MYJSKB.js.map → chunk-ORAK3LNZ.js.map} +0 -0
  370. /package/dist/{chunk-REWQAFSA.js.map → chunk-PD42EYQN.js.map} +0 -0
  371. /package/dist/{chunk-773DPI6T.js.map → chunk-PKMQK2WY.js.map} +0 -0
  372. /package/dist/{chunk-CTOQEZSN.js.map → chunk-QR67FU5G.js.map} +0 -0
  373. /package/dist/{chunk-AU7Q3LSC.js.map → chunk-RKNJBZ55.js.map} +0 -0
  374. /package/dist/{chunk-YI7RZO7Y.js.map → chunk-SQNED75R.js.map} +0 -0
  375. /package/dist/{chunk-5CEJH5ZN.js.map → chunk-TAPSNIMA.js.map} +0 -0
  376. /package/dist/{chunk-5RHM43BJ.js.map → chunk-TXQCG6SE.js.map} +0 -0
  377. /package/dist/{chunk-ANI5DUAE.js.map → chunk-UIXXE6NI.js.map} +0 -0
  378. /package/dist/{chunk-2COL35PK.js.map → chunk-UO5ZHIIH.js.map} +0 -0
  379. /package/dist/{chunk-OJU73BUX.js.map → chunk-VCLGUIFM.js.map} +0 -0
  380. /package/dist/{chunk-5J4OL7TJ.js.map → chunk-VUA4S7P6.js.map} +0 -0
  381. /package/dist/{chunk-A66YRSOS.js.map → chunk-WQYSDPMA.js.map} +0 -0
  382. /package/dist/{chunk-OZ2KQ7HF.js.map → chunk-YEE5KJQQ.js.map} +0 -0
  383. /package/dist/{chunk-WRFKZEO6.js.map → chunk-YQGHW5ML.js.map} +0 -0
  384. /package/dist/{chunk-APUJIMDM.js.map → chunk-Z4RU5VPM.js.map} +0 -0
  385. /package/dist/{chunk-DCWIQFNA.js.map → chunk-ZUDM75KG.js.map} +0 -0
  386. /package/dist/{forget-U66KPDPY.js.map → forget-OEE5OELK.js.map} +0 -0
  387. /package/dist/{graph-edge-decay-JLOF3YW4.js.map → graph-edge-decay-QPRJQ7DL.js.map} +0 -0
@@ -34,6 +34,10 @@ import { log } from "./logger.js";
34
34
  import type { PluginConfig, MemoryFrontmatter, FaithfulnessFrontmatter } from "./types.js";
35
35
  import type { LocalLlmClient } from "./local-llm.js";
36
36
  import { type FallbackLlmClient, gatewayTaskChainOptions } from "./fallback-llm.js";
37
+ import {
38
+ callOpenAiCompatibleChat,
39
+ resolveFaithfulnessGateEndpoint,
40
+ } from "./local-model-endpoint.js";
37
41
  import { extractJsonCandidates } from "./json-extract.js";
38
42
 
39
43
  // Re-export for callers importing from this module.
@@ -280,13 +284,54 @@ async function callFaithfulnessLlm(
280
284
  fallbackLlm: FallbackLlmClient | null,
281
285
  timeoutMs: number,
282
286
  signal?: AbortSignal,
287
+ fetchImpl: typeof fetch = fetch,
283
288
  ): Promise<LlmCallResult> {
284
289
  const messages: Array<{ role: "system" | "user"; content: string }> = [
285
290
  { role: "system", content: systemPrompt },
286
291
  { role: "user", content: userPrompt },
287
292
  ];
288
293
 
289
- const modelOverride = config.extractionFaithfulnessModel || undefined;
294
+ // Issue #1585 model-lab pointer: when an explicit local fine-tuned endpoint
295
+ // is configured (base URL + model name), try it FIRST — it is the cheapest,
296
+ // deterministic path the operator opted into by setting
297
+ // extractionFaithfulnessBaseUrl. Unset (the default) skips this entirely,
298
+ // preserving byte-identical pre-feature routing (rule 39). Any failure
299
+ // (network, non-2xx, malformed body, timeout) returns null and falls through
300
+ // to the configured chain (checklist §4 graceful degradation).
301
+ const localEndpoint = resolveFaithfulnessGateEndpoint(config);
302
+ if (localEndpoint) {
303
+ // Give the local probe a SMALLER budget than the outer batch timeout so a
304
+ // wedged local model returns null (and falls through to the configured
305
+ // chain) before the batch timer fires. The local endpoint is meant to be
306
+ // the fast path; if it cannot answer within half the budget (min 500ms),
307
+ // abandon it and let the fallback chain use the remainder (codex P2 — the
308
+ // advertised graceful fallback must actually reach the chain, not be starved
309
+ // by a hanging probe sharing the full batch budget).
310
+ const probeBudgetMs = Math.max(500, Math.floor(timeoutMs / 2));
311
+ const result = await callOpenAiCompatibleChat(
312
+ localEndpoint,
313
+ messages,
314
+ { temperature: 0.1, maxTokens: 2048, responseFormatJson: true, timeoutMs: probeBudgetMs },
315
+ fetchImpl,
316
+ );
317
+ if (result?.content) {
318
+ return { content: result.content, modelUsed: result.modelUsed };
319
+ }
320
+ log.debug(
321
+ "extraction-faithfulness: local model-lab endpoint unavailable, trying configured chain",
322
+ );
323
+ }
324
+
325
+ // extractionFaithfulnessModel is the LOCAL served model's name (e.g.
326
+ // remnic-faithfulness-gate-v1). When a local endpoint is configured it
327
+ // belongs to that endpoint ONLY — it must NOT leak into the fallback chain
328
+ // as a gateway/local-LLM override, or a local outage forces the configured
329
+ // chain onto an unavailable local-only model and turns graceful fallback
330
+ // into backend_unavailable (codex P2 PRRT_kwDORJXyws6Otp-L). Decouple: the
331
+ // override reaches the fallback chain only when NO local endpoint is
332
+ // configured (the pre-feature model-override contract, preserved
333
+ // byte-identical — rule 39).
334
+ const modelOverride = localEndpoint ? undefined : (config.extractionFaithfulnessModel || undefined);
290
335
 
291
336
  // Skip the local backend when (a) modelSource is "gateway", or (b) a
292
337
  // faithfulness model override is set. The local client always sends
@@ -415,6 +460,12 @@ export async function checkFaithfulnessBatch(
415
460
  config: PluginConfig,
416
461
  localLlm: LocalLlmClient | null,
417
462
  fallbackLlm: FallbackLlmClient | null,
463
+ /**
464
+ * Optional fetch injection (issue #1585). The orchestrator does not pass it
465
+ * (uses global fetch); tests pass a stub to exercise the local model-lab
466
+ * endpoint path without a live server. See callFaithfulnessLlm.
467
+ */
468
+ fetchImpl?: typeof fetch,
418
469
  ): Promise<FaithfulnessBatchResult> {
419
470
  const timeoutMs =
420
471
  typeof config.extractionFaithfulnessTimeoutMs === "number" &&
@@ -486,6 +537,7 @@ export async function checkFaithfulnessBatch(
486
537
  fallbackLlm,
487
538
  timeoutMs,
488
539
  controller.signal,
540
+ fetchImpl,
489
541
  );
490
542
  const settled = await Promise.race([
491
543
  callPromise.then(
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Tests for the extracted faithfulness + correction-intent config parser
3
+ * (issue #1576 / #1585). Pins the behavior-preserving extraction: defaults
4
+ * are byte-identical to the pre-extraction inline block, validation rejects
5
+ * the same inputs with the same messages, and the new model-lab pointer keys
6
+ * default empty (byte-identical pre-feature path, rule 39).
7
+ */
8
+
9
+ import test from "node:test";
10
+ import assert from "node:assert/strict";
11
+
12
+ import { parseFaithfulnessGateConfig, parseCorrectionIntentConfig } from "./faithfulness-config.js";
13
+
14
+ test("parseFaithfulnessGateConfig: empty input → byte-identical defaults (rule 39)", () => {
15
+ const out = parseFaithfulnessGateConfig({});
16
+ assert.equal(out.extractionFaithfulnessGate, "off");
17
+ assert.equal(out.extractionFaithfulnessModel, "");
18
+ assert.equal(out.extractionFaithfulnessBaseUrl, "");
19
+ assert.equal(out.extractionFaithfulnessContextChars, 400);
20
+ assert.equal(out.extractionFaithfulnessTimeoutMs, 8000);
21
+ });
22
+
23
+ test("parseFaithfulnessGateConfig: null/undefined gate → off (not rejected)", () => {
24
+ assert.equal(parseFaithfulnessGateConfig({ extractionFaithfulnessGate: undefined }).extractionFaithfulnessGate, "off");
25
+ assert.equal(parseFaithfulnessGateConfig({ extractionFaithfulnessGate: null }).extractionFaithfulnessGate, "off");
26
+ });
27
+
28
+ test("parseFaithfulnessGateConfig: valid gate modes accepted (case-insensitive, trimmed)", () => {
29
+ for (const v of ["off", "shadow", "enforce", "SHADOW", " Enforce "]) {
30
+ const mode = parseFaithfulnessGateConfig({ extractionFaithfulnessGate: v }).extractionFaithfulnessGate;
31
+ assert.ok(mode === "off" || mode === "shadow" || mode === "enforce", `${v} → ${mode}`);
32
+ }
33
+ assert.equal(parseFaithfulnessGateConfig({ extractionFaithfulnessGate: "SHADOW" }).extractionFaithfulnessGate, "shadow");
34
+ });
35
+
36
+ test("parseFaithfulnessGateConfig: present-but-invalid gate rejects (Ob4RQ — never silently off)", () => {
37
+ for (const bad of [true, 1, {}, "maybe", "on"]) {
38
+ assert.throws(
39
+ () => parseFaithfulnessGateConfig({ extractionFaithfulnessGate: bad }),
40
+ /extractionFaithfulnessGate must be one of "off" \| "shadow" \| "enforce"/,
41
+ );
42
+ }
43
+ });
44
+
45
+ test("parseFaithfulnessGateConfig: context chars clamp to [1, 4000]; invalid rejects (#1634)", () => {
46
+ assert.equal(parseFaithfulnessGateConfig({ extractionFaithfulnessContextChars: 100000 }).extractionFaithfulnessContextChars, 4000);
47
+ assert.equal(parseFaithfulnessGateConfig({ extractionFaithfulnessContextChars: "400" }).extractionFaithfulnessContextChars, 400);
48
+ for (const bad of [0, -1, 1.5, true, NaN, "abc"]) {
49
+ assert.throws(
50
+ () => parseFaithfulnessGateConfig({ extractionFaithfulnessContextChars: bad }),
51
+ /extractionFaithfulnessContextChars must be an integer/,
52
+ );
53
+ }
54
+ });
55
+
56
+ test("parseFaithfulnessGateConfig: timeout clamps to [1, 60000]; invalid rejects", () => {
57
+ assert.equal(parseFaithfulnessGateConfig({ extractionFaithfulnessTimeoutMs: 999999 }).extractionFaithfulnessTimeoutMs, 60_000);
58
+ for (const bad of [0, -5, 2.5, false, "no"]) {
59
+ assert.throws(
60
+ () => parseFaithfulnessGateConfig({ extractionFaithfulnessTimeoutMs: bad }),
61
+ /extractionFaithfulnessTimeoutMs must be an integer/,
62
+ );
63
+ }
64
+ });
65
+
66
+ test("parseFaithfulnessGateConfig: model-lab pointer keys default empty + accept strings (#1585)", () => {
67
+ // Defaults preserve the existing routing chain exactly.
68
+ assert.equal(parseFaithfulnessGateConfig({}).extractionFaithfulnessBaseUrl, "");
69
+ assert.equal(parseFaithfulnessGateConfig({}).extractionFaithfulnessModel, "");
70
+ // Non-string values are ignored → empty (not rejected; mirrors model key).
71
+ assert.equal(parseFaithfulnessGateConfig({ extractionFaithfulnessBaseUrl: 123 }).extractionFaithfulnessBaseUrl, "");
72
+ // Real pointer values round-trip.
73
+ const out = parseFaithfulnessGateConfig({
74
+ extractionFaithfulnessModel: "remnic-faithfulness-gate-v1",
75
+ extractionFaithfulnessBaseUrl: "http://localhost:11434/v1",
76
+ });
77
+ assert.equal(out.extractionFaithfulnessModel, "remnic-faithfulness-gate-v1");
78
+ assert.equal(out.extractionFaithfulnessBaseUrl, "http://localhost:11434/v1");
79
+ });
80
+
81
+ test("parseCorrectionIntentConfig: empty input → empty pointers (rule-based detector stays default)", () => {
82
+ const out = parseCorrectionIntentConfig({});
83
+ assert.equal(out.correctionIntentModel, "");
84
+ assert.equal(out.correctionIntentBaseUrl, "");
85
+ });
86
+
87
+ test("parseCorrectionIntentConfig: pointers round-trip; non-strings → empty", () => {
88
+ const out = parseCorrectionIntentConfig({
89
+ correctionIntentModel: "remnic-correction-intent-v1",
90
+ correctionIntentBaseUrl: "http://localhost:8000/v1",
91
+ });
92
+ assert.equal(out.correctionIntentModel, "remnic-correction-intent-v1");
93
+ assert.equal(out.correctionIntentBaseUrl, "http://localhost:8000/v1");
94
+ assert.equal(parseCorrectionIntentConfig({ correctionIntentModel: true }).correctionIntentModel, "");
95
+ assert.equal(parseCorrectionIntentConfig({ correctionIntentBaseUrl: 5 }).correctionIntentBaseUrl, "");
96
+ });
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Faithfulness-gate + correction-intent config parsing (issue #1576 / #1585).
3
+ *
4
+ * Extracted verbatim from `config.ts` (a watchlisted god file) so the gate
5
+ * config block can grow — the model-lab pointer keys
6
+ * `extractionFaithfulnessBaseUrl` (#1585) and the correction-intent pointers
7
+ * (`correctionIntentModel` / `correctionIntentBaseUrl`) — without growing the
8
+ * god file. This is a behavior-preserving extraction (#1526-style): the
9
+ * validation, defaults, and error messages are byte-identical to the inline
10
+ * block it replaces, which the existing extraction-faithfulness + config
11
+ * tests pin.
12
+ *
13
+ * Why a dedicated module (and not inline): the ratchet
14
+ * (`scripts/check-ratchets.mjs`) forbids `config.ts` from growing, and the
15
+ * model-lab integration needs three new optional keys. Centralizing the whole
16
+ * gate+intent parsing here keeps it unit-testable in isolation and lets
17
+ * `config.ts` spread the result in one line.
18
+ */
19
+
20
+ import { coerceNumber } from "./connectors/coerce.js";
21
+
22
+ /**
23
+ * Parse a value as a strict integer >= `min`, throwing on anything else.
24
+ *
25
+ * Mirrors `parseIntegerAtLeast` in `config.ts` byte-for-byte (issue #1634:
26
+ * reject non-numeric, <=0, non-integer, NaN, Infinity, booleans, objects —
27
+ * gotcha #51). Reimplemented locally rather than exported from `config.ts`
28
+ * so this module has no dependency on the god file (and vice versa).
29
+ */
30
+ function parseIntegerAtLeast(
31
+ value: unknown,
32
+ fallback: number,
33
+ min: number,
34
+ keyName: string,
35
+ ): number {
36
+ if (value === undefined || value === null) return fallback;
37
+ const coerced = coerceNumber(value);
38
+ if (
39
+ coerced === undefined ||
40
+ !Number.isFinite(coerced) ||
41
+ !Number.isInteger(coerced) ||
42
+ coerced < min
43
+ ) {
44
+ throw new Error(
45
+ `${keyName} must be an integer greater than or equal to ${min}; got ${JSON.stringify(value)}`,
46
+ );
47
+ }
48
+ return coerced;
49
+ }
50
+
51
+ /**
52
+ * Parse the extraction faithfulness gate config block.
53
+ *
54
+ * Returns exactly the `PluginConfig` fields this block owns. Defaults preserve
55
+ * the byte-identical pre-feature pipeline (rule 39): gate "off", empty model
56
+ * + base URL, context 400 chars, timeout 8000 ms. Invalid `gate` values reject
57
+ * listing valid options (rule 51); invalid integers throw (issue #1634).
58
+ *
59
+ * `extractionFaithfulnessBaseUrl` (issue #1585) is the model-lab pointer: when
60
+ * set together with `extractionFaithfulnessModel`, the gate routes to that
61
+ * local openai-compatible endpoint before the configured chain; empty string
62
+ * (default) preserves the existing routing exactly.
63
+ */
64
+ export function parseFaithfulnessGateConfig(cfg: Record<string, unknown>): {
65
+ extractionFaithfulnessGate: "off" | "shadow" | "enforce";
66
+ extractionFaithfulnessModel: string;
67
+ extractionFaithfulnessBaseUrl: string;
68
+ extractionFaithfulnessContextChars: number;
69
+ extractionFaithfulnessTimeoutMs: number;
70
+ } {
71
+ const gateValue = cfg.extractionFaithfulnessGate;
72
+ let extractionFaithfulnessGate: "off" | "shadow" | "enforce";
73
+ if (gateValue === undefined || gateValue === null) {
74
+ extractionFaithfulnessGate = "off";
75
+ } else {
76
+ // Present-but-invalid (true/1/{}) must reject, not silently disable the gate (Ob4RQ).
77
+ const raw = typeof gateValue === "string" ? gateValue.trim().toLowerCase() : gateValue;
78
+ if (raw === "off" || raw === "shadow" || raw === "enforce") {
79
+ extractionFaithfulnessGate = raw;
80
+ } else {
81
+ throw new Error(
82
+ `extractionFaithfulnessGate must be one of "off" | "shadow" | "enforce" (got ${JSON.stringify(gateValue)})`,
83
+ );
84
+ }
85
+ }
86
+
87
+ const extractionFaithfulnessModel =
88
+ typeof cfg.extractionFaithfulnessModel === "string" ? cfg.extractionFaithfulnessModel : "";
89
+
90
+ // Issue #1585: model-lab pointer. Empty default → gate uses its existing
91
+ // routing chain (byte-identical). Non-empty + a model name → gate prefers
92
+ // the local served endpoint, falling back to the chain on failure.
93
+ const extractionFaithfulnessBaseUrl =
94
+ typeof cfg.extractionFaithfulnessBaseUrl === "string"
95
+ ? cfg.extractionFaithfulnessBaseUrl
96
+ : "";
97
+
98
+ const extractionFaithfulnessContextChars = Math.min(
99
+ parseIntegerAtLeast(cfg.extractionFaithfulnessContextChars, 400, 1, "extractionFaithfulnessContextChars"),
100
+ 4000,
101
+ );
102
+
103
+ const extractionFaithfulnessTimeoutMs = Math.min(
104
+ parseIntegerAtLeast(cfg.extractionFaithfulnessTimeoutMs, 8000, 1, "extractionFaithfulnessTimeoutMs"),
105
+ 60_000,
106
+ );
107
+
108
+ return {
109
+ extractionFaithfulnessGate,
110
+ extractionFaithfulnessModel,
111
+ extractionFaithfulnessBaseUrl,
112
+ extractionFaithfulnessContextChars,
113
+ extractionFaithfulnessTimeoutMs,
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Parse the correction-intent config block (issue #1581 / #1585).
119
+ *
120
+ * The model-lab pointer for passive-correction detection: when both
121
+ * `correctionIntentModel` and `correctionIntentBaseUrl` are set, the detection
122
+ * path can route to that local fine-tuned classifier; empty defaults (the
123
+ * common case) keep the rule-based `detectPassiveCorrections` heuristic as the
124
+ * sole detector — byte-identical to the pre-feature path. Consumption by the
125
+ * detector is the consuming child's job (#1581's detector is a pure function;
126
+ * wiring a model call into it is out of scope for this config-pointer PR).
127
+ */
128
+ export function parseCorrectionIntentConfig(cfg: Record<string, unknown>): {
129
+ correctionIntentModel: string;
130
+ correctionIntentBaseUrl: string;
131
+ } {
132
+ return {
133
+ correctionIntentModel:
134
+ typeof cfg.correctionIntentModel === "string" ? cfg.correctionIntentModel : "",
135
+ correctionIntentBaseUrl:
136
+ typeof cfg.correctionIntentBaseUrl === "string" ? cfg.correctionIntentBaseUrl : "",
137
+ };
138
+ }
@@ -132,9 +132,9 @@ export function parseGraphSnapshotSince(raw: string | undefined): number | undef
132
132
  export async function buildGraphSnapshot(opts: {
133
133
  memoryDir: string;
134
134
  graphConfig: {
135
- entityGraphEnabled: boolean;
136
- timeGraphEnabled: boolean;
137
- causalGraphEnabled: boolean;
135
+ entityGraph: boolean;
136
+ timeGraph: boolean;
137
+ causalGraph: boolean;
138
138
  };
139
139
  request: GraphSnapshotRequest;
140
140
  loadNode: GraphSnapshotNodeLoader;
package/src/graph.ts CHANGED
@@ -15,6 +15,7 @@ import * as path from "node:path";
15
15
 
16
16
  import { readEdgeConfidence } from "./graph-edge-reinforcement.js";
17
17
  import { emitGraphEvent } from "./graph-events.js";
18
+ import type { GraphConstructionCapabilitySet } from "./capabilities.js";
18
19
 
19
20
  export type GraphType = "entity" | "time" | "causal";
20
21
 
@@ -198,12 +199,12 @@ export async function readEdgesStrict(memoryDir: string, type: GraphType): Promi
198
199
  */
199
200
  export async function readAllEdges(
200
201
  memoryDir: string,
201
- config: Pick<GraphConfig, "entityGraphEnabled" | "timeGraphEnabled" | "causalGraphEnabled">
202
+ graphCaps: Pick<GraphConstructionCapabilitySet, "entityGraph" | "timeGraph" | "causalGraph">,
202
203
  ): Promise<GraphEdge[]> {
203
204
  const parts: GraphEdge[][] = await Promise.all([
204
- config.entityGraphEnabled ? readEdges(memoryDir, "entity") : Promise.resolve([]),
205
- config.timeGraphEnabled ? readEdges(memoryDir, "time") : Promise.resolve([]),
206
- config.causalGraphEnabled ? readEdges(memoryDir, "causal") : Promise.resolve([]),
205
+ graphCaps.entityGraph ? readEdges(memoryDir, "entity") : Promise.resolve([]),
206
+ graphCaps.timeGraph ? readEdges(memoryDir, "time") : Promise.resolve([]),
207
+ graphCaps.causalGraph ? readEdges(memoryDir, "causal") : Promise.resolve([]),
207
208
  ]);
208
209
  return parts.flat();
209
210
  }
@@ -396,9 +397,9 @@ export class GraphIndex {
396
397
  return this.edgeCache.allEdges;
397
398
  }
398
399
  const allEdges = await readAllEdges(this.memoryDir, {
399
- entityGraphEnabled: this.cfg.entityGraphEnabled,
400
- timeGraphEnabled: this.cfg.timeGraphEnabled,
401
- causalGraphEnabled: this.cfg.causalGraphEnabled,
400
+ entityGraph: this.cfg.entityGraphEnabled,
401
+ timeGraph: this.cfg.timeGraphEnabled,
402
+ causalGraph: this.cfg.causalGraphEnabled,
402
403
  });
403
404
  this.edgeCache = { allEdges, loadedAt: Date.now() };
404
405
  return allEdges;
@@ -1,7 +1,9 @@
1
1
  import { describe, it } from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { mkdtemp, readFile, writeFile, rm, mkdir, appendFile } from "node:fs/promises";
3
+ import { mkdtemp, readFile, writeFile, rm, mkdir, appendFile, utimes } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
+ import { spawn } from "node:child_process";
6
+ import { statSync } from "node:fs";
5
7
  import path from "node:path";
6
8
  import { createHash } from "node:crypto";
7
9
  import {
@@ -345,6 +347,245 @@ describe("TombstoneStore — enabled gate (rule 30)", () => {
345
347
  });
346
348
  });
347
349
 
350
+ // ─────────────────────────────────────────────────────────────────────────────
351
+ // Cross-process tombstone write lock (issue #1639)
352
+ // ─────────────────────────────────────────────────────────────────────────────
353
+
354
+ /**
355
+ * A read-merge-write file io that simulates the secure-store append path
356
+ * (read encrypted → decrypt → concat → re-encrypt → atomic rename). Unlike a
357
+ * raw O_APPEND, this is NOT atomic across processes: two writers that each
358
+ * read the same contents and write back drop the other line — the exact
359
+ * lost-write race the #1639 cross-process lock closes. The setImmediate yield
360
+ * widens the race window so a missing lock would demonstrably lose entries.
361
+ */
362
+ function makeReadMergeWriteIo() {
363
+ return {
364
+ read: (p: string) => readFile(p, "utf8"),
365
+ append: async (p: string, c: string) => {
366
+ await mkdir(path.dirname(p), { recursive: true });
367
+ let existing = "";
368
+ try {
369
+ existing = await readFile(p, "utf8");
370
+ } catch (err) {
371
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
372
+ }
373
+ // Yield so two concurrent read-merge-write appends observably overlap
374
+ // when the cross-process lock is absent.
375
+ await new Promise<void>((resolve) => setImmediate(resolve));
376
+ await writeFile(p, existing + c, "utf8");
377
+ },
378
+ write: async (p: string, c: string) => {
379
+ await mkdir(path.dirname(p), { recursive: true });
380
+ await writeFile(p, c, "utf8");
381
+ },
382
+ stat: (p: string) => statSync(p),
383
+ };
384
+ }
385
+
386
+ describe("TombstoneStore — cross-process write lock (issue #1639)", () => {
387
+ it("two processes appending concurrently produce no lost entries", async () => {
388
+ const dir = await mkdtemp(path.join(tmpdir(), "tomb-xproc-"));
389
+ const moduleUrl = new URL("./tombstones.ts", import.meta.url).href;
390
+ const entriesPerWorker = 15;
391
+
392
+ // Worker source: plain JS, no nested template literals. Each child imports
393
+ // the store, points it at the shared tombstones.jsonl with a read-merge-
394
+ // write io, and appends N entries. The cross-process lock serializes the
395
+ // appends across the two processes so no entry is dropped.
396
+ const workerSource = [
397
+ "(async () => {",
398
+ "const { TombstoneStore } = await import(process.argv[1]);",
399
+ "const { mkdir, readFile, writeFile } = await import(\"node:fs/promises\");",
400
+ "const { statSync } = await import(\"node:fs\");",
401
+ "const path = await import(\"node:path\");",
402
+ "const { createHash } = await import(\"node:crypto\");",
403
+ "const dir = process.argv[2];",
404
+ "const workerId = Number(process.argv[3]);",
405
+ "const count = Number(process.argv[4]);",
406
+ "const filePath = path.join(dir, \"tombstones.jsonl\");",
407
+ "function hash(c){return createHash(\"sha256\").update(c).digest(\"hex\");}",
408
+ "function normalize(c){return c;}",
409
+ "const io = {",
410
+ " read: (p) => readFile(p, \"utf8\"),",
411
+ " append: async (p, c) => {",
412
+ " await mkdir(path.dirname(p), { recursive: true });",
413
+ " let existing = \"\";",
414
+ " try { existing = await readFile(p, \"utf8\"); } catch (e) { if (e.code !== \"ENOENT\") throw e; }",
415
+ " await new Promise((r) => setImmediate(r));",
416
+ " await writeFile(p, existing + c, \"utf8\");",
417
+ " },",
418
+ " write: async (p, c) => { await mkdir(path.dirname(p), { recursive: true }); await writeFile(p, c, \"utf8\"); },",
419
+ " stat: (p) => statSync(p),",
420
+ "};",
421
+ "const store = new TombstoneStore(filePath, \"default\", { enabled: true, semanticMatch: false, semanticThreshold: 0.9, hashContent: hash, normalizeText: normalize }, io);",
422
+ "await store.load();",
423
+ "for (let i = 0; i < count; i += 1) {",
424
+ " await store.appendTombstone({ reason: \"correction\", createdBy: \"user_correction\", sourceMemoryId: \"w\" + workerId + \"-f\" + i, rawContent: \"worker \" + workerId + \" fact \" + i });",
425
+ "}",
426
+ "})();",
427
+ ].join("\n");
428
+
429
+ function runWorker(workerId: number): Promise<void> {
430
+ return new Promise((resolve, reject) => {
431
+ const child = spawn(
432
+ process.execPath,
433
+ ["--import", "tsx", "-e", workerSource, moduleUrl, dir, String(workerId), String(entriesPerWorker)],
434
+ { stdio: ["ignore", "pipe", "pipe"] },
435
+ );
436
+ let stderr = "";
437
+ child.stderr.setEncoding("utf8");
438
+ child.stderr.on("data", (chunk) => { stderr += chunk; });
439
+ child.on("error", reject);
440
+ child.on("close", (code) => {
441
+ if (code === 0) resolve();
442
+ else reject(new Error(`worker ${workerId} exited ${code}: ${stderr}`));
443
+ });
444
+ });
445
+ }
446
+
447
+ // Two genuinely concurrent processes contending on the same lockfile.
448
+ await Promise.all([runWorker(0), runWorker(1)]);
449
+
450
+ const raw = await readFile(path.join(dir, "tombstones.jsonl"), "utf8");
451
+ const fileLines = raw.split("\n").filter((l) => l.trim().length > 0);
452
+ // No lost entries: every line parses and the total is exactly 2 × N.
453
+ assert.equal(fileLines.length, entriesPerWorker * 2);
454
+ for (const line of fileLines) {
455
+ assert.doesNotThrow(() => JSON.parse(line));
456
+ }
457
+ // Every worker entry is present (none dropped by a lost write).
458
+ const sourceIds = new Set(fileLines.map((l) => JSON.parse(l).sourceMemoryId));
459
+ for (let w = 0; w < 2; w += 1) {
460
+ for (let i = 0; i < entriesPerWorker; i += 1) {
461
+ assert.ok(sourceIds.has(`w${w}-f${i}`), `missing w${w}-f${i} — lost write`);
462
+ }
463
+ }
464
+ // The advisory lockfile is released after the run (no lingering lock).
465
+ await rm(path.join(dir, "tombstones.lock"), { force: true });
466
+ });
467
+
468
+ it("recovers from a stale lock left by a crashed holder", async () => {
469
+ const dir = await mkdtemp(path.join(tmpdir(), "tomb-stale-lock-"));
470
+ const filePath = path.join(dir, "tombstones.jsonl");
471
+ const lockPath = path.join(dir, "tombstones.lock");
472
+ // Seed a lockfile written by a (now-crashed) holder: <pid> <uuid> <iso>.
473
+ await mkdir(dir, { recursive: true });
474
+ await writeFile(
475
+ lockPath,
476
+ `${process.pid} 00000000-0000-0000-0000-000000000000 2000-01-01T00:00:00.000Z\n`,
477
+ "utf8",
478
+ );
479
+ // Make it older than staleMs so the next acquire breaks it.
480
+ const old = new Date(Date.now() - 120_000);
481
+ await utimes(lockPath, old, old);
482
+
483
+ const store = new TombstoneStore(
484
+ filePath,
485
+ "default",
486
+ { enabled: true, semanticMatch: false, semanticThreshold: 0.9, hashContent: computeHash, normalizeText: normalizeContent, lockStaleMs: 1_000 },
487
+ makeReadMergeWriteIo(),
488
+ );
489
+ // The append must break the stale lock, acquire a fresh one, and succeed.
490
+ const id = await store.appendTombstone({
491
+ reason: "correction",
492
+ createdBy: "user_correction",
493
+ sourceMemoryId: "fact-stale",
494
+ rawContent: "Recovered from a stale lock.",
495
+ });
496
+ assert.match(id, /^tomb-/);
497
+ // The tombstone landed durably.
498
+ const raw = await readFile(filePath, "utf8");
499
+ const lines2 = raw.split("\n").filter((l) => l.trim().length > 0);
500
+ assert.equal(lines2.length, 1);
501
+ // Our fresh lock was released on completion (the stale one was broken).
502
+ await rm(lockPath, { force: true });
503
+ });
504
+
505
+ it("rebuild preserves a peer's revocation appended before rebuild acquired the lock (cursor/codex #1639)", async () => {
506
+ // Regression for the rebuild race: rebuild must re-read the log under the
507
+ // lock (ensureFreshAgainstDisk) before computing its payload, otherwise a
508
+ // revocation a peer wrote while rebuild waited for the lock is dropped and
509
+ // the retired fact silently un-revokes (resurrection).
510
+ const dir = await mkdtemp(path.join(tmpdir(), "tomb-rebuild-race-"));
511
+ const filePath = path.join(dir, "tombstones.jsonl");
512
+ const opts = { enabled: true, semanticMatch: false, semanticThreshold: 0.9, hashContent: computeHash, normalizeText: normalizeContent };
513
+ const io = makeReadMergeWriteIo();
514
+ // Store A writes tombstone-1, then goes stale (never sees the peer write).
515
+ const storeA = new TombstoneStore(filePath, "default", opts, io);
516
+ const t1 = await storeA.appendTombstone({
517
+ reason: "correction",
518
+ createdBy: "user_correction",
519
+ sourceMemoryId: "fact-rebuild-1",
520
+ rawContent: "The DB is MySQL.",
521
+ });
522
+ // Peer (store B, same file) revokes t1 durably. Store A's in-memory index
523
+ // is now stale (it still thinks t1 is active).
524
+ const storeB = new TombstoneStore(filePath, "default", opts, io);
525
+ await storeB.revoke(t1, "user_correction");
526
+ // Store A rebuilds from a retired-memory corpus that reconstructs t1.
527
+ // Without the under-lock re-read, A computes existingRevocations from its
528
+ // stale index (empty), overwrites the file with [rebuilt-t1], and the
529
+ // revocation is LOST — t1 is active again (resurrection). With the fix,
530
+ // ensureFreshAgainstDisk reloads the revocation under the lock and rebuild
531
+ // preserves it.
532
+ await storeA.rebuild([
533
+ {
534
+ memoryId: "fact-rebuild-1",
535
+ rawContent: "The DB is MySQL.",
536
+ reason: "correction" as const,
537
+ createdBy: "user_correction" as const,
538
+ createdAt: "2026-01-01T00:00:00.000Z",
539
+ },
540
+ ]);
541
+ // The rebuilt file must still contain the revocation (the retired fact
542
+ // stays revoked, not silently un-revoked by the rebuild).
543
+ const raw = await readFile(filePath, "utf8");
544
+ const rebuiltLines = raw.split("\n").filter((l) => l.trim().length > 0);
545
+ const hasRevocation = rebuiltLines.some((l) => {
546
+ try { return JSON.parse(l).kind === "revocation"; } catch { return false; }
547
+ });
548
+ assert.ok(hasRevocation, "rebuild dropped the peer's revocation — resurrection risk");
549
+ // And the rebuilt store A must report the fact as revoked (lookup returns null).
550
+ await storeA.load();
551
+ assert.equal(
552
+ storeA.lookup({ namespace: "default", contentHash: computeHash("The DB is MySQL.") }),
553
+ null,
554
+ "revoked fact resurrected after rebuild",
555
+ );
556
+ await rm(path.join(dir, "tombstones.lock"), { force: true });
557
+ });
558
+
559
+ it("in-process concurrent appends still serialize under the lock", async () => {
560
+ const dir = await mkdtemp(path.join(tmpdir(), "tomb-inproc-lock-"));
561
+ const filePath = path.join(dir, "tombstones.jsonl");
562
+ const store = new TombstoneStore(
563
+ filePath,
564
+ "default",
565
+ { enabled: true, semanticMatch: false, semanticThreshold: 0.9, hashContent: computeHash, normalizeText: normalizeContent },
566
+ makeReadMergeWriteIo(),
567
+ );
568
+ const contents = Array.from({ length: 25 }, (_, i) => `locked content ${i}`);
569
+ await Promise.all(
570
+ contents.map((c) =>
571
+ store.appendTombstone({
572
+ reason: "correction",
573
+ createdBy: "user_correction",
574
+ sourceMemoryId: `fact-${c}`,
575
+ rawContent: c,
576
+ }),
577
+ ),
578
+ );
579
+ await store.load();
580
+ const raw = await readFile(filePath, "utf8");
581
+ const lines3 = raw.split("\n").filter((l) => l.trim().length > 0);
582
+ // No lost writes despite the read-merge-write io + concurrent appends.
583
+ assert.equal(lines3.length, contents.length);
584
+ assert.equal(store.stats().count, contents.length);
585
+ await rm(path.join(dir, "tombstones.lock"), { force: true });
586
+ });
587
+ });
588
+
348
589
  describe("parseTombstoneLine", () => {
349
590
  it("rejects malformed input", () => {
350
591
  assert.equal(parseTombstoneLine(""), null);