@remnic/core 9.3.723 → 9.3.724

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 (270) hide show
  1. package/dist/access-boundary.d.ts +6 -11
  2. package/dist/access-boundary.js +9 -9
  3. package/dist/access-cli.js +29 -23
  4. package/dist/access-cli.js.map +1 -1
  5. package/dist/access-http.d.ts +5 -5
  6. package/dist/access-http.js +19 -14
  7. package/dist/access-mcp.d.ts +5 -5
  8. package/dist/access-mcp.js +18 -13
  9. package/dist/access-operations-batch.js +16 -10
  10. package/dist/access-operations.d.ts +5 -5
  11. package/dist/access-operations.js +17 -11
  12. package/dist/{access-service-Dj3I-VBS.d.ts → access-service-B2PlPtxh.d.ts} +3 -3
  13. package/dist/access-service.d.ts +5 -5
  14. package/dist/access-service.js +8 -8
  15. package/dist/access-surface-catalog.d.ts +5 -5
  16. package/dist/access-surface-catalog.js +82 -82
  17. package/dist/access-surface-catalog.js.map +1 -1
  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/behavior-learner.d.ts +1 -1
  22. package/dist/behavior-signals.d.ts +1 -1
  23. package/dist/bootstrap.d.ts +4 -4
  24. package/dist/briefing.d.ts +1 -1
  25. package/dist/briefing.js +3 -3
  26. package/dist/buffer-surprise-report.d.ts +1 -1
  27. package/dist/buffer.d.ts +1 -1
  28. package/dist/calibration.d.ts +1 -1
  29. package/dist/capabilities.d.ts +1 -1
  30. package/dist/{catalog-F-OEVbRQ.d.ts → catalog-CzUb-GyD.d.ts} +1 -1
  31. package/dist/causal-behavior.d.ts +1 -1
  32. package/dist/causal-consolidation.d.ts +1 -1
  33. package/dist/causal-consolidation.js +4 -4
  34. package/dist/causal-trajectory-graph.d.ts +1 -1
  35. package/dist/{chunk-EVPNWN7Z.js → chunk-4R3CGCI5.js} +2 -2
  36. package/dist/{chunk-UO5ZHIIH.js → chunk-4WZ3J7M6.js} +2 -2
  37. package/dist/{chunk-PD42EYQN.js → chunk-5OPYYPJN.js} +2 -2
  38. package/dist/{chunk-EEJMD2L2.js → chunk-6CWGS2LY.js} +2 -2
  39. package/dist/{chunk-7CTNUVHZ.js → chunk-6SQQOJ4U.js} +2 -2
  40. package/dist/{chunk-ILFSEIRT.js → chunk-7AG54AF3.js} +1 -1
  41. package/dist/{chunk-ILFSEIRT.js.map → chunk-7AG54AF3.js.map} +1 -1
  42. package/dist/{chunk-UIXXE6NI.js → chunk-C6BWLYW5.js} +24 -15
  43. package/dist/chunk-C6BWLYW5.js.map +1 -0
  44. package/dist/{chunk-A5XNBK2D.js → chunk-DF3I6JGP.js} +6 -6
  45. package/dist/{chunk-GJSL4WG2.js → chunk-F3XZN24Z.js} +2 -2
  46. package/dist/{chunk-J3JPB7ZI.js → chunk-FACTVMXQ.js} +2 -2
  47. package/dist/{chunk-B7WISIAJ.js → chunk-FN6QP7JQ.js} +154 -1067
  48. package/dist/chunk-FN6QP7JQ.js.map +1 -0
  49. package/dist/{chunk-EBDA5TYA.js → chunk-FXCIOLRB.js} +2 -2
  50. package/dist/{chunk-TXQCG6SE.js → chunk-H7HUQIY7.js} +3 -3
  51. package/dist/{chunk-QD6QP5P6.js → chunk-I2HL5M6T.js} +17 -17
  52. package/dist/{chunk-M7R5R6OJ.js → chunk-IAIPNEHD.js} +20 -2
  53. package/dist/{chunk-M7R5R6OJ.js.map → chunk-IAIPNEHD.js.map} +1 -1
  54. package/dist/{chunk-Z4RU5VPM.js → chunk-J334N6QV.js} +2 -2
  55. package/dist/{chunk-NXRUUDUF.js → chunk-JJJCVCCQ.js} +2 -2
  56. package/dist/{chunk-GDUOCHCD.js → chunk-KGZF2VO3.js} +10 -10
  57. package/dist/{chunk-LA3HZKES.js → chunk-KLFYKC3K.js} +448 -49
  58. package/dist/chunk-KLFYKC3K.js.map +1 -0
  59. package/dist/{chunk-LLUGAZ3B.js → chunk-NT36GU6X.js} +30 -4
  60. package/dist/chunk-NT36GU6X.js.map +1 -0
  61. package/dist/{chunk-OHTOPFRF.js → chunk-SC2YAWRF.js} +2 -2
  62. package/dist/{chunk-YEE5KJQQ.js → chunk-T55EGZSJ.js} +2 -2
  63. package/dist/{chunk-N77ELJ2D.js → chunk-TT5U3UDO.js} +4 -4
  64. package/dist/{chunk-6FAMMBQ3.js → chunk-UBHVAGO7.js} +2 -2
  65. package/dist/chunk-VI5SGGXB.js +420 -0
  66. package/dist/chunk-VI5SGGXB.js.map +1 -0
  67. package/dist/{chunk-EDOQM4E4.js → chunk-WAFDHKES.js} +2 -2
  68. package/dist/{chunk-EXUCUPV2.js → chunk-WM7DNKBD.js} +2 -2
  69. package/dist/{dreams-ledger-3WSCI5V4.js → chunk-YF6FMLBT.js} +5 -6
  70. package/dist/{dreams-ledger-3WSCI5V4.js.map → chunk-YF6FMLBT.js.map} +1 -1
  71. package/dist/{chunk-WQYSDPMA.js → chunk-YQMPUYHE.js} +2 -2
  72. package/dist/{chunk-5O42UKXI.js → chunk-YUYPWJD3.js} +2 -2
  73. package/dist/{chunk-VCLGUIFM.js → chunk-Z3FGC3QE.js} +2 -2
  74. package/dist/{chunk-A7TJZLPK.js → chunk-ZK2L72VN.js} +2 -2
  75. package/dist/{chunk-BXEOHV7K.js → chunk-ZOHMPFOB.js} +3 -3
  76. package/dist/{graph-edge-decay-QPRJQ7DL.js → chunk-ZQNCHPTZ.js} +4 -5
  77. package/dist/{graph-edge-decay-QPRJQ7DL.js.map → chunk-ZQNCHPTZ.js.map} +1 -1
  78. package/dist/{cli-P_GaYcCY.d.ts → cli-IOlDHSBT.d.ts} +3 -3
  79. package/dist/cli.d.ts +6 -6
  80. package/dist/cli.js +31 -26
  81. package/dist/compounding/engine.d.ts +1 -1
  82. package/dist/compounding/engine.js +3 -3
  83. package/dist/compounding/preference-consolidator.d.ts +1 -1
  84. package/dist/compression-optimizer.d.ts +1 -1
  85. package/dist/config.d.ts +1 -1
  86. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  87. package/dist/connectors/codex-materialize-runner.js +3 -3
  88. package/dist/connectors/codex-materialize.d.ts +1 -1
  89. package/dist/connectors/index.d.ts +1 -1
  90. package/dist/connectors/index.js +3 -3
  91. package/dist/consolidation-provenance-check.d.ts +1 -1
  92. package/dist/consolidation-undo.d.ts +1 -1
  93. package/dist/contradiction/index.d.ts +1 -1
  94. package/dist/conversation-index/backend.d.ts +1 -1
  95. package/dist/conversation-index/chunker.d.ts +1 -1
  96. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  97. package/dist/conversation-index/indexer.d.ts +1 -1
  98. package/dist/conversation-index/search.d.ts +1 -1
  99. package/dist/day-summary.d.ts +1 -1
  100. package/dist/delinearize.d.ts +1 -1
  101. package/dist/direct-answer-wiring.d.ts +1 -1
  102. package/dist/direct-answer.d.ts +1 -1
  103. package/dist/dreams-ledger-6UODQOX6.js +22 -0
  104. package/dist/dreams-ledger-6UODQOX6.js.map +1 -0
  105. package/dist/embedding-fallback.d.ts +1 -1
  106. package/dist/enrichment/index.d.ts +1 -1
  107. package/dist/entity-retrieval.d.ts +1 -1
  108. package/dist/entity-retrieval.js +3 -3
  109. package/dist/entity-schema.d.ts +1 -1
  110. package/dist/event-time.d.ts +33 -1
  111. package/dist/event-time.js +3 -1
  112. package/dist/explicit-capture.d.ts +4 -4
  113. package/dist/extraction-faithfulness.d.ts +1 -1
  114. package/dist/extraction-judge-telemetry.d.ts +1 -1
  115. package/dist/extraction-judge-training.d.ts +1 -1
  116. package/dist/extraction-judge.d.ts +1 -1
  117. package/dist/extraction.d.ts +1 -1
  118. package/dist/fallback-llm.d.ts +1 -1
  119. package/dist/graph-dashboard-diff.d.ts +1 -1
  120. package/dist/graph-dashboard-key.d.ts +1 -1
  121. package/dist/graph-dashboard-parser.d.ts +1 -1
  122. package/dist/graph-edge-decay-RXUCJIN4.js +22 -0
  123. package/dist/graph-edge-decay-RXUCJIN4.js.map +1 -0
  124. package/dist/graph-edge-reinforcement.d.ts +1 -1
  125. package/dist/graph-snapshot.d.ts +1 -1
  126. package/dist/graph.d.ts +1 -1
  127. package/dist/identity-continuity.d.ts +1 -1
  128. package/dist/importance.d.ts +1 -1
  129. package/dist/index.d.ts +9 -9
  130. package/dist/index.js +58 -56
  131. package/dist/index.js.map +1 -1
  132. package/dist/intent.d.ts +1 -1
  133. package/dist/lcm/engine.d.ts +1 -1
  134. package/dist/lcm/index.d.ts +1 -1
  135. package/dist/lcm/tools.d.ts +1 -1
  136. package/dist/lifecycle.d.ts +1 -1
  137. package/dist/live-connectors-runner.d.ts +1 -1
  138. package/dist/local-llm.d.ts +1 -1
  139. package/dist/local-model-endpoint.d.ts +1 -1
  140. package/dist/maintenance/memory-governance.d.ts +1 -1
  141. package/dist/maintenance/memory-governance.js +3 -3
  142. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  143. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  144. package/dist/mcp-memory-inspector-app.d.ts +5 -5
  145. package/dist/memory-action-policy.d.ts +1 -1
  146. package/dist/memory-cache.d.ts +1 -1
  147. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  148. package/dist/memory-projection-store.d.ts +1 -1
  149. package/dist/memory-provenance.d.ts +1 -1
  150. package/dist/memory-worth-outcomes.d.ts +1 -1
  151. package/dist/models-json.d.ts +1 -1
  152. package/dist/namespaces/migrate.d.ts +2 -2
  153. package/dist/namespaces/migrate.js +4 -4
  154. package/dist/namespaces/principal.d.ts +1 -1
  155. package/dist/namespaces/search.d.ts +1 -1
  156. package/dist/namespaces/storage.d.ts +2 -2
  157. package/dist/namespaces/storage.js +3 -3
  158. package/dist/native-knowledge.d.ts +1 -1
  159. package/dist/operator-toolkit.d.ts +1 -1
  160. package/dist/operator-toolkit.js +8 -8
  161. package/dist/orchestration/maintenance.d.ts +2 -2
  162. package/dist/orchestration/maintenance.js +5 -5
  163. package/dist/{orchestrator-f5xv19kp.d.ts → orchestrator-C_OMSIMZ.d.ts} +34 -3
  164. package/dist/orchestrator.d.ts +4 -4
  165. package/dist/orchestrator.js +17 -17
  166. package/dist/patterns-cli.d.ts +1 -1
  167. package/dist/policy-runtime.d.ts +1 -1
  168. package/dist/provenance.d.ts +1 -1
  169. package/dist/qmd-recall-cache.d.ts +1 -1
  170. package/dist/qmd.d.ts +1 -1
  171. package/dist/recall-disclosure-escalation.d.ts +1 -1
  172. package/dist/recall-explain-renderer.d.ts +1 -1
  173. package/dist/recall-explain-renderer.js +3 -3
  174. package/dist/recall-planner-llm.d.ts +1 -1
  175. package/dist/recall-state.d.ts +1 -1
  176. package/dist/recall-tag-filter.d.ts +1 -1
  177. package/dist/recall-xray-cli.d.ts +1 -1
  178. package/dist/recall-xray-cli.js +4 -4
  179. package/dist/recall-xray-renderer.d.ts +1 -1
  180. package/dist/recall-xray-renderer.js +3 -3
  181. package/dist/recall-xray.d.ts +1 -1
  182. package/dist/recall-xray.js +2 -2
  183. package/dist/resolve-auth-token.d.ts +1 -1
  184. package/dist/retrieval-agents.d.ts +1 -1
  185. package/dist/retrieval-tiers.d.ts +1 -1
  186. package/dist/routing/engine.d.ts +1 -1
  187. package/dist/routing/store.d.ts +1 -1
  188. package/dist/schemas.d.ts +22 -22
  189. package/dist/search/embed-helper.d.ts +1 -1
  190. package/dist/search/factory.d.ts +1 -1
  191. package/dist/search/index.d.ts +1 -1
  192. package/dist/search/lancedb-backend.d.ts +1 -1
  193. package/dist/search/meilisearch-backend.d.ts +1 -1
  194. package/dist/search/noop-backend.d.ts +1 -1
  195. package/dist/search/orama-backend.d.ts +1 -1
  196. package/dist/search/port.d.ts +1 -1
  197. package/dist/search/remote-backend.d.ts +1 -1
  198. package/dist/{semantic-consolidation-BQm1e1Pn.d.ts → semantic-consolidation-B8LiYtvx.d.ts} +1 -1
  199. package/dist/semantic-consolidation.d.ts +2 -2
  200. package/dist/semantic-consolidation.js +4 -4
  201. package/dist/semantic-rule-promotion.js +3 -3
  202. package/dist/semantic-rule-verifier.d.ts +1 -1
  203. package/dist/semantic-rule-verifier.js +3 -3
  204. package/dist/session-observer-bands.d.ts +1 -1
  205. package/dist/session-observer-state.d.ts +1 -1
  206. package/dist/shared-context/manager.d.ts +1 -1
  207. package/dist/signal.d.ts +1 -1
  208. package/dist/storage.d.ts +1 -1
  209. package/dist/storage.js +2 -2
  210. package/dist/summarizer.d.ts +1 -1
  211. package/dist/summary-snapshot.d.ts +1 -1
  212. package/dist/temporal-supersession.d.ts +1 -1
  213. package/dist/temporal-validity.d.ts +1 -1
  214. package/dist/threading.d.ts +1 -1
  215. package/dist/tier-migration.d.ts +1 -1
  216. package/dist/tier-routing.d.ts +1 -1
  217. package/dist/topics.d.ts +1 -1
  218. package/dist/transcript.d.ts +1 -1
  219. package/dist/transfer/types.d.ts +12 -12
  220. package/dist/trust-score-stage.d.ts +1 -1
  221. package/dist/trust-score.d.ts +1 -1
  222. package/dist/{types-DwO1Ot4n.d.ts → types-DmTaqob2.d.ts} +14 -0
  223. package/dist/types.d.ts +1 -1
  224. package/dist/types.js +1 -1
  225. package/dist/utility-runtime.d.ts +1 -1
  226. package/dist/verified-recall.js +3 -3
  227. package/package.json +2 -2
  228. package/src/access-boundary.ts +36 -2
  229. package/src/access-http.ts +17 -8
  230. package/src/access-mcp.ts +149 -1189
  231. package/src/access-operations-batch.ts +227 -1439
  232. package/src/access-surface-catalog.test.ts +4 -15
  233. package/src/access-surface-catalog.ts +82 -82
  234. package/src/bitemporal-dedup-backfill.test.ts +169 -0
  235. package/src/event-time.ts +57 -0
  236. package/src/extraction-eventtime-wiring.test.ts +101 -1
  237. package/src/orchestrator.ts +238 -43
  238. package/src/recall/archive-scoring.ts +526 -0
  239. package/src/types.ts +14 -0
  240. package/dist/chunk-B7WISIAJ.js.map +0 -1
  241. package/dist/chunk-EM2LDXJ2.js +0 -1315
  242. package/dist/chunk-EM2LDXJ2.js.map +0 -1
  243. package/dist/chunk-LA3HZKES.js.map +0 -1
  244. package/dist/chunk-LLUGAZ3B.js.map +0 -1
  245. package/dist/chunk-UIXXE6NI.js.map +0 -1
  246. /package/dist/{chunk-EVPNWN7Z.js.map → chunk-4R3CGCI5.js.map} +0 -0
  247. /package/dist/{chunk-UO5ZHIIH.js.map → chunk-4WZ3J7M6.js.map} +0 -0
  248. /package/dist/{chunk-PD42EYQN.js.map → chunk-5OPYYPJN.js.map} +0 -0
  249. /package/dist/{chunk-EEJMD2L2.js.map → chunk-6CWGS2LY.js.map} +0 -0
  250. /package/dist/{chunk-7CTNUVHZ.js.map → chunk-6SQQOJ4U.js.map} +0 -0
  251. /package/dist/{chunk-A5XNBK2D.js.map → chunk-DF3I6JGP.js.map} +0 -0
  252. /package/dist/{chunk-GJSL4WG2.js.map → chunk-F3XZN24Z.js.map} +0 -0
  253. /package/dist/{chunk-J3JPB7ZI.js.map → chunk-FACTVMXQ.js.map} +0 -0
  254. /package/dist/{chunk-EBDA5TYA.js.map → chunk-FXCIOLRB.js.map} +0 -0
  255. /package/dist/{chunk-TXQCG6SE.js.map → chunk-H7HUQIY7.js.map} +0 -0
  256. /package/dist/{chunk-QD6QP5P6.js.map → chunk-I2HL5M6T.js.map} +0 -0
  257. /package/dist/{chunk-Z4RU5VPM.js.map → chunk-J334N6QV.js.map} +0 -0
  258. /package/dist/{chunk-NXRUUDUF.js.map → chunk-JJJCVCCQ.js.map} +0 -0
  259. /package/dist/{chunk-GDUOCHCD.js.map → chunk-KGZF2VO3.js.map} +0 -0
  260. /package/dist/{chunk-OHTOPFRF.js.map → chunk-SC2YAWRF.js.map} +0 -0
  261. /package/dist/{chunk-YEE5KJQQ.js.map → chunk-T55EGZSJ.js.map} +0 -0
  262. /package/dist/{chunk-N77ELJ2D.js.map → chunk-TT5U3UDO.js.map} +0 -0
  263. /package/dist/{chunk-6FAMMBQ3.js.map → chunk-UBHVAGO7.js.map} +0 -0
  264. /package/dist/{chunk-EDOQM4E4.js.map → chunk-WAFDHKES.js.map} +0 -0
  265. /package/dist/{chunk-EXUCUPV2.js.map → chunk-WM7DNKBD.js.map} +0 -0
  266. /package/dist/{chunk-WQYSDPMA.js.map → chunk-YQMPUYHE.js.map} +0 -0
  267. /package/dist/{chunk-5O42UKXI.js.map → chunk-YUYPWJD3.js.map} +0 -0
  268. /package/dist/{chunk-VCLGUIFM.js.map → chunk-Z3FGC3QE.js.map} +0 -0
  269. /package/dist/{chunk-A7TJZLPK.js.map → chunk-ZK2L72VN.js.map} +0 -0
  270. /package/dist/{chunk-BXEOHV7K.js.map → chunk-ZOHMPFOB.js.map} +0 -0
