@remnic/core 9.3.725 → 9.3.727

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 (352) hide show
  1. package/dist/access-boundary.d.ts +5 -5
  2. package/dist/access-boundary.js +28 -27
  3. package/dist/access-cli.js +65 -63
  4. package/dist/access-cli.js.map +1 -1
  5. package/dist/access-http.d.ts +20 -5
  6. package/dist/access-http.js +34 -33
  7. package/dist/access-mcp.d.ts +5 -5
  8. package/dist/access-mcp.js +31 -30
  9. package/dist/access-operations-batch.js +29 -28
  10. package/dist/access-operations.d.ts +5 -5
  11. package/dist/access-operations.js +30 -29
  12. package/dist/{access-service-B2PlPtxh.d.ts → access-service-Bgpjxo4f.d.ts} +36 -4
  13. package/dist/access-service.d.ts +5 -5
  14. package/dist/access-service.js +27 -26
  15. package/dist/access-surface-catalog.d.ts +5 -5
  16. package/dist/action-confidence.d.ts +1 -1
  17. package/dist/active-memory-bridge.d.ts +1 -1
  18. package/dist/active-recall.d.ts +1 -1
  19. package/dist/active-recall.js +3 -3
  20. package/dist/adapters/index.js +4 -4
  21. package/dist/adapters/registry.js +2 -2
  22. package/dist/behavior-learner.d.ts +1 -1
  23. package/dist/behavior-signals.d.ts +1 -1
  24. package/dist/bootstrap.d.ts +4 -4
  25. package/dist/briefing.d.ts +1 -1
  26. package/dist/briefing.js +9 -8
  27. package/dist/buffer-surprise-report.d.ts +1 -1
  28. package/dist/buffer.d.ts +1 -1
  29. package/dist/calibration.d.ts +1 -1
  30. package/dist/capabilities.d.ts +1 -1
  31. package/dist/{catalog-CzUb-GyD.d.ts → catalog-bMczlO2G.d.ts} +1 -1
  32. package/dist/causal-behavior.d.ts +1 -1
  33. package/dist/causal-consolidation.d.ts +1 -1
  34. package/dist/causal-consolidation.js +10 -9
  35. package/dist/causal-consolidation.js.map +1 -1
  36. package/dist/causal-trajectory-graph.d.ts +1 -1
  37. package/dist/{chunk-KGZF2VO3.js → chunk-27QQC7ZX.js} +80 -19
  38. package/dist/{chunk-KGZF2VO3.js.map → chunk-27QQC7ZX.js.map} +1 -1
  39. package/dist/{chunk-HCVWF657.js → chunk-2KAYTPPT.js} +12 -12
  40. package/dist/{chunk-4SF232E6.js → chunk-36SEDFAU.js} +19 -7
  41. package/dist/chunk-36SEDFAU.js.map +1 -0
  42. package/dist/{chunk-ZK2L72VN.js → chunk-37436GML.js} +2 -2
  43. package/dist/{chunk-SC2YAWRF.js → chunk-3TSTGI6F.js} +2 -2
  44. package/dist/{chunk-FACTVMXQ.js → chunk-4H4F3BFL.js} +2 -2
  45. package/dist/{chunk-WAFDHKES.js → chunk-4NOI577P.js} +2 -2
  46. package/dist/{chunk-YQMPUYHE.js → chunk-4NR2PDZ5.js} +2 -2
  47. package/dist/{chunk-TAPSNIMA.js → chunk-5CEJH5ZN.js} +4 -4
  48. package/dist/{chunk-FN6QP7JQ.js → chunk-6PY5YJAV.js} +100 -14
  49. package/dist/{chunk-FN6QP7JQ.js.map → chunk-6PY5YJAV.js.map} +1 -1
  50. package/dist/{chunk-PKMQK2WY.js → chunk-773DPI6T.js} +4 -4
  51. package/dist/{chunk-4R3CGCI5.js → chunk-7RLCCNHU.js} +15 -12
  52. package/dist/chunk-7RLCCNHU.js.map +1 -0
  53. package/dist/{chunk-XGTBNVIU.js → chunk-AAD2ZS6P.js} +11 -1
  54. package/dist/chunk-AAD2ZS6P.js.map +1 -0
  55. package/dist/{chunk-Z3FGC3QE.js → chunk-AQLOP5F4.js} +139 -30
  56. package/dist/chunk-AQLOP5F4.js.map +1 -0
  57. package/dist/{chunk-FXCIOLRB.js → chunk-ASKXIAHD.js} +2 -2
  58. package/dist/{chunk-5OPYYPJN.js → chunk-CBWI22JN.js} +2 -2
  59. package/dist/{chunk-ZOHMPFOB.js → chunk-CKFLLAIS.js} +3 -3
  60. package/dist/{chunk-GZQXOQQ2.js → chunk-CSRXQ6NB.js} +5 -5
  61. package/dist/{chunk-QR67FU5G.js → chunk-CTOQEZSN.js} +2 -2
  62. package/dist/{chunk-ZUDM75KG.js → chunk-DCWIQFNA.js} +4 -4
  63. package/dist/{chunk-6CWGS2LY.js → chunk-DKJCRGWM.js} +3 -3
  64. package/dist/{chunk-JJJCVCCQ.js → chunk-DMQWV6SQ.js} +2 -2
  65. package/dist/{chunk-F3XZN24Z.js → chunk-DRUDC2UX.js} +2 -2
  66. package/dist/{chunk-4WZ3J7M6.js → chunk-EY6MDMHK.js} +2 -2
  67. package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
  68. package/dist/{chunk-C4X4QGIO.js → chunk-G6NKALRN.js} +5 -2
  69. package/dist/chunk-G6NKALRN.js.map +1 -0
  70. package/dist/{chunk-6SQQOJ4U.js → chunk-GKRLPHPU.js} +2 -2
  71. package/dist/{chunk-D7EBTQ24.js → chunk-HOHVIVDR.js} +2 -2
  72. package/dist/{chunk-RKNJBZ55.js → chunk-JBPKEARU.js} +4 -4
  73. package/dist/{chunk-UBHVAGO7.js → chunk-JYLFZWUX.js} +2 -2
  74. package/dist/{chunk-LD53WPMU.js → chunk-LQ6JI4VH.js} +4 -4
  75. package/dist/chunk-LTHVM4XN.js +109 -0
  76. package/dist/chunk-LTHVM4XN.js.map +1 -0
  77. package/dist/{chunk-WM7DNKBD.js → chunk-M4V2JVSZ.js} +2 -2
  78. package/dist/{chunk-I2HL5M6T.js → chunk-MJIHJ7OG.js} +52 -52
  79. package/dist/{chunk-J334N6QV.js → chunk-MLKWJOAJ.js} +2 -2
  80. package/dist/{chunk-C6BWLYW5.js → chunk-N3VNUM4Z.js} +171 -79
  81. package/dist/chunk-N3VNUM4Z.js.map +1 -0
  82. package/dist/{chunk-DF3I6JGP.js → chunk-NSA5ZRUL.js} +15 -15
  83. package/dist/{chunk-VI5SGGXB.js → chunk-OEEW4KJA.js} +4 -4
  84. package/dist/{chunk-7AG54AF3.js → chunk-PUR2CKX2.js} +1 -1
  85. package/dist/chunk-PUR2CKX2.js.map +1 -0
  86. package/dist/{chunk-NT36GU6X.js → chunk-R7IYU5DZ.js} +2 -2
  87. package/dist/{chunk-TT5U3UDO.js → chunk-SNKPUJHR.js} +4 -4
  88. package/dist/{chunk-T55EGZSJ.js → chunk-UVPL565C.js} +2 -2
  89. package/dist/chunk-W5H6ZRPT.js +14 -0
  90. package/dist/chunk-W5H6ZRPT.js.map +1 -0
  91. package/dist/{chunk-YQGHW5ML.js → chunk-WRFKZEO6.js} +4 -4
  92. package/dist/{chunk-KLFYKC3K.js → chunk-XPHVAOBT.js} +195 -74
  93. package/dist/chunk-XPHVAOBT.js.map +1 -0
  94. package/dist/{chunk-H7HUQIY7.js → chunk-Y4YD5TYW.js} +3 -3
  95. package/dist/{chunk-MDBZTH7N.js → chunk-YKBHOBJK.js} +44 -10
  96. package/dist/chunk-YKBHOBJK.js.map +1 -0
  97. package/dist/{chunk-YUYPWJD3.js → chunk-YOLZS7NY.js} +2 -2
  98. package/dist/{chunk-6OIDDKAA.js → chunk-YTU7WKYN.js} +35 -9
  99. package/dist/chunk-YTU7WKYN.js.map +1 -0
  100. package/dist/{cli-IOlDHSBT.d.ts → cli-BLME420h.d.ts} +3 -3
  101. package/dist/cli.d.ts +6 -6
  102. package/dist/cli.js +60 -59
  103. package/dist/compat/codex-cli-fallback.d.ts +1 -0
  104. package/dist/compat/codex-cli-fallback.js +22 -0
  105. package/dist/compat/codex-cli-fallback.js.map +1 -0
  106. package/dist/compat/codex-thread-key.d.ts +1 -0
  107. package/dist/compat/codex-thread-key.js +18 -0
  108. package/dist/compat/codex-thread-key.js.map +1 -0
  109. package/dist/compounding/engine.d.ts +1 -1
  110. package/dist/compounding/engine.js +9 -8
  111. package/dist/compounding/preference-consolidator.d.ts +1 -1
  112. package/dist/compression-optimizer.d.ts +1 -1
  113. package/dist/config.d.ts +1 -1
  114. package/dist/config.js +3 -3
  115. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  116. package/dist/connectors/codex-materialize-runner.js +9 -8
  117. package/dist/connectors/codex-materialize.d.ts +1 -1
  118. package/dist/connectors/index.d.ts +1 -1
  119. package/dist/connectors/index.js +9 -8
  120. package/dist/consolidation-provenance-check.d.ts +1 -1
  121. package/dist/consolidation-undo.d.ts +1 -1
  122. package/dist/content-hash.d.ts +14 -0
  123. package/dist/content-hash.js +10 -0
  124. package/dist/content-hash.js.map +1 -0
  125. package/dist/contradiction/index.d.ts +1 -1
  126. package/dist/conversation-index/backend.d.ts +1 -1
  127. package/dist/conversation-index/backend.js +2 -2
  128. package/dist/conversation-index/chunker.d.ts +1 -1
  129. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  130. package/dist/conversation-index/indexer.d.ts +1 -1
  131. package/dist/conversation-index/search.d.ts +1 -1
  132. package/dist/dashboard-runtime.js +2 -2
  133. package/dist/day-summary.d.ts +1 -1
  134. package/dist/delinearize.d.ts +1 -1
  135. package/dist/direct-answer-wiring.d.ts +1 -1
  136. package/dist/direct-answer.d.ts +1 -1
  137. package/dist/embedding-fallback.d.ts +1 -1
  138. package/dist/enrichment/index.d.ts +1 -1
  139. package/dist/entity-retrieval.d.ts +1 -1
  140. package/dist/entity-retrieval.js +9 -8
  141. package/dist/entity-schema.d.ts +1 -1
  142. package/dist/explicit-capture.d.ts +4 -4
  143. package/dist/extraction-faithfulness.d.ts +1 -1
  144. package/dist/extraction-faithfulness.js +2 -2
  145. package/dist/extraction-judge-telemetry.d.ts +1 -1
  146. package/dist/extraction-judge-training.d.ts +1 -1
  147. package/dist/extraction-judge.d.ts +1 -1
  148. package/dist/extraction-redaction-rules.d.ts +33 -0
  149. package/dist/extraction-redaction-rules.js +14 -0
  150. package/dist/extraction-redaction-rules.js.map +1 -0
  151. package/dist/extraction.d.ts +1 -1
  152. package/dist/extraction.js +3 -3
  153. package/dist/faithfulness-config.d.ts +1 -0
  154. package/dist/faithfulness-config.js +1 -1
  155. package/dist/fallback-llm.d.ts +1 -1
  156. package/dist/{forget-OEE5OELK.js → forget-WYLR4ZFV.js} +2 -2
  157. package/dist/graph-dashboard-diff.d.ts +1 -1
  158. package/dist/graph-dashboard-key.d.ts +1 -1
  159. package/dist/graph-dashboard-parser.d.ts +1 -1
  160. package/dist/graph-edge-reinforcement.d.ts +1 -1
  161. package/dist/graph-snapshot.d.ts +1 -1
  162. package/dist/graph.d.ts +1 -1
  163. package/dist/identity-continuity.d.ts +1 -1
  164. package/dist/importance.d.ts +1 -1
  165. package/dist/index.d.ts +9 -9
  166. package/dist/index.js +107 -105
  167. package/dist/index.js.map +1 -1
  168. package/dist/intent.d.ts +1 -1
  169. package/dist/lcm/engine.d.ts +1 -1
  170. package/dist/lcm/engine.js +2 -2
  171. package/dist/lcm/index.d.ts +1 -1
  172. package/dist/lcm/index.js +4 -4
  173. package/dist/lcm/tools.d.ts +1 -1
  174. package/dist/lifecycle.d.ts +1 -1
  175. package/dist/live-connectors-runner.d.ts +1 -1
  176. package/dist/local-llm.d.ts +1 -1
  177. package/dist/local-model-endpoint.d.ts +9 -4
  178. package/dist/local-model-endpoint.js +1 -1
  179. package/dist/maintenance/memory-governance.d.ts +1 -1
  180. package/dist/maintenance/memory-governance.js +9 -8
  181. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +9 -8
  182. package/dist/maintenance/rebuild-memory-projection.js +10 -9
  183. package/dist/mcp-memory-inspector-app.d.ts +5 -5
  184. package/dist/memory-action-policy.d.ts +1 -1
  185. package/dist/memory-cache.d.ts +1 -1
  186. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  187. package/dist/memory-projection-store.d.ts +1 -1
  188. package/dist/memory-provenance.d.ts +1 -1
  189. package/dist/memory-worth-outcomes.d.ts +1 -1
  190. package/dist/models-json.d.ts +1 -1
  191. package/dist/namespaces/migrate.d.ts +2 -2
  192. package/dist/namespaces/migrate.js +19 -18
  193. package/dist/namespaces/principal.d.ts +1 -1
  194. package/dist/namespaces/search.d.ts +1 -1
  195. package/dist/namespaces/search.js +9 -9
  196. package/dist/namespaces/storage.d.ts +2 -2
  197. package/dist/namespaces/storage.js +9 -8
  198. package/dist/native-knowledge.d.ts +1 -1
  199. package/dist/operator-toolkit.d.ts +1 -1
  200. package/dist/operator-toolkit.js +28 -27
  201. package/dist/orchestration/maintenance.d.ts +2 -2
  202. package/dist/orchestration/maintenance.js +11 -10
  203. package/dist/{orchestrator-C_OMSIMZ.d.ts → orchestrator-DqPAz8ot.d.ts} +3 -3
  204. package/dist/orchestrator.d.ts +4 -4
  205. package/dist/orchestrator.js +51 -49
  206. package/dist/patterns-cli.d.ts +1 -1
  207. package/dist/policy-runtime.d.ts +1 -1
  208. package/dist/provenance.d.ts +1 -1
  209. package/dist/provenance.js +1 -1
  210. package/dist/qmd-recall-cache.d.ts +1 -1
  211. package/dist/qmd.d.ts +1 -1
  212. package/dist/recall-disclosure-escalation.d.ts +1 -1
  213. package/dist/recall-explain-renderer.d.ts +1 -1
  214. package/dist/recall-explain-renderer.js +3 -3
  215. package/dist/recall-planner-llm.d.ts +1 -1
  216. package/dist/recall-state.d.ts +1 -1
  217. package/dist/recall-tag-filter.d.ts +1 -1
  218. package/dist/recall-xray-cli.d.ts +1 -1
  219. package/dist/recall-xray-cli.js +4 -4
  220. package/dist/recall-xray-renderer.d.ts +1 -1
  221. package/dist/recall-xray-renderer.js +3 -3
  222. package/dist/recall-xray.d.ts +1 -1
  223. package/dist/recall-xray.js +2 -2
  224. package/dist/resolve-auth-token.d.ts +1 -1
  225. package/dist/resume-bundles.js +4 -4
  226. package/dist/retrieval-agents.d.ts +1 -1
  227. package/dist/retrieval-tiers.d.ts +1 -1
  228. package/dist/routing/engine.d.ts +1 -1
  229. package/dist/routing/store.d.ts +1 -1
  230. package/dist/schemas.d.ts +22 -22
  231. package/dist/search/embed-helper.d.ts +1 -1
  232. package/dist/search/factory.d.ts +1 -1
  233. package/dist/search/factory.js +8 -8
  234. package/dist/search/index.d.ts +1 -1
  235. package/dist/search/index.js +14 -14
  236. package/dist/search/lancedb-backend.d.ts +1 -1
  237. package/dist/search/lancedb-backend.js +2 -2
  238. package/dist/search/meilisearch-backend.d.ts +1 -1
  239. package/dist/search/meilisearch-backend.js +2 -2
  240. package/dist/search/noop-backend.d.ts +1 -1
  241. package/dist/search/orama-backend.d.ts +1 -1
  242. package/dist/search/orama-backend.js +2 -2
  243. package/dist/search/port.d.ts +1 -1
  244. package/dist/search/remote-backend.d.ts +1 -1
  245. package/dist/{semantic-consolidation-B8LiYtvx.d.ts → semantic-consolidation-Cl15ksZy.d.ts} +1 -1
  246. package/dist/semantic-consolidation.d.ts +2 -2
  247. package/dist/semantic-consolidation.js +10 -9
  248. package/dist/semantic-rule-promotion.js +9 -8
  249. package/dist/semantic-rule-verifier.d.ts +1 -1
  250. package/dist/semantic-rule-verifier.js +9 -8
  251. package/dist/session-observer-bands.d.ts +1 -1
  252. package/dist/session-observer-state.d.ts +1 -1
  253. package/dist/shared-context/manager.d.ts +1 -1
  254. package/dist/signal.d.ts +1 -1
  255. package/dist/storage.d.ts +3 -3
  256. package/dist/storage.js +9 -8
  257. package/dist/summarizer.d.ts +1 -1
  258. package/dist/summary-snapshot.d.ts +1 -1
  259. package/dist/temporal-supersession.d.ts +1 -1
  260. package/dist/temporal-validity.d.ts +1 -1
  261. package/dist/threading.d.ts +1 -1
  262. package/dist/tier-migration.d.ts +1 -1
  263. package/dist/tier-routing.d.ts +1 -1
  264. package/dist/topics.d.ts +1 -1
  265. package/dist/transcript.d.ts +1 -1
  266. package/dist/transfer/types.d.ts +12 -12
  267. package/dist/trust-score-stage.d.ts +1 -1
  268. package/dist/trust-score.d.ts +1 -1
  269. package/dist/{types-DmTaqob2.d.ts → types-ChQb9D6s.d.ts} +8 -0
  270. package/dist/types.d.ts +1 -1
  271. package/dist/types.js +1 -1
  272. package/dist/utility-runtime.d.ts +1 -1
  273. package/dist/verified-recall.js +9 -8
  274. package/package.json +26 -2
  275. package/src/access-http.ts +68 -1
  276. package/src/admin/admin-surfaces.test.ts +289 -0
  277. package/src/admin/admin-surfaces.ts +138 -7
  278. package/src/bitemporal-dedup-backfill.test.ts +294 -0
  279. package/src/chat/chat-engine.test.ts +66 -1
  280. package/src/chat/chat-http.test.ts +119 -1
  281. package/src/chat/chat-http.ts +252 -97
  282. package/src/chat/chat-session.ts +143 -7
  283. package/src/compat/codex-cli-fallback.ts +23 -0
  284. package/src/compat/codex-subpath-aliases.test.ts +93 -0
  285. package/src/compat/codex-thread-key.ts +23 -0
  286. package/src/config.test.ts +50 -0
  287. package/src/config.ts +21 -4
  288. package/src/content-hash.ts +24 -0
  289. package/src/correction/correction-access-wiring.ts +104 -19
  290. package/src/correction/correction-contract.ts +68 -10
  291. package/src/correction/correction-executor.test.ts +112 -2
  292. package/src/correction/correction-executor.ts +65 -5
  293. package/src/correction/correction-planner.test.ts +47 -0
  294. package/src/correction/correction-planner.ts +40 -1
  295. package/src/correction/correction-storage-integration.test.ts +183 -0
  296. package/src/extraction-faithfulness.test.ts +240 -0
  297. package/src/extraction-faithfulness.ts +58 -6
  298. package/src/extraction-redaction-rules.test.ts +213 -0
  299. package/src/extraction-redaction-rules.ts +208 -0
  300. package/src/faithfulness-config.ts +16 -1
  301. package/src/local-model-endpoint.ts +24 -3
  302. package/src/orchestrator.ts +252 -9
  303. package/src/provenance-extraction.test.ts +266 -0
  304. package/src/provenance.ts +66 -16
  305. package/src/storage.ts +6 -10
  306. package/src/types.ts +8 -0
  307. package/dist/chunk-4R3CGCI5.js.map +0 -1
  308. package/dist/chunk-4SF232E6.js.map +0 -1
  309. package/dist/chunk-6OIDDKAA.js.map +0 -1
  310. package/dist/chunk-7AG54AF3.js.map +0 -1
  311. package/dist/chunk-C4X4QGIO.js.map +0 -1
  312. package/dist/chunk-C6BWLYW5.js.map +0 -1
  313. package/dist/chunk-KLFYKC3K.js.map +0 -1
  314. package/dist/chunk-MDBZTH7N.js.map +0 -1
  315. package/dist/chunk-XGTBNVIU.js.map +0 -1
  316. package/dist/chunk-Z3FGC3QE.js.map +0 -1
  317. /package/dist/{chunk-HCVWF657.js.map → chunk-2KAYTPPT.js.map} +0 -0
  318. /package/dist/{chunk-ZK2L72VN.js.map → chunk-37436GML.js.map} +0 -0
  319. /package/dist/{chunk-SC2YAWRF.js.map → chunk-3TSTGI6F.js.map} +0 -0
  320. /package/dist/{chunk-FACTVMXQ.js.map → chunk-4H4F3BFL.js.map} +0 -0
  321. /package/dist/{chunk-WAFDHKES.js.map → chunk-4NOI577P.js.map} +0 -0
  322. /package/dist/{chunk-YQMPUYHE.js.map → chunk-4NR2PDZ5.js.map} +0 -0
  323. /package/dist/{chunk-TAPSNIMA.js.map → chunk-5CEJH5ZN.js.map} +0 -0
  324. /package/dist/{chunk-PKMQK2WY.js.map → chunk-773DPI6T.js.map} +0 -0
  325. /package/dist/{chunk-FXCIOLRB.js.map → chunk-ASKXIAHD.js.map} +0 -0
  326. /package/dist/{chunk-5OPYYPJN.js.map → chunk-CBWI22JN.js.map} +0 -0
  327. /package/dist/{chunk-ZOHMPFOB.js.map → chunk-CKFLLAIS.js.map} +0 -0
  328. /package/dist/{chunk-GZQXOQQ2.js.map → chunk-CSRXQ6NB.js.map} +0 -0
  329. /package/dist/{chunk-QR67FU5G.js.map → chunk-CTOQEZSN.js.map} +0 -0
  330. /package/dist/{chunk-ZUDM75KG.js.map → chunk-DCWIQFNA.js.map} +0 -0
  331. /package/dist/{chunk-6CWGS2LY.js.map → chunk-DKJCRGWM.js.map} +0 -0
  332. /package/dist/{chunk-JJJCVCCQ.js.map → chunk-DMQWV6SQ.js.map} +0 -0
  333. /package/dist/{chunk-F3XZN24Z.js.map → chunk-DRUDC2UX.js.map} +0 -0
  334. /package/dist/{chunk-4WZ3J7M6.js.map → chunk-EY6MDMHK.js.map} +0 -0
  335. /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
  336. /package/dist/{chunk-6SQQOJ4U.js.map → chunk-GKRLPHPU.js.map} +0 -0
  337. /package/dist/{chunk-D7EBTQ24.js.map → chunk-HOHVIVDR.js.map} +0 -0
  338. /package/dist/{chunk-RKNJBZ55.js.map → chunk-JBPKEARU.js.map} +0 -0
  339. /package/dist/{chunk-UBHVAGO7.js.map → chunk-JYLFZWUX.js.map} +0 -0
  340. /package/dist/{chunk-LD53WPMU.js.map → chunk-LQ6JI4VH.js.map} +0 -0
  341. /package/dist/{chunk-WM7DNKBD.js.map → chunk-M4V2JVSZ.js.map} +0 -0
  342. /package/dist/{chunk-I2HL5M6T.js.map → chunk-MJIHJ7OG.js.map} +0 -0
  343. /package/dist/{chunk-J334N6QV.js.map → chunk-MLKWJOAJ.js.map} +0 -0
  344. /package/dist/{chunk-DF3I6JGP.js.map → chunk-NSA5ZRUL.js.map} +0 -0
  345. /package/dist/{chunk-VI5SGGXB.js.map → chunk-OEEW4KJA.js.map} +0 -0
  346. /package/dist/{chunk-NT36GU6X.js.map → chunk-R7IYU5DZ.js.map} +0 -0
  347. /package/dist/{chunk-TT5U3UDO.js.map → chunk-SNKPUJHR.js.map} +0 -0
  348. /package/dist/{chunk-T55EGZSJ.js.map → chunk-UVPL565C.js.map} +0 -0
  349. /package/dist/{chunk-YQGHW5ML.js.map → chunk-WRFKZEO6.js.map} +0 -0
  350. /package/dist/{chunk-H7HUQIY7.js.map → chunk-Y4YD5TYW.js.map} +0 -0
  351. /package/dist/{chunk-YUYPWJD3.js.map → chunk-YOLZS7NY.js.map} +0 -0
  352. /package/dist/{forget-OEE5OELK.js.map → forget-WYLR4ZFV.js.map} +0 -0
