@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
@@ -0,0 +1,526 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Off-thread archive scoring for the cold-fallback recall path (issue #1674).
3
+ //
4
+ // `searchLongTermArchiveFallback` (orchestrator.ts) falls back to scanning
5
+ // EVERY archived memory file when hybrid/vector search returns zero hits.
6
+ // The scoring loop — for each memory × for each token, `haystack.includes(token)`
7
+ // — is fully synchronous, unbounded, and CPU-bound. Under concurrent recall
8
+ // load this monopolized the JS main thread: N concurrent recalls serialized
9
+ // on one core and each blew past the client-side timeout even though total
10
+ // CPU work would have finished comfortably if parallelized.
11
+ //
12
+ // This module extracts the scoring loop into a pure function and provides two
13
+ // strategies:
14
+ //
15
+ // 1. `SyncArchiveScoring` — runs the pure function on the calling thread
16
+ // (the OLD behavior; preserved for prove-fail
17
+ // tests and as the graceful fallback).
18
+ // 2. `OffThreadArchiveScoring` — dispatches the pure function to a
19
+ // `worker_threads` pool so concurrent recalls
20
+ // run on separate cores instead of serializing
21
+ // on the main thread. Falls back to sync if
22
+ // workers cannot be created.
23
+ //
24
+ // Both strategies share the identical `scoreArchiveMemories` pure function,
25
+ // so the scoring semantics are byte-identical regardless of which path runs.
26
+ // ---------------------------------------------------------------------------
27
+
28
+ import os from "node:os";
29
+ import { Worker } from "node:worker_threads";
30
+ import { log } from "../logger.js";
31
+ import type { MemoryFile } from "../types.js";
32
+
33
+ // ─────────────────────────────────────────────────────────────────────────────
34
+ // Wire types — plain-serializable shapes that cross the worker boundary via
35
+ // structured clone. Only the fields the scoring loop reads are included.
36
+ // ─────────────────────────────────────────────────────────────────────────────
37
+
38
+ /** Minimal serializable projection of {@link MemoryFile} for scoring. */
39
+ export interface ArchiveScoreItem {
40
+ id: string;
41
+ path: string;
42
+ content: string;
43
+ category: string;
44
+ tags: string[];
45
+ }
46
+
47
+ /** Scoring output — maps 1:1 to the relevant QmdSearchResult fields. */
48
+ export interface ArchiveScoreResult {
49
+ docid: string;
50
+ path: string;
51
+ score: number;
52
+ snippet: string;
53
+ }
54
+
55
+ /** Worker request envelope. */
56
+ interface ScoreTask {
57
+ items: ArchiveScoreItem[];
58
+ tokens: string[];
59
+ }
60
+
61
+ /** Worker reply envelope. */
62
+ type ScoreReply = { ok: true; results: ArchiveScoreResult[] } | { ok: false; error: string };
63
+
64
+ // ─────────────────────────────────────────────────────────────────────────────
65
+ // Pure scoring function — shared by both strategies and by the inline worker.
66
+ // Extracted verbatim from the original inline loop in
67
+ // `searchLongTermArchiveFallback` so behavior is identical.
68
+ // ─────────────────────────────────────────────────────────────────────────────
69
+
70
+ /**
71
+ * Score archived memories against query tokens using substring overlap.
72
+ *
73
+ * For each memory, builds a lowercase haystack from `[content, category, ...tags]`,
74
+ * counts how many distinct tokens appear in it, and scores by `hits / tokens.length`.
75
+ * Memories with zero hits are dropped. Snippets are the first 400 chars of content
76
+ * with newlines collapsed to spaces — matching the original orchestrator behavior.
77
+ *
78
+ * This function is intentionally synchronous and CPU-bound; that is precisely
79
+ * why the off-thread strategy exists.
80
+ */
81
+ export function scoreArchiveMemories(
82
+ items: ReadonlyArray<ArchiveScoreItem>,
83
+ tokens: ReadonlyArray<string>
84
+ ): ArchiveScoreResult[] {
85
+ if (items.length === 0 || tokens.length === 0) return [];
86
+
87
+ const scored: ArchiveScoreResult[] = [];
88
+ for (const item of items) {
89
+ const haystack = [item.content, item.category, ...item.tags].join(" ").toLowerCase();
90
+ let hits = 0;
91
+ for (const token of tokens) {
92
+ if (haystack.includes(token)) hits += 1;
93
+ }
94
+ if (hits === 0) continue;
95
+ scored.push({
96
+ docid: item.id,
97
+ path: item.path,
98
+ score: hits / tokens.length,
99
+ snippet: item.content.slice(0, 400).replace(/\n/g, " "),
100
+ });
101
+ }
102
+ return scored;
103
+ }
104
+
105
+ /**
106
+ * Project a {@link MemoryFile} into the minimal serializable shape the scoring
107
+ * function consumes. Called on the main thread BEFORE dispatching to a worker
108
+ * so the heavy `MemoryFrontmatter` (dozens of optional fields, nested objects)
109
+ * never crosses the worker boundary.
110
+ */
111
+ export function memoryFileToScoreItem(memory: MemoryFile): ArchiveScoreItem {
112
+ return {
113
+ id: memory.frontmatter.id,
114
+ path: memory.path,
115
+ content: memory.content,
116
+ category: memory.frontmatter.category,
117
+ tags: memory.frontmatter.tags ?? [],
118
+ };
119
+ }
120
+
121
+ // ─────────────────────────────────────────────────────────────────────────────
122
+ // Strategy interface
123
+ // ─────────────────────────────────────────────────────────────────────────────
124
+
125
+ /**
126
+ * Pluggable scoring backend. The orchestrator holds one instance and calls
127
+ * `score()` from the cold-fallback path. The default is off-thread; tests and
128
+ * restricted environments can swap in the sync strategy.
129
+ */
130
+ export interface ArchiveScoringStrategy {
131
+ score(
132
+ items: ReadonlyArray<ArchiveScoreItem>,
133
+ tokens: ReadonlyArray<string>,
134
+ abortSignal?: AbortSignal
135
+ ): Promise<ArchiveScoreResult[]>;
136
+ }
137
+
138
+ // ─────────────────────────────────────────────────────────────────────────────
139
+ // 1. SyncArchiveScoring — the OLD serialized behavior, preserved for fallback
140
+ // ─────────────────────────────────────────────────────────────────────────────
141
+
142
+ /**
143
+ * Runs the scoring loop synchronously on the calling thread.
144
+ *
145
+ * This is the exact behavior that caused issue #1674: the synchronous loop
146
+ * blocks the event loop for its entire duration, so concurrent recall
147
+ * requests serialize behind each other. It is retained as the graceful
148
+ * fallback when worker_threads are unavailable, and as the prove-fail
149
+ * baseline in regression tests.
150
+ */
151
+ export class SyncArchiveScoring implements ArchiveScoringStrategy {
152
+ async score(
153
+ items: ReadonlyArray<ArchiveScoreItem>,
154
+ tokens: ReadonlyArray<string>,
155
+ abortSignal?: AbortSignal
156
+ ): Promise<ArchiveScoreResult[]> {
157
+ if (items.length === 0 || tokens.length === 0) return [];
158
+ if (abortSignal?.aborted) return [];
159
+ // Process in chunks so an abort during a large archive scan is observed
160
+ // without burning the full synchronous CPU pass (#1674 review: sync
161
+ // fallback should check mid-scoring abort, like the old inline loop).
162
+ const CHUNK = 500;
163
+ if (items.length <= CHUNK) return scoreArchiveMemories(items, tokens);
164
+ const results: ArchiveScoreResult[] = [];
165
+ for (let i = 0; i < items.length; i += CHUNK) {
166
+ if (abortSignal?.aborted) return [];
167
+ const scored = scoreArchiveMemories(items.slice(i, i + CHUNK), tokens);
168
+ for (const r of scored) results.push(r);
169
+ }
170
+ return results;
171
+ }
172
+ }
173
+
174
+ // ─────────────────────────────────────────────────────────────────────────────
175
+ // Inline worker code (eval mode) — eliminates file-resolution / build-config
176
+ // issues entirely. The worker is self-contained CJS (no imports), so it runs
177
+ // identically under tsx, compiled dist, and published npm packages.
178
+ // ─────────────────────────────────────────────────────────────────────────────
179
+
180
+ /**
181
+ * Inline worker source. Runs via `new Worker(code, { eval: true })`.
182
+ *
183
+ * MUST stay byte-identical to {@link scoreArchiveMemories} above. The
184
+ * regression test "worker scoring matches canonical scoreArchiveMemories"
185
+ * asserts this equivalence at runtime.
186
+ */
187
+ const WORKER_SOURCE = String.raw`
188
+ const { parentPort } = require('node:worker_threads');
189
+
190
+ function scoreArchiveMemories(items, tokens) {
191
+ if (items.length === 0 || tokens.length === 0) return [];
192
+ var scored = [];
193
+ for (var i = 0; i < items.length; i++) {
194
+ var item = items[i];
195
+ var haystack = [item.content, item.category].concat(item.tags || []).join(' ').toLowerCase();
196
+ var hits = 0;
197
+ for (var j = 0; j < tokens.length; j++) {
198
+ if (haystack.indexOf(tokens[j]) !== -1) hits++;
199
+ }
200
+ if (hits === 0) continue;
201
+ scored.push({
202
+ docid: item.id,
203
+ path: item.path,
204
+ score: hits / tokens.length,
205
+ snippet: item.content.slice(0, 400).replace(/\n/g, ' '),
206
+ });
207
+ }
208
+ return scored;
209
+ }
210
+
211
+ if (parentPort) {
212
+ parentPort.on('message', function(task) {
213
+ try {
214
+ var results = scoreArchiveMemories(task.items, task.tokens);
215
+ parentPort.postMessage({ ok: true, results: results });
216
+ } catch (err) {
217
+ parentPort.postMessage({ ok: false, error: err && err.message ? err.message : String(err) });
218
+ }
219
+ });
220
+ }
221
+ `;
222
+
223
+ // ─────────────────────────────────────────────────────────────────────────────
224
+ // 2. OffThreadArchiveScoring — worker pool for genuine multi-core parallelism
225
+ // ─────────────────────────────────────────────────────────────────────────────
226
+
227
+ /**
228
+ * Default worker pool size. Uses `availableParallelism()` (Node 19.4+) minus
229
+ * one so the main thread always has a dedicated core for I/O dispatch. Capped
230
+ * at 8 to bound memory overhead (each worker has its own V8 heap).
231
+ */
232
+ function defaultPoolSize(): number {
233
+ const cpus = typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length;
234
+ return Math.max(1, Math.min(Math.max(1, cpus - 1), 8));
235
+ }
236
+
237
+ /**
238
+ * Safety-net timeout for a single dispatch. The cold-fallback pipeline already
239
+ * has its own deadline mechanism (runColdStepWithinDeadline); this is a
240
+ * last-resort guard so a hung worker never blocks recall indefinitely.
241
+ * Generously large to avoid interfering with large corpora (issue #1674
242
+ * reported scans up to ~70s on large archives).
243
+ */
244
+ const DISPATCH_TIMEOUT_MS = 120_000;
245
+
246
+ /** Lazy worker pool. Workers are created on first use and recycled. */
247
+ class ArchiveScoringWorkerPool {
248
+ private readonly targetSize: number;
249
+ private workers: Worker[] = [];
250
+ private idle: Worker[] = [];
251
+ private waiters: Array<{ resolve: (worker: Worker) => void; reject: (err: Error) => void }> = [];
252
+ private terminated = false;
253
+
254
+ constructor(size: number = defaultPoolSize()) {
255
+ this.targetSize = Math.max(1, size);
256
+ }
257
+
258
+ async run(task: ScoreTask, abortSignal?: AbortSignal): Promise<ArchiveScoreResult[]> {
259
+ if (this.terminated) throw new Error("archive-scoring pool terminated");
260
+ const worker = await this.acquire(abortSignal);
261
+ // If the caller already aborted before dispatch, return the worker to idle
262
+ // instead of retiring it — it was never posted to (#1674).
263
+ if (abortSignal?.aborted) {
264
+ this.release(worker);
265
+ return [];
266
+ }
267
+ let abandoned = false;
268
+ try {
269
+ return await this.dispatch(worker, task, abortSignal, () => {
270
+ abandoned = true;
271
+ });
272
+ } finally {
273
+ if (abandoned) this.retireWorker(worker);
274
+ else this.release(worker);
275
+ }
276
+ }
277
+
278
+ async terminate(): Promise<void> {
279
+ if (this.terminated) return;
280
+ this.terminated = true;
281
+ // Reject all queued waiters so they don't hang indefinitely (#1674).
282
+ const queued = this.waiters;
283
+ this.waiters = [];
284
+ for (const w of queued) w.reject(new Error("archive-scoring pool terminated"));
285
+ const all = [...this.workers];
286
+ this.workers = [];
287
+ this.idle = [];
288
+ await Promise.allSettled(all.map((w) => w.terminate()));
289
+ }
290
+
291
+ private async acquire(abortSignal?: AbortSignal): Promise<Worker> {
292
+ const idle = this.idle.pop();
293
+ if (idle) return idle;
294
+ if (this.workers.length < this.targetSize) return this.spawn();
295
+ // Park until a worker is released. If the caller aborts (or the pool
296
+ // terminates) while queued, reject so the recall falls back to sync
297
+ // instead of consuming a worker for a request that already timed out.
298
+ return new Promise<Worker>((resolve, reject) => {
299
+ const entry = { resolve, reject };
300
+ this.waiters.push(entry);
301
+ if (!abortSignal) return;
302
+ const onAbort = () => {
303
+ const idx = this.waiters.indexOf(entry);
304
+ if (idx !== -1) this.waiters.splice(idx, 1);
305
+ reject(new Error("archive-scoring acquire aborted"));
306
+ };
307
+ if (abortSignal.aborted) { onAbort(); return; }
308
+ abortSignal.addEventListener("abort", onAbort, { once: true });
309
+ });
310
+ }
311
+
312
+ private release(worker: Worker): void {
313
+ const next = this.waiters.shift();
314
+ if (next) {
315
+ next.resolve(worker);
316
+ } else if (!this.terminated) {
317
+ this.idle.push(worker);
318
+ } else {
319
+ void worker.terminate();
320
+ }
321
+ }
322
+
323
+ /** Terminate a worker that may still be busy, then spawn a replacement
324
+ * if a waiter is queued. */
325
+ private retireWorker(worker: Worker): void {
326
+ const idx = this.workers.indexOf(worker);
327
+ if (idx !== -1) this.workers.splice(idx, 1);
328
+ void worker.terminate();
329
+ const next = this.waiters.shift();
330
+ if (next) next.resolve(this.spawn());
331
+ }
332
+
333
+ private spawn(): Worker {
334
+ const worker = new Worker(WORKER_SOURCE, { eval: true });
335
+ // Unref so idle workers never keep the event loop alive (#1674).
336
+ worker.unref();
337
+ this.workers.push(worker);
338
+ return worker;
339
+ }
340
+
341
+ private dispatch(
342
+ worker: Worker,
343
+ task: ScoreTask,
344
+ abortSignal: AbortSignal | undefined,
345
+ onAbandon: () => void
346
+ ): Promise<ArchiveScoreResult[]> {
347
+ return new Promise<ArchiveScoreResult[]>((resolve, reject) => {
348
+ let settled = false;
349
+ const timer = setTimeout(() => {
350
+ if (settled) return;
351
+ settled = true;
352
+ cleanup();
353
+ onAbandon();
354
+ log.debug(`archive-scoring dispatch timed out after ${DISPATCH_TIMEOUT_MS}ms — falling back to sync`);
355
+ reject(new Error(`archive-scoring dispatch timed out after ${DISPATCH_TIMEOUT_MS}ms`));
356
+ }, DISPATCH_TIMEOUT_MS);
357
+
358
+ const onMessage = (reply: ScoreReply) => {
359
+ if (settled) return;
360
+ settled = true;
361
+ cleanup();
362
+ if (reply.ok) resolve(reply.results);
363
+ else reject(new Error(reply.error));
364
+ };
365
+ const onError = (err: Error) => {
366
+ if (settled) return;
367
+ settled = true;
368
+ cleanup();
369
+ onAbandon();
370
+ reject(err);
371
+ };
372
+ // worker.terminate() ends via 'exit', not 'error' — listen so in-flight
373
+ // dispatches during pool shutdown reject immediately instead of hanging
374
+ // until the 120s timeout (#1674).
375
+ const onExit = (code: number) => {
376
+ if (settled) return;
377
+ settled = true;
378
+ cleanup();
379
+ onAbandon();
380
+ reject(new Error(`archive-scoring worker exited with code ${code}`));
381
+ };
382
+ const onAbort = () => {
383
+ if (settled) return;
384
+ settled = true;
385
+ cleanup();
386
+ onAbandon();
387
+ resolve([]);
388
+ };
389
+
390
+ const cleanup = () => {
391
+ clearTimeout(timer);
392
+ worker.off("message", onMessage);
393
+ worker.off("error", onError);
394
+ worker.off("exit", onExit);
395
+ abortSignal?.removeEventListener("abort", onAbort);
396
+ };
397
+
398
+ worker.on("message", onMessage);
399
+ worker.on("error", onError);
400
+ worker.on("exit", onExit);
401
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
402
+ worker.postMessage(task);
403
+ });
404
+ }
405
+ }
406
+
407
+ /**
408
+ * Off-thread scoring via a `worker_threads` pool.
409
+ *
410
+ * Concurrent `score()` calls are dispatched to separate workers, giving
411
+ * genuine multi-core parallelism: K concurrent recalls run on K cores
412
+ * instead of serializing on the main JS thread. If the pool cannot be
413
+ * created (e.g. restricted runtime), it transparently falls back to
414
+ * {@link SyncArchiveScoring} so recall never breaks.
415
+ */
416
+ export class OffThreadArchiveScoring implements ArchiveScoringStrategy {
417
+ private pool: ArchiveScoringWorkerPool | null = null;
418
+ private poolFailed = false;
419
+ private readonly syncFallback = new SyncArchiveScoring();
420
+
421
+ constructor(poolSize?: number) {
422
+ if (poolSize !== undefined) {
423
+ this.pool = new ArchiveScoringWorkerPool(poolSize);
424
+ }
425
+ }
426
+
427
+ async score(
428
+ items: ReadonlyArray<ArchiveScoreItem>,
429
+ tokens: ReadonlyArray<string>,
430
+ abortSignal?: AbortSignal
431
+ ): Promise<ArchiveScoreResult[]> {
432
+ if (items.length === 0 || tokens.length === 0) return [];
433
+ if (abortSignal?.aborted) return [];
434
+
435
+ // Lazy pool init — workers are only created when the cold-fallback path
436
+ // is first hit, so hot-path recall pays zero overhead.
437
+ if (this.pool === null && !this.poolFailed) {
438
+ try {
439
+ this.pool = new ArchiveScoringWorkerPool();
440
+ } catch (err) {
441
+ this.poolFailed = true;
442
+ log.debug(`archive-scoring: worker pool unavailable, using sync fallback — ${(err as Error).message}`);
443
+ }
444
+ }
445
+
446
+ if (this.pool !== null) {
447
+ const dispatchStart = Date.now();
448
+ try {
449
+ const task: ScoreTask = {
450
+ items: items as ArchiveScoreItem[],
451
+ tokens: tokens as string[],
452
+ };
453
+ const results = await this.pool.run(task, abortSignal);
454
+ if (abortSignal?.aborted) return [];
455
+ return results;
456
+ } catch (err) {
457
+ // Timeout or worker error — fall back to sync scoring so recall
458
+ // quality is never silently dropped. An already-aborted signal
459
+ // short-circuits first. If the dispatch consumed most of the timeout
460
+ // budget (genuine timeout, not a fast error), skip the sync rescore —
461
+ // the recall deadline has very likely expired by then (#1674).
462
+ if (abortSignal?.aborted) return [];
463
+ if (Date.now() - dispatchStart > DISPATCH_TIMEOUT_MS * 0.5) return [];
464
+ log.debug(`archive-scoring: worker dispatch failed, using sync fallback — ${(err as Error).message}`);
465
+ }
466
+ }
467
+
468
+ return this.syncFallback.score(items, tokens, abortSignal);
469
+ }
470
+
471
+ /** @internal — terminate the underlying pool (tests / shutdown). */
472
+ async terminate(): Promise<void> {
473
+ if (this.pool !== null) {
474
+ await this.pool.terminate();
475
+ this.pool = null;
476
+ }
477
+ }
478
+ }
479
+
480
+ // ─────────────────────────────────────────────────────────────────────────────
481
+ // Factory + process-wide default
482
+ // ─────────────────────────────────────────────────────────────────────────────
483
+
484
+ let defaultStrategy: ArchiveScoringStrategy | null = null;
485
+
486
+ /**
487
+ * Process-wide default archive-scoring strategy. Lazily creates an
488
+ * {@link OffThreadArchiveScoring} on first use. All orchestrator instances
489
+ * share one pool — a single daemon serves all concurrent sessions, so one
490
+ * shared pool is the correct sizing unit.
491
+ */
492
+ export function getDefaultArchiveScoring(): ArchiveScoringStrategy {
493
+ if (defaultStrategy === null) {
494
+ defaultStrategy = new OffThreadArchiveScoring();
495
+ }
496
+ return defaultStrategy;
497
+ }
498
+
499
+ /**
500
+ * Dispose the process-wide default archive-scoring strategy, terminating any
501
+ * worker threads. Called from `Orchestrator.destroy()` so worker threads don't
502
+ * outlive the orchestrator (#1674). The strategy is lazily recreated on the
503
+ * next cold-fallback recall, so this is safe to call from tests that create
504
+ * and destroy orchestrator instances.
505
+ */
506
+ export async function disposeDefaultArchiveScoring(): Promise<void> {
507
+ if (defaultStrategy !== null) {
508
+ const strategy = defaultStrategy;
509
+ defaultStrategy = null;
510
+ if (strategy instanceof OffThreadArchiveScoring) {
511
+ await strategy.terminate();
512
+ }
513
+ }
514
+ }
515
+
516
+ /**
517
+ * Create a fresh strategy instance (for tests that need isolation or
518
+ * explicit control over pool size / sync vs off-thread).
519
+ */
520
+ export function createArchiveScoring(opts?: {
521
+ poolSize?: number;
522
+ sync?: boolean;
523
+ }): ArchiveScoringStrategy {
524
+ if (opts?.sync) return new SyncArchiveScoring();
525
+ return new OffThreadArchiveScoring(opts?.poolSize);
526
+ }
package/src/types.ts CHANGED
@@ -3055,6 +3055,20 @@ export interface ExtractedFact {
3055
3055
  * when `temporal.biTemporal` is off.
3056
3056
  */
3057
3057
  eventTime?: string;
3058
+ /**
3059
+ * Per-fact source-turn timestamp for bi-temporal event-time resolution
3060
+ * (#1670). When set, `resolveFactEventTime` anchors this fact's
3061
+ * `eventTime` expression against THIS turn's timestamp instead of the
3062
+ * batch-wide latest turn timestamp — so a buffered conversation spanning
3063
+ * a date boundary resolves "yesterday" on an early-turn fact against
3064
+ * that early turn's date, not the last turn's.
3065
+ *
3066
+ * Programmatic-only: extractors that know the exact source turn set this
3067
+ * directly. The LLM extraction prompt never emits it (models cannot know
3068
+ * turn timestamps). Falls back to the earliest provenance span's
3069
+ * `observedAt`, then to the batch anchor, when absent.
3070
+ */
3071
+ sourceTurnTimestamp?: string;
3058
3072
  }
3059
3073
 
3060
3074
  export interface ExtractedReasoningTraceStep {