@@ -37,21 +37,10 @@ import {
37
37
  type McpToolEntry,
38
38
  } from "./access-surface-catalog.js";
39
39
 
40
- // The ratchet baseline. Decrease this constant (and run
41
- // `node scripts/check-ratchets.mjs --update`) whenever a follow-up PR
42
- // migrates a handler. It may NEVER increase — an increase means a new handler
43
- // shipped without going through the boundary. The only exception is a
44
- // catalog-completeness correction: adding routes that were always live but
45
- // omitted from the catalog (review-caught). Such a bump MUST be accompanied
46
- // by the newly-cataloged entries; the higher count is the honest baseline.
47
- // #1580 Correction Contract adds GET /engram/v1/correction/pending — a read-only
48
- // list route (namespace/sessionKey query params, no body), so it stays unmigrated
49
- // alongside the other GET list routes (baseline 80 from #1525 + 1 = 81). Also a
50
- // catalog-completeness correction: POST /engram/v1/memories (operation memory_store)
51
- // was always live but omitted from HTTP_ROUTES — it is migrated, so no count change.
52
- // #1583 remnic chat adds memory_chat — an unmigrated handler dispatched to
53
- // processChatMessage (carries user input; not a boundary operation) → 81 + 1 = 82.
54
- const UNMIGRATED_HANDLER_BASELINE = 82;
40
+ // #1668: all remaining MCP tools migrated through the strict-schema boundary
41
+ // (recall, capsule, continuity, work, shared-context, peer, dreams, etc.).
42
+ // memory_chat chat_message; GET /correction/pending review_queue_list.
43
+ const UNMIGRATED_HANDLER_BASELINE = 0;
55
44
 