@@ -167,3 +167,297 @@ test("#1671: re-extraction with resolved invalidAt backfills an old promoted cop
167
167
  await cleanup();
168
168
  }
169
169
  });
170
+
171
+ // ─── #1707 widenings (promoted-cascade, valid_at, non-fact guard) ────────
172
+
173
+ // Thread 2 — valid_at propagation. A re-extracted duplicate whose event time
174
+ // yields only a start bound ("since 2024", "yesterday") must get the corrected
175
+ // per-fact validFrom onto the existing copy so as-of recall uses the corrected
176
+ // start. The orchestrator helper's rule: patch valid_at when the incoming
177
+ // validFrom is EXTRACTED and the copy's start bound is batch-anchored
178
+ // (assumed/legacy); never clobber a copy that already carries an extracted
179
+ // per-fact anchor. This test pins that rule against the storage patch the
180
+ // helper issues.
181
+ test("#1707 thread 2: corrected extracted validFrom overwrites a batch-anchored valid_at", async () => {
182
+ const { storage, cleanup } = await makeStorage("bitemporal-1707-validFrom-");
183
+ try {
184
+ // Old promoted copy: batch-anchored valid_at, no per-fact event-time.
185
+ const content = "We have used Stripe for payments since 2024.";
186
+ const batchAnchor = "2026-06-01T00:00:00.000Z";
187
+ const id = await storage.writeMemory("fact", content, {
188
+ confidence: 0.9,
189
+ validAt: batchAnchor,
190
+ // No eventTimeSource → batch-anchored (legacy pre-#1670 copy).
191
+ });
192
+ const before = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id);
193
+ assert.ok(before);
194
+ assert.equal(before!.frontmatter.valid_at, batchAnchor);
195
+ assert.equal(before!.frontmatter.eventTimeSource, undefined);
196
+
197
+ // Simulate the helper's rule: incoming validFrom is extracted → patch
198
+ // because the copy is NOT extracted-anchored (fm.eventTimeSource !== "extracted").
199
+ const correctedValidFrom = "2024-01-01T00:00:00.000Z";
200
+ const incomingExtracted = true;
201
+ const copyExtracted = before!.frontmatter.eventTimeSource === "extracted";
202
+ const shouldPatchValidAt =
203
+ incomingExtracted && (copyExtracted === false || !before!.frontmatter.valid_at);
204
+ assert.equal(shouldPatchValidAt, true, "must correct the batch-anchored valid_at");
205
+
206
+ const ok = await storage.writeMemoryFrontmatter(before!, {
207
+ valid_at: correctedValidFrom,
208
+ eventTimeSource: "extracted",
209
+ observedAt: "2026-06-20T00:00:00.000Z",
210
+ });
211
+ assert.equal(ok, true);
212
+
213
+ const after = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id);
214
+ assert.ok(after);
215
+ assert.equal(after!.frontmatter.valid_at, correctedValidFrom);
216
+ assert.equal(after!.frontmatter.eventTimeSource, "extracted");
217
+ } finally {
218
+ await cleanup();
219
+ }
220
+ });
221
+
222
+ test("#1707 thread 2: identical extracted validFrom is a no-op (equality short-circuit)", async () => {
223
+ const { storage, cleanup } = await makeStorage("bitemporal-1707-validFrom-equal-");
224
+ try {
225
+ // Copy already carries an extracted per-fact anchor.
226
+ const content = "The migration to MySQL completed in March 2025.";
227
+ const existingValidAt: string = "2025-03-01T00:00:00.000Z";
228
+ const id = await storage.writeMemory("fact", content, {
229
+ confidence: 0.9,
230
+ validAt: existingValidAt,
231
+ observedAt: "2025-06-01T00:00:00.000Z",
232
+ eventTimeSource: "extracted",
233
+ });
234
+ const before = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id);
235
+ assert.ok(before);
236
+
237
+ // Re-extraction resolves the SAME start bound (deterministic per #1670).
238
+ // The helper's equality short-circuit (fm.valid_at !== bounds.validFrom)
239
+ // skips the redundant write — the only no-clobber that holds without a
240
+ // fragile provenance heuristic (review codex PRRT_Ov7LKC).
241
+ const helperWouldPatch = existingValidAt !== existingValidAt; // equal → false
242
+ assert.equal(helperWouldPatch, false, "identical validFrom must not rewrite");
243
+
244
+ // No patch issued → valid_at unchanged.
245
+ const after = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id);
246
+ assert.ok(after);
247
+ assert.equal(after!.frontmatter.valid_at, existingValidAt);
248
+ } finally {
249
+ await cleanup();
250
+ }
251
+ });
252
+
253
+ test("#1707 thread 2: differing extracted validFrom overwrites (authoritative re-evaluation)", async () => {
254
+ const { storage, cleanup } = await makeStorage("bitemporal-1707-validFrom-differ-");
255
+ try {
256
+ // Copy carries an older (batch-anchored) valid_at.
257
+ const content = "We have used Stripe for payments since 2024.";
258
+ const staleValidAt: string = "2026-06-01T00:00:00.000Z"; // batch anchor
259
+ const id = await storage.writeMemory("fact", content, {
260
+ confidence: 0.9,
261
+ validAt: staleValidAt,
262
+ });
263
+ const before = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id);
264
+ assert.ok(before);
265
+
266
+ // Re-extraction resolves a DIFFERING extracted start bound. Exact-content
267
+ // dedup + #1670 determinism means stable content re-resolves to the same
268
+ // value (equality no-op above); a divergence means the copy's start is
269
+ // stale (batch/assumed) and the extracted validFrom is authoritative.
270
+ const correctedValidFrom: string = "2024-01-01T00:00:00.000Z";
271
+ const helperWouldPatch = staleValidAt !== correctedValidFrom; // differ → true
272
+ assert.equal(helperWouldPatch, true, "differing extracted validFrom must overwrite");
273
+
274
+ const ok = await storage.writeMemoryFrontmatter(before!, {
275
+ valid_at: correctedValidFrom,
276
+ eventTimeSource: "extracted",
277
+ });
278
+ assert.equal(ok, true);
279
+
280
+ const after = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id);
281
+ assert.ok(after);
282
+ assert.equal(after!.frontmatter.valid_at, correctedValidFrom);
283
+ assert.equal(after!.frontmatter.eventTimeSource, "extracted");
284
+ } finally {
285
+ await cleanup();
286
+ }
287
+ });
288
+
289
+ // Review (codex PRRT_Ov68oA + PRRT_Ov7LKC): an END-only extraction ("through
290
+ // 2026") sets eventTimeSource = "extracted" (end resolved) but leaves valid_at
291
+ // batch/assumed-anchored. A later START-bound extraction must still correct
292
+ // valid_at. The helper now uses an equality short-circuit (no fragile
293
+ // provenance heuristic): the differing extracted validFrom overwrites the
294
+ // stale start regardless of the copy's eventTimeSource.
295
+ test("#1707 thread 2: end-only extracted provenance does NOT block a later start-bound correction", async () => {
296
+ const { storage, cleanup } = await makeStorage("bitemporal-1707-endonly-");
297
+ try {
298
+ // Copy state after an end-only backfill: eventTimeSource = "extracted"
299
+ // (the end "through 2026" resolved), but valid_at is still the ingestion
300
+ // anchor (assumed start).
301
+ const observedAt = "2026-06-01T00:00:00.000Z";
302
+ const content = "The service was maintained through 2026.";
303
+ const id = await storage.writeMemory("fact", content, {
304
+ confidence: 0.9,
305
+ validAt: observedAt, // valid_at === observedAt (assumed/batch start)
306
+ observedAt,
307
+ eventTimeSource: "extracted", // set by the end-only extraction
308
+ invalidAt: "2026-12-31T00:00:00.000Z", // the extracted end bound
309
+ });
310
+ const before = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id);
311
+ assert.ok(before);
312
+ assert.equal(before!.frontmatter.eventTimeSource, "extracted");
313
+ assert.equal(before!.frontmatter.valid_at, before!.frontmatter.observedAt);
314
+
315
+ // The helper's valid_at gate (mirrors orchestrator.ts):
316
+ // bounds.validFrom && eventTimeSource === "extracted" && valid_at !== validFrom
317
+ // The stale valid_at differs from the incoming real start → overwrite.
318
+
319
+ // A later re-extraction with a real extracted start must correct valid_at.
320
+ const realStart = "2024-03-01T00:00:00.000Z";
321
+ const ok = await storage.writeMemoryFrontmatter(before!, {
322
+ valid_at: realStart,
323
+ });
324
+ assert.equal(ok, true);
325
+
326
+ const after = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id);
327
+ assert.ok(after);
328
+ assert.equal(after!.frontmatter.valid_at, realStart);
329
+ } finally {
330
+ await cleanup();
331
+ }
332
+ });
333
+
334
+ // Gate guard (review cursor PRRT_OvHk / codex PRRT_OvHxVH): resolveFactEventTime
335
+ // sets a validFrom for EVERY fact (assumed = ingestion anchor). The helper's
336
+ // I/O gate must NOT scan on an assumed validFrom — only an EXTRACTED start bound
337
+ // changes recall. This pins the gate decision so ordinary duplicate facts
338
+ // without an extracted event time do not pay a readAllMemories scan on every
339
+ // dedup hit.
340
+ test("#1707 thread 2: assumed (batch-anchored) validFrom does NOT trigger a backfill scan", () => {
341
+ // Mirror the helper's I/O gate (orchestrator.ts backfillTemporalBoundsOnDedupHit):
342
+ // const hasExtractedStart = bounds.validFrom !== undefined && bounds.eventTimeSource === "extracted";
343
+ // if (!bounds.invalidAt && !hasExtractedStart) return;
344
+ const gateWouldScan = (bounds: {
345
+ invalidAt?: string;
346
+ validFrom?: string;
347
+ eventTimeSource?: "extracted" | "assumed";
348
+ }) =>
349
+ Boolean(bounds.invalidAt) ||
350
+ (bounds.validFrom !== undefined && bounds.eventTimeSource === "extracted");
351
+
352
+ // Assumed validFrom alone (the common no-event-time case) → no scan.
353
+ assert.equal(
354
+ gateWouldScan({ validFrom: "2026-06-01T00:00:00.000Z", eventTimeSource: "assumed" }),
355
+ false,
356
+ "assumed validFrom must not trigger a scan",
357
+ );
358
+ // Extracted validFrom → scan (corrected start bound is recall-relevant).
359
+ assert.equal(
360
+ gateWouldScan({ validFrom: "2024-01-01T00:00:00.000Z", eventTimeSource: "extracted" }),
361
+ true,
362
+ );
363
+ // End bound alone → scan (expires the fact).
364
+ assert.equal(gateWouldScan({ invalidAt: "2025-06-01T00:00:00.000Z" }), true);
365
+ // No bounds at all → no scan.
366
+ assert.equal(gateWouldScan({}), false);
367
+ });
368
+ // Thread 1 — promoted-cascade backfill. When the source-namespace dedup
369
+ // short-circuit fires, the helper must also patch promotion-target copies
370
+ // (profile + shared namespaces) so cross-namespace recall does not surface an
371
+ // expired fact. This proves the mechanism the orchestrator's promotion-cascade
372
+ // closure relies on: backfilling two separate storages patches each
373
+ // independently.
374
+ test("#1707 thread 1: backfill patches promoted copies across multiple storages (cascade)", async () => {
375
+ const { storage: sourceStorage, cleanup: cleanupSource } = await makeStorage(
376
+ "bitemporal-1707-cascade-source-",
377
+ );
378
+ const { storage: promotedStorage, cleanup: cleanupPromoted } = await makeStorage(
379
+ "bitemporal-1707-cascade-promoted-",
380
+ );
381
+ try {
382
+ const content = "The API rate limit is 100 req/min until June 2025.";
383
+ // Both namespaces hold the same fact, written WITHOUT bounds (stale copies).
384
+ const sourceId = await sourceStorage.writeMemory("fact", content, { confidence: 0.9 });
385
+ const promotedId = await promotedStorage.writeMemory("fact", content, { confidence: 0.9 });
386
+
387
+ // Simulate re-extraction that now carries a resolved invalidAt. The
388
+ // promotion-cascade closure calls the same backfill against each target.
389
+ const bounds = {
390
+ invalid_at: "2025-06-01T00:00:00.000Z",
391
+ observedAt: "2025-06-20T00:00:00.000Z",
392
+ eventTimeSource: "extracted" as const,
393
+ };
394
+ for (const storage of [sourceStorage, promotedStorage]) {
395
+ const all = await storage.readAllMemories();
396
+ const existing = all.find(
397
+ (m) =>
398
+ m.frontmatter.category === "fact" &&
399
+ (m.frontmatter.status ?? "active") === "active" &&
400
+ !m.frontmatter.invalid_at &&
401
+ ContentHashIndex.normalizeContent(m.content ?? "") ===
402
+ ContentHashIndex.normalizeContent(content),
403
+ );
404
+ assert.ok(existing, "must find the existing copy in each storage");
405
+ const ok = await storage.writeMemoryFrontmatter(existing!, bounds);
406
+ assert.equal(ok, true);
407
+ }
408
+
409
+ // Both copies now carry the resolved end bound — cross-namespace recall
410
+ // honours the expiry instead of surfacing the stale promoted copy.
411
+ const sourceAfter = (await sourceStorage.readAllMemories()).find(
412
+ (m) => m.frontmatter.id === sourceId,
413
+ );
414
+ const promotedAfter = (await promotedStorage.readAllMemories()).find(
415
+ (m) => m.frontmatter.id === promotedId,
416
+ );
417
+ assert.ok(sourceAfter && promotedAfter);
418
+ assert.equal(sourceAfter!.frontmatter.invalid_at, "2025-06-01T00:00:00.000Z");
419
+ assert.equal(promotedAfter!.frontmatter.invalid_at, "2025-06-01T00:00:00.000Z");
420
+ } finally {
421
+ await cleanupSource();
422
+ await cleanupPromoted();
423
+ }
424
+ });
425
+
426
+ // Thread 3 — non-fact guard. The source dedup backfill call site is gated on
427
+ // writeCategory === "fact", so a non-fact duplicate (preference/decision/
428
+ // procedure) never reaches the fact-only backfill scan. This proves the
429
+ // helper's category guard that makes that call-site gate correct: a non-fact
430
+ // copy is never matched/patched even if its content hash collides.
431
+ test("#1707 thread 3: backfill never patches a non-fact copy (category guard)", async () => {
432
+ const { storage, cleanup } = await makeStorage("bitemporal-1707-nonfact-");
433
+ try {
434
+ const content = "The user prefers dark mode.";
435
+ // A preference (non-fact) whose content would collide with an incoming
436
+ // duplicate candidate.
437
+ const id = await storage.writeMemory("preference", content, { confidence: 0.9 });
438
+ const before = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id);
439
+ assert.ok(before);
440
+ assert.equal(before!.frontmatter.category, "preference");
441
+ assert.equal(before!.frontmatter.invalid_at, undefined);
442
+
443
+ // The helper's lookup filters category === "fact" — a preference copy is
444
+ // never eligible, so no patch is issued even when bounds are present.
445
+ const all = await storage.readAllMemories();
446
+ const existing = all.find(
447
+ (m) =>
448
+ m.frontmatter.category === "fact" && // ← the guard under test
449
+ (m.frontmatter.status ?? "active") === "active" &&
450
+ ContentHashIndex.normalizeContent(m.content ?? "") ===
451
+ ContentHashIndex.normalizeContent(content),
452
+ );
453
+ assert.equal(existing, undefined, "non-fact copy must not be matched by the fact-only lookup");
454
+
455
+ // No patch → the preference copy is untouched.
456
+ const after = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id);
457
+ assert.ok(after);
458
+ assert.equal(after!.frontmatter.invalid_at, undefined);
459
+ assert.equal(after!.frontmatter.category, "preference");
460
+ } finally {
461
+ await cleanup();
462
+ }
463
+ });
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import { strict as assert } from "node:assert";
10
- import { mkdir, rm, readFile } from "node:fs/promises";
10
+ import { mkdir, rm, readFile, utimes, unlink } from "node:fs/promises";
11
11
  import { join } from "node:path";
