@remnic/core 9.3.753 → 9.3.755

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