56
45
  // Keep the import live — `getOperation` is the call surfaces use at dispatch
57
46
  // time; referencing it here pins the registry's lookup contract.
@@ -49,35 +49,35 @@ export interface HttpRouteEntry {
49
49
  // `tools/list` response matches this list by short name, so a new tool cannot
50
50
  // land without either migrating it or acknowledging the unmigrated count.
51
51
  export const MCP_TOOLS: readonly McpToolEntry[] = [
52
- { tool: "recall", operation: null },
53
- { tool: "recall_explain", operation: null },
54
- { tool: "set_coding_context", operation: null },
55
- { tool: "recall_tier_explain", operation: null },
56
- { tool: "recall_xray", operation: null },
57
- { tool: "wearables_status", operation: null },
58
- { tool: "wearables_sync", operation: null },
59
- { tool: "transcript_day", operation: null },
60
- { tool: "transcript_search", operation: null },
61
- { tool: "transcript_memories", operation: null },
62
- { tool: "action_confidence", operation: null },
63
- { tool: "chatgpt_memory_inspector", operation: null },
64
- { tool: "day_summary", operation: null },
65
- { tool: "capsule_export", operation: null },
66
- { tool: "capsule_import", operation: null },
67
- { tool: "capsule_list", operation: null },
68
- { tool: "memory_governance_run", operation: null },
69
- { tool: "procedure_mining_run", operation: null },
70
- { tool: "pattern_reinforcement_run", operation: null },
71
- { tool: "procedural_stats", operation: null },
52
+ { tool: "recall", operation: "recall" },
53
+ { tool: "recall_explain", operation: "recall_explain" },
54
+ { tool: "set_coding_context", operation: "set_coding_context" },
55
+ { tool: "recall_tier_explain", operation: "recall_tier_explain" },
56
+ { tool: "recall_xray", operation: "recall_xray" },
57
+ { tool: "wearables_status", operation: "wearables_status" },
58
+ { tool: "wearables_sync", operation: "wearables_sync" },
59
+ { tool: "transcript_day", operation: "transcript_day" },
60
+ { tool: "transcript_search", operation: "transcript_search" },
61
+ { tool: "transcript_memories", operation: "transcript_memories" },
62
+ { tool: "action_confidence", operation: "action_confidence" },
63
+ { tool: "chatgpt_memory_inspector", operation: "chatgpt_memory_inspector" },
64
+ { tool: "day_summary", operation: "day_summary" },
65
+ { tool: "capsule_export", operation: "capsule_export" },
66
+ { tool: "capsule_import", operation: "capsule_import" },
67
+ { tool: "capsule_list", operation: "capsule_list" },
68
+ { tool: "memory_governance_run", operation: "memory_governance_run" },
69
+ { tool: "procedure_mining_run", operation: "procedure_mining_run" },
70
+ { tool: "pattern_reinforcement_run", operation: "pattern_reinforcement_run" },
71
+ { tool: "procedural_stats", operation: "procedural_stats" },
72
72
  { tool: "memory_get", operation: "memory_get" },
73
- { tool: "memory_timeline", operation: null },
73
+ { tool: "memory_timeline", operation: "memory_timeline" },
74
74
  { tool: "memory_store", operation: "memory_store" },
75
75
  // Correction Contract (issue #1580) — one plan/apply pipeline.
76
76
  { tool: "memory_correct_plan", operation: "memory_correct_plan" },
77
77
  { tool: "memory_correct_apply", operation: "memory_correct_apply" },
78
78
  // Conversational memory inspection + correction (issue #1583) — unmigrated
79
79
  // handler dispatched to processChatMessage (not a boundary operation).
80
- { tool: "memory_chat", operation: null },
80
+ { tool: "memory_chat", operation: "chat_message" },
81
81
  { tool: "coding_decision", operation: "coding_decision" },
82
82
  { tool: "coding_architecture", operation: "coding_architecture" },
83
83
  // codegraph parity tools (issue #1554)
@@ -96,66 +96,66 @@ export const MCP_TOOLS: readonly McpToolEntry[] = [
96
96
  { tool: "codegraph_manage_adr", operation: "codegraph_manage_adr" },
97
97
  { tool: "codegraph_ingest_traces", operation: "codegraph_ingest_traces" },
98
98
  { tool: "coding_delta", operation: "coding_delta" },
99
- { tool: "suggestion_submit", operation: null },
100
- { tool: "entity_get", operation: null },
101
- { tool: "review_queue_list", operation: null },
102
- { tool: "observe", operation: null },
103
- { tool: "lcm_search", operation: null },
104
- { tool: "lcm_compaction_flush", operation: null },
105
- { tool: "lcm_compaction_record", operation: null },
106
- { tool: "continuity_audit_generate", operation: null },
107
- { tool: "continuity_incident_open", operation: null },
108
- { tool: "continuity_incident_close", operation: null },
109
- { tool: "continuity_incident_list", operation: null },
110
- { tool: "continuity_loop_add_or_update", operation: null },
111
- { tool: "continuity_loop_review", operation: null },
112
- { tool: "identity_anchor_get", operation: null },
113
- { tool: "identity_anchor_update", operation: null },
114
- { tool: "memory_identity", operation: null },
115
- { tool: "work_task", operation: null },
116
- { tool: "work_project", operation: null },
117
- { tool: "work_board", operation: null },
118
- { tool: "shared_context_write_output", operation: null },
119
- { tool: "shared_feedback_record", operation: null },
120
- { tool: "shared_priorities_append", operation: null },
121
- { tool: "shared_context_cross_signals_run", operation: null },
122
- { tool: "shared_context_curate_daily", operation: null },
123
- { tool: "compounding_weekly_synthesize", operation: null },
124
- { tool: "compounding_promote_candidate", operation: null },
125
- { tool: "compression_guidelines_optimize", operation: null },
126
- { tool: "compression_guidelines_activate", operation: null },
99
+ { tool: "suggestion_submit", operation: "suggestion_submit" },
100
+ { tool: "entity_get", operation: "entity_get" },
101
+ { tool: "review_queue_list", operation: "review_queue_list" },
102
+ { tool: "observe", operation: "observe" },
103
+ { tool: "lcm_search", operation: "lcm_search" },
104
+ { tool: "lcm_compaction_flush", operation: "lcm_compaction_flush" },
105
+ { tool: "lcm_compaction_record", operation: "lcm_compaction_record" },
106
+ { tool: "continuity_audit_generate", operation: "continuity_audit_generate" },
107
+ { tool: "continuity_incident_open", operation: "continuity_incident_open" },
108
+ { tool: "continuity_incident_close", operation: "continuity_incident_close" },
109
+ { tool: "continuity_incident_list", operation: "continuity_incident_list" },
110
+ { tool: "continuity_loop_add_or_update", operation: "continuity_loop_add_or_update" },
111
+ { tool: "continuity_loop_review", operation: "continuity_loop_review" },
112
+ { tool: "identity_anchor_get", operation: "identity_anchor_get" },
113
+ { tool: "identity_anchor_update", operation: "identity_anchor_update" },
114
+ { tool: "memory_identity", operation: "memory_identity" },
115
+ { tool: "work_task", operation: "work_task" },
116
+ { tool: "work_project", operation: "work_project" },
117
+ { tool: "work_board", operation: "work_board" },
118
+ { tool: "shared_context_write_output", operation: "shared_context_write_output" },
119
+ { tool: "shared_feedback_record", operation: "shared_feedback_record" },
120
+ { tool: "shared_priorities_append", operation: "shared_priorities_append" },
121
+ { tool: "shared_context_cross_signals_run", operation: "shared_context_cross_signals_run" },
122
+ { tool: "shared_context_curate_daily", operation: "shared_context_curate_daily" },
123
+ { tool: "compounding_weekly_synthesize", operation: "compounding_weekly_synthesize" },
124
+ { tool: "compounding_promote_candidate", operation: "compounding_promote_candidate" },
125
+ { tool: "compression_guidelines_optimize", operation: "compression_guidelines_optimize" },
126
+ { tool: "compression_guidelines_activate", operation: "compression_guidelines_activate" },
127
127
  { tool: "memory_search", operation: "memory_search" },
128
- { tool: "memory_profile", operation: null },
129
- { tool: "memory_entities_list", operation: null },
130
- { tool: "memory_questions", operation: null },
131
- { tool: "memory_last_recall", operation: null },
132
- { tool: "memory_intent_debug", operation: null },
133
- { tool: "memory_qmd_debug", operation: null },
134
- { tool: "memory_graph_explain", operation: null },
135
- { tool: "graph_snapshot", operation: null },
136
- { tool: "memory_feedback", operation: null },
137
- { tool: "memory_promote", operation: null },
138
- { tool: "memory_outcome", operation: null },
139
- { tool: "memory_action_apply", operation: null },
140
- { tool: "context_checkpoint", operation: null },
141
- { tool: "briefing", operation: null },
142
- { tool: "review_list", operation: null },
143
- { tool: "review_resolve", operation: null },
144
- { tool: "contradiction_scan_run", operation: null },
145
- { tool: "memory_summarize_hourly", operation: null },
146
- { tool: "conversation_index_update", operation: null },
147
- { tool: "profiling_report", operation: null },
148
- { tool: "graph_edge_decay_run", operation: null },
149
- { tool: "live_connectors_run", operation: null },
150
- { tool: "peer_list", operation: null },
151
- { tool: "peer_get", operation: null },
152
- { tool: "peer_set", operation: null },
153
- { tool: "peer_delete", operation: null },
154
- { tool: "peer_profile_get", operation: null },
155
- { tool: "peer_forget", operation: null },
156
- { tool: "console_state", operation: null },
157
- { tool: "dreams_status", operation: null },
158
- { tool: "dreams_run", operation: null },
128
+ { tool: "memory_profile", operation: "memory_profile" },
129
+ { tool: "memory_entities_list", operation: "memory_entities_list" },
130
+ { tool: "memory_questions", operation: "memory_questions" },
131
+ { tool: "memory_last_recall", operation: "memory_last_recall" },
132
+ { tool: "memory_intent_debug", operation: "memory_intent_debug" },
133
+ { tool: "memory_qmd_debug", operation: "memory_qmd_debug" },
134
+ { tool: "memory_graph_explain", operation: "memory_graph_explain" },
135
+ { tool: "graph_snapshot", operation: "graph_snapshot" },
136
+ { tool: "memory_feedback", operation: "memory_feedback" },
137
+ { tool: "memory_promote", operation: "memory_promote" },
138
+ { tool: "memory_outcome", operation: "memory_outcome" },
139
+ { tool: "memory_action_apply", operation: "memory_action_apply" },
140
+ { tool: "context_checkpoint", operation: "context_checkpoint" },
141
+ { tool: "briefing", operation: "briefing" },
142
+ { tool: "review_list", operation: "review_list" },
143
+ { tool: "review_resolve", operation: "review_resolve" },
144
+ { tool: "contradiction_scan_run", operation: "contradiction_scan_run" },
145
+ { tool: "memory_summarize_hourly", operation: "memory_summarize_hourly" },
146
+ { tool: "conversation_index_update", operation: "conversation_index_update" },
147
+ { tool: "profiling_report", operation: "profiling_report" },
148
+ { tool: "graph_edge_decay_run", operation: "graph_edge_decay_run" },
149
+ { tool: "live_connectors_run", operation: "live_connectors_run" },
150
+ { tool: "peer_list", operation: "peer_list" },
151
+ { tool: "peer_get", operation: "peer_get" },
152
+ { tool: "peer_set", operation: "peer_set" },
153
+ { tool: "peer_delete", operation: "peer_delete" },
154
+ { tool: "peer_profile_get", operation: "peer_profile_get" },
155
+ { tool: "peer_forget", operation: "peer_forget" },
156
+ { tool: "console_state", operation: "console_state" },
157
+ { tool: "dreams_status", operation: "dreams_status" },
158
+ { tool: "dreams_run", operation: "dreams_run" },
159
159
  ];
160
160
 
161
161
  // Each route below corresponds 1:1 to a service-invoking route branch in
@@ -193,7 +193,7 @@ export const HTTP_ROUTES: readonly HttpRouteEntry[] = [
193
193
  // Correction Contract (issue #1580) — plan/apply/pending.
194
194
  { method: "POST", pathname: "/engram/v1/correction/plan", operation: "memory_correct_plan" },
195
195
  { method: "POST", pathname: "/engram/v1/correction/apply", operation: "memory_correct_apply" },
196
- { method: "GET", pathname: "/engram/v1/correction/pending", operation: null },
196
+ { method: "GET", pathname: "/engram/v1/correction/pending", operation: "correction_pending" },
197
197
  { method: "POST", pathname: "/engram/v1/memories", operation: "memory_store" },
198
198
  { method: "POST", pathname: "/engram/v1/coding/decisions", operation: "coding_decision" },
199
199
  { method: "POST", pathname: "/engram/v1/coding/architecture", operation: "coding_architecture" },
@@ -0,0 +1,169 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { mkdtemp, rm } from "node:fs/promises";
6
+
7
+ import { StorageManager, ContentHashIndex } from "./storage.js";
8
+
9
+ /**
10
+ * Issue #1671 — backfill invalid_at/observedAt onto deduped promotion copies.
11
+ *
12
+ * When a fact is re-extracted and now carries a resolved bi-temporal end bound
13
+ * (e.g. "until June 2025"), but the existing copy (source-namespace or promoted)
14
+ * was written before this metadata existed, the dedup short-circuit must patch
15
+ * the existing copy's frontmatter — otherwise recall keeps surfacing an expired
16
+ * fact. These tests prove the storage-level backfill mechanism the orchestrator
17
+ * helper (`backfillTemporalBoundsOnDedupHit`) relies on:
18
+ *
19
+ * 1. writeMemoryFrontmatter patches invalid_at/observedAt/eventTimeSource.
20
+ * 2. The content-hash lookup (ContentHashIndex.normalizeContent) finds the
21
+ * existing copy by normalized content.
22
+ * 3. The patch only fills fields the copy LACKS — never clobbers an existing
23
+ * bound.
24
+ */
25
+ async function makeStorage(prefix: string): Promise<{
26
+ storage: StorageManager;
27
+ cleanup: () => Promise<void>;
28
+ }> {
29
+ const dir = await mkdtemp(path.join(os.tmpdir(), prefix));
30
+ const storage = new StorageManager(dir);
31
+ await storage.ensureDirectories();
32
+ return {
33
+ storage,
34
+ cleanup: async () => {
35
+ await rm(dir, { recursive: true, force: true });
36
+ },
37
+ };
38
+ }
39
+
40
+ test("#1671: writeMemoryFrontmatter patches invalid_at onto an existing fact that lacks it", async () => {
41
+ const { storage, cleanup } = await makeStorage("bitemporal-backfill-patch-");
42
+ try {
43
+ // Write a fact WITHOUT bi-temporal bounds (simulates a pre-#1578 copy).
44
+ const id = await storage.writeMemory("fact", "We use PostgreSQL for the main db.", {
45
+ confidence: 0.9,
46
+ });
47
+ const before = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id);
48
+ assert.ok(before, "fact must exist");
49
+ assert.equal(before!.frontmatter.invalid_at, undefined);
50
+ assert.equal(before!.frontmatter.observedAt, undefined);
51
+
52
+ // Backfill: patch invalid_at + observedAt + eventTimeSource.
53
+ const ok = await storage.writeMemoryFrontmatter(before!, {
54
+ invalid_at: "2025-07-01T00:00:00.000Z",
55
+ observedAt: "2025-06-20T00:00:00.000Z",
56
+ eventTimeSource: "extracted",
57
+ });
58
+ assert.equal(ok, true);
59
+
60
+ const after = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id);
61
+ assert.ok(after, "fact must still exist after patch");
62
+ assert.equal(after!.frontmatter.invalid_at, "2025-07-01T00:00:00.000Z");
63
+ assert.equal(after!.frontmatter.observedAt, "2025-06-20T00:00:00.000Z");
64
+ assert.equal(after!.frontmatter.eventTimeSource, "extracted");
65
+ } finally {
66
+ await cleanup();
67
+ }
68
+ });
69
+
70
+ test("#1671: existing copy found by normalized content hash for backfill", async () => {
71
+ const { storage, cleanup } = await makeStorage("bitemporal-backfill-lookup-");
72
+ try {
73
+ const content = "The database is MySQL.";
74
+ await storage.writeMemory("fact", content, {
75
+ confidence: 0.9,
76
+ });
77
+
78
+ // Simulate the orchestrator's lookup: normalize the incoming content and
79
+ // find the active fact whose normalized content matches.
80
+ const normalizedIncoming = ContentHashIndex.normalizeContent(content);
81
+ const all = await storage.readAllMemories();
82
+ const existing = all.find(
83
+ (m) =>
84
+ m.frontmatter.category === "fact" &&
85
+ (m.frontmatter.status ?? "active") === "active" &&
86
+ ContentHashIndex.normalizeContent(m.content ?? "") === normalizedIncoming,
87
+ );
88
+ assert.ok(existing, "must find the existing fact by normalized content");
89
+ } finally {
90
+ await cleanup();
91
+ }
92
+ });
93
+
94
+ test("#1671: backfill does NOT clobber an existing invalid_at on the copy", async () => {
95
+ const { storage, cleanup } = await makeStorage("bitemporal-backfill-noclobber-");
96
+ try {
97
+ // The existing copy already has an explicit invalid_at.
98
+ const id = await storage.writeMemory("fact", "We use MySQL until December.", {
99
+ confidence: 0.9,
100
+ invalidAt: "2025-12-01T00:00:00.000Z",
101
+ observedAt: "2025-06-01T00:00:00.000Z",
102
+ eventTimeSource: "extracted",
103
+ });
104
+ const before = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id);
105
+ assert.ok(before);
106
+
107
+ // Attempt to backfill a DIFFERENT invalid_at — the patch should only fill
108
+ // MISSING fields, not overwrite. (The orchestrator helper checks before
109
+ // patching; here we prove the helper's gate logic is correct by simulating
110
+ // the same condition: only patch if the field is absent/empty.)
111
+ const patch: Record<string, unknown> = {};
112
+ const fm = before!.frontmatter;
113
+ if (!fm.invalid_at || fm.invalid_at.length === 0) {
114
+ patch.invalid_at = "2025-07-01T00:00:00.000Z";
115
+ }
116
+ // The existing copy already has invalid_at, so the patch is empty.
117
+ assert.equal(Object.keys(patch).length, 0,
118
+ "must not patch invalid_at when the copy already has it");
119
+ } finally {
120
+ await cleanup();
121
+ }
122
+ });
123
+
124
+ test("#1671: re-extraction with resolved invalidAt backfills an old promoted copy (end-to-end storage)", async () => {
125
+ const { storage, cleanup } = await makeStorage("bitemporal-backfill-e2e-");
126
+ try {
127
+ // Step 1: write a fact WITHOUT bounds (old promoted copy).
128
+ const content = "The API rate limit is 100 req/min.";
129
+ const id = await storage.writeMemory("fact", content, {
130
+ confidence: 0.9,
131
+ });
132
+
133
+ // Step 2: simulate re-extraction that now carries a resolved invalidAt.
134
+ // The dedup short-circuit finds the existing copy and patches it.
135
+ const normalizedIncoming = ContentHashIndex.normalizeContent(content);
136
+ const all = await storage.readAllMemories();
137
+ const existing = all.find(
138
+ (m) =>
139
+ m.frontmatter.category === "fact" &&
140
+ (m.frontmatter.status ?? "active") === "active" &&
141
+ ContentHashIndex.normalizeContent(m.content ?? "") === normalizedIncoming,
142
+ );
143
+ assert.ok(existing, "must find the existing copy");
144
+
145
+ const patch: Record<string, string> = {};
146
+ if (!existing!.frontmatter.invalid_at) {
147
+ patch.invalid_at = "2025-09-01T00:00:00.000Z";
148
+ }
149
+ if (!existing!.frontmatter.observedAt) {
150
+ patch.observedAt = "2025-06-20T00:00:00.000Z";
151
+ }
152
+ if (!existing!.frontmatter.eventTimeSource) {
153
+ patch.eventTimeSource = "extracted";
154
+ }
155
+ assert.ok(Object.keys(patch).length > 0, "must have fields to backfill");
156
+
157
+ const ok = await storage.writeMemoryFrontmatter(existing!, patch);
158
+ assert.equal(ok, true);
159
+
160
+ // Step 3: verify the patched copy now expires correctly.
161
+ const after = (await storage.readAllMemories()).find((m) => m.frontmatter.id === id);
162
+ assert.ok(after);
163
+ assert.equal(after!.frontmatter.invalid_at, "2025-09-01T00:00:00.000Z");
164
+ assert.equal(after!.frontmatter.observedAt, "2025-06-20T00:00:00.000Z");
165
+ assert.equal(after!.frontmatter.eventTimeSource, "extracted");
166
+ } finally {
167
+ await cleanup();
168
+ }
169
+ });
package/src/event-time.ts CHANGED
@@ -619,3 +619,60 @@ export function resolveFactEventTime(
619
619
  eventTimeSource: "assumed",
620
620
  };