12
12
  import { tmpdir } from "node:os";
13
13
  import { test } from "node:test";
@@ -270,3 +270,68 @@ test("budget exhaustion summary LLM failure does not crash (Thread 16)", async (
270
270
  assert.ok(result.reply.length > 0, "Should return a fallback reply");
271
271
  assert.ok(result.skippedTools !== undefined, "Should report skipped tools");
272
272
  });
273
+
274
+ // ---------------------------------------------------------------------------
275
+ // TTL boundary (issue #1685 item 1 / #1687 Thread 21)
276
+ // ---------------------------------------------------------------------------
277
+
278
+ test("cleanupExpiredChatSessions honors TTL boundary [0, ttlHours) — age >= ttl expires", async () => {
279
+ const dir = await makeTempDir();
280
+ const ttlHours = 2;
281
+ const ttlMs = ttlHours * 3600 * 1000;
282
+
283
+ // Session aged past the TTL (mtime set to ttlHours ago; by the time the
284
+ // sweep stat()s it a few ms have elapsed, so age >= ttl deterministically
285
+ // — the boundary is inclusive: age ∈ [0, ttl) survives, age >= ttl expires).
286
+ const expired = await createChatSession(dir, { principal: "expired" });
287
+ const expiredFile = chatSessionFile(dir, expired.id);
288
+ const atTtlSeconds = (Date.now() - ttlMs) / 1000;
289
+ await utimes(expiredFile, atTtlSeconds, atTtlSeconds);
290
+
291
+ // Session aged well under the TTL → must survive.
292
+ const fresh = await createChatSession(dir, { principal: "fresh" });
293
+ const freshFile = chatSessionFile(dir, fresh.id);
294
+ const underTtlSeconds = (Date.now() - (ttlMs - 6 * 60 * 1000)) / 1000; // ~1.9h old
295
+ await utimes(freshFile, underTtlSeconds, underTtlSeconds);
296
+
297
+ const removed = await cleanupExpiredChatSessions(dir, ttlHours);
298
+ assert.equal(removed, 1, "only the expired session should be removed");
299
+
300
+ // The fresh session file must still be on disk.
301
+ const freshStillExists = await readFile(freshFile, "utf8").then(() => true).catch(() => false);
302
+ assert.ok(freshStillExists, "session aged under the TTL must survive the sweep");
303
+
304
+ // The expired session file must be gone.
305
+ const expiredGone = await readFile(expiredFile, "utf8").then(() => false).catch(() => true);
306
+ assert.ok(expiredGone, "session aged at/over the TTL must be swept");
307
+ });
308
+
309
+
310
+ test("appendTranscriptEntry emits strictly-increasing seqs even within the same millisecond (issue #1687 review)", async () => {
311
+ const dir = await makeTempDir();
312
+ const session = await createChatSession(dir, { principal: "seq" });
313
+ // Append several entries as fast as possible (same-ms collisions would have
314
+ // shared a Date.now()-based seq before the monotonic counter fix).
315
+ const entries: number[] = [];
316
+ for (let i = 0; i < 8; i++) {
317
+ const e = await appendTranscriptEntry(dir, session.id, { role: "user", content: `m${i}` });
318
+ entries.push(e.seq);
319
+ }
320
+ // Every seq must be distinct and strictly increasing.
321
+ for (let i = 1; i < entries.length; i++) {
322
+ assert.ok(entries[i]! > entries[i - 1]!, `seq ${entries[i]} must exceed ${entries[i - 1]}`);
323
+ }
324
+ assert.equal(new Set(entries).size, entries.length, "seqs must be unique");
325
+ });
326
+
327
+ test("appendTranscriptEntry refuses to recreate a swept session (chat_session_expired) — codex P2 #1687", async () => {
328
+ const dir = await makeTempDir();
329
+ const session = await createChatSession(dir, { principal: "alice" });
330
+ // Simulate the TTL sweep unlinking the file mid-turn (resurrection race).
331
+ await unlink(chatSessionFile(dir, session.id));
332
+ // Must throw instead of silently recreating a headerless (public) file.
333
+ await assert.rejects(
334
+ appendTranscriptEntry(dir, session.id, { role: "user", content: "x" }),
335
+ /chat_session_expired/,
336
+ );
337
+ });
@@ -17,7 +17,7 @@ import { test } from "node:test";
17
17
  import type { EngramAccessService } from "../access-service.js";
