@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.
- package/dist/access-admin-ops-surface.d.ts +5 -5
- package/dist/access-admin-ops-surface.js +34 -34
- package/dist/access-boundary.d.ts +5 -5
- package/dist/access-boundary.js +35 -35
- package/dist/access-cli.js +83 -83
- package/dist/access-http.d.ts +5 -5
- package/dist/access-http.js +44 -44
- package/dist/access-lcm-surface.d.ts +5 -5
- package/dist/access-lcm-surface.js +34 -34
- package/dist/access-mcp.d.ts +5 -5
- package/dist/access-mcp.js +41 -41
- package/dist/access-observe-write-surface.d.ts +225 -0
- package/dist/access-observe-write-surface.js +128 -0
- package/dist/access-operations-batch.js +38 -38
- package/dist/access-operations.d.ts +5 -5
- package/dist/access-operations.js +40 -40
- package/dist/access-recall-surface.d.ts +5 -5
- package/dist/access-recall-surface.js +34 -34
- package/dist/access-schema.d.ts +10 -10
- package/dist/access-schema.js +3 -3
- package/dist/{access-service-DKRV1zxh.d.ts → access-service-CJwcDoQp.d.ts} +7 -8
- package/dist/access-service.d.ts +5 -5
- package/dist/access-service.js +34 -34
- package/dist/access-surface-catalog.d.ts +5 -5
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +1 -1
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/{auto-sync-G4YKBPOU.js → auto-sync-RZO5ZPHR.js} +3 -3
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +4 -4
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +7 -7
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +1 -1
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/{capsule-crypto-CZJSLEFG.js → capsule-crypto-FFWJ4X4A.js} +2 -2
- package/dist/{catalog-BEmNVRN0.d.ts → catalog-DaKAFbSJ.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-behavior.js +2 -2
- package/dist/causal-chain.js +2 -2
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +15 -15
- package/dist/causal-retrieval.js +2 -2
- package/dist/causal-trajectory-graph.d.ts +1 -1
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-M5QKGHCR.js → chunk-3I2IASNK.js} +2 -2
- package/dist/{chunk-JBO6UBIE.js → chunk-4AC57RCM.js} +4 -4
- package/dist/{chunk-3DOD6ICJ.js → chunk-4I45UZ4V.js} +5 -5
- package/dist/{chunk-TAPSNIMA.js → chunk-5CEJH5ZN.js} +4 -4
- package/dist/{chunk-CXCZ4LAI.js → chunk-5VWRYFQE.js} +7 -7
- package/dist/{chunk-TXALJRYO.js → chunk-5ZKT7WTR.js} +7 -7
- package/dist/{chunk-PYVZKEZZ.js → chunk-6DA5BBLS.js} +120 -120
- package/dist/{chunk-WS6GLJ7P.js → chunk-6DXXIXED.js} +4 -4
- package/dist/{chunk-THZ4WR5L.js → chunk-7APVFQ34.js} +1182 -1009
- package/dist/chunk-7APVFQ34.js.map +1 -0
- package/dist/{chunk-OH66EBCS.js → chunk-7CFGXWYX.js} +2 -2
- package/dist/{chunk-TJATYB3F.js → chunk-A6XCHI53.js} +5 -5
- package/dist/{chunk-2MHCWXX6.js → chunk-BTH6MQJT.js} +7 -7
- package/dist/{chunk-G7LCG56O.js → chunk-CXW6Y7SI.js} +4 -4
- package/dist/{chunk-L4AS7TIG.js → chunk-DWDDQLSZ.js} +2300 -2239
- package/dist/chunk-DWDDQLSZ.js.map +1 -0
- package/dist/{chunk-DS4QM6I7.js → chunk-EEKDRAWX.js} +4 -4
- package/dist/{chunk-GAH5755F.js → chunk-EF3VFQC4.js} +2 -2
- package/dist/{chunk-437S3G37.js → chunk-EJJQDP3H.js} +1 -1
- package/dist/{chunk-V4C4SFZB.js → chunk-EVVLGYHR.js} +3 -3
- package/dist/{chunk-ZJMSTFOR.js → chunk-GBXP62CL.js} +10 -10
- package/dist/{chunk-7AL5WNPW.js → chunk-GG6G2RYR.js} +9 -9
- package/dist/{chunk-BERWPOXU.js → chunk-H2T4OJNI.js} +4 -4
- package/dist/{chunk-2MR3MFQB.js → chunk-JTYM4HPA.js} +2 -2
- package/dist/{chunk-LD53WPMU.js → chunk-LQ6JI4VH.js} +4 -4
- package/dist/{chunk-GPFXPC2V.js → chunk-NG2NHB4C.js} +7 -7
- package/dist/{chunk-COZEVVXZ.js → chunk-NLXKSM46.js} +2 -2
- package/dist/{chunk-VSU2NZGC.js → chunk-OLAVQS6S.js} +6 -6
- package/dist/{chunk-ER3CCWK2.js → chunk-ORAK3LNZ.js} +2 -2
- package/dist/{chunk-WUVUTOUV.js → chunk-PBZQVVLG.js} +5 -5
- package/dist/{chunk-PJQMISPD.js → chunk-PPAEJXVF.js} +5 -5
- package/dist/{chunk-KURUULFF.js → chunk-PRX77NPC.js} +5 -5
- package/dist/{chunk-24463PIV.js → chunk-PS572XYO.js} +2 -2
- package/dist/{chunk-U2SMR57B.js → chunk-REZ4TUD2.js} +10 -10
- package/dist/{chunk-2QSZNTDO.js → chunk-RKNJBZ55.js} +4 -4
- package/dist/{chunk-V5UV7BOA.js → chunk-SQNED75R.js} +1 -1
- package/dist/{chunk-FLVL4L3L.js → chunk-TCKP4EHY.js} +2 -2
- package/dist/{chunk-YRBXBHU7.js → chunk-TXV3I2O6.js} +7 -7
- package/dist/{chunk-CJ276MDI.js → chunk-UNV6ZGTS.js} +2 -2
- package/dist/{chunk-2BFLDH5F.js → chunk-WEPWY4HB.js} +2 -2
- package/dist/{chunk-YQGHW5ML.js → chunk-WRFKZEO6.js} +4 -4
- package/dist/{chunk-7CAJLUNW.js → chunk-Y4TAFMUE.js} +6 -6
- package/dist/{chunk-TIFF32QK.js → chunk-YMGQXBPS.js} +2 -2
- package/dist/{chunk-T422SYM6.js → chunk-ZUDM75KG.js} +5 -5
- package/dist/{chunk-4DVYRPLL.js → chunk-ZUHD6XZZ.js} +43 -43
- package/dist/{chunk-BQ6J6P3K.js → chunk-ZW5SZ3TH.js} +12 -12
- package/dist/{cli-D-xw8Sp1.d.ts → cli-cZyANcmY.d.ts} +3 -3
- package/dist/cli.d.ts +6 -6
- package/dist/cli.js +59 -59
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +8 -8
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +8 -8
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +9 -9
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/contradiction/index.d.ts +1 -1
- package/dist/contradiction/index.js +2 -2
- package/dist/{contradiction-scan-2SODRNUF.js → contradiction-scan-3FJYWS3G.js} +3 -3
- package/dist/contradiction-scan-3FJYWS3G.js.map +1 -0
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/backend.js +2 -2
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +8 -8
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +4 -4
- package/dist/extraction-faithfulness.d.ts +1 -1
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/extraction.js +2 -2
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/graph-dashboard-diff.d.ts +1 -1
- package/dist/graph-dashboard-key.d.ts +1 -1
- package/dist/graph-dashboard-parser.d.ts +1 -1
- package/dist/graph-edge-reinforcement.d.ts +1 -1
- package/dist/graph-snapshot.d.ts +1 -1
- package/dist/graph.d.ts +1 -1
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +9 -9
- package/dist/index.js +126 -126
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/index.js +2 -2
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/local-model-endpoint.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +8 -8
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -7
- package/dist/maintenance/rebuild-memory-projection.js +9 -9
- package/dist/mcp-memory-inspector-app.d.ts +5 -5
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-worth-outcomes.d.ts +1 -1
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +2 -2
- package/dist/namespaces/migrate.js +18 -18
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +9 -9
- package/dist/namespaces/storage.d.ts +2 -2
- package/dist/namespaces/storage.js +9 -9
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +24 -24
- package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +12 -12
- package/dist/{orchestrator-D7ZGOQLX.d.ts → orchestrator-Xm6sK5jw.d.ts} +3 -3
- package/dist/orchestrator.d.ts +4 -4
- package/dist/orchestrator.js +82 -82
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray.d.ts +1 -1
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +2 -2
- package/dist/retrieval-agents.d.ts +1 -1
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +8 -8
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +8 -8
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/lancedb-backend.js +2 -2
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.js +2 -2
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/orama-backend.js +2 -2
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-7Sndms2M.d.ts → semantic-consolidation-bfXJNKVf.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +11 -11
- package/dist/semantic-rule-promotion.js +7 -7
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +8 -8
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/shared-context/manager.d.ts +3 -3
- package/dist/signal.d.ts +1 -1
- package/dist/storage.d.ts +69 -97
- package/dist/storage.js +22 -6
- package/dist/summarizer.d.ts +1 -1
- package/dist/summarizer.js +2 -2
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/capsule-import.js +2 -2
- package/dist/transfer/types.d.ts +12 -12
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-DTEFxFuL.d.ts → types-D1iBqhJA.d.ts} +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +8 -8
- package/package.json +2 -2
- package/src/access-observe-write-surface.ts +1007 -0
- package/src/access-service.ts +48 -794
- package/src/orchestration/self-deps.ts +9 -1
- package/src/storage/entity-store.ts +674 -0
- package/src/storage/memory-read-store.ts +756 -0
- package/src/storage.ts +111 -1191
- package/dist/chunk-L4AS7TIG.js.map +0 -1
- package/dist/chunk-THZ4WR5L.js.map +0 -1
- /package/dist/{capsule-crypto-CZJSLEFG.js.map → access-observe-write-surface.js.map} +0 -0
- /package/dist/{auto-sync-G4YKBPOU.js.map → auto-sync-RZO5ZPHR.js.map} +0 -0
- /package/dist/{contradiction-scan-2SODRNUF.js.map → capsule-crypto-FFWJ4X4A.js.map} +0 -0
- /package/dist/{chunk-M5QKGHCR.js.map → chunk-3I2IASNK.js.map} +0 -0
- /package/dist/{chunk-JBO6UBIE.js.map → chunk-4AC57RCM.js.map} +0 -0
- /package/dist/{chunk-3DOD6ICJ.js.map → chunk-4I45UZ4V.js.map} +0 -0
- /package/dist/{chunk-TAPSNIMA.js.map → chunk-5CEJH5ZN.js.map} +0 -0
- /package/dist/{chunk-CXCZ4LAI.js.map → chunk-5VWRYFQE.js.map} +0 -0
- /package/dist/{chunk-TXALJRYO.js.map → chunk-5ZKT7WTR.js.map} +0 -0
- /package/dist/{chunk-PYVZKEZZ.js.map → chunk-6DA5BBLS.js.map} +0 -0
- /package/dist/{chunk-WS6GLJ7P.js.map → chunk-6DXXIXED.js.map} +0 -0
- /package/dist/{chunk-OH66EBCS.js.map → chunk-7CFGXWYX.js.map} +0 -0
- /package/dist/{chunk-TJATYB3F.js.map → chunk-A6XCHI53.js.map} +0 -0
- /package/dist/{chunk-2MHCWXX6.js.map → chunk-BTH6MQJT.js.map} +0 -0
- /package/dist/{chunk-G7LCG56O.js.map → chunk-CXW6Y7SI.js.map} +0 -0
- /package/dist/{chunk-DS4QM6I7.js.map → chunk-EEKDRAWX.js.map} +0 -0
- /package/dist/{chunk-GAH5755F.js.map → chunk-EF3VFQC4.js.map} +0 -0
- /package/dist/{chunk-437S3G37.js.map → chunk-EJJQDP3H.js.map} +0 -0
- /package/dist/{chunk-V4C4SFZB.js.map → chunk-EVVLGYHR.js.map} +0 -0
- /package/dist/{chunk-ZJMSTFOR.js.map → chunk-GBXP62CL.js.map} +0 -0
- /package/dist/{chunk-7AL5WNPW.js.map → chunk-GG6G2RYR.js.map} +0 -0
- /package/dist/{chunk-BERWPOXU.js.map → chunk-H2T4OJNI.js.map} +0 -0
- /package/dist/{chunk-2MR3MFQB.js.map → chunk-JTYM4HPA.js.map} +0 -0
- /package/dist/{chunk-LD53WPMU.js.map → chunk-LQ6JI4VH.js.map} +0 -0
- /package/dist/{chunk-GPFXPC2V.js.map → chunk-NG2NHB4C.js.map} +0 -0
- /package/dist/{chunk-COZEVVXZ.js.map → chunk-NLXKSM46.js.map} +0 -0
- /package/dist/{chunk-VSU2NZGC.js.map → chunk-OLAVQS6S.js.map} +0 -0
- /package/dist/{chunk-ER3CCWK2.js.map → chunk-ORAK3LNZ.js.map} +0 -0
- /package/dist/{chunk-WUVUTOUV.js.map → chunk-PBZQVVLG.js.map} +0 -0
- /package/dist/{chunk-PJQMISPD.js.map → chunk-PPAEJXVF.js.map} +0 -0
- /package/dist/{chunk-KURUULFF.js.map → chunk-PRX77NPC.js.map} +0 -0
- /package/dist/{chunk-24463PIV.js.map → chunk-PS572XYO.js.map} +0 -0
- /package/dist/{chunk-U2SMR57B.js.map → chunk-REZ4TUD2.js.map} +0 -0
- /package/dist/{chunk-2QSZNTDO.js.map → chunk-RKNJBZ55.js.map} +0 -0
- /package/dist/{chunk-V5UV7BOA.js.map → chunk-SQNED75R.js.map} +0 -0
- /package/dist/{chunk-FLVL4L3L.js.map → chunk-TCKP4EHY.js.map} +0 -0
- /package/dist/{chunk-YRBXBHU7.js.map → chunk-TXV3I2O6.js.map} +0 -0
- /package/dist/{chunk-CJ276MDI.js.map → chunk-UNV6ZGTS.js.map} +0 -0
- /package/dist/{chunk-2BFLDH5F.js.map → chunk-WEPWY4HB.js.map} +0 -0
- /package/dist/{chunk-YQGHW5ML.js.map → chunk-WRFKZEO6.js.map} +0 -0
- /package/dist/{chunk-7CAJLUNW.js.map → chunk-Y4TAFMUE.js.map} +0 -0
- /package/dist/{chunk-TIFF32QK.js.map → chunk-YMGQXBPS.js.map} +0 -0
- /package/dist/{chunk-T422SYM6.js.map → chunk-ZUDM75KG.js.map} +0 -0
- /package/dist/{chunk-4DVYRPLL.js.map → chunk-ZUHD6XZZ.js.map} +0 -0
- /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
|
-
|
|
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
|
+
}
|