621
621
  }
622
+
623
+ /**
624
+ * Input shape for {@link pickFactEventTimeAnchor} — a structural slice of
625
+ * `ExtractedFact` so the helper stays decoupled from the full type.
626
+ */
627
+ export interface FactAnchorInput {
628
+ /** Explicit per-fact source-turn timestamp (#1670). */
629
+ sourceTurnTimestamp?: string;
630
+ /** Claim-level provenance spans (#1575), each carrying the turn's `observedAt`. */
631
+ sources?: { observedAt: string }[];
632
+ }
633
+
634
+ /**
635
+ * Issue #1670 — select the per-fact event-time anchor.
636
+ *
637
+ * Prefers the fact's explicit `sourceTurnTimestamp` (set by extractors that
638
+ * know the exact source turn). Falls back to the earliest provenance span's
639
+ * `observedAt` (the turn where the supporting quote was located). Finally
640
+ * falls back to the batch-wide anchor (latest source turn timestamp) when
641
+ * neither per-fact signal is present — preserving legacy behaviour for
642
+ * extractors that do not emit provenance.
643
+ *
644
+ * The earliest provenance span wins because a fact backed by quotes from
645
+ * multiple turns was *first* uttered at the earliest turn: a relative
646
+ * expression ("yesterday") on that fact should resolve against that turn,
647
+ * not a later turn that merely restated the claim.
648
+ *
649
+ * Returns `undefined` only when no signal is usable AND `batchAnchor` is
650
+ * absent — in which case the caller skips bi-temporal resolution entirely
651
+ * (identical to the pre-feature pipeline when biTemporal is off).
652
+ */
653
+ export function pickFactEventTimeAnchor(
654
+ fact: FactAnchorInput,
655
+ batchAnchor: string | undefined,
656
+ ): string | undefined {
657
+ // 1. Explicit per-fact source-turn timestamp (highest priority).
658
+ if (
659
+ typeof fact.sourceTurnTimestamp === "string" &&
660
+ fact.sourceTurnTimestamp.trim().length > 0
661
+ ) {
662
+ const ms = parseFlexibleIsoTimestamp(fact.sourceTurnTimestamp.trim());
663
+ if (ms !== null) return new Date(ms).toISOString();
664
+ }
665
+ // 2. Earliest provenance span turn timestamp.
666
+ if (fact.sources && fact.sources.length > 0) {
667
+ let earliestMs: number | null = null;
668
+ for (const src of fact.sources) {
669
+ if (typeof src.observedAt !== "string") continue;
670
+ const ms = parseFlexibleIsoTimestamp(src.observedAt.trim());
671
+ if (ms === null) continue;
672
+ if (earliestMs === null || ms < earliestMs) earliestMs = ms;
673
+ }
674
+ if (earliestMs !== null) return new Date(earliestMs).toISOString();
675
+ }
676
+ // 3. Batch-wide anchor (legacy fallback).
677
+ return batchAnchor;
678
+ }
@@ -4,7 +4,7 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { mkdtemp, rm } from "node:fs/promises";
6
6
 
