@remnic/core 9.3.754 → 9.3.756

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 (306) hide show
  1. package/dist/access-admin-ops-surface.d.ts +5 -5
  2. package/dist/access-admin-ops-surface.js +34 -34
  3. package/dist/access-boundary.d.ts +5 -5
  4. package/dist/access-boundary.js +35 -35
  5. package/dist/access-cli.js +83 -83
  6. package/dist/access-http.d.ts +5 -5
  7. package/dist/access-http.js +44 -44
  8. package/dist/access-lcm-surface.d.ts +5 -5
  9. package/dist/access-lcm-surface.js +34 -34
  10. package/dist/access-mcp.d.ts +5 -5
  11. package/dist/access-mcp.js +41 -41
  12. package/dist/access-observe-write-surface.d.ts +225 -0
  13. package/dist/access-observe-write-surface.js +128 -0
  14. package/dist/access-operations-batch.js +38 -38
  15. package/dist/access-operations.d.ts +5 -5
  16. package/dist/access-operations.js +40 -40
  17. package/dist/access-recall-surface.d.ts +5 -5
  18. package/dist/access-recall-surface.js +34 -34
  19. package/dist/access-schema.d.ts +10 -10
  20. package/dist/access-schema.js +3 -3
  21. package/dist/{access-service-DKRV1zxh.d.ts → access-service-CJwcDoQp.d.ts} +7 -8
  22. package/dist/access-service.d.ts +5 -5
  23. package/dist/access-service.js +34 -34
  24. package/dist/access-surface-catalog.d.ts +5 -5
  25. package/dist/action-confidence.d.ts +1 -1
  26. package/dist/active-memory-bridge.d.ts +1 -1
  27. package/dist/active-recall.d.ts +1 -1
  28. package/dist/active-recall.js +1 -1
  29. package/dist/adapters/index.js +4 -4
  30. package/dist/adapters/registry.js +2 -2
  31. package/dist/{auto-sync-G4YKBPOU.js → auto-sync-RZO5ZPHR.js} +3 -3
  32. package/dist/behavior-learner.d.ts +1 -1
  33. package/dist/behavior-signals.d.ts +1 -1
  34. package/dist/bootstrap.d.ts +4 -4
  35. package/dist/briefing.d.ts +1 -1
  36. package/dist/briefing.js +7 -7
  37. package/dist/buffer-surprise-report.d.ts +1 -1
  38. package/dist/buffer.d.ts +1 -1
  39. package/dist/calibration.d.ts +1 -1
  40. package/dist/capabilities.d.ts +1 -1
  41. package/dist/{capsule-crypto-CZJSLEFG.js → capsule-crypto-FFWJ4X4A.js} +2 -2
  42. package/dist/{catalog-BEmNVRN0.d.ts → catalog-DaKAFbSJ.d.ts} +1 -1
  43. package/dist/causal-behavior.d.ts +1 -1
  44. package/dist/causal-behavior.js +2 -2
  45. package/dist/causal-chain.js +2 -2
  46. package/dist/causal-consolidation.d.ts +1 -1
  47. package/dist/causal-consolidation.js +15 -15
  48. package/dist/causal-retrieval.js +2 -2
  49. package/dist/causal-trajectory-graph.d.ts +1 -1
  50. package/dist/causal-trajectory.js +1 -1
  51. package/dist/{chunk-M5QKGHCR.js → chunk-3I2IASNK.js} +2 -2
  52. package/dist/{chunk-JBO6UBIE.js → chunk-4AC57RCM.js} +4 -4
  53. package/dist/{chunk-3DOD6ICJ.js → chunk-4I45UZ4V.js} +5 -5
  54. package/dist/{chunk-TAPSNIMA.js → chunk-5CEJH5ZN.js} +4 -4
  55. package/dist/{chunk-CXCZ4LAI.js → chunk-5VWRYFQE.js} +7 -7
  56. package/dist/{chunk-TXALJRYO.js → chunk-5ZKT7WTR.js} +7 -7
  57. package/dist/{chunk-PYVZKEZZ.js → chunk-6DA5BBLS.js} +120 -120
  58. package/dist/{chunk-WS6GLJ7P.js → chunk-6DXXIXED.js} +4 -4
  59. package/dist/{chunk-THZ4WR5L.js → chunk-7APVFQ34.js} +1182 -1009
  60. package/dist/chunk-7APVFQ34.js.map +1 -0
  61. package/dist/{chunk-OH66EBCS.js → chunk-7CFGXWYX.js} +2 -2
  62. package/dist/{chunk-TJATYB3F.js → chunk-A6XCHI53.js} +5 -5
  63. package/dist/{chunk-2MHCWXX6.js → chunk-BTH6MQJT.js} +7 -7
  64. package/dist/{chunk-G7LCG56O.js → chunk-CXW6Y7SI.js} +4 -4
  65. package/dist/{chunk-L4AS7TIG.js → chunk-DWDDQLSZ.js} +2300 -2239
  66. package/dist/chunk-DWDDQLSZ.js.map +1 -0
  67. package/dist/{chunk-DS4QM6I7.js → chunk-EEKDRAWX.js} +4 -4
  68. package/dist/{chunk-GAH5755F.js → chunk-EF3VFQC4.js} +2 -2
  69. package/dist/{chunk-437S3G37.js → chunk-EJJQDP3H.js} +1 -1
  70. package/dist/{chunk-V4C4SFZB.js → chunk-EVVLGYHR.js} +3 -3
  71. package/dist/{chunk-ZJMSTFOR.js → chunk-GBXP62CL.js} +10 -10
  72. package/dist/{chunk-7AL5WNPW.js → chunk-GG6G2RYR.js} +9 -9
  73. package/dist/{chunk-BERWPOXU.js → chunk-H2T4OJNI.js} +4 -4
  74. package/dist/{chunk-2MR3MFQB.js → chunk-JTYM4HPA.js} +2 -2
  75. package/dist/{chunk-LD53WPMU.js → chunk-LQ6JI4VH.js} +4 -4
  76. package/dist/{chunk-GPFXPC2V.js → chunk-NG2NHB4C.js} +7 -7
  77. package/dist/{chunk-COZEVVXZ.js → chunk-NLXKSM46.js} +2 -2
  78. package/dist/{chunk-VSU2NZGC.js → chunk-OLAVQS6S.js} +6 -6
  79. package/dist/{chunk-ER3CCWK2.js → chunk-ORAK3LNZ.js} +2 -2
  80. package/dist/{chunk-WUVUTOUV.js → chunk-PBZQVVLG.js} +5 -5
  81. package/dist/{chunk-PJQMISPD.js → chunk-PPAEJXVF.js} +5 -5
  82. package/dist/{chunk-KURUULFF.js → chunk-PRX77NPC.js} +5 -5
  83. package/dist/{chunk-24463PIV.js → chunk-PS572XYO.js} +2 -2
  84. package/dist/{chunk-U2SMR57B.js → chunk-REZ4TUD2.js} +10 -10
  85. package/dist/{chunk-2QSZNTDO.js → chunk-RKNJBZ55.js} +4 -4
  86. package/dist/{chunk-V5UV7BOA.js → chunk-SQNED75R.js} +1 -1
  87. package/dist/{chunk-FLVL4L3L.js → chunk-TCKP4EHY.js} +2 -2
  88. package/dist/{chunk-YRBXBHU7.js → chunk-TXV3I2O6.js} +7 -7
  89. package/dist/{chunk-CJ276MDI.js → chunk-UNV6ZGTS.js} +2 -2
  90. package/dist/{chunk-2BFLDH5F.js → chunk-WEPWY4HB.js} +2 -2
  91. package/dist/{chunk-YQGHW5ML.js → chunk-WRFKZEO6.js} +4 -4
  92. package/dist/{chunk-7CAJLUNW.js → chunk-Y4TAFMUE.js} +6 -6
  93. package/dist/{chunk-TIFF32QK.js → chunk-YMGQXBPS.js} +2 -2
  94. package/dist/{chunk-T422SYM6.js → chunk-ZUDM75KG.js} +5 -5
  95. package/dist/{chunk-4DVYRPLL.js → chunk-ZUHD6XZZ.js} +43 -43
  96. package/dist/{chunk-BQ6J6P3K.js → chunk-ZW5SZ3TH.js} +12 -12
  97. package/dist/{cli-D-xw8Sp1.d.ts → cli-cZyANcmY.d.ts} +3 -3
  98. package/dist/cli.d.ts +6 -6
  99. package/dist/cli.js +59 -59
  100. package/dist/compounding/engine.d.ts +1 -1
  101. package/dist/compounding/engine.js +8 -8
  102. package/dist/compounding/preference-consolidator.d.ts +1 -1
  103. package/dist/compression-optimizer.d.ts +1 -1
  104. package/dist/config.d.ts +1 -1
  105. package/dist/config.js +1 -1
  106. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  107. package/dist/connectors/codex-materialize-runner.js +8 -8
  108. package/dist/connectors/codex-materialize.d.ts +1 -1
  109. package/dist/connectors/index.d.ts +1 -1
  110. package/dist/connectors/index.js +9 -9
  111. package/dist/consolidation-provenance-check.d.ts +1 -1
  112. package/dist/consolidation-undo.d.ts +1 -1
  113. package/dist/contradiction/index.d.ts +1 -1
  114. package/dist/contradiction/index.js +2 -2
  115. package/dist/{contradiction-scan-2SODRNUF.js → contradiction-scan-3FJYWS3G.js} +3 -3
  116. package/dist/contradiction-scan-3FJYWS3G.js.map +1 -0
  117. package/dist/conversation-index/backend.d.ts +1 -1
  118. package/dist/conversation-index/backend.js +2 -2
  119. package/dist/conversation-index/chunker.d.ts +1 -1
  120. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  121. package/dist/conversation-index/indexer.d.ts +1 -1
  122. package/dist/conversation-index/search.d.ts +1 -1
  123. package/dist/day-summary.d.ts +1 -1
  124. package/dist/delinearize.d.ts +1 -1
  125. package/dist/direct-answer-wiring.d.ts +1 -1
  126. package/dist/direct-answer.d.ts +1 -1
  127. package/dist/embedding-fallback.d.ts +1 -1
  128. package/dist/enrichment/index.d.ts +1 -1
  129. package/dist/entity-retrieval.d.ts +1 -1
  130. package/dist/entity-retrieval.js +8 -8
  131. package/dist/entity-schema.d.ts +1 -1
  132. package/dist/explicit-capture.d.ts +4 -4
  133. package/dist/extraction-faithfulness.d.ts +1 -1
  134. package/dist/extraction-judge-telemetry.d.ts +1 -1
  135. package/dist/extraction-judge-training.d.ts +1 -1
  136. package/dist/extraction-judge.d.ts +1 -1
  137. package/dist/extraction.d.ts +1 -1
  138. package/dist/extraction.js +2 -2
  139. package/dist/fallback-llm.d.ts +1 -1
  140. package/dist/graph-dashboard-diff.d.ts +1 -1
  141. package/dist/graph-dashboard-key.d.ts +1 -1
  142. package/dist/graph-dashboard-parser.d.ts +1 -1
  143. package/dist/graph-edge-reinforcement.d.ts +1 -1
  144. package/dist/graph-snapshot.d.ts +1 -1
  145. package/dist/graph.d.ts +1 -1
  146. package/dist/identity-continuity.d.ts +1 -1
  147. package/dist/importance.d.ts +1 -1
  148. package/dist/index.d.ts +9 -9
  149. package/dist/index.js +126 -126
  150. package/dist/intent.d.ts +1 -1
  151. package/dist/lcm/engine.d.ts +1 -1
  152. package/dist/lcm/engine.js +2 -2
  153. package/dist/lcm/index.d.ts +1 -1
  154. package/dist/lcm/index.js +2 -2
  155. package/dist/lcm/tools.d.ts +1 -1
  156. package/dist/lifecycle.d.ts +1 -1
  157. package/dist/live-connectors-runner.d.ts +1 -1
  158. package/dist/local-llm.d.ts +1 -1
  159. package/dist/local-model-endpoint.d.ts +1 -1
  160. package/dist/maintenance/memory-governance.d.ts +1 -1
  161. package/dist/maintenance/memory-governance.js +8 -8
  162. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -7
  163. package/dist/maintenance/rebuild-memory-projection.js +9 -9
  164. package/dist/mcp-memory-inspector-app.d.ts +5 -5
  165. package/dist/memory-action-policy.d.ts +1 -1
  166. package/dist/memory-cache.d.ts +1 -1
  167. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  168. package/dist/memory-projection-store.d.ts +1 -1
  169. package/dist/memory-provenance.d.ts +1 -1
  170. package/dist/memory-worth-outcomes.d.ts +1 -1
  171. package/dist/models-json.d.ts +1 -1
  172. package/dist/namespaces/migrate.d.ts +2 -2
  173. package/dist/namespaces/migrate.js +18 -18
  174. package/dist/namespaces/principal.d.ts +1 -1
  175. package/dist/namespaces/search.d.ts +1 -1
  176. package/dist/namespaces/search.js +9 -9
  177. package/dist/namespaces/storage.d.ts +2 -2
  178. package/dist/namespaces/storage.js +9 -9
  179. package/dist/native-knowledge.d.ts +1 -1
  180. package/dist/operator-toolkit.d.ts +1 -1
  181. package/dist/operator-toolkit.js +24 -24
  182. package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
  183. package/dist/orchestration/maintenance.d.ts +2 -2
  184. package/dist/orchestration/maintenance.js +12 -12
  185. package/dist/{orchestrator-D7ZGOQLX.d.ts → orchestrator-Xm6sK5jw.d.ts} +3 -3
  186. package/dist/orchestrator.d.ts +4 -4
  187. package/dist/orchestrator.js +82 -82
  188. package/dist/patterns-cli.d.ts +1 -1
  189. package/dist/policy-runtime.d.ts +1 -1
  190. package/dist/provenance.d.ts +1 -1
  191. package/dist/qmd-recall-cache.d.ts +1 -1
  192. package/dist/qmd.d.ts +1 -1
  193. package/dist/recall-disclosure-escalation.d.ts +1 -1
  194. package/dist/recall-explain-renderer.d.ts +1 -1
  195. package/dist/recall-planner-llm.d.ts +1 -1
  196. package/dist/recall-state.d.ts +1 -1
  197. package/dist/recall-tag-filter.d.ts +1 -1
  198. package/dist/recall-xray-cli.d.ts +1 -1
  199. package/dist/recall-xray-renderer.d.ts +1 -1
  200. package/dist/recall-xray.d.ts +1 -1
  201. package/dist/resolve-auth-token.d.ts +1 -1
  202. package/dist/resume-bundles.js +2 -2
  203. package/dist/retrieval-agents.d.ts +1 -1
  204. package/dist/retrieval-tiers.d.ts +1 -1
  205. package/dist/routing/engine.d.ts +1 -1
  206. package/dist/routing/store.d.ts +1 -1
  207. package/dist/search/embed-helper.d.ts +1 -1
  208. package/dist/search/factory.d.ts +1 -1
  209. package/dist/search/factory.js +8 -8
  210. package/dist/search/index.d.ts +1 -1
  211. package/dist/search/index.js +8 -8
  212. package/dist/search/lancedb-backend.d.ts +1 -1
  213. package/dist/search/lancedb-backend.js +2 -2
  214. package/dist/search/meilisearch-backend.d.ts +1 -1
  215. package/dist/search/meilisearch-backend.js +2 -2
  216. package/dist/search/noop-backend.d.ts +1 -1
  217. package/dist/search/orama-backend.d.ts +1 -1
  218. package/dist/search/orama-backend.js +2 -2
  219. package/dist/search/port.d.ts +1 -1
  220. package/dist/search/remote-backend.d.ts +1 -1
  221. package/dist/{semantic-consolidation-7Sndms2M.d.ts → semantic-consolidation-bfXJNKVf.d.ts} +1 -1
  222. package/dist/semantic-consolidation.d.ts +2 -2
  223. package/dist/semantic-consolidation.js +11 -11
  224. package/dist/semantic-rule-promotion.js +7 -7
  225. package/dist/semantic-rule-verifier.d.ts +1 -1
  226. package/dist/semantic-rule-verifier.js +8 -8
  227. package/dist/session-observer-bands.d.ts +1 -1
  228. package/dist/session-observer-state.d.ts +1 -1
  229. package/dist/shared-context/manager.d.ts +3 -3
  230. package/dist/signal.d.ts +1 -1
  231. package/dist/storage.d.ts +69 -97
  232. package/dist/storage.js +22 -6
  233. package/dist/summarizer.d.ts +1 -1
  234. package/dist/summarizer.js +2 -2
  235. package/dist/summary-snapshot.d.ts +1 -1
  236. package/dist/temporal-supersession.d.ts +1 -1
  237. package/dist/temporal-validity.d.ts +1 -1
  238. package/dist/threading.d.ts +1 -1
  239. package/dist/tier-migration.d.ts +1 -1
  240. package/dist/tier-routing.d.ts +1 -1
  241. package/dist/topics.d.ts +1 -1
  242. package/dist/transcript.d.ts +1 -1
  243. package/dist/transfer/backup.js +2 -2
  244. package/dist/transfer/capsule-export.js +2 -2
  245. package/dist/transfer/capsule-import.js +2 -2
  246. package/dist/transfer/types.d.ts +12 -12
  247. package/dist/trust-score-stage.d.ts +1 -1
  248. package/dist/trust-score.d.ts +1 -1
  249. package/dist/{types-DTEFxFuL.d.ts → types-D1iBqhJA.d.ts} +1 -1
  250. package/dist/types.d.ts +1 -1
  251. package/dist/utility-runtime.d.ts +1 -1
  252. package/dist/verified-recall.js +8 -8
  253. package/package.json +2 -2
  254. package/src/access-observe-write-surface.ts +1007 -0
  255. package/src/access-service.ts +48 -794
  256. package/src/orchestration/self-deps.ts +9 -1
  257. package/src/storage/entity-store.ts +674 -0
  258. package/src/storage/memory-read-store.ts +756 -0
  259. package/src/storage.ts +111 -1191
  260. package/dist/chunk-L4AS7TIG.js.map +0 -1
  261. package/dist/chunk-THZ4WR5L.js.map +0 -1
  262. /package/dist/{capsule-crypto-CZJSLEFG.js.map → access-observe-write-surface.js.map} +0 -0
  263. /package/dist/{auto-sync-G4YKBPOU.js.map → auto-sync-RZO5ZPHR.js.map} +0 -0
  264. /package/dist/{contradiction-scan-2SODRNUF.js.map → capsule-crypto-FFWJ4X4A.js.map} +0 -0
  265. /package/dist/{chunk-M5QKGHCR.js.map → chunk-3I2IASNK.js.map} +0 -0
  266. /package/dist/{chunk-JBO6UBIE.js.map → chunk-4AC57RCM.js.map} +0 -0
  267. /package/dist/{chunk-3DOD6ICJ.js.map → chunk-4I45UZ4V.js.map} +0 -0
  268. /package/dist/{chunk-TAPSNIMA.js.map → chunk-5CEJH5ZN.js.map} +0 -0
  269. /package/dist/{chunk-CXCZ4LAI.js.map → chunk-5VWRYFQE.js.map} +0 -0
  270. /package/dist/{chunk-TXALJRYO.js.map → chunk-5ZKT7WTR.js.map} +0 -0
  271. /package/dist/{chunk-PYVZKEZZ.js.map → chunk-6DA5BBLS.js.map} +0 -0
  272. /package/dist/{chunk-WS6GLJ7P.js.map → chunk-6DXXIXED.js.map} +0 -0
  273. /package/dist/{chunk-OH66EBCS.js.map → chunk-7CFGXWYX.js.map} +0 -0
  274. /package/dist/{chunk-TJATYB3F.js.map → chunk-A6XCHI53.js.map} +0 -0
  275. /package/dist/{chunk-2MHCWXX6.js.map → chunk-BTH6MQJT.js.map} +0 -0
  276. /package/dist/{chunk-G7LCG56O.js.map → chunk-CXW6Y7SI.js.map} +0 -0
  277. /package/dist/{chunk-DS4QM6I7.js.map → chunk-EEKDRAWX.js.map} +0 -0
  278. /package/dist/{chunk-GAH5755F.js.map → chunk-EF3VFQC4.js.map} +0 -0
  279. /package/dist/{chunk-437S3G37.js.map → chunk-EJJQDP3H.js.map} +0 -0
  280. /package/dist/{chunk-V4C4SFZB.js.map → chunk-EVVLGYHR.js.map} +0 -0
  281. /package/dist/{chunk-ZJMSTFOR.js.map → chunk-GBXP62CL.js.map} +0 -0
  282. /package/dist/{chunk-7AL5WNPW.js.map → chunk-GG6G2RYR.js.map} +0 -0
  283. /package/dist/{chunk-BERWPOXU.js.map → chunk-H2T4OJNI.js.map} +0 -0
  284. /package/dist/{chunk-2MR3MFQB.js.map → chunk-JTYM4HPA.js.map} +0 -0
  285. /package/dist/{chunk-LD53WPMU.js.map → chunk-LQ6JI4VH.js.map} +0 -0
  286. /package/dist/{chunk-GPFXPC2V.js.map → chunk-NG2NHB4C.js.map} +0 -0
  287. /package/dist/{chunk-COZEVVXZ.js.map → chunk-NLXKSM46.js.map} +0 -0
  288. /package/dist/{chunk-VSU2NZGC.js.map → chunk-OLAVQS6S.js.map} +0 -0
  289. /package/dist/{chunk-ER3CCWK2.js.map → chunk-ORAK3LNZ.js.map} +0 -0
  290. /package/dist/{chunk-WUVUTOUV.js.map → chunk-PBZQVVLG.js.map} +0 -0
  291. /package/dist/{chunk-PJQMISPD.js.map → chunk-PPAEJXVF.js.map} +0 -0
  292. /package/dist/{chunk-KURUULFF.js.map → chunk-PRX77NPC.js.map} +0 -0
  293. /package/dist/{chunk-24463PIV.js.map → chunk-PS572XYO.js.map} +0 -0
  294. /package/dist/{chunk-U2SMR57B.js.map → chunk-REZ4TUD2.js.map} +0 -0
  295. /package/dist/{chunk-2QSZNTDO.js.map → chunk-RKNJBZ55.js.map} +0 -0
  296. /package/dist/{chunk-V5UV7BOA.js.map → chunk-SQNED75R.js.map} +0 -0
  297. /package/dist/{chunk-FLVL4L3L.js.map → chunk-TCKP4EHY.js.map} +0 -0
  298. /package/dist/{chunk-YRBXBHU7.js.map → chunk-TXV3I2O6.js.map} +0 -0
  299. /package/dist/{chunk-CJ276MDI.js.map → chunk-UNV6ZGTS.js.map} +0 -0
  300. /package/dist/{chunk-2BFLDH5F.js.map → chunk-WEPWY4HB.js.map} +0 -0
  301. /package/dist/{chunk-YQGHW5ML.js.map → chunk-WRFKZEO6.js.map} +0 -0
  302. /package/dist/{chunk-7CAJLUNW.js.map → chunk-Y4TAFMUE.js.map} +0 -0
  303. /package/dist/{chunk-TIFF32QK.js.map → chunk-YMGQXBPS.js.map} +0 -0
  304. /package/dist/{chunk-T422SYM6.js.map → chunk-ZUDM75KG.js.map} +0 -0
  305. /package/dist/{chunk-4DVYRPLL.js.map → chunk-ZUHD6XZZ.js.map} +0 -0
  306. /package/dist/{chunk-BQ6J6P3K.js.map → chunk-ZW5SZ3TH.js.map} +0 -0