18
18
  import { parseConfig } from "../config.js";
19
19
  import { handleChatMessage, handleChatEventsSSE } from "./chat-http.js";
20
- import { createChatSession, appendTranscriptEntry, chatSessionFile } from "./chat-session.js";
20
+ import { createChatSession, loadChatSession, appendTranscriptEntry, chatSessionFile } from "./chat-session.js";
21
21
  import { DEFAULT_CHAT_CONFIG } from "./chat-config.js";
22
22
 
23
23
  // Minimal mock res capturing writes. All tracking fields live on one
@@ -195,3 +195,121 @@ test("HTTP chat/events SSE: 403 for a session owned by another principal", async
195
195
  assert.equal(m.status, 403);
196
196
  });
197
197
  });
198
+
199
+
200
+ test("HTTP chat/events SSE: pushes new transcript entries live (issue #1687)", async () => {
201
+ await withTempDir(async (memoryDir) => {
202
+ const session = await createChatSession(memoryDir, { principal: "alice" });
203
+ await appendTranscriptEntry(memoryDir, session.id, { role: "user", content: "first" });
204
+
205
+ const service = makeService({
206
+ memoryDir,
207
+ configRef: parseConfig({ memoryDir, chat: { ...DEFAULT_CHAT_CONFIG, enabled: true } }),
208
+ });
209
+ const m = mockRes();
210
+ const req = mockReq();
211
+ await handleChatEventsSSE(req as never, m as never, session.id, { service, config: service.configRef!.chat, memoryDir }, "alice");
212
+ assert.equal(m.status, 200);
213
+ assert.equal(m.headers["content-type"], "text/event-stream");
214
+ // The stream must advertise a retry directive so reconnects are safe.
215
+ assert.ok(m.body.includes("retry: 5000"), "SSE must emit a retry: directive");
216
+
217
+ const initialFrames = m.body.split("\n\n").filter((f) => f.startsWith("data:"));
218
+
219
+ // Append a NEW entry AFTER the client connected — it must be pushed live
220
+ // to the open SSE connection via the per-session pub/sub.
221
+ await appendTranscriptEntry(memoryDir, session.id, { role: "assistant", content: "pushed-live" });
222
+
223
+ const allFrames = m.body.split("\n\n").filter((f) => f.startsWith("data:"));
224
+ assert.ok(allFrames.length > initialFrames.length, "expected a pushed frame after append");
225
+ assert.ok(m.body.includes("pushed-live"), "the appended entry must reach the live SSE stream");
226
+ // The pushed frame must be well-formed SSE (data: <json>\n\n).
227
+ assert.ok(m.body.includes("data: {"), "frames must be data: <json> framed");
228
+ req.emit("close");
229
+ });
230
+ });
231
+
232
+ test("HTTP chat/message: concurrent identical requests are deduplicated (issue #1687)", async () => {
233
+ await withTempDir(async (memoryDir) => {
234
+ const session = await createChatSession(memoryDir, { principal: "alice" });
235
+ let llmCalls = 0;
236
+ const service = makeService({
237
+ memoryDir,
238
+ configRef: parseConfig({ memoryDir, chat: { ...DEFAULT_CHAT_CONFIG, enabled: true } }),
239
+ fallbackLlmRef: ({
240
+ chatCompletion: async () => {
241
+ llmCalls++;
242
+ // Simulate enough work that the second request lands while the
243
+ // first is still in-flight (the dedup window).
244
+ await new Promise((r) => setTimeout(r, 30));
245
+ return { content: "shared-reply", modelUsed: "test-model" };
246
+ },
247
+ }) as never,
248
+ });
249
+ const opts = { service, config: service.configRef!.chat, memoryDir };
250
+ const m1 = mockRes();
251
+ const m2 = mockRes();
252
+ await Promise.all([
253
+ handleChatMessage(mockReq() as never, m1 as never, { message: "duplicate", chatSessionId: session.id }, opts, "alice"),
254
+ handleChatMessage(mockReq() as never, m2 as never, { message: "duplicate", chatSessionId: session.id }, opts, "alice"),
255
+ ]);
256
+ // Exactly ONE LLM call despite two concurrent identical requests.
257
+ assert.equal(llmCalls, 1, "a concurrent duplicate must not re-process");
258
+ // Both callers receive the (same) reply.
259
+ assert.equal(m1.status, 200);
260
+ assert.equal(m2.status, 200);
261
+ assert.ok(m1.body.includes("shared-reply"));
262
+ assert.ok(m2.body.includes("shared-reply"));
263
+ // The transcript must contain exactly one user "duplicate" line — the
264
+ // duplicate did not double-append.
265
+ const loaded = await loadChatSession(memoryDir, session.id);
266
+ const userDups = loaded!.transcript.filter((e) => e.role === "user" && e.content === "duplicate");
267
+ assert.equal(userDups.length, 1, "duplicate request must not append a second user transcript line");
268
+ });
269
+ });
270
+
271
+ test("HTTP chat/message: distinct messages are NOT deduplicated (issue #1687)", async () => {
272
+ await withTempDir(async (memoryDir) => {
273
+ const session = await createChatSession(memoryDir, { principal: "alice" });
274
+ let llmCalls = 0;
275
+ const service = makeService({
276
+ memoryDir,
277
+ configRef: parseConfig({ memoryDir, chat: { ...DEFAULT_CHAT_CONFIG, enabled: true } }),
278
+ fallbackLlmRef: ({
279
+ chatCompletion: async () => {
280
+ llmCalls++;
281
+ await new Promise((r) => setTimeout(r, 20));
282
+ return { content: "reply", modelUsed: "test-model" };
283
+ },
284
+ }) as never,
285
+ });
286
+ const opts = { service, config: service.configRef!.chat, memoryDir };
287
+ await Promise.all([
288
+ handleChatMessage(mockReq() as never, mockRes() as never, { message: "alpha", chatSessionId: session.id }, opts, "alice"),
289
+ handleChatMessage(mockReq() as never, mockRes() as never, { message: "beta", chatSessionId: session.id }, opts, "alice"),
290
+ ]);
291
+ // Distinct messages both process independently.
292
+ assert.equal(llmCalls, 2, "distinct messages must not be coalesced");
293
+ });
294
+ });
295
+
296
+ test("HTTP chat/events SSE: early disconnect during load bails cleanly — no write to ended response, no leak (issue #1687 review)", async () => {
297
+ await withTempDir(async (memoryDir) => {
298
+ const session = await createChatSession(memoryDir, { principal: "alice" });
299
+ const service = makeService({
300
+ memoryDir,
301
+ configRef: parseConfig({ memoryDir, chat: { ...DEFAULT_CHAT_CONFIG, enabled: true } }),
302
+ });
303
+ const m = mockRes();
304
+ const req = mockReq();
305
+ // Emit close as a microtask so it fires while the handler is awaiting
306
+ // loadChatSession (before writeHead). The handler must bail out without
307
+ // writing to the ended response and without setting up a heartbeat.
308
+ queueMicrotask(() => req.emit("close"));
309
+ // Resolving without throwing is the core contract.
310
+ await handleChatEventsSSE(req as never, m as never, session.id, { service, config: service.configRef!.chat, memoryDir }, "alice");
311
+ // writeHead was skipped (status stays 0) because the handler returned at
312
+ // the `if (closed) return` guard before any SSE framing.
313
+ assert.equal(m.status, 0, "handler must not writeHead after an early close");
314
+ });
315
+ });