7
- import { resolveFactEventTime } from "./event-time.js";
7
+ import { pickFactEventTimeAnchor, resolveFactEventTime } from "./event-time.js";
8
8
  import { ExtractionEngine } from "./extraction.js";
9
9
  import { ExtractedFactSchema } from "./schemas.js";
10
10
  import { StorageManager } from "./storage.js";
@@ -267,3 +267,103 @@ test("resolveFactEventTime: bare '2025-03' (YYYY-MM) → validFrom = first of mo
267
267
  assert.equal(result.validFrom, "2025-03-01T00:00:00.000Z");
268
268
  assert.equal(result.validUntil, undefined);
269
269
  });
270
+
271
+ // ── #1670: per-fact source-turn anchor selection ─────────────────────────
272
+
273
+ test("pickFactEventTimeAnchor: explicit sourceTurnTimestamp wins over batch anchor", () => {
274
+ // A buffered batch's latest turn is 2025-04-12, but the fact was extracted
275
+ // from an earlier turn on 2025-04-10. The per-fact timestamp must win.
276
+ const anchor = pickFactEventTimeAnchor(
277
+ { sourceTurnTimestamp: "2025-04-10T12:00:00.000Z" },
278
+ "2025-04-12T12:00:00.000Z",
279
+ );
280
+ assert.equal(anchor, "2025-04-10T12:00:00.000Z");
281
+ });
282
+
283
+ test("pickFactEventTimeAnchor: earliest provenance span observedAt used when no sourceTurnTimestamp", () => {
284
+ // A fact backed by spans from two turns: the EARLIEST span's observedAt is
285
+ // the turn where the claim was first uttered — the correct anchor for a
286
+ // relative expression like "yesterday".
287
+ const anchor = pickFactEventTimeAnchor(
288
+ {
289
+ sources: [
290
+ { observedAt: "2025-04-11T08:00:00.000Z" },
291
+ { observedAt: "2025-04-10T08:00:00.000Z" },
292
+ { observedAt: "2025-04-12T08:00:00.000Z" },
293
+ ],
294
+ },
295
+ "2025-04-12T08:00:00.000Z",
296
+ );
297
+ assert.equal(anchor, "2025-04-10T08:00:00.000Z");
298
+ });
299
+
300
+ test("pickFactEventTimeAnchor: sourceTurnTimestamp takes precedence over provenance spans", () => {
301
+ const anchor = pickFactEventTimeAnchor(
302
+ {
303
+ sourceTurnTimestamp: "2025-04-09T00:00:00.000Z",
304
+ sources: [{ observedAt: "2025-04-11T00:00:00.000Z" }],
305
+ },
306
+ "2025-04-12T00:00:00.000Z",
307
+ );
308
+ assert.equal(anchor, "2025-04-09T00:00:00.000Z");
309
+ });
310
+
311
+ test("pickFactEventTimeAnchor: falls back to batch anchor when no per-fact signal", () => {
312
+ const anchor = pickFactEventTimeAnchor({}, "2025-04-12T12:00:00.000Z");
313
+ assert.equal(anchor, "2025-04-12T12:00:00.000Z");
314
+ });
315
+
316
+ test("pickFactEventTimeAnchor: returns undefined when no signal and no batch anchor", () => {
317
+ const anchor = pickFactEventTimeAnchor({}, undefined);
318
+ assert.equal(anchor, undefined);
319
+ });
320
+
321
+ test("pickFactEventTimeAnchor: ignores corrupt sourceTurnTimestamp, falls through to provenance", () => {
322
+ const anchor = pickFactEventTimeAnchor(
323
+ {
324
+ sourceTurnTimestamp: "not-a-timestamp",
325
+ sources: [{ observedAt: "2025-04-10T08:00:00.000Z" }],
326
+ },
327
+ "2025-04-12T08:00:00.000Z",
328
+ );
329
+ assert.equal(anchor, "2025-04-10T08:00:00.000Z");
330
+ });
331
+
332
+ test("pickFactEventTimeAnchor: ignores corrupt provenance observedAt entries, falls through to batch", () => {
333
+ const anchor = pickFactEventTimeAnchor(
334
+ { sources: [{ observedAt: "garbage" }, { observedAt: "also-bad" }] },
335
+ "2025-04-12T08:00:00.000Z",
336
+ );
337
+ assert.equal(anchor, "2025-04-12T08:00:00.000Z");
338
+ });
339
+
340
+ test("#1670 integration: 'yesterday' on an early-turn fact resolves against the early turn, not the batch", () => {
341
+ // Simulates the exact scenario from the issue: a buffered conversation
342
+ // spanning a date boundary. The fact carries provenance from the early turn.
343
+ const earlyAnchor = pickFactEventTimeAnchor(
344
+ { sources: [{ observedAt: "2025-01-15T10:00:00.000Z" }] },
345
+ "2025-01-17T10:00:00.000Z", // batch latest = 3 days later
346
+ );
347
+ assert.ok(earlyAnchor, "anchor must resolve from provenance");
348
+ const result = resolveFactEventTime("yesterday", earlyAnchor);
349
+ assert.ok(result.validFrom!.startsWith("2025-01-14"),
350
+ `yesterday relative to early turn should be 2025-01-14, got ${result.validFrom}`);
351
+ });
352
+
353
+ test("#1670 integration: same expression, batch vs source-turn anchor → different validFrom", () => {
354
+ // Without per-fact provenance, "yesterday" resolves against the batch
355
+ // (latest turn = 2025-01-17 → 2025-01-16). With provenance from the early
356
+ // turn (2025-01-15), it resolves to 2025-01-14. The two must differ.
357
+ const batchAnchor = pickFactEventTimeAnchor({}, "2025-01-17T10:00:00.000Z");
358
+ assert.ok(batchAnchor);
359
+ const batchResult = resolveFactEventTime("yesterday", batchAnchor);
360
+ const provenanceAnchor = pickFactEventTimeAnchor(
361
+ { sources: [{ observedAt: "2025-01-15T10:00:00.000Z" }] },
362
+ "2025-01-17T10:00:00.000Z",
363
+ );
364
+ assert.ok(provenanceAnchor);
365
+ const provenanceResult = resolveFactEventTime("yesterday", provenanceAnchor);
366
+ assert.notEqual(batchResult.validFrom, provenanceResult.validFrom);
367
+ assert.ok(batchResult.validFrom!.startsWith("2025-01-16"));
368
+ assert.ok(provenanceResult.validFrom!.startsWith("2025-01-14"));
369
+ });