@@ -28,7 +28,15 @@ export function selfDeps<T extends object>(self: object): T {
28
28
  return new Proxy(Object.create(null) as object, {
29
29
  get(_target, prop) {
30
30
  const value = source[prop as string];
31
- return typeof value === "function"
31
+ // Bind ONLY prototype/concise methods (they have no own `prototype`
32
+ // property). Class-valued deps (e.g. `storageManagerClass`, used by
33
+ // the storage decomposition to reach shared static caches) must pass
34
+ // through unbound: Function.prototype.bind returns a bound function
35
+ // WITHOUT the original's own static properties, which silently
36
+ // strips the class's statics. Plain data and arrows also pass
37
+ // through unchanged (arrows ignore bind receivers anyway).
38
+ return typeof value === "function" &&
39
+ !Object.prototype.hasOwnProperty.call(value, "prototype")
32
40
  ? (value as (...args: unknown[]) => unknown).bind(self)
33
41
  : value;
34
42
  },
@@ -0,0 +1,674 @@
1
+ /**
2
+ * Entity store (extracted from storage.ts StorageManager; god-file
3
+ * decomposition, #1526 playbook: verbatim move + live selfDeps wiring so
4
+ * Object.create(prototype) fakes and instance stubs keep working).
5
+ *
6
+ * Owns the entity file subsystem:
7
+ * - entity name normalization (alias table aware)
8
+ * - entity read/write (incl. schema-validated structured sections)
9
+ * - entity synthesis updates and knowledge-index construction
10
+ * - fragmented-entity merge (consolidation)
11
+ */
12
+
13
+ import { readdir, unlink } from "node:fs/promises";
14
+ import path from "node:path";
15
+ import { normalizeEntityStructuredSection, sortStructuredSectionsBySchema } from "../entity-schema.js";
16
+ import { log } from "../logger.js";
17
+ import { getCachedEntities, setCachedEntities } from "../memory-cache.js";
18
+ import type { VersionTrigger } from "../page-versioning.js";
19
+ import { SecureStoreLockedError } from "../secure-store/secure-fs.js";
20
+ import type { EntityFile, EntityStructuredSection, PluginConfig, ScoredEntity } from "../types.js";
21
+ import { isErrnoCode } from "../utils/errno.js";
22
+ import {
23
+ buildEntitySchemaCacheKey,
24
+ compareEntityTimestamps,
25
+ compileEntityFacts,
26
+ countEntityStructuredFacts,
27
+ fingerprintEntityStructuredFacts,
28
+ normalizeEntityName as normalizeEntityNameShared,
29
+ normalizeStructuredSectionFacts,
30
+ parseEntityFile,
31
+ serializeEntityFile,
32
+ StorageManager,
33
+ } from "../storage.js";
34
+
35
+ export interface EntityStoreDeps {
36
+ /** Live class object of the host instance — shared static caches (see storage.ts storageManagerClass). */
37
+ readonly storageManagerClass: typeof StorageManager;
38
+ readonly baseDir: string;
39
+ bumpMemoryStatusVersion(): void;
40
+ ensureDirectories(): Promise<void>;
41
+ readonly entitiesDir: string;
42
+ readonly entitySchemas: PluginConfig["entitySchemas"] | undefined;
43
+ findMatchingEntity(proposedName: string, type: string): Promise<string | null>;
44
+ getEntityCacheSecureStoreKey(): string;
45
+ getMemoryStatusVersion(): number;
46
+ invalidateKnowledgeIndexCache(): void;
47
+ knowledgeIndexCache: { result: string; builtAt: number } | null;
48
+ normalizeEntityName(raw: string, type: string): string;
49
+ readAllEntityFiles(): Promise<EntityFile[]>;
50
+ readStorageSecureFile(filePath: string): Promise<string>;
51
+ removeEntitySynthesisQueueEntries(entityNames: string[]): Promise<void>;
52
+ resolveEntityFilePath(name: string): string | null;
53
+ snapshotBeforeWrite(filePath: string, trigger: VersionTrigger): Promise<void>;
54
+ readonly userAliases: Record<string, string>;
55
+ writeStorageSecureFile(filePath: string, content: string | Buffer): Promise<void>;
56
+ }
57
+
58
+ export class EntityStore {
59
+ constructor(
60
+ private readonly deps: EntityStoreDeps,
61
+ ) {}
62
+
63
+ /** Normalize an entity name using this store's alias table. */
64
+ normalizeEntityName(raw: string, type: string): string {
65
+ return normalizeEntityNameShared(raw, type, this.deps.userAliases);
66
+ }
67
+
68
+ async writeEntity(
69
+ name: string,
70
+ type: string,
71
+ facts: string[],
72
+ options: {
73
+ timestamp?: string;
74
+ source?: string;
75
+ sessionKey?: string;
76
+ principal?: string;
77
+ structuredSections?: EntityStructuredSection[];
78
+ } = {},
79
+ ): Promise<string> {
80
+ await this.deps.ensureDirectories();
81
+ if (typeof name !== "string" || !name.trim() || typeof type !== "string" || !type.trim()) {
82
+ log.warn("writeEntity: invalid entity payload, skipping", {
83
+ nameType: typeof name,
84
+ typeType: typeof type,
85
+ });
86
+ return "";
87
+ }
88
+ const safeFacts = Array.isArray(facts)
89
+ ? [...new Set(
90
+ facts
91
+ .filter((fact) => typeof fact === "string")
92
+ .map((fact) => fact.trim())
93
+ .filter((fact) => fact.length > 0),
94
+ )]
95
+ : [];
96
+ let normalized = this.deps.normalizeEntityName(name, type);
97
+
98
+ // Check for fuzzy match against existing entities before creating a new file
99
+ const match = await this.deps.findMatchingEntity(name, type);
100
+ if (match && match !== normalized) {
101
+ log.debug(`fuzzy match: "${normalized}" → existing "${match}"`);
102
+ normalized = match;
103
+ }
104
+
105
+ const filePath = path.join(this.deps.entitiesDir, `${normalized}.md`);
106
+
107
+ // Parse existing file to preserve relationships/activity/aliases/summary
108
+ let entity: EntityFile = {
109
+ name,
110
+ type,
111
+ created: "",
112
+ updated: new Date().toISOString(),
113
+ facts: [],
114
+ summary: undefined,
115
+ synthesis: undefined,
116
+ synthesisUpdatedAt: undefined,
117
+ synthesisVersion: undefined,
118
+ synthesisStructuredFactCount: undefined,
119
+ synthesisStructuredFactDigest: undefined,
120
+ timeline: [],
121
+ relationships: [],
122
+ activity: [],
123
+ aliases: [],
124
+ };
125
+ try {
126
+ const existing = await this.deps.readStorageSecureFile(filePath);
127
+ entity = parseEntityFile(existing, this.deps.entitySchemas);
128
+ } catch (err) {
129
+ if (err instanceof SecureStoreLockedError) throw err;
130
+ if (!isErrnoCode(err, "ENOENT")) throw err;
131
+ // File doesn't exist yet
132
+ }
133
+
134
+ const timestamp = options.timestamp?.trim() || new Date().toISOString();
135
+ const source = options.source?.trim() || undefined;
136
+ const sessionKey = options.sessionKey?.trim() || undefined;
137
+ const principal = options.principal?.trim() || undefined;
138
+ const structuredSectionMap = new Map(
139
+ (entity.structuredSections ?? []).map((section) => [section.key, {
140
+ ...section,
141
+ facts: [...section.facts],
142
+ }]),
143
+ );
144
+ for (const section of options.structuredSections ?? []) {
145
+ const normalizedSection = normalizeEntityStructuredSection(type, section, this.deps.entitySchemas);
146
+ const normalizedFacts = normalizeStructuredSectionFacts(section.facts);
147
+ if (normalizedFacts.length === 0) continue;
148
+ const existingSection = structuredSectionMap.get(normalizedSection.key);
149
+ if (!existingSection) {
150
+ structuredSectionMap.set(normalizedSection.key, {
151
+ key: normalizedSection.key,
152
+ title: normalizedSection.title,
153
+ facts: normalizedFacts,
154
+ });
155
+ continue;
156
+ }
157
+ existingSection.facts = normalizeStructuredSectionFacts([...existingSection.facts, ...normalizedFacts]);
158
+ if (!existingSection.title.trim() && normalizedSection.title.trim()) {
159
+ existingSection.title = normalizedSection.title;
160
+ }
161
+ }
162
+ for (const fact of safeFacts) {
163
+ const nextEntry = {
164
+ timestamp,
165
+ text: fact,
166
+ ...(source ? { source } : {}),
167
+ ...(sessionKey ? { sessionKey } : {}),
168
+ ...(principal ? { principal } : {}),
169
+ };
170
+ const alreadyPresent = entity.timeline.some((entry) =>
171
+ entry.timestamp === nextEntry.timestamp
172
+ && entry.text === nextEntry.text
173
+ && entry.source === nextEntry.source
174
+ && entry.sessionKey === nextEntry.sessionKey
175
+ && entry.principal === nextEntry.principal
176
+ );
177
+ if (alreadyPresent) continue;
178
+ entity.timeline.push(nextEntry);
179
+ }
180
+ entity.structuredSections = sortStructuredSectionsBySchema(
181
+ type,
182
+ Array.from(structuredSectionMap.values()).filter((section) => section.facts.length > 0),
183
+ this.deps.entitySchemas,
184
+ );
185
+ entity.facts = compileEntityFacts(entity.timeline, entity.structuredSections);
186
+ entity.summary = entity.synthesis || entity.summary;
187
+ entity.name = name;
188
+ entity.type = type;
189
+ entity.created = entity.created || timestamp;
190
+ entity.updated = new Date().toISOString();
191
+
192
+ await this.deps.snapshotBeforeWrite(filePath, "write");
193
+ await this.deps.writeStorageSecureFile(filePath, serializeEntityFile(entity, this.deps.entitySchemas));
194
+ this.deps.invalidateKnowledgeIndexCache();
195
+ this.deps.bumpMemoryStatusVersion(); // invalidate entity cache
196
+ log.debug(`wrote entity ${normalized}`);
197
+ return normalized;
198
+ }
199
+
200
+ async readEntity(name: string): Promise<string> {
201
+ const filePath = this.deps.resolveEntityFilePath(name);
202
+ if (filePath === null) {
203
+ return "";
204
+ }
205
+ try {
206
+ return await this.deps.readStorageSecureFile(filePath);
207
+ } catch (err) {
208
+ if (err instanceof SecureStoreLockedError) throw err;
209
+ if (!isErrnoCode(err, "ENOENT")) throw err;
210
+ return "";
211
+ }
212
+ }
213
+
214
+ /** Return sorted list of entity filenames (without .md extension) */
215
+ async listEntityNames(): Promise<string[]> {
216
+ try {
217
+ const entries = await readdir(this.deps.entitiesDir);
218
+ return entries
219
+ .filter((e) => e.endsWith(".md"))
220
+ .map((e) => e.replace(".md", ""))
221
+ .sort();
222
+ } catch {
223
+ return [];
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Set or rewrite the synthesis layer of an entity file.
229
+ */
230
+ async updateEntitySynthesis(
231
+ name: string,
232
+ synthesis: string,
233
+ options: {
234
+ entityUpdatedAt?: string;
235
+ synthesisStructuredFactCount?: number;
236
+ synthesisStructuredFactDigest?: string;
237
+ synthesisTimelineCount?: number;
238
+ updatedAt?: string;
239
+ incrementVersion?: boolean;
240
+ } = {},
241
+ ): Promise<void> {
242
+ const filePath = path.join(this.deps.entitiesDir, `${name}.md`);
243
+ let entity: EntityFile;
244
+ try {
245
+ const content = await this.deps.readStorageSecureFile(filePath);
246
+ entity = parseEntityFile(content, this.deps.entitySchemas);
247
+ } catch (err) {
248
+ if (err instanceof SecureStoreLockedError) throw err;
249
+ if (!isErrnoCode(err, "ENOENT")) throw err;
250
+ log.debug(`updateEntitySynthesis: entity file ${name}.md not found`);
251
+ return;
252
+ }
253
+
254
+ const updatedAt = options.updatedAt?.trim() || entity.synthesisUpdatedAt?.trim() || undefined;
255
+ const entityUpdatedAt = options.entityUpdatedAt?.trim() || updatedAt || entity.updated || new Date().toISOString();
256
+ const synthesisTimelineCount = Number.isInteger(options.synthesisTimelineCount)
257
+ && (options.synthesisTimelineCount ?? 0) >= 0
258
+ ? options.synthesisTimelineCount
259
+ : undefined;
260
+ const synthesisStructuredFactCount = Number.isInteger(options.synthesisStructuredFactCount)
261
+ && (options.synthesisStructuredFactCount ?? 0) >= 0
262
+ ? options.synthesisStructuredFactCount
263
+ : countEntityStructuredFacts(entity);
264
+ const synthesisStructuredFactDigest = options.synthesisStructuredFactDigest?.trim()
265
+ || fingerprintEntityStructuredFacts(entity);
266
+ entity.synthesis = synthesis.trim();
267
+ entity.summary = entity.synthesis;
268
+ entity.synthesisUpdatedAt = updatedAt;
269
+ entity.synthesisTimelineCount = synthesisTimelineCount;
270
+ entity.synthesisStructuredFactCount = synthesisStructuredFactCount;
271
+ entity.synthesisStructuredFactDigest = synthesisStructuredFactDigest;
272
+ entity.synthesisVersion = Math.max(0, entity.synthesisVersion ?? 0)
273
+ + (options.incrementVersion === false ? 0 : 1);
274
+ entity.updated = entityUpdatedAt;
275
+ await this.deps.writeStorageSecureFile(filePath, serializeEntityFile(entity, this.deps.entitySchemas));
276
+ await this.deps.removeEntitySynthesisQueueEntries([
277
+ ...new Set([name, this.deps.normalizeEntityName(entity.name, entity.type)]),
278
+ ]);
279
+ this.deps.invalidateKnowledgeIndexCache();
280
+ this.deps.bumpMemoryStatusVersion(); // invalidate entity cache
281
+ }
282
+
283
+ /**
284
+ * Read all entity files and return lightweight EntityFile objects.
285
+ * Parsing is fast (~50-100ms for ~1,800 files) since entity files are small.
286
+ */
287
+ async readAllEntityFiles(): Promise<EntityFile[]> {
288
+ const currentVersion = this.deps.getMemoryStatusVersion();
289
+ const schemaCacheKey = buildEntitySchemaCacheKey(this.deps.entitySchemas);
290
+ const cacheKey = `${this.deps.getEntityCacheSecureStoreKey()}\u0000${schemaCacheKey}`;
291
+ const cached = getCachedEntities(this.deps.baseDir, currentVersion, cacheKey);
292
+ if (cached) return cached;
293
+
294
+ try {
295
+ const entries = await readdir(this.deps.entitiesDir);
296
+ const mdFiles = entries.filter((e) => e.endsWith(".md"));
297
+ if (mdFiles.length === 0) return [];
298
+
299
+ // Read all entity files in parallel batches to avoid O(N) sequential I/O.
300
+ // With 3000+ entity files, sequential reads can take 15-20s under load.
301
+ // Batching at 100 keeps file-descriptor pressure manageable while staying fast.
302
+ const BATCH_SIZE = 100;
303
+ const entities: EntityFile[] = [];
304
+ for (let i = 0; i < mdFiles.length; i += BATCH_SIZE) {
305
+ const batch = mdFiles.slice(i, i + BATCH_SIZE);
306
+ const results = await Promise.all(
307
+ batch.map(async (entry) => {
308
+ try {
309
+ return await this.deps.readStorageSecureFile(path.join(this.deps.entitiesDir, entry));
310
+ } catch (err) {
311
+ if (err instanceof SecureStoreLockedError) throw err;
312
+ if (!isErrnoCode(err, "ENOENT")) throw err;
313
+ return null;
314
+ }
315
+ }),
316
+ );
317
+ for (const content of results) {
318
+ if (content !== null) entities.push(parseEntityFile(content, this.deps.entitySchemas));
319
+ }
320
+ }
321
+
322
+ setCachedEntities(this.deps.baseDir, entities, currentVersion, cacheKey);
323
+ return entities;
324
+ } catch (err) {
325
+ if (err instanceof SecureStoreLockedError || !isErrnoCode(err, "ENOENT")) throw err;
326
+ // Directory doesn't exist yet
327
+ return [];
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Build the Knowledge Index: a compact markdown table of top-scored entities.
333
+ * Respects maxEntities and maxChars limits from config.
334
+ */
335
+ async buildKnowledgeIndex(
336
+ config: PluginConfig,
337
+ overrides?: { maxEntities?: number; maxChars?: number },
338
+ ): Promise<{ result: string; cached: boolean }> {
339
+ const useDefaultLimits =
340
+ overrides?.maxEntities === undefined &&
341
+ overrides?.maxChars === undefined;
342
+ // Return cached index if still fresh
343
+ if (
344
+ useDefaultLimits &&
345
+ this.deps.knowledgeIndexCache &&
346
+ Date.now() - this.deps.knowledgeIndexCache.builtAt < this.deps.storageManagerClass.KNOWLEDGE_INDEX_CACHE_TTL_MS
347
+ ) {
348
+ return { result: this.deps.knowledgeIndexCache.result, cached: true };
349
+ }
350
+
351
+ const entities = await this.deps.readAllEntityFiles();
352
+ if (entities.length === 0) {
353
+ if (useDefaultLimits) this.deps.knowledgeIndexCache = { result: "", builtAt: Date.now() };
354
+ return { result: "", cached: false };
355
+ }
356
+
357
+ const now = new Date();
358
+ const scored: ScoredEntity[] = entities.map((e) => ({
359
+ name: e.name,
360
+ type: e.type,
361
+ score: this.deps.storageManagerClass.scoreEntity(e, now),
362
+ factCount: e.facts.length,
363
+ summary: e.synthesis ?? e.summary,
364
+ topRelationships: e.relationships.slice(0, 3).map((r) => r.target),
365
+ }));
366
+
367
+ // Sort by score descending, take top N
368
+ scored.sort((a, b) => b.score - a.score);
369
+ const maxEntities = typeof overrides?.maxEntities === "number"
370
+ ? Math.max(0, Math.floor(overrides.maxEntities))
371
+ : config.knowledgeIndexMaxEntities;
372
+ const topN = scored.slice(0, maxEntities);
373
+
374
+ if (topN.length === 0) {
375
+ if (useDefaultLimits) this.deps.knowledgeIndexCache = { result: "", builtAt: Date.now() };
376
+ return { result: "", cached: false };
377
+ }
378
+
379
+ // Build markdown table
380
+ const header = "## Knowledge Index\n\n| Entity | Type | Summary | Connected to |\n|--------|------|---------|-------------|";
381
+ const rows: string[] = [];
382
+ let totalChars = header.length;
383
+ const maxChars = typeof overrides?.maxChars === "number"
384
+ ? Math.max(0, Math.floor(overrides.maxChars))
385
+ : config.knowledgeIndexMaxChars;
386
+
387
+ for (const entity of topN) {
388
+ const summary = entity.summary || `${entity.factCount} facts`;
389
+ const connected = entity.topRelationships.length > 0
390
+ ? entity.topRelationships.join(", ")
391
+ : "—";
392
+ const row = `| ${entity.name} | ${entity.type} | ${summary} | ${connected} |`;
393
+
394
+ if (totalChars + row.length + 1 > maxChars) break;
395
+ rows.push(row);
396
+ totalChars += row.length + 1;
397
+ }
398
+
399
+ const result = rows.length === 0 ? "" : `${header}\n${rows.join("\n")}\n`;
400
+ if (useDefaultLimits) this.deps.knowledgeIndexCache = { result, builtAt: Date.now() };
401
+ return { result, cached: false };
402
+ }
403
+
404
+ /**
405
+ * Merge fragmented entity files that resolve to the same canonical name.
406
+ * Preserves relationships, activity, aliases, and summary from all fragments.
407
+ * Returns count of files merged.
408
+ */
409
+ async mergeFragmentedEntities(): Promise<number> {
410
+ let merged = 0;
411
+ try {
412
+ const entries = await readdir(this.deps.entitiesDir);
413
+ const mdFiles = entries.filter((e) => e.endsWith(".md"));
414
+
415
+ // Group files by their canonical name
416
+ const groups = new Map<string, string[]>();
417
+ for (const file of mdFiles) {
418
+ const baseName = file.replace(".md", "");
419
+ // Extract type and name from filename (type-rest-of-name)
420
+ const dashIdx = baseName.indexOf("-");
421
+ if (dashIdx === -1) continue;
422
+ const type = baseName.slice(0, dashIdx);
423
+ const restOfName = baseName.slice(dashIdx + 1);
424
+ const canonical = this.deps.normalizeEntityName(restOfName, type);
425
+
426
+ if (!groups.has(canonical)) groups.set(canonical, []);
427
+ groups.get(canonical)!.push(file);
428
+ }
429
+
430
+ // Merge groups with more than one file
431
+ for (const [canonical, files] of groups) {
432
+ if (files.length <= 1) continue;
433
+
434
+ // Parse all files and merge into a single EntityFile
435
+ const mergedEntity: EntityFile = {
436
+ name: "",
437
+ type: "other",
438
+ created: "",
439
+ updated: "",
440
+ extraFrontmatterLines: [],
441
+ preSectionLines: [],
442
+ facts: [],
443
+ summary: undefined,
444
+ synthesis: undefined,
445
+ synthesisUpdatedAt: undefined,
446
+ synthesisTimelineCount: undefined,
447
+ synthesisStructuredFactCount: undefined,
448
+ synthesisStructuredFactDigest: undefined,
449
+ synthesisVersion: undefined,
450
+ timeline: [],
451
+ relationships: [],
452
+ activity: [],
453
+ aliases: [],
454
+ structuredSections: [],
455
+ extraSections: [],
456
+ };
457
+
458
+ for (const file of files) {
459
+ const filePath = path.join(this.deps.entitiesDir, file);
460
+ try {
461
+ const content = await this.deps.readStorageSecureFile(filePath);
462
+ const parsed = parseEntityFile(content, this.deps.entitySchemas);
463
+
464
+ // Prefer specific types over "other"
465
+ if (!mergedEntity.type || mergedEntity.type === "other") {
466
+ mergedEntity.type = parsed.type;
467
+ }
468
+
469
+ // Keep latest update time
470
+ if (!mergedEntity.updated || compareEntityTimestamps(parsed.updated, mergedEntity.updated) > 0) {
471
+ mergedEntity.updated = parsed.updated;
472
+ }
473
+
474
+ const parsedCreated = parsed.created || parsed.updated;
475
+ const mergedCreated = mergedEntity.created?.trim() || "";
476
+ const parsedCreatedMs = parsedCreated ? Date.parse(parsedCreated) : Number.NaN;
477
+ const mergedCreatedMs = mergedCreated ? Date.parse(mergedCreated) : Number.NaN;
478
+ const parsedCreatedIsValid = Number.isFinite(parsedCreatedMs);
479
+ const mergedCreatedIsValid = Number.isFinite(mergedCreatedMs);
480
+ if (
481
+ parsedCreated &&
482
+ (
483
+ !mergedCreated
484
+ || (parsedCreatedIsValid && !mergedCreatedIsValid)
485
+ || (
486
+ parsedCreatedIsValid
487
+ && mergedCreatedIsValid
488
+ && parsedCreatedMs < mergedCreatedMs
489
+ )
490
+ || (
491
+ !parsedCreatedIsValid
492
+ && !mergedCreatedIsValid
493
+ && compareEntityTimestamps(parsedCreated, mergedCreated) < 0
494
+ )
495
+ )
496
+ ) {
497
+ mergedEntity.created = parsedCreated;
498
+ }
499
+
500
+ // Keep longest/best name
501
+ if (parsed.name.length > mergedEntity.name.length) {
502
+ mergedEntity.name = parsed.name;
503
+ }
504
+
505
+ const parsedSynthesisUpdatedAt = parsed.synthesisUpdatedAt?.trim() || undefined;
506
+ const mergedSynthesisUpdatedAt = mergedEntity.synthesisUpdatedAt?.trim() || undefined;
507
+
508
+ // Prefer the freshest synthesis/summary available.
509
+ if (
510
+ parsed.synthesis &&
511
+ (!mergedEntity.synthesis
512
+ || (!mergedSynthesisUpdatedAt && Boolean(parsedSynthesisUpdatedAt))
513
+ || (Boolean(mergedSynthesisUpdatedAt)
514
+ && Boolean(parsedSynthesisUpdatedAt)
515
+ && compareEntityTimestamps(parsedSynthesisUpdatedAt, mergedSynthesisUpdatedAt) > 0))
516
+ ) {
517
+ mergedEntity.synthesis = parsed.synthesis;
518
+ mergedEntity.summary = parsed.synthesis;
519
+ mergedEntity.synthesisUpdatedAt = parsedSynthesisUpdatedAt;
520
+ mergedEntity.synthesisTimelineCount = parsed.synthesisTimelineCount;
521
+ mergedEntity.synthesisStructuredFactCount = parsed.synthesisStructuredFactCount;
522
+ mergedEntity.synthesisStructuredFactDigest = parsed.synthesisStructuredFactDigest;
523
+ mergedEntity.synthesisVersion = parsed.synthesisVersion;
524
+ } else if (!mergedEntity.summary && parsed.summary) {
525
+ mergedEntity.summary = parsed.summary;
526
+ mergedEntity.synthesis = parsed.summary;
527
+ mergedEntity.synthesisUpdatedAt = parsedSynthesisUpdatedAt;
528
+ mergedEntity.synthesisTimelineCount = parsed.synthesisTimelineCount;
529
+ mergedEntity.synthesisStructuredFactCount = parsed.synthesisStructuredFactCount;
530
+ mergedEntity.synthesisStructuredFactDigest = parsed.synthesisStructuredFactDigest;
531
+ mergedEntity.synthesisVersion = parsed.synthesisVersion;
532
+ }
533
+
534
+ // Collect all timeline evidence; facts are derived below.
535
+ mergedEntity.timeline.push(...parsed.timeline);
536
+
537
+ // Collect relationships (dedup later)
538
+ mergedEntity.relationships.push(...parsed.relationships);
539
+
540
+ // Collect activity entries
541
+ mergedEntity.activity.push(...parsed.activity);
542
+
543
+ // Collect aliases
544
+ mergedEntity.aliases.push(...parsed.aliases);
545
+
546
+ const mergedStructuredSectionMap = new Map(
547
+ (mergedEntity.structuredSections ?? []).map((section) => [section.key, {
548
+ ...section,
549
+ facts: [...section.facts],
550
+ }]),
551
+ );
552
+ for (const section of parsed.structuredSections ?? []) {
553
+ const existingSection = mergedStructuredSectionMap.get(section.key);
554
+ if (!existingSection) {
555
+ mergedStructuredSectionMap.set(section.key, {
556
+ key: section.key,
557
+ title: section.title,
558
+ facts: [...new Set(section.facts.map((fact) => fact.trim()).filter((fact) => fact.length > 0))],
559
+ });
560
+ continue;
561
+ }
562
+
563
+ const mergedFacts = new Set(existingSection.facts.map((fact) => fact.trim()));
564
+ for (const fact of section.facts) {
565
+ const trimmed = fact.trim();
566
+ if (!trimmed) continue;
567
+ mergedFacts.add(trimmed);
568
+ }
569
+ existingSection.facts = Array.from(mergedFacts);
570
+ if (!existingSection.title.trim() && section.title.trim()) {
571
+ existingSection.title = section.title;
572
+ }
573
+ }
574
+ mergedEntity.structuredSections = Array.from(mergedStructuredSectionMap.values());
575
+
576
+ // Preserve custom metadata and user-authored freeform content from fragments.
577
+ mergedEntity.extraFrontmatterLines!.push(...(parsed.extraFrontmatterLines ?? []));
578
+ mergedEntity.preSectionLines!.push(...(parsed.preSectionLines ?? []));
579
+ mergedEntity.extraSections!.push(...(parsed.extraSections ?? []).map((section) => ({
580
+ title: section.title,
581
+ lines: [...section.lines],
582
+ })));
583
+ } catch (err) {
584
+ if (err instanceof SecureStoreLockedError) throw err;
585
+ if (!isErrnoCode(err, "ENOENT")) throw err;
586
+ // Skip unreadable
587
+ }
588
+ }
589
+
590
+ // Deduplicate timeline entries and derive facts from the timeline.
591
+ const timelineKeys = new Set<string>();
592
+ mergedEntity.timeline = mergedEntity.timeline.filter((entry) => {
593
+ const key = JSON.stringify([
594
+ entry.timestamp,
595
+ entry.source ?? "",
596
+ entry.sessionKey ?? "",
597
+ entry.principal ?? "",
598
+ entry.text,
599
+ ]);
600
+ if (timelineKeys.has(key)) return false;
601
+ timelineKeys.add(key);
602
+ return true;
603
+ });
604
+ // Deduplicate relationships by target+label
605
+ const relKeys = new Set<string>();
606
+ mergedEntity.relationships = mergedEntity.relationships.filter((r) => {
607
+ const key = `${r.target}::${r.label}`;
608
+ if (relKeys.has(key)) return false;
609
+ relKeys.add(key);
610
+ return true;
611
+ });
612
+
613
+ // Sort activity by date descending, deduplicate by date+note
614
+ const actKeys = new Set<string>();
615
+ mergedEntity.activity = mergedEntity.activity
616
+ .filter((a) => {
617
+ const key = `${a.date}::${a.note}`;
618
+ if (actKeys.has(key)) return false;
619
+ actKeys.add(key);
620
+ return true;
621
+ })
622
+ .sort((a, b) => b.date.localeCompare(a.date));
623
+
624
+ // Deduplicate aliases
625
+ mergedEntity.aliases = [...new Set(mergedEntity.aliases)];
626
+ mergedEntity.structuredSections = sortStructuredSectionsBySchema(
627
+ mergedEntity.type,
628
+ mergedEntity.structuredSections ?? [],
629
+ this.deps.entitySchemas,
630
+ );
631
+ mergedEntity.facts = compileEntityFacts(mergedEntity.timeline, mergedEntity.structuredSections);
632
+
633
+ const extraSectionKeys = new Set<string>();
634
+ mergedEntity.extraSections = (mergedEntity.extraSections ?? []).filter((section) => {
635
+ const key = `${section.title}::${section.lines.join("\n")}`;
636
+ if (extraSectionKeys.has(key)) return false;
637
+ extraSectionKeys.add(key);
638
+ return true;
639
+ });
640
+
641
+ // Fallback name from canonical
642
+ if (!mergedEntity.name) {
643
+ const dashIdx = canonical.indexOf("-");
644
+ mergedEntity.name = dashIdx !== -1 ? canonical.slice(dashIdx + 1) : canonical;
645
+ }
646
+
647
+ mergedEntity.created = mergedEntity.created || mergedEntity.updated || new Date().toISOString();
648
+ mergedEntity.updated = mergedEntity.updated || new Date().toISOString();
649
+
650
+ const canonicalPath = path.join(this.deps.entitiesDir, `${canonical}.md`);
651
+ await this.deps.writeStorageSecureFile(canonicalPath, serializeEntityFile(mergedEntity, this.deps.entitySchemas));
652
+
653
+ // Remove non-canonical files
654
+ for (const file of files) {
655
+ const filePath = path.join(this.deps.entitiesDir, file);
656
+ if (filePath !== canonicalPath) {
657
+ try {
658
+ await unlink(filePath);
659
+ merged++;
660
+ log.debug(`merged entity ${file} → ${canonical}.md`);
661
+ } catch {
662
+ // Ignore
663
+ }
664
+ }
665
+ }
666
+ }
667
+ } catch (err) {
668
+ if (err instanceof SecureStoreLockedError || !isErrnoCode(err, "ENOENT")) throw err;
669
+ // Directory doesn't exist yet
670
+ }
671
+
672
+ return merged;
673
+ }
674
+ }