@remnic/core 9.50.2 → 9.51.0
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 +13 -11
- package/dist/access-admin-ops-surface.js +41 -39
- package/dist/access-authorization-probe.d.ts +13 -11
- package/dist/access-authorization-probe.js +42 -40
- package/dist/access-boundary.d.ts +13 -11
- package/dist/access-boundary.js +41 -39
- package/dist/access-cli.js +109 -104
- package/dist/access-cli.js.map +1 -1
- package/dist/access-coding-context-resolution.d.ts +3 -1
- package/dist/access-extraction-force-flush.d.ts +13 -11
- package/dist/access-extraction-force-flush.js +41 -39
- package/dist/access-health-types.d.ts +3 -1
- package/dist/access-http-lcm-compaction.d.ts +13 -11
- package/dist/access-http-lifecycle-flush.d.ts +13 -11
- package/dist/access-http-offline-stream.d.ts +13 -11
- package/dist/access-http-query.js +41 -39
- package/dist/access-http.d.ts +13 -11
- package/dist/access-http.js +52 -50
- package/dist/access-identity-continuity-surface.d.ts +11 -9
- package/dist/access-identity-continuity-surface.js +41 -39
- package/dist/access-lcm-surface.d.ts +13 -11
- package/dist/access-lcm-surface.js +41 -39
- package/dist/access-mcp.d.ts +13 -11
- package/dist/access-mcp.js +47 -45
- package/dist/access-memory-search-fanout.d.ts +4 -2
- package/dist/{access-namespace-preflight-Dj2QolFE.d.ts → access-namespace-preflight-Ds6CGlCP.d.ts} +1 -1
- package/dist/access-namespace-preflight.d.ts +5 -3
- package/dist/access-namespace-preflight.js +41 -39
- package/dist/access-observe-write-surface.d.ts +13 -11
- package/dist/access-observe-write-surface.js +41 -39
- package/dist/access-offline-manifest.d.ts +11 -9
- package/dist/access-operations-batch.js +43 -41
- package/dist/access-operations.d.ts +13 -11
- package/dist/access-operations.js +46 -44
- package/dist/access-recall-concurrency.d.ts +13 -11
- package/dist/access-recall-concurrency.js +41 -39
- package/dist/access-recall-response.d.ts +13 -11
- package/dist/access-recall-response.js +41 -39
- package/dist/access-recall-surface.d.ts +13 -11
- package/dist/access-recall-surface.js +41 -39
- package/dist/access-schema.js +3 -3
- package/dist/{access-service-DesbnTSa.d.ts → access-service-BY0KJ44c.d.ts} +7 -7
- package/dist/access-service-helpers.d.ts +13 -11
- package/dist/access-service.d.ts +13 -11
- package/dist/access-service.js +41 -39
- package/dist/access-surface-catalog.d.ts +13 -11
- package/dist/access-wearables-meetings-surface.d.ts +5 -3
- package/dist/action-confidence.d.ts +3 -1
- package/dist/active-memory-bridge.d.ts +3 -1
- package/dist/active-recall.d.ts +3 -1
- package/dist/active-recall.js +5 -3
- package/dist/active-recall.js.map +1 -1
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/ambient-provenance.d.ts +3 -1
- package/dist/archive-mutation-version.d.ts +9 -0
- package/dist/archive-mutation-version.js +20 -0
- package/dist/artifact-search.d.ts +3 -1
- package/dist/behavior-learner.d.ts +3 -1
- package/dist/behavior-signals.d.ts +3 -1
- package/dist/bootstrap.d.ts +11 -9
- package/dist/briefing.d.ts +4 -2
- package/dist/briefing.js +10 -9
- package/dist/buffer-surprise-report.d.ts +3 -1
- package/dist/buffer-turn-helpers.d.ts +3 -1
- package/dist/buffer.d.ts +4 -2
- package/dist/buffer.js +5 -5
- package/dist/bulk-import/index.d.ts +5 -3
- package/dist/calibration.d.ts +3 -1
- package/dist/capabilities.d.ts +3 -1
- package/dist/{catalog-D-G7RgXR.d.ts → catalog-D3B4RDy1.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +3 -1
- package/dist/causal-behavior.js +2 -2
- package/dist/causal-chain.js +2 -2
- package/dist/causal-consolidation.d.ts +3 -1
- package/dist/causal-consolidation.js +13 -12
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/causal-retrieval.js +2 -2
- package/dist/causal-trajectory-graph.d.ts +4 -2
- package/dist/causal-trajectory-graph.js +2 -1
- package/dist/causal-trajectory-graph.js.map +1 -1
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-KDG2KTWW.js → chunk-33SV3E43.js} +4 -4
- package/dist/{chunk-2HGUDBRJ.js → chunk-37ZO6LLJ.js} +2 -2
- package/dist/{chunk-QW3WFHKU.js → chunk-45BRYON6.js} +2 -2
- package/dist/{chunk-GZICBRWK.js → chunk-4F6X3RXW.js} +2 -2
- package/dist/{chunk-7BAYBYY5.js → chunk-4PC4PRJE.js} +2 -2
- package/dist/{chunk-VJXY5ZCT.js → chunk-52UKFTUK.js} +2 -2
- package/dist/{chunk-ESG7Z255.js → chunk-5UDMFE3Q.js} +2 -2
- package/dist/{chunk-JPHEOE6Z.js → chunk-6MTSFX7A.js} +15 -15
- package/dist/chunk-6ZN34ISG.js +44 -0
- package/dist/chunk-6ZN34ISG.js.map +1 -0
- package/dist/{chunk-M4MP34AQ.js → chunk-AHZOMHWN.js} +1 -1
- package/dist/{chunk-Z3RHPGSY.js → chunk-AIUBBHSF.js} +3 -3
- package/dist/{chunk-S3AYIQCU.js → chunk-AXIDQG5A.js} +12 -12
- package/dist/{chunk-QN7NTKRC.js → chunk-AZ2QBGYB.js} +1 -1
- package/dist/{chunk-47J4EDGR.js → chunk-AZM4CCAC.js} +8 -8
- package/dist/{chunk-IXXABW5A.js → chunk-BHXYAVUM.js} +11 -5
- package/dist/chunk-BHXYAVUM.js.map +1 -0
- package/dist/{chunk-L2DPF4DF.js → chunk-BIJAWZYW.js} +54 -12
- package/dist/chunk-BIJAWZYW.js.map +1 -0
- package/dist/{chunk-LEJPSMKM.js → chunk-C5QLI33R.js} +3 -3
- package/dist/{chunk-SV3PSA7Y.js → chunk-CV6BBVYV.js} +5 -5
- package/dist/{chunk-V7X767RT.js → chunk-DCZBSE4F.js} +52 -52
- package/dist/chunk-FC67T4E6.js +70 -0
- package/dist/chunk-FC67T4E6.js.map +1 -0
- package/dist/{chunk-7PKFPLLZ.js → chunk-FXTYIA46.js} +2 -2
- package/dist/{chunk-64BHFFKZ.js → chunk-HLYR2TQL.js} +5 -5
- package/dist/{chunk-5C2HNJMO.js → chunk-J5LLX6IL.js} +32 -32
- package/dist/{chunk-RKNJBZ55.js → chunk-JBPKEARU.js} +4 -4
- package/dist/{chunk-PLKFO2HJ.js → chunk-JL43BVDE.js} +2 -2
- package/dist/{chunk-2S72LWKX.js → chunk-JYL3BVVZ.js} +2 -2
- package/dist/{chunk-IPLYGWQF.js → chunk-KQAFEZQX.js} +5 -5
- package/dist/chunk-LXY5VVGM.js +61 -0
- package/dist/chunk-LXY5VVGM.js.map +1 -0
- package/dist/{chunk-PYKKWQZ2.js → chunk-M7BXDKPR.js} +6 -6
- package/dist/{chunk-Z54EM5YZ.js → chunk-MAXVH44E.js} +7 -7
- package/dist/{chunk-LYMNZZIM.js → chunk-NIS256NW.js} +2 -2
- package/dist/{chunk-67QG75MJ.js → chunk-NLK2VZYZ.js} +2 -2
- package/dist/{chunk-A4QMKFFO.js → chunk-O5FFBO2M.js} +14 -28
- package/dist/chunk-O5FFBO2M.js.map +1 -0
- package/dist/{chunk-JYIFGGEM.js → chunk-OUKBMQ2O.js} +2 -2
- package/dist/{chunk-75XOWNRS.js → chunk-OYESWJ43.js} +4 -4
- package/dist/{chunk-TS4AJ2GI.js → chunk-PA45XCUD.js} +3 -3
- package/dist/{chunk-BJYET5TX.js → chunk-PRDJMOST.js} +7 -1
- package/dist/chunk-PRDJMOST.js.map +1 -0
- package/dist/{chunk-4MBPIL4V.js → chunk-PUSLMZBQ.js} +2 -2
- package/dist/{chunk-RVO6TCTU.js → chunk-QCECHYZL.js} +6 -6
- package/dist/{chunk-GFIKG4FR.js → chunk-QGR6OVGT.js} +2 -2
- package/dist/{chunk-NSGU76YO.js → chunk-QHFV6VNU.js} +6 -2
- package/dist/chunk-QHFV6VNU.js.map +1 -0
- package/dist/{chunk-PDEKQWDV.js → chunk-QT2FRYI4.js} +18 -2
- package/dist/chunk-QT2FRYI4.js.map +1 -0
- package/dist/{chunk-HNZKVLAN.js → chunk-S354QO4R.js} +2 -2
- package/dist/{chunk-NO37MR45.js → chunk-SOEN6KCF.js} +2 -2
- package/dist/{chunk-FXX6RQYC.js → chunk-TPTECFWR.js} +2 -2
- package/dist/{chunk-GDDGZVFI.js → chunk-UEVLYVNW.js} +2 -2
- package/dist/{chunk-YQ4IQ2GA.js → chunk-UT5BV5OG.js} +2 -2
- package/dist/{chunk-LAQ33B5D.js → chunk-UU3G6GUP.js} +1 -1
- package/dist/chunk-UU3G6GUP.js.map +1 -0
- package/dist/{chunk-LNTUXKBT.js → chunk-UYYW2KPU.js} +2 -2
- package/dist/{chunk-BOFVT6P3.js → chunk-VBHPOWBJ.js} +175 -98
- package/dist/chunk-VBHPOWBJ.js.map +1 -0
- package/dist/{chunk-QSYL5CXK.js → chunk-VKXWK44Q.js} +3 -3
- package/dist/{chunk-KMWYZTIE.js → chunk-VMMQDEN5.js} +2 -2
- package/dist/{chunk-JB6D7EFB.js → chunk-VVG2BOKA.js} +2 -2
- package/dist/chunk-W5BIQOAJ.js +69 -0
- package/dist/chunk-W5BIQOAJ.js.map +1 -0
- package/dist/{chunk-V5H6OJCR.js → chunk-W5XVQFLM.js} +2 -2
- package/dist/{chunk-OIOZ6MOP.js → chunk-W6FNX2LP.js} +2 -2
- package/dist/{chunk-6QPHF6RR.js → chunk-WQM326GS.js} +2 -2
- package/dist/{chunk-TQFHQPWD.js → chunk-XADV5IZ3.js} +8 -8
- package/dist/{chunk-DATDLJAO.js → chunk-XAPMEU3K.js} +2 -2
- package/dist/chunk-XXSOVESA.js +42 -0
- package/dist/chunk-XXSOVESA.js.map +1 -0
- package/dist/{chunk-IK6X5QPO.js → chunk-ZHY5TMJG.js} +1193 -443
- package/dist/chunk-ZHY5TMJG.js.map +1 -0
- package/dist/{chunk-N3LL5NEU.js → chunk-ZMYDYNI4.js} +2 -2
- package/dist/{cli-D4vvtxjY.d.ts → cli-CnOkH9hA.d.ts} +6 -6
- package/dist/cli.d.ts +15 -13
- package/dist/cli.js +74 -70
- package/dist/coding/pre-action-gate.d.ts +3 -1
- package/dist/coding/pre-action-gate.js +2 -2
- package/dist/compounding/engine.d.ts +4 -2
- package/dist/compounding/engine.js +10 -9
- package/dist/compounding/preference-consolidator.d.ts +3 -1
- package/dist/compression-optimizer.d.ts +3 -1
- package/dist/config.d.ts +3 -1
- package/dist/config.js +5 -3
- package/dist/connectors/codex-materialize-runner.d.ts +3 -1
- package/dist/connectors/codex-materialize-runner.js +10 -9
- package/dist/connectors/codex-materialize.d.ts +3 -1
- package/dist/connectors/index.d.ts +3 -1
- package/dist/connectors/index.js +10 -9
- package/dist/consolidation-provenance-check.d.ts +4 -2
- package/dist/consolidation-undo.d.ts +4 -2
- package/dist/contradiction/index.d.ts +4 -2
- package/dist/contradiction-config.d.ts +29 -0
- package/dist/contradiction-config.js +12 -0
- package/dist/contradiction-config.js.map +1 -0
- package/dist/converge-config.d.ts +3 -1
- package/dist/convergence-refresh.d.ts +4 -2
- package/dist/convergence-refresh.js +12 -11
- package/dist/conversation-index/backend.d.ts +3 -1
- package/dist/conversation-index/chunker.d.ts +3 -1
- package/dist/conversation-index/faiss-adapter.d.ts +3 -1
- package/dist/conversation-index/indexer.d.ts +3 -1
- package/dist/conversation-index/search.d.ts +3 -1
- package/dist/corpus-watermark.d.ts +3 -1
- package/dist/corpus-watermark.js +21 -20
- package/dist/day-summary.d.ts +3 -1
- package/dist/delinearize.d.ts +3 -1
- package/dist/dependency-propagation-config.d.ts +3 -1
- package/dist/direct-answer-wiring.d.ts +3 -1
- package/dist/direct-answer.d.ts +3 -1
- package/dist/embedding-fallback.d.ts +3 -1
- package/dist/enrichment/index.d.ts +3 -1
- package/dist/entity-retrieval.d.ts +4 -2
- package/dist/entity-retrieval.js +10 -9
- package/dist/entity-schema.d.ts +3 -1
- package/dist/explicit-capture.d.ts +11 -9
- package/dist/explicit-capture.js +3 -2
- package/dist/external-wiki-access.d.ts +13 -11
- package/dist/external-wiki-access.js +42 -40
- package/dist/external-wiki-collection-registration.d.ts +3 -1
- package/dist/external-wiki-collection.d.ts +3 -1
- package/dist/external-wiki-mcp-tools.d.ts +13 -11
- package/dist/extraction-error-classification.d.ts +3 -1
- package/dist/extraction-faithfulness.d.ts +3 -1
- package/dist/extraction-judge-telemetry.d.ts +3 -1
- package/dist/extraction-judge-training.d.ts +3 -1
- package/dist/extraction-judge.d.ts +3 -1
- package/dist/extraction-liveness.d.ts +3 -1
- package/dist/extraction-normalization.d.ts +3 -1
- package/dist/extraction-prompt.d.ts +3 -1
- package/dist/extraction-source-grounding-rules.d.ts +3 -1
- package/dist/extraction-source-grounding.d.ts +3 -1
- package/dist/extraction.d.ts +3 -1
- package/dist/extraction.js +22 -21
- package/dist/fallback-llm.d.ts +3 -1
- package/dist/graph-444E-DrF.d.ts +349 -0
- package/dist/graph-dashboard-diff.d.ts +4 -2
- package/dist/graph-dashboard-key.d.ts +4 -2
- package/dist/graph-dashboard-parser.d.ts +4 -2
- package/dist/{graph-edge-decay-Z6ENALU2.js → graph-edge-decay-65KMOJWY.js} +4 -3
- package/dist/graph-edge-decay-65KMOJWY.js.map +1 -0
- package/dist/graph-edge-reinforcement.d.ts +4 -2
- package/dist/graph-path-reconstruction.d.ts +10 -0
- package/dist/graph-path-reconstruction.js +12 -0
- package/dist/graph-path-reconstruction.js.map +1 -0
- package/dist/graph-path-scoring-config.d.ts +8 -0
- package/dist/graph-path-scoring-config.js +10 -0
- package/dist/graph-path-scoring-config.js.map +1 -0
- package/dist/graph-path-scoring.d.ts +36 -0
- package/dist/graph-path-scoring.js +11 -0
- package/dist/graph-path-scoring.js.map +1 -0
- package/dist/graph-snapshot.d.ts +4 -2
- package/dist/graph-snapshot.js +3 -2
- package/dist/graph.d.ts +5 -318
- package/dist/graph.js +5 -1
- package/dist/harmonic-retrieval.js +1 -1
- package/dist/importance.d.ts +3 -1
- package/dist/importers/index.d.ts +3 -1
- package/dist/in-flight-reads.d.ts +3 -1
- package/dist/index.d.ts +22 -20
- package/dist/index.js +149 -144
- package/dist/intent.d.ts +3 -1
- package/dist/lcm/engine.d.ts +3 -1
- package/dist/lcm/engine.js +3 -3
- package/dist/lcm/index.d.ts +3 -1
- package/dist/lcm/index.js +6 -6
- package/dist/lcm/tools.d.ts +3 -1
- package/dist/lifecycle.d.ts +3 -1
- package/dist/live-connectors-runner.d.ts +3 -1
- package/dist/local-llm.d.ts +3 -1
- package/dist/local-model-endpoint.d.ts +3 -1
- package/dist/maintenance/memory-governance.d.ts +3 -1
- package/dist/maintenance/memory-governance.js +10 -9
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +4 -2
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +10 -9
- package/dist/maintenance/rebuild-memory-projection.d.ts +4 -2
- package/dist/maintenance/rebuild-memory-projection.js +11 -10
- package/dist/{maintenance-ClmUVpGI.d.ts → maintenance-CKwxpdRx.d.ts} +3 -3
- package/dist/mcp-memory-inspector-app.d.ts +13 -11
- package/dist/memory-action-policy.d.ts +3 -1
- package/dist/memory-cache.d.ts +3 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +3 -1
- package/dist/memory-projection-store.d.ts +3 -1
- package/dist/memory-provenance.d.ts +3 -1
- package/dist/memory-snapshot.d.ts +3 -1
- package/dist/memory-worth-outcomes.d.ts +4 -2
- package/dist/models-json.d.ts +3 -1
- package/dist/namespaces/migrate.d.ts +5 -3
- package/dist/namespaces/migrate.js +20 -19
- package/dist/namespaces/principal.d.ts +3 -1
- package/dist/namespaces/search.d.ts +3 -1
- package/dist/namespaces/search.js +7 -7
- package/dist/namespaces/storage.d.ts +5 -3
- package/dist/namespaces/storage.js +10 -9
- package/dist/native-knowledge.d.ts +3 -1
- package/dist/offline-sync-impression-drain.d.ts +3 -1
- package/dist/operator-doctor-corpus.d.ts +3 -1
- package/dist/operator-doctor-corpus.js +22 -21
- package/dist/operator-doctor-replica.d.ts +3 -1
- package/dist/operator-toolkit.d.ts +6 -4
- package/dist/operator-toolkit.js +33 -29
- package/dist/orchestration/compression-guideline-coordinator.d.ts +4 -2
- package/dist/orchestration/maintenance.d.ts +6 -4
- package/dist/orchestration/maintenance.js +15 -14
- package/dist/{orchestrator-qJ07zI8H.d.ts → orchestrator-BshGam-o.d.ts} +45 -19
- package/dist/orchestrator.d.ts +11 -9
- package/dist/orchestrator.js +109 -104
- package/dist/patterns-cli.d.ts +3 -1
- package/dist/{pipeline-WFCxhHOn.d.ts → pipeline-D21Rs6gW.d.ts} +1 -1
- package/dist/policy-runtime.d.ts +3 -1
- package/dist/proactive-contention.d.ts +3 -1
- package/dist/provenance.d.ts +3 -1
- package/dist/{qmd-5tzkQ44y.d.ts → qmd-Dhle1P4X.d.ts} +1 -1
- package/dist/qmd-preflight.d.ts +4 -2
- package/dist/qmd-recall-cache.d.ts +3 -1
- package/dist/qmd.d.ts +4 -2
- package/dist/qmd.js +4 -4
- package/dist/recall-concurrency-config.d.ts +3 -1
- package/dist/recall-disclosure-escalation.d.ts +3 -1
- package/dist/recall-explain-renderer.d.ts +3 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-memory-map.d.ts +3 -1
- package/dist/recall-planner-llm.d.ts +3 -1
- package/dist/recall-planner-llm.js +3 -3
- package/dist/recall-state.d.ts +7 -1
- package/dist/recall-state.js +1 -1
- package/dist/recall-tag-filter.d.ts +3 -1
- package/dist/recall-timings.d.ts +3 -1
- package/dist/recall-xray-cli.d.ts +3 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +3 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +7 -1
- package/dist/recall-xray.js +2 -2
- package/dist/reconcile/cursor.d.ts +3 -1
- package/dist/reconcile/manifest.d.ts +3 -1
- package/dist/reconcile/plan.d.ts +3 -1
- package/dist/replay/normalizers/chatgpt.d.ts +3 -1
- package/dist/replay/normalizers/claude.d.ts +3 -1
- package/dist/replay/normalizers/openclaw.d.ts +3 -1
- package/dist/replay/normalizers/shared.d.ts +3 -1
- package/dist/replay/runner.d.ts +3 -1
- package/dist/replay/types.d.ts +3 -1
- package/dist/replica-divergence.d.ts +3 -1
- package/dist/replica-peers-config.d.ts +3 -1
- package/dist/resolve-auth-token.d.ts +3 -1
- package/dist/resume-bundles.js +6 -4
- package/dist/retrieval-agents.d.ts +4 -2
- package/dist/retrieval-tiers.d.ts +3 -1
- package/dist/routing/engine.d.ts +3 -1
- package/dist/routing/store.d.ts +3 -1
- package/dist/salvage-envelope.d.ts +3 -1
- package/dist/{scope-profiles--vfXPl3X.d.ts → scope-profiles-NzGPDXLn.d.ts} +1 -1
- package/dist/search/embed-helper.d.ts +3 -1
- package/dist/search/factory.d.ts +3 -1
- package/dist/search/factory.js +6 -6
- package/dist/search/index.d.ts +3 -1
- package/dist/search/index.js +12 -12
- package/dist/search/lancedb-backend.d.ts +3 -1
- package/dist/search/meilisearch-backend.d.ts +3 -1
- package/dist/search/noop-backend.d.ts +3 -1
- package/dist/search/orama-backend.d.ts +3 -1
- package/dist/search/port.d.ts +3 -1
- package/dist/search/remote-backend.d.ts +3 -1
- package/dist/{semantic-consolidation-BBDuP9nJ.d.ts → semantic-consolidation-DLDKvyy5.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +4 -2
- package/dist/semantic-consolidation.js +11 -10
- package/dist/semantic-rule-promotion.js +10 -9
- package/dist/semantic-rule-verifier.d.ts +3 -1
- package/dist/semantic-rule-verifier.js +10 -9
- package/dist/{service-B8DtbpCM.d.ts → service-BIgVM7fL.d.ts} +2 -2
- package/dist/session-observer-bands.d.ts +3 -1
- package/dist/session-observer-state.d.ts +3 -1
- package/dist/shared-context/manager.d.ts +3 -1
- package/dist/signal.d.ts +3 -1
- package/dist/source-agent-qualifier.d.ts +3 -1
- package/dist/source-agent-qualifier.js +18 -17
- package/dist/{storage-BkYN1RIy.d.ts → storage-CVfGWqCI.d.ts} +7 -6
- package/dist/storage.d.ts +4 -2
- package/dist/storage.js +9 -8
- package/dist/summarizer.d.ts +3 -1
- package/dist/summary-snapshot.d.ts +3 -1
- package/dist/temporal-supersession.d.ts +4 -2
- package/dist/temporal-timeline-recall.d.ts +3 -1
- package/dist/temporal-validity.d.ts +3 -1
- package/dist/threading.d.ts +3 -1
- package/dist/tier-migration.d.ts +4 -2
- package/dist/tier-routing.d.ts +3 -1
- package/dist/topics.d.ts +3 -1
- package/dist/transcript.d.ts +3 -1
- package/dist/transfer/autodetect.js +1 -1
- package/dist/transfer/backup.js +1 -1
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/trust-score-stage.d.ts +3 -1
- package/dist/trust-score.d.ts +3 -1
- package/dist/{types-u2s9j1uD.d.ts → types-CkiDUorT.d.ts} +10 -16
- package/dist/types.d.ts +3 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +3 -1
- package/dist/verified-recall.js +10 -9
- package/dist/write-envelope.d.ts +3 -1
- package/package.json +2 -2
- package/src/archive-mutation-version.ts +59 -0
- package/src/config.test.ts +45 -0
- package/src/config.ts +10 -30
- package/src/contradiction/contradiction.test.ts +86 -1
- package/src/contradiction-config.ts +92 -0
- package/src/graph-path-reconstruction.ts +91 -0
- package/src/graph-path-scoring-config.ts +55 -0
- package/src/graph-path-scoring.test.ts +238 -0
- package/src/graph-path-scoring.ts +77 -0
- package/src/graph.test.ts +225 -5
- package/src/graph.ts +72 -5
- package/src/orchestration/contradiction-linking-coordinator.ts +246 -76
- package/src/orchestration/contradiction-localization.integration.test.ts +1953 -0
- package/src/orchestration/extraction-anchor-snapshot.ts +83 -0
- package/src/orchestration/extraction-persist-deps.ts +8 -2
- package/src/orchestration/extraction-persist.ts +43 -34
- package/src/orchestration/graph-path-state-loader.test.ts +801 -0
- package/src/orchestration/graph-path-state-loader.ts +318 -0
- package/src/orchestration/graph-recall-coordinator.integration.test.ts +1163 -0
- package/src/orchestration/graph-recall-coordinator.ts +206 -29
- package/src/orchestration/graph-recall-seam.ts +19 -0
- package/src/orchestration/recall-internal-deps.ts +7 -13
- package/src/orchestration/recall-internal.ts +5 -0
- package/src/orchestration/recall-introspection.ts +9 -1
- package/src/orchestration/recall-search-pipeline.ts +8 -13
- package/src/orchestration/update-localization.test.ts +784 -0
- package/src/orchestration/update-localization.ts +173 -0
- package/src/orchestrator.ts +19 -18
- package/src/recall-state.test.ts +29 -1
- package/src/recall-state.ts +13 -0
- package/src/recall-xray-renderer.test.ts +45 -0
- package/src/recall-xray-renderer.ts +16 -0
- package/src/recall-xray.ts +13 -0
- package/src/storage/supersession-side-effects.ts +97 -0
- package/src/storage/tombstone-blocked-capture-sync.ts +13 -5
- package/src/storage-contract/cache-coherence.test.ts +299 -0
- package/src/storage.ts +86 -88
- package/src/types.ts +12 -15
- package/dist/chunk-A4QMKFFO.js.map +0 -1
- package/dist/chunk-BJYET5TX.js.map +0 -1
- package/dist/chunk-BOFVT6P3.js.map +0 -1
- package/dist/chunk-IK6X5QPO.js.map +0 -1
- package/dist/chunk-IXXABW5A.js.map +0 -1
- package/dist/chunk-L2DPF4DF.js.map +0 -1
- package/dist/chunk-LAQ33B5D.js.map +0 -1
- package/dist/chunk-NSGU76YO.js.map +0 -1
- package/dist/chunk-PDEKQWDV.js.map +0 -1
- /package/dist/{graph-edge-decay-Z6ENALU2.js.map → archive-mutation-version.js.map} +0 -0
- /package/dist/{chunk-KDG2KTWW.js.map → chunk-33SV3E43.js.map} +0 -0
- /package/dist/{chunk-2HGUDBRJ.js.map → chunk-37ZO6LLJ.js.map} +0 -0
- /package/dist/{chunk-QW3WFHKU.js.map → chunk-45BRYON6.js.map} +0 -0
- /package/dist/{chunk-GZICBRWK.js.map → chunk-4F6X3RXW.js.map} +0 -0
- /package/dist/{chunk-7BAYBYY5.js.map → chunk-4PC4PRJE.js.map} +0 -0
- /package/dist/{chunk-VJXY5ZCT.js.map → chunk-52UKFTUK.js.map} +0 -0
- /package/dist/{chunk-ESG7Z255.js.map → chunk-5UDMFE3Q.js.map} +0 -0
- /package/dist/{chunk-JPHEOE6Z.js.map → chunk-6MTSFX7A.js.map} +0 -0
- /package/dist/{chunk-M4MP34AQ.js.map → chunk-AHZOMHWN.js.map} +0 -0
- /package/dist/{chunk-Z3RHPGSY.js.map → chunk-AIUBBHSF.js.map} +0 -0
- /package/dist/{chunk-S3AYIQCU.js.map → chunk-AXIDQG5A.js.map} +0 -0
- /package/dist/{chunk-QN7NTKRC.js.map → chunk-AZ2QBGYB.js.map} +0 -0
- /package/dist/{chunk-47J4EDGR.js.map → chunk-AZM4CCAC.js.map} +0 -0
- /package/dist/{chunk-LEJPSMKM.js.map → chunk-C5QLI33R.js.map} +0 -0
- /package/dist/{chunk-SV3PSA7Y.js.map → chunk-CV6BBVYV.js.map} +0 -0
- /package/dist/{chunk-V7X767RT.js.map → chunk-DCZBSE4F.js.map} +0 -0
- /package/dist/{chunk-7PKFPLLZ.js.map → chunk-FXTYIA46.js.map} +0 -0
- /package/dist/{chunk-64BHFFKZ.js.map → chunk-HLYR2TQL.js.map} +0 -0
- /package/dist/{chunk-5C2HNJMO.js.map → chunk-J5LLX6IL.js.map} +0 -0
- /package/dist/{chunk-RKNJBZ55.js.map → chunk-JBPKEARU.js.map} +0 -0
- /package/dist/{chunk-PLKFO2HJ.js.map → chunk-JL43BVDE.js.map} +0 -0
- /package/dist/{chunk-2S72LWKX.js.map → chunk-JYL3BVVZ.js.map} +0 -0
- /package/dist/{chunk-IPLYGWQF.js.map → chunk-KQAFEZQX.js.map} +0 -0
- /package/dist/{chunk-PYKKWQZ2.js.map → chunk-M7BXDKPR.js.map} +0 -0
- /package/dist/{chunk-Z54EM5YZ.js.map → chunk-MAXVH44E.js.map} +0 -0
- /package/dist/{chunk-LYMNZZIM.js.map → chunk-NIS256NW.js.map} +0 -0
- /package/dist/{chunk-67QG75MJ.js.map → chunk-NLK2VZYZ.js.map} +0 -0
- /package/dist/{chunk-JYIFGGEM.js.map → chunk-OUKBMQ2O.js.map} +0 -0
- /package/dist/{chunk-75XOWNRS.js.map → chunk-OYESWJ43.js.map} +0 -0
- /package/dist/{chunk-TS4AJ2GI.js.map → chunk-PA45XCUD.js.map} +0 -0
- /package/dist/{chunk-4MBPIL4V.js.map → chunk-PUSLMZBQ.js.map} +0 -0
- /package/dist/{chunk-RVO6TCTU.js.map → chunk-QCECHYZL.js.map} +0 -0
- /package/dist/{chunk-GFIKG4FR.js.map → chunk-QGR6OVGT.js.map} +0 -0
- /package/dist/{chunk-HNZKVLAN.js.map → chunk-S354QO4R.js.map} +0 -0
- /package/dist/{chunk-NO37MR45.js.map → chunk-SOEN6KCF.js.map} +0 -0
- /package/dist/{chunk-FXX6RQYC.js.map → chunk-TPTECFWR.js.map} +0 -0
- /package/dist/{chunk-GDDGZVFI.js.map → chunk-UEVLYVNW.js.map} +0 -0
- /package/dist/{chunk-YQ4IQ2GA.js.map → chunk-UT5BV5OG.js.map} +0 -0
- /package/dist/{chunk-LNTUXKBT.js.map → chunk-UYYW2KPU.js.map} +0 -0
- /package/dist/{chunk-QSYL5CXK.js.map → chunk-VKXWK44Q.js.map} +0 -0
- /package/dist/{chunk-KMWYZTIE.js.map → chunk-VMMQDEN5.js.map} +0 -0
- /package/dist/{chunk-JB6D7EFB.js.map → chunk-VVG2BOKA.js.map} +0 -0
- /package/dist/{chunk-V5H6OJCR.js.map → chunk-W5XVQFLM.js.map} +0 -0
- /package/dist/{chunk-OIOZ6MOP.js.map → chunk-W6FNX2LP.js.map} +0 -0
- /package/dist/{chunk-6QPHF6RR.js.map → chunk-WQM326GS.js.map} +0 -0
- /package/dist/{chunk-TQFHQPWD.js.map → chunk-XADV5IZ3.js.map} +0 -0
- /package/dist/{chunk-DATDLJAO.js.map → chunk-XAPMEU3K.js.map} +0 -0
- /package/dist/{chunk-N3LL5NEU.js.map → chunk-ZMYDYNI4.js.map} +0 -0
|
@@ -0,0 +1,1953 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import { parseConfig } from "../config.js";
|
|
8
|
+
import { Orchestrator } from "../orchestrator.js";
|
|
9
|
+
import type { GraphConstructionCapabilitySet } from "../capabilities.js";
|
|
10
|
+
import type { ExtractionEngine } from "../extraction.js";
|
|
11
|
+
import type { ExtractionResult, MemoryFile, PluginConfig } from "../types.js";
|
|
12
|
+
import type { StorageManager } from "../storage.js";
|
|
13
|
+
import { initLogger, resetLogger } from "../logger.js";
|
|
14
|
+
import { ContradictionLinkingCoordinator } from "./contradiction-linking-coordinator.js";
|
|
15
|
+
import { ExtractionAnchorSnapshot } from "./extraction-anchor-snapshot.js";
|
|
16
|
+
|
|
17
|
+
function baseConfig(memoryDir: string, overrides: Record<string, unknown> = {}) {
|
|
18
|
+
return parseConfig({
|
|
19
|
+
openaiApiKey: "test-key",
|
|
20
|
+
memoryDir,
|
|
21
|
+
workspaceDir: path.join(memoryDir, "workspace"),
|
|
22
|
+
qmdEnabled: false,
|
|
23
|
+
embeddingFallbackEnabled: false,
|
|
24
|
+
contradictionDetectionEnabled: true,
|
|
25
|
+
contradictionLocalization: {
|
|
26
|
+
anchorEnabled: true,
|
|
27
|
+
anchorCandidates: 5,
|
|
28
|
+
searchCandidates: 5,
|
|
29
|
+
maxCandidates: 8,
|
|
30
|
+
},
|
|
31
|
+
...overrides,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function extractionResult(fact: Record<string, unknown>): ExtractionResult {
|
|
36
|
+
return {
|
|
37
|
+
facts: [fact],
|
|
38
|
+
entities: [],
|
|
39
|
+
relationships: [],
|
|
40
|
+
questions: [],
|
|
41
|
+
profileUpdates: [],
|
|
42
|
+
} as unknown as ExtractionResult;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
test("persistExtraction forwards ExtractedFact anchors to contradiction detection", async () => {
|
|
46
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-contradiction-anchor-"));
|
|
47
|
+
try {
|
|
48
|
+
const orchestrator = new Orchestrator(baseConfig(memoryDir)) as unknown as {
|
|
49
|
+
qmd: { isAvailable: () => boolean };
|
|
50
|
+
getStorage: (namespace: string) => Promise<StorageManager>;
|
|
51
|
+
persistExtraction: (result: ExtractionResult, storage: StorageManager, threadId: null) => Promise<unknown>;
|
|
52
|
+
contradictionLinkingCoordinator: ContradictionLinkingCoordinator;
|
|
53
|
+
};
|
|
54
|
+
orchestrator.qmd = { isAvailable: () => true };
|
|
55
|
+
const calls: unknown[][] = [];
|
|
56
|
+
orchestrator.contradictionLinkingCoordinator.checkForContradiction = async (...args: unknown[]) => {
|
|
57
|
+
calls.push(args);
|
|
58
|
+
return null;
|
|
59
|
+
};
|
|
60
|
+
const storage = await orchestrator.getStorage("default");
|
|
61
|
+
await storage.ensureDirectories();
|
|
62
|
+
|
|
63
|
+
await orchestrator.persistExtraction(
|
|
64
|
+
extractionResult({
|
|
65
|
+
category: "fact",
|
|
66
|
+
content: "Alice lives in New York",
|
|
67
|
+
confidence: 0.95,
|
|
68
|
+
tags: [],
|
|
69
|
+
entityRef: "person:alice",
|
|
70
|
+
structuredAttributes: { city: "New York" },
|
|
71
|
+
}),
|
|
72
|
+
storage,
|
|
73
|
+
null,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
assert.equal(calls.length, 1);
|
|
77
|
+
const anchor = calls[0]?.[3] as {
|
|
78
|
+
entityRef?: string;
|
|
79
|
+
structuredAttributes?: Record<string, string>;
|
|
80
|
+
storageSnapshot?: unknown;
|
|
81
|
+
};
|
|
82
|
+
assert.deepEqual(
|
|
83
|
+
{
|
|
84
|
+
entityRef: anchor.entityRef,
|
|
85
|
+
structuredAttributes: anchor.structuredAttributes,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
entityRef: "person:alice",
|
|
89
|
+
structuredAttributes: { city: "New York" },
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
assert.ok(Array.isArray(anchor.storageSnapshot));
|
|
93
|
+
} finally {
|
|
94
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
test("persistExtraction keeps malformed fact entityRef out of anchor lookup", async () => {
|
|
98
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-contradiction-anchor-invalid-ref-"));
|
|
99
|
+
try {
|
|
100
|
+
const orchestrator = new Orchestrator(baseConfig(memoryDir)) as unknown as {
|
|
101
|
+
qmd: { isAvailable: () => boolean };
|
|
102
|
+
getStorage: (namespace: string) => Promise<StorageManager>;
|
|
103
|
+
persistExtraction: (result: ExtractionResult, storage: StorageManager, threadId: null) => Promise<unknown>;
|
|
104
|
+
contradictionLinkingCoordinator: ContradictionLinkingCoordinator;
|
|
105
|
+
};
|
|
106
|
+
orchestrator.qmd = { isAvailable: () => true };
|
|
107
|
+
const calls: unknown[][] = [];
|
|
108
|
+
orchestrator.contradictionLinkingCoordinator.checkForContradiction = async (...args: unknown[]) => {
|
|
109
|
+
calls.push(args);
|
|
110
|
+
return null;
|
|
111
|
+
};
|
|
112
|
+
const storage = await orchestrator.getStorage("default");
|
|
113
|
+
await storage.ensureDirectories();
|
|
114
|
+
|
|
115
|
+
await orchestrator.persistExtraction(
|
|
116
|
+
extractionResult({
|
|
117
|
+
category: "fact",
|
|
118
|
+
content: "Alice lives in New York",
|
|
119
|
+
confidence: 0.95,
|
|
120
|
+
tags: [],
|
|
121
|
+
entityRef: 42,
|
|
122
|
+
}),
|
|
123
|
+
storage,
|
|
124
|
+
null,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
assert.equal(calls.length, 1);
|
|
128
|
+
const anchor = calls[0]?.[3] as {
|
|
129
|
+
entityRef?: unknown;
|
|
130
|
+
storageSnapshot?: unknown;
|
|
131
|
+
};
|
|
132
|
+
assert.equal(anchor.entityRef, undefined);
|
|
133
|
+
assert.equal(anchor.storageSnapshot, undefined);
|
|
134
|
+
} finally {
|
|
135
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
test("persistExtraction treats string false anchorEnabled as disabled", async () => {
|
|
141
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-contradiction-anchor-false-"));
|
|
142
|
+
try {
|
|
143
|
+
const config = baseConfig(memoryDir);
|
|
144
|
+
const rawLocalization = config.contradictionLocalization as unknown as Record<string, unknown>;
|
|
145
|
+
rawLocalization.anchorEnabled = "false";
|
|
146
|
+
const orchestrator = new Orchestrator(config) as unknown as {
|
|
147
|
+
qmd: { isAvailable: () => boolean };
|
|
148
|
+
getStorage: (namespace: string) => Promise<StorageManager>;
|
|
149
|
+
persistExtraction: (result: ExtractionResult, storage: StorageManager, threadId: null) => Promise<unknown>;
|
|
150
|
+
contradictionLinkingCoordinator: ContradictionLinkingCoordinator;
|
|
151
|
+
};
|
|
152
|
+
orchestrator.qmd = { isAvailable: () => true };
|
|
153
|
+
const calls: unknown[][] = [];
|
|
154
|
+
orchestrator.contradictionLinkingCoordinator.checkForContradiction = async (...args: unknown[]) => {
|
|
155
|
+
calls.push(args);
|
|
156
|
+
return null;
|
|
157
|
+
};
|
|
158
|
+
const storage = await orchestrator.getStorage("default");
|
|
159
|
+
await storage.ensureDirectories();
|
|
160
|
+
|
|
161
|
+
await orchestrator.persistExtraction(
|
|
162
|
+
extractionResult({
|
|
163
|
+
category: "fact",
|
|
164
|
+
content: "Alice lives in New York",
|
|
165
|
+
confidence: 0.95,
|
|
166
|
+
tags: [],
|
|
167
|
+
entityRef: "person:alice",
|
|
168
|
+
}),
|
|
169
|
+
storage,
|
|
170
|
+
null,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const anchor = calls[0]?.[3];
|
|
174
|
+
assert.ok(anchor && typeof anchor === "object" && "storageSnapshot" in anchor);
|
|
175
|
+
const storageSnapshot =
|
|
176
|
+
anchor && typeof anchor === "object" && "storageSnapshot" in anchor ? anchor.storageSnapshot : "missing";
|
|
177
|
+
assert.equal(storageSnapshot, undefined);
|
|
178
|
+
} finally {
|
|
179
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("persistExtraction memoizes one anchor snapshot for all facts in one pass", async () => {
|
|
184
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-contradiction-snapshot-"));
|
|
185
|
+
try {
|
|
186
|
+
const orchestrator = new Orchestrator(baseConfig(memoryDir)) as unknown as {
|
|
187
|
+
qmd: { isAvailable: () => boolean };
|
|
188
|
+
getStorage: (namespace: string) => Promise<StorageManager>;
|
|
189
|
+
persistExtraction: (
|
|
190
|
+
result: ExtractionResult,
|
|
191
|
+
storage: StorageManager,
|
|
192
|
+
threadId: null,
|
|
193
|
+
sourceContext?: undefined,
|
|
194
|
+
baseNamespace?: undefined,
|
|
195
|
+
scopeProfileWritePlan?: undefined,
|
|
196
|
+
sourceText?: undefined,
|
|
197
|
+
graphCaps?: GraphConstructionCapabilitySet,
|
|
198
|
+
) => Promise<unknown>;
|
|
199
|
+
contradictionLinkingCoordinator: ContradictionLinkingCoordinator;
|
|
200
|
+
};
|
|
201
|
+
const storage = await orchestrator.getStorage("default");
|
|
202
|
+
await storage.ensureDirectories();
|
|
203
|
+
await storage.getSharedFactHashIndex();
|
|
204
|
+
let hotReadCount = 0;
|
|
205
|
+
let coldReadCount = 0;
|
|
206
|
+
const readAllMemories = storage.readAllMemories.bind(storage);
|
|
207
|
+
storage.readAllMemories = async () => {
|
|
208
|
+
hotReadCount++;
|
|
209
|
+
return readAllMemories();
|
|
210
|
+
};
|
|
211
|
+
const readAllColdMemories = storage.readAllColdMemories.bind(storage);
|
|
212
|
+
storage.readAllColdMemories = async () => {
|
|
213
|
+
coldReadCount++;
|
|
214
|
+
return readAllColdMemories();
|
|
215
|
+
};
|
|
216
|
+
const snapshots: unknown[] = [];
|
|
217
|
+
orchestrator.contradictionLinkingCoordinator.checkForContradiction = async (...args: unknown[]) => {
|
|
218
|
+
const anchor = args[3];
|
|
219
|
+
if (anchor && typeof anchor === "object" && "storageSnapshot" in anchor) {
|
|
220
|
+
snapshots.push(anchor.storageSnapshot);
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
};
|
|
224
|
+
const result = extractionResult({
|
|
225
|
+
category: "fact",
|
|
226
|
+
content: "Alice lives in Austin",
|
|
227
|
+
confidence: 0.95,
|
|
228
|
+
tags: [],
|
|
229
|
+
entityRef: "person:alice",
|
|
230
|
+
});
|
|
231
|
+
result.facts.push({
|
|
232
|
+
category: "fact",
|
|
233
|
+
content: "Alice works in Austin",
|
|
234
|
+
confidence: 0.95,
|
|
235
|
+
tags: [],
|
|
236
|
+
entityRef: "person:alice",
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
await orchestrator.persistExtraction(
|
|
240
|
+
result,
|
|
241
|
+
storage,
|
|
242
|
+
null,
|
|
243
|
+
undefined,
|
|
244
|
+
undefined,
|
|
245
|
+
undefined,
|
|
246
|
+
undefined,
|
|
247
|
+
{
|
|
248
|
+
entityGraph: false,
|
|
249
|
+
timeGraph: false,
|
|
250
|
+
causalGraph: false,
|
|
251
|
+
multiGraphMemory: false,
|
|
252
|
+
graphWriteSessionAdjacency: false,
|
|
253
|
+
},
|
|
254
|
+
);
|
|
255
|
+
await orchestrator.persistExtraction(
|
|
256
|
+
extractionResult({
|
|
257
|
+
category: "fact",
|
|
258
|
+
content: "Bob lives in Dallas",
|
|
259
|
+
confidence: 0.95,
|
|
260
|
+
tags: [],
|
|
261
|
+
entityRef: "person:bob",
|
|
262
|
+
}),
|
|
263
|
+
storage,
|
|
264
|
+
null,
|
|
265
|
+
undefined,
|
|
266
|
+
undefined,
|
|
267
|
+
undefined,
|
|
268
|
+
undefined,
|
|
269
|
+
{
|
|
270
|
+
entityGraph: false,
|
|
271
|
+
timeGraph: false,
|
|
272
|
+
causalGraph: false,
|
|
273
|
+
multiGraphMemory: false,
|
|
274
|
+
graphWriteSessionAdjacency: false,
|
|
275
|
+
},
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
assert.equal(snapshots.length, 3);
|
|
279
|
+
assert.equal(snapshots[0], snapshots[1]);
|
|
280
|
+
assert.notEqual(snapshots[1], snapshots[2]);
|
|
281
|
+
assert.equal(hotReadCount, 2);
|
|
282
|
+
assert.equal(coldReadCount, 2);
|
|
283
|
+
} finally {
|
|
284
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("keeps durable fact writes when anchor snapshot refresh read fails", async () => {
|
|
289
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-contradiction-anchor-refresh-failure-"));
|
|
290
|
+
const warnings: string[] = [];
|
|
291
|
+
initLogger(
|
|
292
|
+
{
|
|
293
|
+
info: () => {},
|
|
294
|
+
warn: (message) => warnings.push(message),
|
|
295
|
+
error: () => {},
|
|
296
|
+
debug: () => {},
|
|
297
|
+
},
|
|
298
|
+
false,
|
|
299
|
+
{ timestamps: false },
|
|
300
|
+
);
|
|
301
|
+
try {
|
|
302
|
+
const orchestrator = new Orchestrator(baseConfig(memoryDir)) as unknown as {
|
|
303
|
+
qmd: { isAvailable: () => boolean };
|
|
304
|
+
getStorage: (namespace: string) => Promise<StorageManager>;
|
|
305
|
+
persistExtraction: (
|
|
306
|
+
result: ExtractionResult,
|
|
307
|
+
storage: StorageManager,
|
|
308
|
+
threadId: null,
|
|
309
|
+
sourceContext?: undefined,
|
|
310
|
+
baseNamespace?: undefined,
|
|
311
|
+
scopeProfileWritePlan?: undefined,
|
|
312
|
+
sourceText?: undefined,
|
|
313
|
+
graphCaps?: GraphConstructionCapabilitySet,
|
|
314
|
+
) => Promise<{ persistedIds: string[] }>;
|
|
315
|
+
contradictionLinkingCoordinator: ContradictionLinkingCoordinator;
|
|
316
|
+
};
|
|
317
|
+
const snapshots: unknown[] = [];
|
|
318
|
+
orchestrator.contradictionLinkingCoordinator.checkForContradiction = async (...args: unknown[]) => {
|
|
319
|
+
const anchor = args[3];
|
|
320
|
+
if (anchor && typeof anchor === "object" && "storageSnapshot" in anchor) {
|
|
321
|
+
snapshots.push(anchor.storageSnapshot);
|
|
322
|
+
} else {
|
|
323
|
+
snapshots.push(undefined);
|
|
324
|
+
}
|
|
325
|
+
return null;
|
|
326
|
+
};
|
|
327
|
+
const storage = await orchestrator.getStorage("default");
|
|
328
|
+
await storage.ensureDirectories();
|
|
329
|
+
const readMemoryByPath = storage.readMemoryByPath.bind(storage);
|
|
330
|
+
let failRefreshRead = true;
|
|
331
|
+
storage.readMemoryByPath = async (memoryPath: string) => {
|
|
332
|
+
const memory = await readMemoryByPath(memoryPath);
|
|
333
|
+
if (failRefreshRead && memory) {
|
|
334
|
+
failRefreshRead = false;
|
|
335
|
+
throw new Error("anchor snapshot refresh read failed");
|
|
336
|
+
}
|
|
337
|
+
return memory;
|
|
338
|
+
};
|
|
339
|
+
const result = extractionResult({
|
|
340
|
+
category: "fact",
|
|
341
|
+
content: "Alice lives in Austin",
|
|
342
|
+
confidence: 0.95,
|
|
343
|
+
tags: [],
|
|
344
|
+
entityRef: "person:alice",
|
|
345
|
+
});
|
|
346
|
+
result.facts.push({
|
|
347
|
+
category: "fact",
|
|
348
|
+
content: "Alice works in Austin",
|
|
349
|
+
confidence: 0.95,
|
|
350
|
+
tags: [],
|
|
351
|
+
entityRef: "person:alice",
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const persisted = await orchestrator.persistExtraction(
|
|
355
|
+
result,
|
|
356
|
+
storage,
|
|
357
|
+
null,
|
|
358
|
+
undefined,
|
|
359
|
+
undefined,
|
|
360
|
+
undefined,
|
|
361
|
+
undefined,
|
|
362
|
+
{
|
|
363
|
+
entityGraph: false,
|
|
364
|
+
timeGraph: false,
|
|
365
|
+
causalGraph: false,
|
|
366
|
+
multiGraphMemory: false,
|
|
367
|
+
graphWriteSessionAdjacency: false,
|
|
368
|
+
},
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
assert.equal(persisted.persistedIds.length, 2);
|
|
372
|
+
assert.equal((await storage.readAllMemories()).length, 2);
|
|
373
|
+
assert.equal(snapshots.length, 2);
|
|
374
|
+
assert.ok(Array.isArray(snapshots[0]));
|
|
375
|
+
assert.deepEqual(snapshots[1], []);
|
|
376
|
+
assert.ok(warnings.some((warning) => warning.includes("anchor snapshot update failed")));
|
|
377
|
+
} finally {
|
|
378
|
+
resetLogger();
|
|
379
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("disables anchor snapshot reuse after a rejected read", async () => {
|
|
384
|
+
let hotReads = 0;
|
|
385
|
+
const storage = {
|
|
386
|
+
dir: "/tmp/remnic-anchor-snapshot-test",
|
|
387
|
+
readAllMemories: async () => {
|
|
388
|
+
hotReads++;
|
|
389
|
+
if (hotReads === 1) throw new Error("transient snapshot read");
|
|
390
|
+
return [];
|
|
391
|
+
},
|
|
392
|
+
readAllColdMemories: async () => [],
|
|
393
|
+
} as unknown as StorageManager;
|
|
394
|
+
const snapshots = new ExtractionAnchorSnapshot();
|
|
395
|
+
|
|
396
|
+
assert.deepEqual(await snapshots.get(storage, "person:alice"), []);
|
|
397
|
+
assert.deepEqual(await snapshots.get(storage, "person:alice"), []);
|
|
398
|
+
assert.equal(hotReads, 1);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("anchor snapshot prefers an active cold copy over a path-archived hot duplicate", async () => {
|
|
402
|
+
const storageDir = "/tmp/remnic-anchor-snapshot-archive-duplicate";
|
|
403
|
+
const hot = {
|
|
404
|
+
path: path.join(storageDir, "archive", "duplicate.md"),
|
|
405
|
+
content: "hot archived",
|
|
406
|
+
frontmatter: {
|
|
407
|
+
id: "duplicate",
|
|
408
|
+
category: "fact",
|
|
409
|
+
created: "2026-08-01T00:00:00.000Z",
|
|
410
|
+
updated: "2026-08-01T00:00:00.000Z",
|
|
411
|
+
status: "active",
|
|
412
|
+
archivedAt: "2026-08-08T00:00:00.000Z",
|
|
413
|
+
},
|
|
414
|
+
} as unknown as MemoryFile;
|
|
415
|
+
const cold = {
|
|
416
|
+
path: path.join(storageDir, "cold", "duplicate.md"),
|
|
417
|
+
content: "cold active",
|
|
418
|
+
frontmatter: {
|
|
419
|
+
...hot.frontmatter,
|
|
420
|
+
status: "active",
|
|
421
|
+
archivedAt: undefined,
|
|
422
|
+
},
|
|
423
|
+
} as unknown as MemoryFile;
|
|
424
|
+
const storage = {
|
|
425
|
+
dir: storageDir,
|
|
426
|
+
readAllMemories: async () => [hot],
|
|
427
|
+
readAllColdMemories: async () => [cold],
|
|
428
|
+
} as unknown as StorageManager;
|
|
429
|
+
const snapshots = new ExtractionAnchorSnapshot();
|
|
430
|
+
|
|
431
|
+
assert.deepEqual(await snapshots.get(storage, "person:alice"), [cold]);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("isolates anchor snapshot failures per storage", async () => {
|
|
435
|
+
const snapshots = new ExtractionAnchorSnapshot();
|
|
436
|
+
const failedStorage = {
|
|
437
|
+
dir: "/tmp/remnic-anchor-snapshot-failed",
|
|
438
|
+
readAllMemories: async () => {
|
|
439
|
+
throw new Error("storage A read failed");
|
|
440
|
+
},
|
|
441
|
+
readAllColdMemories: async () => [],
|
|
442
|
+
} as unknown as StorageManager;
|
|
443
|
+
const replacement = {
|
|
444
|
+
path: "memories/storage-b.md",
|
|
445
|
+
content: "storage B",
|
|
446
|
+
frontmatter: { id: "storage-b", category: "fact" },
|
|
447
|
+
} as unknown as MemoryFile;
|
|
448
|
+
const healthyStorage = {
|
|
449
|
+
dir: "/tmp/remnic-anchor-snapshot-healthy",
|
|
450
|
+
readAllMemories: async () => [],
|
|
451
|
+
readAllColdMemories: async () => [],
|
|
452
|
+
readMemoryByPath: async () => replacement,
|
|
453
|
+
} as unknown as StorageManager;
|
|
454
|
+
|
|
455
|
+
assert.deepEqual(await snapshots.get(failedStorage, "person:alice"), []);
|
|
456
|
+
const healthySnapshot = await snapshots.get(healthyStorage, "person:bob");
|
|
457
|
+
assert.deepEqual(healthySnapshot, []);
|
|
458
|
+
await snapshots.replace(
|
|
459
|
+
healthyStorage,
|
|
460
|
+
"storage-b",
|
|
461
|
+
"fact",
|
|
462
|
+
new Map([["storage-b", "memories/storage-b.md"]]),
|
|
463
|
+
);
|
|
464
|
+
assert.deepEqual(healthySnapshot, [replacement]);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
test("keeps durable fact writes when initial anchor snapshot read fails", async () => {
|
|
469
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-contradiction-anchor-initial-read-failure-"));
|
|
470
|
+
const warnings: string[] = [];
|
|
471
|
+
initLogger(
|
|
472
|
+
{
|
|
473
|
+
info: () => {},
|
|
474
|
+
warn: (message) => warnings.push(message),
|
|
475
|
+
error: () => {},
|
|
476
|
+
debug: () => {},
|
|
477
|
+
},
|
|
478
|
+
false,
|
|
479
|
+
{ timestamps: false },
|
|
480
|
+
);
|
|
481
|
+
try {
|
|
482
|
+
const orchestrator = new Orchestrator(
|
|
483
|
+
baseConfig(memoryDir, { semanticDedupEnabled: false }),
|
|
484
|
+
) as unknown as {
|
|
485
|
+
qmd: { isAvailable: () => boolean };
|
|
486
|
+
getStorage: (namespace: string) => Promise<StorageManager>;
|
|
487
|
+
persistExtraction: (
|
|
488
|
+
result: ExtractionResult,
|
|
489
|
+
storage: StorageManager,
|
|
490
|
+
threadId: null,
|
|
491
|
+
sourceContext?: undefined,
|
|
492
|
+
baseNamespace?: undefined,
|
|
493
|
+
scopeProfileWritePlan?: undefined,
|
|
494
|
+
sourceText?: undefined,
|
|
495
|
+
graphCaps?: GraphConstructionCapabilitySet,
|
|
496
|
+
) => Promise<{ persistedIds: string[] }>;
|
|
497
|
+
contradictionLinkingCoordinator: ContradictionLinkingCoordinator;
|
|
498
|
+
};
|
|
499
|
+
orchestrator.qmd = { isAvailable: () => false };
|
|
500
|
+
const storage = await orchestrator.getStorage("default");
|
|
501
|
+
await storage.ensureDirectories();
|
|
502
|
+
const readAllMemories = storage.readAllMemories.bind(storage);
|
|
503
|
+
const readAllColdMemories = storage.readAllColdMemories.bind(storage);
|
|
504
|
+
let hotReadActive = false;
|
|
505
|
+
let failedCorpusReads = 0;
|
|
506
|
+
storage.readAllMemories = async () => {
|
|
507
|
+
hotReadActive = true;
|
|
508
|
+
try {
|
|
509
|
+
return await readAllMemories();
|
|
510
|
+
} finally {
|
|
511
|
+
hotReadActive = false;
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
storage.readAllColdMemories = async () => {
|
|
515
|
+
if (hotReadActive && failedCorpusReads === 0) {
|
|
516
|
+
failedCorpusReads++;
|
|
517
|
+
throw new Error("initial anchor snapshot read failed");
|
|
518
|
+
}
|
|
519
|
+
return readAllColdMemories();
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
const persisted = await orchestrator.persistExtraction(
|
|
523
|
+
extractionResult({
|
|
524
|
+
category: "fact",
|
|
525
|
+
content: "Alice lives in Austin",
|
|
526
|
+
confidence: 0.95,
|
|
527
|
+
tags: [],
|
|
528
|
+
entityRef: "person:alice",
|
|
529
|
+
}),
|
|
530
|
+
storage,
|
|
531
|
+
null,
|
|
532
|
+
undefined,
|
|
533
|
+
undefined,
|
|
534
|
+
undefined,
|
|
535
|
+
undefined,
|
|
536
|
+
{
|
|
537
|
+
entityGraph: false,
|
|
538
|
+
timeGraph: false,
|
|
539
|
+
causalGraph: false,
|
|
540
|
+
multiGraphMemory: false,
|
|
541
|
+
graphWriteSessionAdjacency: false,
|
|
542
|
+
},
|
|
543
|
+
);
|
|
544
|
+
assert.equal(persisted.persistedIds.length, 1);
|
|
545
|
+
assert.equal((await storage.readAllMemories()).length, 1);
|
|
546
|
+
|
|
547
|
+
assert.equal(failedCorpusReads, 1);
|
|
548
|
+
assert.ok(warnings.some((warning) => warning.includes("anchor snapshot update failed")));
|
|
549
|
+
} finally {
|
|
550
|
+
resetLogger();
|
|
551
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
test("updates the memoized anchor snapshot after each same-batch write", async () => {
|
|
555
|
+
|
|
556
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-contradiction-batch-snapshot-"));
|
|
557
|
+
try {
|
|
558
|
+
const orchestrator = new Orchestrator(baseConfig(memoryDir)) as unknown as {
|
|
559
|
+
qmd: { isAvailable: () => boolean };
|
|
560
|
+
getStorage: (namespace: string) => Promise<StorageManager>;
|
|
561
|
+
persistExtraction: (
|
|
562
|
+
result: ExtractionResult,
|
|
563
|
+
storage: StorageManager,
|
|
564
|
+
threadId: null,
|
|
565
|
+
sourceContext?: undefined,
|
|
566
|
+
baseNamespace?: undefined,
|
|
567
|
+
scopeProfileWritePlan?: undefined,
|
|
568
|
+
sourceText?: undefined,
|
|
569
|
+
graphCaps?: GraphConstructionCapabilitySet,
|
|
570
|
+
) => Promise<unknown>;
|
|
571
|
+
contradictionLinkingCoordinator: ContradictionLinkingCoordinator;
|
|
572
|
+
};
|
|
573
|
+
orchestrator.qmd = { isAvailable: () => false };
|
|
574
|
+
const storage = await orchestrator.getStorage("default");
|
|
575
|
+
await storage.ensureDirectories();
|
|
576
|
+
const snapshots: MemoryFile[][] = [];
|
|
577
|
+
orchestrator.contradictionLinkingCoordinator.checkForContradiction = async (...args: unknown[]) => {
|
|
578
|
+
const anchor = args[3];
|
|
579
|
+
snapshots.push(
|
|
580
|
+
anchor && typeof anchor === "object" && "storageSnapshot" in anchor
|
|
581
|
+
? [...((anchor as { storageSnapshot?: MemoryFile[] }).storageSnapshot ?? [])]
|
|
582
|
+
: [],
|
|
583
|
+
);
|
|
584
|
+
return null;
|
|
585
|
+
};
|
|
586
|
+
const result = extractionResult({
|
|
587
|
+
category: "fact",
|
|
588
|
+
content: "Alice lives in Austin",
|
|
589
|
+
confidence: 0.95,
|
|
590
|
+
tags: [],
|
|
591
|
+
entityRef: "person:alice",
|
|
592
|
+
});
|
|
593
|
+
result.facts.push({
|
|
594
|
+
category: "fact",
|
|
595
|
+
content: "Alice lives in New York",
|
|
596
|
+
confidence: 0.95,
|
|
597
|
+
tags: [],
|
|
598
|
+
entityRef: "person:alice",
|
|
599
|
+
});
|
|
600
|
+
await orchestrator.persistExtraction(result, storage, null, undefined, undefined, undefined, undefined, {
|
|
601
|
+
entityGraph: false,
|
|
602
|
+
timeGraph: false,
|
|
603
|
+
|
|
604
|
+
causalGraph: false,
|
|
605
|
+
multiGraphMemory: false,
|
|
606
|
+
graphWriteSessionAdjacency: false,
|
|
607
|
+
});
|
|
608
|
+
assert.equal(snapshots.length, 2);
|
|
609
|
+
assert.equal(snapshots[0]?.some((memory) => memory.content.includes("Austin")), false);
|
|
610
|
+
assert.equal(snapshots[1]?.some((memory) => memory.content.includes("Austin")), true);
|
|
611
|
+
} finally {
|
|
612
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
test("clears a losing supersedes link and removes the raced anchor", async () => {
|
|
616
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-contradiction-supersede-race-"));
|
|
617
|
+
try {
|
|
618
|
+
const orchestrator = new Orchestrator(
|
|
619
|
+
baseConfig(memoryDir, { contradictionAutoResolve: true }),
|
|
620
|
+
) as unknown as {
|
|
621
|
+
getStorage: (namespace: string) => Promise<StorageManager>;
|
|
622
|
+
persistExtraction: (
|
|
623
|
+
result: ExtractionResult,
|
|
624
|
+
storage: StorageManager,
|
|
625
|
+
threadId: null,
|
|
626
|
+
sourceContext?: undefined,
|
|
627
|
+
baseNamespace?: undefined,
|
|
628
|
+
scopeProfileWritePlan?: undefined,
|
|
629
|
+
sourceText?: undefined,
|
|
630
|
+
graphCaps?: GraphConstructionCapabilitySet,
|
|
631
|
+
) => Promise<unknown>;
|
|
632
|
+
contradictionLinkingCoordinator: ContradictionLinkingCoordinator;
|
|
633
|
+
};
|
|
634
|
+
const storage = await orchestrator.getStorage("default");
|
|
635
|
+
await storage.ensureDirectories();
|
|
636
|
+
const { id: oldId } = await storage.writeMemory("fact", "Alice lives in Austin", {
|
|
637
|
+
entityRef: "person:alice",
|
|
638
|
+
source: "test",
|
|
639
|
+
});
|
|
640
|
+
const supersedeMemory = storage.supersedeMemory.bind(storage);
|
|
641
|
+
storage.supersedeMemory = async (...args: Parameters<StorageManager["supersedeMemory"]>) => {
|
|
642
|
+
await supersedeMemory(...args);
|
|
643
|
+
return false;
|
|
644
|
+
};
|
|
645
|
+
const snapshots: MemoryFile[][] = [];
|
|
646
|
+
orchestrator.contradictionLinkingCoordinator.checkForContradiction = async (...args: unknown[]) => {
|
|
647
|
+
const anchor = args[3];
|
|
648
|
+
snapshots.push(
|
|
649
|
+
anchor && typeof anchor === "object" && "storageSnapshot" in anchor
|
|
650
|
+
? [...((anchor as { storageSnapshot?: MemoryFile[] }).storageSnapshot ?? [])]
|
|
651
|
+
: [],
|
|
652
|
+
);
|
|
653
|
+
return {
|
|
654
|
+
supersededId: oldId,
|
|
655
|
+
confidence: 0.99,
|
|
656
|
+
reason: "same entity conflict",
|
|
657
|
+
supersededPath: `/synthetic/${oldId}.md`,
|
|
658
|
+
supersededCreated: "2026-08-09T00:00:00.000Z",
|
|
659
|
+
supersededTags: [],
|
|
660
|
+
};
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
const result = extractionResult({
|
|
664
|
+
category: "fact",
|
|
665
|
+
content: "Alice lives in Boston",
|
|
666
|
+
confidence: 0.95,
|
|
667
|
+
tags: [],
|
|
668
|
+
entityRef: "person:alice",
|
|
669
|
+
});
|
|
670
|
+
result.facts.push({
|
|
671
|
+
category: "fact",
|
|
672
|
+
content: "Alice lives in Dallas",
|
|
673
|
+
confidence: 0.95,
|
|
674
|
+
tags: [],
|
|
675
|
+
entityRef: "person:alice",
|
|
676
|
+
});
|
|
677
|
+
await orchestrator.persistExtraction(result, storage, null, undefined, undefined, undefined, undefined, {
|
|
678
|
+
entityGraph: false,
|
|
679
|
+
timeGraph: false,
|
|
680
|
+
causalGraph: false,
|
|
681
|
+
multiGraphMemory: false,
|
|
682
|
+
graphWriteSessionAdjacency: false,
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
assert.equal(snapshots.length, 2);
|
|
686
|
+
assert.equal(snapshots[0]?.some((memory) => memory.frontmatter.id === oldId), true);
|
|
687
|
+
assert.equal(snapshots[1]?.some((memory) => memory.frontmatter.id === oldId), false);
|
|
688
|
+
const persisted = (await storage.readAllMemories()).filter(
|
|
689
|
+
(memory) => memory.frontmatter.id !== oldId,
|
|
690
|
+
);
|
|
691
|
+
const losingWrites = persisted.filter(
|
|
692
|
+
(memory) => memory.content.includes("Boston") || memory.content.includes("Dallas"),
|
|
693
|
+
);
|
|
694
|
+
assert.equal(losingWrites.length, 2);
|
|
695
|
+
assert.equal(losingWrites.every((memory) => memory.frontmatter.supersedes === undefined), true);
|
|
696
|
+
assert.equal(
|
|
697
|
+
(await storage.getMemoryById(oldId))?.frontmatter.status,
|
|
698
|
+
"superseded",
|
|
699
|
+
);
|
|
700
|
+
} finally {
|
|
701
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
test("preserves an active cold anchor when a supersede race sees an inactive hot copy", async () => {
|
|
705
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-contradiction-cold-race-"));
|
|
706
|
+
try {
|
|
707
|
+
const config = baseConfig(memoryDir, { contradictionAutoResolve: true });
|
|
708
|
+
const activeCold = {
|
|
709
|
+
path: path.join(memoryDir, "cold", "old.md"),
|
|
710
|
+
content: "Alice lives in Austin",
|
|
711
|
+
frontmatter: {
|
|
712
|
+
id: "old",
|
|
713
|
+
category: "fact",
|
|
714
|
+
created: "2026-08-01T00:00:00.000Z",
|
|
715
|
+
updated: "2026-08-01T00:00:00.000Z",
|
|
716
|
+
source: "test",
|
|
717
|
+
confidence: 0.9,
|
|
718
|
+
confidenceTier: "explicit",
|
|
719
|
+
tags: [],
|
|
720
|
+
status: "active",
|
|
721
|
+
},
|
|
722
|
+
} as unknown as MemoryFile;
|
|
723
|
+
const inactiveHot = {
|
|
724
|
+
...activeCold,
|
|
725
|
+
path: path.join(memoryDir, "old.md"),
|
|
726
|
+
frontmatter: {
|
|
727
|
+
...activeCold.frontmatter,
|
|
728
|
+
status: "superseded",
|
|
729
|
+
supersededBy: "other",
|
|
730
|
+
},
|
|
731
|
+
} as unknown as MemoryFile;
|
|
732
|
+
const losing = {
|
|
733
|
+
...activeCold,
|
|
734
|
+
path: path.join(memoryDir, "new.md"),
|
|
735
|
+
content: "Alice lives in Boston",
|
|
736
|
+
frontmatter: {
|
|
737
|
+
...activeCold.frontmatter,
|
|
738
|
+
id: "new",
|
|
739
|
+
supersedes: "old",
|
|
740
|
+
},
|
|
741
|
+
} as unknown as MemoryFile;
|
|
742
|
+
let clearCalls = 0;
|
|
743
|
+
const storage = {
|
|
744
|
+
dir: memoryDir,
|
|
745
|
+
getMemoryById: async (id: string) =>
|
|
746
|
+
id === "old" ? inactiveHot : id === "new" ? losing : null,
|
|
747
|
+
readAllMemories: async () => [inactiveHot, losing],
|
|
748
|
+
readAllColdMemories: async () => [activeCold],
|
|
749
|
+
supersedeMemory: async () => false,
|
|
750
|
+
writeMemoryFrontmatter: async (memory: MemoryFile) => {
|
|
751
|
+
clearCalls += 1;
|
|
752
|
+
memory.frontmatter.supersedes = undefined;
|
|
753
|
+
return true;
|
|
754
|
+
},
|
|
755
|
+
} as unknown as StorageManager;
|
|
756
|
+
const coordinator = new ContradictionLinkingCoordinator({
|
|
757
|
+
getConfig: () => config,
|
|
758
|
+
isSearchAvailable: () => false,
|
|
759
|
+
searchAcrossNamespaces: async () => [],
|
|
760
|
+
extractMemoryIdsFromResults: () => [],
|
|
761
|
+
namespaceFromPath: () => "default",
|
|
762
|
+
storageForNamespace: async () => storage,
|
|
763
|
+
getExtraction: () => ({}) as ExtractionEngine,
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
const outcome = await coordinator.applyDeferredContradictionResolve(
|
|
767
|
+
{
|
|
768
|
+
supersededId: "old",
|
|
769
|
+
reason: "city changed",
|
|
770
|
+
supersededPath: activeCold.path,
|
|
771
|
+
supersededCreated: activeCold.frontmatter.created,
|
|
772
|
+
supersededTags: [],
|
|
773
|
+
},
|
|
774
|
+
storage,
|
|
775
|
+
"new",
|
|
776
|
+
false,
|
|
777
|
+
);
|
|
778
|
+
|
|
779
|
+
assert.equal(outcome, "supersede_failed");
|
|
780
|
+
assert.equal(activeCold.frontmatter.status, "active");
|
|
781
|
+
assert.equal(losing.frontmatter.supersedes, undefined);
|
|
782
|
+
assert.equal(clearCalls, 1);
|
|
783
|
+
} finally {
|
|
784
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
test("cleanup I/O failure with an active target returns supersedes_clear_failed and preserves the anchor", async () => {
|
|
788
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-contradiction-active-clear-io-"));
|
|
789
|
+
try {
|
|
790
|
+
const config = baseConfig(memoryDir, { contradictionAutoResolve: true });
|
|
791
|
+
const activeOld = {
|
|
792
|
+
path: path.join(memoryDir, "old.md"),
|
|
793
|
+
content: "Alice lives in Austin",
|
|
794
|
+
frontmatter: {
|
|
795
|
+
id: "old",
|
|
796
|
+
category: "fact",
|
|
797
|
+
created: "2026-08-01T00:00:00.000Z",
|
|
798
|
+
updated: "2026-08-01T00:00:00.000Z",
|
|
799
|
+
source: "test",
|
|
800
|
+
confidence: 0.9,
|
|
801
|
+
confidenceTier: "explicit",
|
|
802
|
+
tags: [],
|
|
803
|
+
status: "active",
|
|
804
|
+
},
|
|
805
|
+
} as unknown as MemoryFile;
|
|
806
|
+
const losing = {
|
|
807
|
+
...activeOld,
|
|
808
|
+
path: path.join(memoryDir, "new.md"),
|
|
809
|
+
content: "Alice lives in Boston",
|
|
810
|
+
frontmatter: {
|
|
811
|
+
...activeOld.frontmatter,
|
|
812
|
+
id: "new",
|
|
813
|
+
supersedes: "old",
|
|
814
|
+
},
|
|
815
|
+
} as unknown as MemoryFile;
|
|
816
|
+
const storage = {
|
|
817
|
+
dir: memoryDir,
|
|
818
|
+
getMemoryById: async (id: string) => (id === "old" ? activeOld : id === "new" ? losing : null),
|
|
819
|
+
readAllMemories: async () => [activeOld, losing],
|
|
820
|
+
readAllColdMemories: async () => [],
|
|
821
|
+
supersedeMemory: async () => false,
|
|
822
|
+
writeMemoryFrontmatter: async () => {
|
|
823
|
+
throw new Error("supersedes clear I/O failed");
|
|
824
|
+
},
|
|
825
|
+
} as unknown as StorageManager;
|
|
826
|
+
const coordinator = new ContradictionLinkingCoordinator({
|
|
827
|
+
getConfig: () => config,
|
|
828
|
+
isSearchAvailable: () => false,
|
|
829
|
+
searchAcrossNamespaces: async () => [],
|
|
830
|
+
extractMemoryIdsFromResults: () => [],
|
|
831
|
+
namespaceFromPath: () => "default",
|
|
832
|
+
storageForNamespace: async () => storage,
|
|
833
|
+
getExtraction: () => ({}) as ExtractionEngine,
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
const outcome = await coordinator.applyDeferredContradictionResolve(
|
|
837
|
+
{
|
|
838
|
+
supersededId: "old",
|
|
839
|
+
reason: "city changed",
|
|
840
|
+
supersededPath: activeOld.path,
|
|
841
|
+
supersededCreated: activeOld.frontmatter.created,
|
|
842
|
+
supersededTags: [],
|
|
843
|
+
},
|
|
844
|
+
storage,
|
|
845
|
+
"new",
|
|
846
|
+
false,
|
|
847
|
+
);
|
|
848
|
+
|
|
849
|
+
assert.equal(outcome, "supersedes_clear_failed");
|
|
850
|
+
assert.equal(activeOld.frontmatter.status, "active");
|
|
851
|
+
assert.equal(losing.frontmatter.supersedes, "old");
|
|
852
|
+
} finally {
|
|
853
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
test("propagation snapshots the canonical active cold contradiction target", async () => {
|
|
857
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-contradiction-propagation-cold-"));
|
|
858
|
+
try {
|
|
859
|
+
const config = baseConfig(memoryDir, {
|
|
860
|
+
contradictionAutoResolve: true,
|
|
861
|
+
dependencyPropagation: {
|
|
862
|
+
enabled: true,
|
|
863
|
+
linkTypes: ["supports"],
|
|
864
|
+
maxDependents: 10,
|
|
865
|
+
timeoutMs: 50,
|
|
866
|
+
dryRun: false,
|
|
867
|
+
},
|
|
868
|
+
});
|
|
869
|
+
const activeCold = {
|
|
870
|
+
path: path.join(memoryDir, "cold", "old.md"),
|
|
871
|
+
content: "canonical cold contradiction",
|
|
872
|
+
frontmatter: {
|
|
873
|
+
id: "old",
|
|
874
|
+
category: "fact",
|
|
875
|
+
created: "2026-08-01T00:00:00.000Z",
|
|
876
|
+
updated: "2026-08-01T00:00:00.000Z",
|
|
877
|
+
source: "test",
|
|
878
|
+
confidence: 0.9,
|
|
879
|
+
confidenceTier: "explicit",
|
|
880
|
+
tags: [],
|
|
881
|
+
status: "active",
|
|
882
|
+
links: [{ targetId: "dependent", linkType: "supports", strength: 0.9 }],
|
|
883
|
+
},
|
|
884
|
+
} as unknown as MemoryFile;
|
|
885
|
+
const inactiveHot = {
|
|
886
|
+
...activeCold,
|
|
887
|
+
path: path.join(memoryDir, "old.md"),
|
|
888
|
+
content: "inactive hot duplicate",
|
|
889
|
+
frontmatter: {
|
|
890
|
+
...activeCold.frontmatter,
|
|
891
|
+
status: "superseded",
|
|
892
|
+
supersededBy: "previous",
|
|
893
|
+
links: [],
|
|
894
|
+
},
|
|
895
|
+
} as unknown as MemoryFile;
|
|
896
|
+
const replacement = {
|
|
897
|
+
...activeCold,
|
|
898
|
+
path: path.join(memoryDir, "new.md"),
|
|
899
|
+
content: "replacement contradiction",
|
|
900
|
+
frontmatter: {
|
|
901
|
+
...activeCold.frontmatter,
|
|
902
|
+
id: "new",
|
|
903
|
+
links: [],
|
|
904
|
+
},
|
|
905
|
+
} as unknown as MemoryFile;
|
|
906
|
+
const dependent = {
|
|
907
|
+
...activeCold,
|
|
908
|
+
path: path.join(memoryDir, "dependent.md"),
|
|
909
|
+
content: "dependent claim",
|
|
910
|
+
frontmatter: {
|
|
911
|
+
...activeCold.frontmatter,
|
|
912
|
+
id: "dependent",
|
|
913
|
+
links: [],
|
|
914
|
+
},
|
|
915
|
+
} as unknown as MemoryFile;
|
|
916
|
+
let revalidationInput: {
|
|
917
|
+
superseded: { id: string; content: string };
|
|
918
|
+
dependents: Array<{ id: string; content: string }>;
|
|
919
|
+
} | null = null;
|
|
920
|
+
const storage = {
|
|
921
|
+
dir: memoryDir,
|
|
922
|
+
getMemoryById: async (id: string) =>
|
|
923
|
+
id === "old" ? inactiveHot : id === "new" ? replacement : id === "dependent" ? dependent : null,
|
|
924
|
+
readAllMemories: async () => [inactiveHot, replacement, dependent],
|
|
925
|
+
readAllColdMemories: async () => [activeCold],
|
|
926
|
+
supersedeMemory: async () => true,
|
|
927
|
+
} as unknown as StorageManager;
|
|
928
|
+
const extraction = {
|
|
929
|
+
revalidateDependents: async (
|
|
930
|
+
superseded: { id: string; content: string },
|
|
931
|
+
_replacement: { id: string; content: string } | null,
|
|
932
|
+
dependents: Array<{ id: string; content: string }>,
|
|
933
|
+
) => {
|
|
934
|
+
revalidationInput = {
|
|
935
|
+
superseded,
|
|
936
|
+
dependents: dependents.map(({ id, content }) => ({ id, content })),
|
|
937
|
+
};
|
|
938
|
+
return {
|
|
939
|
+
verdicts: dependents.map((item) => ({
|
|
940
|
+
memoryId: item.id,
|
|
941
|
+
verdict: "still_valid" as const,
|
|
942
|
+
})),
|
|
943
|
+
};
|
|
944
|
+
},
|
|
945
|
+
} as unknown as ExtractionEngine;
|
|
946
|
+
const coordinator = new ContradictionLinkingCoordinator({
|
|
947
|
+
getConfig: () => config,
|
|
948
|
+
isSearchAvailable: () => false,
|
|
949
|
+
searchAcrossNamespaces: async () => [],
|
|
950
|
+
extractMemoryIdsFromResults: () => [],
|
|
951
|
+
namespaceFromPath: () => "default",
|
|
952
|
+
storageForNamespace: async () => storage,
|
|
953
|
+
getExtraction: () => extraction,
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
const outcome = await coordinator.applyDeferredContradictionResolve(
|
|
957
|
+
{
|
|
958
|
+
supersededId: "old",
|
|
959
|
+
reason: "city changed",
|
|
960
|
+
supersededPath: activeCold.path,
|
|
961
|
+
supersededCreated: activeCold.frontmatter.created,
|
|
962
|
+
supersededTags: [],
|
|
963
|
+
},
|
|
964
|
+
storage,
|
|
965
|
+
"new",
|
|
966
|
+
false,
|
|
967
|
+
);
|
|
968
|
+
|
|
969
|
+
assert.equal(outcome, "resolved");
|
|
970
|
+
assert.deepEqual(revalidationInput, {
|
|
971
|
+
superseded: { id: "old", content: "canonical cold contradiction" },
|
|
972
|
+
dependents: [{ id: "dependent", content: "dependent claim" }],
|
|
973
|
+
});
|
|
974
|
+
} finally {
|
|
975
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
test("preserves the anchor when the supersede race target is missing", async () => {
|
|
979
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-contradiction-missing-race-"));
|
|
980
|
+
try {
|
|
981
|
+
const config = baseConfig(memoryDir, { contradictionAutoResolve: true });
|
|
982
|
+
const old = {
|
|
983
|
+
path: path.join(memoryDir, "old.md"),
|
|
984
|
+
content: "Alice lives in Austin",
|
|
985
|
+
frontmatter: {
|
|
986
|
+
id: "old",
|
|
987
|
+
category: "fact",
|
|
988
|
+
created: "2026-08-01T00:00:00.000Z",
|
|
989
|
+
updated: "2026-08-01T00:00:00.000Z",
|
|
990
|
+
source: "test",
|
|
991
|
+
confidence: 0.9,
|
|
992
|
+
confidenceTier: "explicit",
|
|
993
|
+
tags: [],
|
|
994
|
+
status: "active",
|
|
995
|
+
},
|
|
996
|
+
} as unknown as MemoryFile;
|
|
997
|
+
const losing = {
|
|
998
|
+
...old,
|
|
999
|
+
path: path.join(memoryDir, "new.md"),
|
|
1000
|
+
content: "Alice lives in Boston",
|
|
1001
|
+
frontmatter: {
|
|
1002
|
+
...old.frontmatter,
|
|
1003
|
+
id: "new",
|
|
1004
|
+
supersedes: "old",
|
|
1005
|
+
},
|
|
1006
|
+
} as unknown as MemoryFile;
|
|
1007
|
+
let clearCalls = 0;
|
|
1008
|
+
const storage = {
|
|
1009
|
+
dir: memoryDir,
|
|
1010
|
+
getMemoryById: async (id: string) => (id === "old" ? old : id === "new" ? losing : null),
|
|
1011
|
+
readAllMemories: async () => [],
|
|
1012
|
+
readAllColdMemories: async () => [],
|
|
1013
|
+
supersedeMemory: async () => false,
|
|
1014
|
+
writeMemoryFrontmatter: async () => {
|
|
1015
|
+
clearCalls += 1;
|
|
1016
|
+
return true;
|
|
1017
|
+
},
|
|
1018
|
+
} as unknown as StorageManager;
|
|
1019
|
+
const coordinator = new ContradictionLinkingCoordinator({
|
|
1020
|
+
getConfig: () => config,
|
|
1021
|
+
isSearchAvailable: () => false,
|
|
1022
|
+
searchAcrossNamespaces: async () => [],
|
|
1023
|
+
extractMemoryIdsFromResults: () => [],
|
|
1024
|
+
namespaceFromPath: () => "default",
|
|
1025
|
+
storageForNamespace: async () => storage,
|
|
1026
|
+
getExtraction: () => ({}) as ExtractionEngine,
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
const outcome = await coordinator.applyDeferredContradictionResolve(
|
|
1030
|
+
{
|
|
1031
|
+
supersededId: "old",
|
|
1032
|
+
reason: "city changed",
|
|
1033
|
+
supersededPath: old.path,
|
|
1034
|
+
supersededCreated: old.frontmatter.created,
|
|
1035
|
+
supersededTags: [],
|
|
1036
|
+
},
|
|
1037
|
+
storage,
|
|
1038
|
+
"new",
|
|
1039
|
+
false,
|
|
1040
|
+
);
|
|
1041
|
+
|
|
1042
|
+
assert.equal(outcome, "supersede_failed");
|
|
1043
|
+
assert.equal(old.frontmatter.status, "active");
|
|
1044
|
+
assert.equal(losing.frontmatter.supersedes, "old");
|
|
1045
|
+
assert.equal(clearCalls, 0);
|
|
1046
|
+
} finally {
|
|
1047
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
test("clears the canonical active new-memory copy after a confirmed race", async () => {
|
|
1052
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-contradiction-new-copy-race-"));
|
|
1053
|
+
try {
|
|
1054
|
+
const config = baseConfig(memoryDir, { contradictionAutoResolve: true });
|
|
1055
|
+
const inactiveTarget = {
|
|
1056
|
+
path: path.join(memoryDir, "old.md"),
|
|
1057
|
+
content: "Alice lives in Austin",
|
|
1058
|
+
frontmatter: {
|
|
1059
|
+
id: "old",
|
|
1060
|
+
category: "fact",
|
|
1061
|
+
created: "2026-08-01T00:00:00.000Z",
|
|
1062
|
+
updated: "2026-08-01T00:00:00.000Z",
|
|
1063
|
+
source: "test",
|
|
1064
|
+
confidence: 0.9,
|
|
1065
|
+
confidenceTier: "explicit",
|
|
1066
|
+
tags: [],
|
|
1067
|
+
status: "superseded",
|
|
1068
|
+
},
|
|
1069
|
+
} as unknown as MemoryFile;
|
|
1070
|
+
const losingHot = {
|
|
1071
|
+
...inactiveTarget,
|
|
1072
|
+
path: path.join(memoryDir, "new.md"),
|
|
1073
|
+
content: "Alice lives in Boston",
|
|
1074
|
+
frontmatter: {
|
|
1075
|
+
...inactiveTarget.frontmatter,
|
|
1076
|
+
id: "new",
|
|
1077
|
+
status: "superseded",
|
|
1078
|
+
supersedes: "old",
|
|
1079
|
+
},
|
|
1080
|
+
} as unknown as MemoryFile;
|
|
1081
|
+
const losingCold = {
|
|
1082
|
+
...losingHot,
|
|
1083
|
+
path: path.join(memoryDir, "cold", "new.md"),
|
|
1084
|
+
frontmatter: {
|
|
1085
|
+
...losingHot.frontmatter,
|
|
1086
|
+
status: "active",
|
|
1087
|
+
},
|
|
1088
|
+
} as unknown as MemoryFile;
|
|
1089
|
+
const clearedPaths: string[] = [];
|
|
1090
|
+
const storage = {
|
|
1091
|
+
dir: memoryDir,
|
|
1092
|
+
getMemoryById: async (id: string) =>
|
|
1093
|
+
id === "old" ? inactiveTarget : id === "new" ? losingHot : null,
|
|
1094
|
+
readAllMemories: async () => [inactiveTarget, losingHot],
|
|
1095
|
+
readAllColdMemories: async () => [losingCold],
|
|
1096
|
+
supersedeMemory: async () => false,
|
|
1097
|
+
writeMemoryFrontmatter: async (memory: MemoryFile) => {
|
|
1098
|
+
clearedPaths.push(memory.path);
|
|
1099
|
+
memory.frontmatter.supersedes = undefined;
|
|
1100
|
+
return true;
|
|
1101
|
+
},
|
|
1102
|
+
} as unknown as StorageManager;
|
|
1103
|
+
const coordinator = new ContradictionLinkingCoordinator({
|
|
1104
|
+
getConfig: () => config,
|
|
1105
|
+
isSearchAvailable: () => false,
|
|
1106
|
+
searchAcrossNamespaces: async () => [],
|
|
1107
|
+
extractMemoryIdsFromResults: () => [],
|
|
1108
|
+
namespaceFromPath: () => "default",
|
|
1109
|
+
storageForNamespace: async () => storage,
|
|
1110
|
+
getExtraction: () => ({}) as ExtractionEngine,
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
const outcome = await coordinator.applyDeferredContradictionResolve(
|
|
1114
|
+
{
|
|
1115
|
+
supersededId: "old",
|
|
1116
|
+
reason: "city changed",
|
|
1117
|
+
supersededPath: inactiveTarget.path,
|
|
1118
|
+
supersededCreated: inactiveTarget.frontmatter.created,
|
|
1119
|
+
supersededTags: [],
|
|
1120
|
+
},
|
|
1121
|
+
storage,
|
|
1122
|
+
"new",
|
|
1123
|
+
false,
|
|
1124
|
+
);
|
|
1125
|
+
|
|
1126
|
+
assert.equal(outcome, "lost_race");
|
|
1127
|
+
assert.deepEqual(clearedPaths, [losingCold.path]);
|
|
1128
|
+
assert.equal(losingHot.frontmatter.supersedes, "old");
|
|
1129
|
+
assert.equal(losingCold.frontmatter.supersedes, undefined);
|
|
1130
|
+
} finally {
|
|
1131
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
1134
|
+
test("preserves the anchor when the canonical new-memory copy is unreadable or inactive", async () => {
|
|
1135
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-contradiction-new-missing-race-"));
|
|
1136
|
+
try {
|
|
1137
|
+
const config = baseConfig(memoryDir, { contradictionAutoResolve: true });
|
|
1138
|
+
const inactiveTarget = {
|
|
1139
|
+
path: path.join(memoryDir, "old.md"),
|
|
1140
|
+
content: "Alice lives in Austin",
|
|
1141
|
+
frontmatter: {
|
|
1142
|
+
id: "old",
|
|
1143
|
+
category: "fact",
|
|
1144
|
+
created: "2026-08-01T00:00:00.000Z",
|
|
1145
|
+
updated: "2026-08-01T00:00:00.000Z",
|
|
1146
|
+
source: "test",
|
|
1147
|
+
confidence: 0.9,
|
|
1148
|
+
confidenceTier: "explicit",
|
|
1149
|
+
tags: [],
|
|
1150
|
+
status: "superseded",
|
|
1151
|
+
},
|
|
1152
|
+
} as unknown as MemoryFile;
|
|
1153
|
+
const inactiveLosing = {
|
|
1154
|
+
...inactiveTarget,
|
|
1155
|
+
path: path.join(memoryDir, "new.md"),
|
|
1156
|
+
content: "Alice lives in Boston",
|
|
1157
|
+
frontmatter: {
|
|
1158
|
+
...inactiveTarget.frontmatter,
|
|
1159
|
+
id: "new",
|
|
1160
|
+
supersedes: "old",
|
|
1161
|
+
},
|
|
1162
|
+
} as unknown as MemoryFile;
|
|
1163
|
+
let hotMemories: MemoryFile[] = [inactiveTarget];
|
|
1164
|
+
let clearCalls = 0;
|
|
1165
|
+
const storage = {
|
|
1166
|
+
dir: memoryDir,
|
|
1167
|
+
getMemoryById: async (id: string) => (id === "old" ? inactiveTarget : null),
|
|
1168
|
+
readAllMemories: async () => hotMemories,
|
|
1169
|
+
readAllColdMemories: async () => [],
|
|
1170
|
+
supersedeMemory: async () => false,
|
|
1171
|
+
writeMemoryFrontmatter: async () => {
|
|
1172
|
+
clearCalls += 1;
|
|
1173
|
+
return true;
|
|
1174
|
+
},
|
|
1175
|
+
} as unknown as StorageManager;
|
|
1176
|
+
const coordinator = new ContradictionLinkingCoordinator({
|
|
1177
|
+
getConfig: () => config,
|
|
1178
|
+
isSearchAvailable: () => false,
|
|
1179
|
+
searchAcrossNamespaces: async () => [],
|
|
1180
|
+
extractMemoryIdsFromResults: () => [],
|
|
1181
|
+
namespaceFromPath: () => "default",
|
|
1182
|
+
storageForNamespace: async () => storage,
|
|
1183
|
+
getExtraction: () => ({}) as ExtractionEngine,
|
|
1184
|
+
});
|
|
1185
|
+
const contradiction = {
|
|
1186
|
+
supersededId: "old",
|
|
1187
|
+
reason: "city changed",
|
|
1188
|
+
supersededPath: inactiveTarget.path,
|
|
1189
|
+
supersededCreated: inactiveTarget.frontmatter.created,
|
|
1190
|
+
supersededTags: [],
|
|
1191
|
+
};
|
|
1192
|
+
|
|
1193
|
+
const missingOutcome = await coordinator.applyDeferredContradictionResolve(
|
|
1194
|
+
contradiction,
|
|
1195
|
+
storage,
|
|
1196
|
+
"new",
|
|
1197
|
+
false,
|
|
1198
|
+
);
|
|
1199
|
+
hotMemories = [inactiveTarget, inactiveLosing];
|
|
1200
|
+
const inactiveOutcome = await coordinator.applyDeferredContradictionResolve(
|
|
1201
|
+
contradiction,
|
|
1202
|
+
storage,
|
|
1203
|
+
"new",
|
|
1204
|
+
false,
|
|
1205
|
+
);
|
|
1206
|
+
|
|
1207
|
+
assert.equal(missingOutcome, "supersede_failed");
|
|
1208
|
+
assert.equal(inactiveOutcome, "supersede_failed");
|
|
1209
|
+
assert.equal(inactiveTarget.frontmatter.status, "superseded");
|
|
1210
|
+
assert.equal(inactiveLosing.frontmatter.supersedes, "old");
|
|
1211
|
+
assert.equal(clearCalls, 0);
|
|
1212
|
+
} finally {
|
|
1213
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
1214
|
+
}
|
|
1215
|
+
});
|
|
1216
|
+
test("preserves the anchor when supersede I/O throws", async () => {
|
|
1217
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-contradiction-supersede-io-"));
|
|
1218
|
+
try {
|
|
1219
|
+
const config = baseConfig(memoryDir, { contradictionAutoResolve: true });
|
|
1220
|
+
const old = {
|
|
1221
|
+
path: path.join(memoryDir, "old.md"),
|
|
1222
|
+
content: "Alice lives in Austin",
|
|
1223
|
+
frontmatter: {
|
|
1224
|
+
id: "old",
|
|
1225
|
+
category: "fact",
|
|
1226
|
+
created: "2026-08-01T00:00:00.000Z",
|
|
1227
|
+
updated: "2026-08-01T00:00:00.000Z",
|
|
1228
|
+
source: "test",
|
|
1229
|
+
confidence: 0.9,
|
|
1230
|
+
confidenceTier: "explicit",
|
|
1231
|
+
tags: [],
|
|
1232
|
+
status: "active",
|
|
1233
|
+
},
|
|
1234
|
+
} as unknown as MemoryFile;
|
|
1235
|
+
const storage = {
|
|
1236
|
+
dir: memoryDir,
|
|
1237
|
+
getMemoryById: async (id: string) => (id === "old" ? old : null),
|
|
1238
|
+
readAllColdMemories: async () => [],
|
|
1239
|
+
supersedeMemory: async () => {
|
|
1240
|
+
throw new Error("supersede I/O failed");
|
|
1241
|
+
},
|
|
1242
|
+
} as unknown as StorageManager;
|
|
1243
|
+
const coordinator = new ContradictionLinkingCoordinator({
|
|
1244
|
+
getConfig: () => config,
|
|
1245
|
+
isSearchAvailable: () => false,
|
|
1246
|
+
searchAcrossNamespaces: async () => [],
|
|
1247
|
+
extractMemoryIdsFromResults: () => [],
|
|
1248
|
+
namespaceFromPath: () => "default",
|
|
1249
|
+
storageForNamespace: async () => storage,
|
|
1250
|
+
getExtraction: () => ({}) as ExtractionEngine,
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
const outcome = await coordinator.applyDeferredContradictionResolve(
|
|
1254
|
+
{
|
|
1255
|
+
supersededId: "old",
|
|
1256
|
+
reason: "city changed",
|
|
1257
|
+
supersededPath: old.path,
|
|
1258
|
+
supersededCreated: old.frontmatter.created,
|
|
1259
|
+
supersededTags: [],
|
|
1260
|
+
},
|
|
1261
|
+
storage,
|
|
1262
|
+
"new",
|
|
1263
|
+
false,
|
|
1264
|
+
);
|
|
1265
|
+
assert.equal(outcome, "supersede_failed");
|
|
1266
|
+
assert.equal(old.frontmatter.status, "active");
|
|
1267
|
+
} finally {
|
|
1268
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
1269
|
+
}
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
test("preserves the anchor when a losing supersedes clear fails", async () => {
|
|
1273
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-contradiction-clear-io-"));
|
|
1274
|
+
try {
|
|
1275
|
+
const config = baseConfig(memoryDir, { contradictionAutoResolve: true });
|
|
1276
|
+
const old = {
|
|
1277
|
+
path: path.join(memoryDir, "old.md"),
|
|
1278
|
+
content: "Alice lives in Austin",
|
|
1279
|
+
frontmatter: {
|
|
1280
|
+
id: "old",
|
|
1281
|
+
category: "fact",
|
|
1282
|
+
created: "2026-08-01T00:00:00.000Z",
|
|
1283
|
+
updated: "2026-08-01T00:00:00.000Z",
|
|
1284
|
+
source: "test",
|
|
1285
|
+
confidence: 0.9,
|
|
1286
|
+
confidenceTier: "explicit",
|
|
1287
|
+
tags: [],
|
|
1288
|
+
status: "active",
|
|
1289
|
+
},
|
|
1290
|
+
} as unknown as MemoryFile;
|
|
1291
|
+
const losing = {
|
|
1292
|
+
path: path.join(memoryDir, "new.md"),
|
|
1293
|
+
content: "Alice lives in Boston",
|
|
1294
|
+
frontmatter: {
|
|
1295
|
+
...old.frontmatter,
|
|
1296
|
+
id: "new",
|
|
1297
|
+
supersedes: "old",
|
|
1298
|
+
},
|
|
1299
|
+
} as unknown as MemoryFile;
|
|
1300
|
+
const storage = {
|
|
1301
|
+
dir: memoryDir,
|
|
1302
|
+
getMemoryById: async (id: string) => (id === "old" ? old : id === "new" ? losing : null),
|
|
1303
|
+
readAllColdMemories: async () => [],
|
|
1304
|
+
supersedeMemory: async () => {
|
|
1305
|
+
old.frontmatter.status = "superseded";
|
|
1306
|
+
return false;
|
|
1307
|
+
},
|
|
1308
|
+
writeMemoryFrontmatter: async () => {
|
|
1309
|
+
throw new Error("supersedes clear I/O failed");
|
|
1310
|
+
},
|
|
1311
|
+
} as unknown as StorageManager;
|
|
1312
|
+
const coordinator = new ContradictionLinkingCoordinator({
|
|
1313
|
+
getConfig: () => config,
|
|
1314
|
+
isSearchAvailable: () => false,
|
|
1315
|
+
searchAcrossNamespaces: async () => [],
|
|
1316
|
+
extractMemoryIdsFromResults: () => [],
|
|
1317
|
+
namespaceFromPath: () => "default",
|
|
1318
|
+
storageForNamespace: async () => storage,
|
|
1319
|
+
getExtraction: () => ({}) as ExtractionEngine,
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
const outcome = await coordinator.applyDeferredContradictionResolve(
|
|
1323
|
+
{
|
|
1324
|
+
supersededId: "old",
|
|
1325
|
+
reason: "city changed",
|
|
1326
|
+
supersededPath: old.path,
|
|
1327
|
+
supersededCreated: old.frontmatter.created,
|
|
1328
|
+
supersededTags: [],
|
|
1329
|
+
},
|
|
1330
|
+
storage,
|
|
1331
|
+
"new",
|
|
1332
|
+
false,
|
|
1333
|
+
);
|
|
1334
|
+
assert.equal(outcome, "supersedes_clear_failed");
|
|
1335
|
+
assert.equal(old.frontmatter.status, "superseded");
|
|
1336
|
+
assert.equal(losing.frontmatter.supersedes, "old");
|
|
1337
|
+
} finally {
|
|
1338
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
1339
|
+
}
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
test("removes temporally superseded anchors before the next same-batch fact", async () => {
|
|
1343
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-contradiction-temporal-snapshot-"));
|
|
1344
|
+
try {
|
|
1345
|
+
const orchestrator = new Orchestrator(baseConfig(memoryDir, { tombstonesEnabled: false })) as unknown as {
|
|
1346
|
+
qmd: { isAvailable: () => boolean };
|
|
1347
|
+
getStorage: (namespace: string) => Promise<StorageManager>;
|
|
1348
|
+
persistExtraction: (
|
|
1349
|
+
result: ExtractionResult,
|
|
1350
|
+
storage: StorageManager,
|
|
1351
|
+
threadId: null,
|
|
1352
|
+
sourceContext?: undefined,
|
|
1353
|
+
baseNamespace?: undefined,
|
|
1354
|
+
scopeProfileWritePlan?: undefined,
|
|
1355
|
+
sourceText?: undefined,
|
|
1356
|
+
graphCaps?: GraphConstructionCapabilitySet,
|
|
1357
|
+
) => Promise<unknown>;
|
|
1358
|
+
contradictionLinkingCoordinator: ContradictionLinkingCoordinator;
|
|
1359
|
+
};
|
|
1360
|
+
orchestrator.qmd = { isAvailable: () => false };
|
|
1361
|
+
const storage = await orchestrator.getStorage("default");
|
|
1362
|
+
await storage.ensureDirectories();
|
|
1363
|
+
const snapshots: MemoryFile[][] = [];
|
|
1364
|
+
orchestrator.contradictionLinkingCoordinator.checkForContradiction = async (...args: unknown[]) => {
|
|
1365
|
+
const anchor = args[3];
|
|
1366
|
+
snapshots.push(
|
|
1367
|
+
anchor && typeof anchor === "object" && "storageSnapshot" in anchor
|
|
1368
|
+
? [...((anchor as { storageSnapshot?: MemoryFile[] }).storageSnapshot ?? [])]
|
|
1369
|
+
: [],
|
|
1370
|
+
);
|
|
1371
|
+
return null;
|
|
1372
|
+
};
|
|
1373
|
+
|
|
1374
|
+
await orchestrator.persistExtraction(
|
|
1375
|
+
extractionResult({
|
|
1376
|
+
category: "fact",
|
|
1377
|
+
content: "Alice lives in Austin",
|
|
1378
|
+
confidence: 0.95,
|
|
1379
|
+
tags: [],
|
|
1380
|
+
entityRef: "person:alice",
|
|
1381
|
+
structuredAttributes: { city: "Austin" },
|
|
1382
|
+
}),
|
|
1383
|
+
storage,
|
|
1384
|
+
null,
|
|
1385
|
+
undefined,
|
|
1386
|
+
undefined,
|
|
1387
|
+
undefined,
|
|
1388
|
+
undefined,
|
|
1389
|
+
{
|
|
1390
|
+
entityGraph: false,
|
|
1391
|
+
timeGraph: false,
|
|
1392
|
+
causalGraph: false,
|
|
1393
|
+
multiGraphMemory: false,
|
|
1394
|
+
graphWriteSessionAdjacency: false,
|
|
1395
|
+
} as unknown as GraphConstructionCapabilitySet,
|
|
1396
|
+
);
|
|
1397
|
+
|
|
1398
|
+
const result = extractionResult({
|
|
1399
|
+
category: "fact",
|
|
1400
|
+
content: "Alice lives in New York",
|
|
1401
|
+
confidence: 0.95,
|
|
1402
|
+
tags: [],
|
|
1403
|
+
entityRef: "person:alice",
|
|
1404
|
+
structuredAttributes: { city: "New York" },
|
|
1405
|
+
});
|
|
1406
|
+
result.facts.push({
|
|
1407
|
+
category: "fact",
|
|
1408
|
+
content: "The current city for Alice is Boston, Massachusetts",
|
|
1409
|
+
confidence: 0.95,
|
|
1410
|
+
tags: [],
|
|
1411
|
+
entityRef: "person:alice",
|
|
1412
|
+
structuredAttributes: { city: "Boston" },
|
|
1413
|
+
});
|
|
1414
|
+
await orchestrator.persistExtraction(
|
|
1415
|
+
result,
|
|
1416
|
+
storage,
|
|
1417
|
+
null,
|
|
1418
|
+
undefined,
|
|
1419
|
+
undefined,
|
|
1420
|
+
undefined,
|
|
1421
|
+
undefined,
|
|
1422
|
+
{
|
|
1423
|
+
entityGraph: false,
|
|
1424
|
+
timeGraph: false,
|
|
1425
|
+
causalGraph: false,
|
|
1426
|
+
multiGraphMemory: false,
|
|
1427
|
+
graphWriteSessionAdjacency: false,
|
|
1428
|
+
},
|
|
1429
|
+
);
|
|
1430
|
+
assert.equal(snapshots.length, 3);
|
|
1431
|
+
assert.equal(snapshots[1]?.some((memory) => memory.content.includes("Austin")), true);
|
|
1432
|
+
assert.equal(snapshots[2]?.some((memory) => memory.content.includes("Austin")), false);
|
|
1433
|
+
assert.equal(snapshots[2]?.some((memory) => memory.content.includes("New York")), true);
|
|
1434
|
+
assert.equal(snapshots[2]?.some((memory) => memory.content.includes("Boston")), false);
|
|
1435
|
+
} finally {
|
|
1436
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
1437
|
+
}
|
|
1438
|
+
});
|
|
1439
|
+
test("removes superseded anchors from the same-batch snapshot", async () => {
|
|
1440
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-contradiction-superseded-snapshot-"));
|
|
1441
|
+
try {
|
|
1442
|
+
const orchestrator = new Orchestrator(baseConfig(memoryDir)) as unknown as {
|
|
1443
|
+
qmd: { isAvailable: () => boolean };
|
|
1444
|
+
getStorage: (namespace: string) => Promise<StorageManager>;
|
|
1445
|
+
persistExtraction: (
|
|
1446
|
+
result: ExtractionResult,
|
|
1447
|
+
storage: StorageManager,
|
|
1448
|
+
threadId: null,
|
|
1449
|
+
sourceContext?: undefined,
|
|
1450
|
+
baseNamespace?: undefined,
|
|
1451
|
+
scopeProfileWritePlan?: undefined,
|
|
1452
|
+
sourceText?: undefined,
|
|
1453
|
+
graphCaps?: GraphConstructionCapabilitySet,
|
|
1454
|
+
) => Promise<unknown>;
|
|
1455
|
+
contradictionLinkingCoordinator: ContradictionLinkingCoordinator;
|
|
1456
|
+
};
|
|
1457
|
+
orchestrator.qmd = { isAvailable: () => false };
|
|
1458
|
+
const storage = await orchestrator.getStorage("default");
|
|
1459
|
+
await storage.ensureDirectories();
|
|
1460
|
+
let checkCount = 0;
|
|
1461
|
+
let oldId = "";
|
|
1462
|
+
orchestrator.contradictionLinkingCoordinator.checkForContradiction = async (...args: unknown[]) => {
|
|
1463
|
+
const anchor = args[3];
|
|
1464
|
+
const snapshot =
|
|
1465
|
+
anchor && typeof anchor === "object" && "storageSnapshot" in anchor
|
|
1466
|
+
? ((anchor as { storageSnapshot?: MemoryFile[] }).storageSnapshot ?? [])
|
|
1467
|
+
: [];
|
|
1468
|
+
if (checkCount === 0) {
|
|
1469
|
+
checkCount++;
|
|
1470
|
+
return null;
|
|
1471
|
+
}
|
|
1472
|
+
if (checkCount === 1) {
|
|
1473
|
+
assert.ok(snapshot.some((memory) => memory.frontmatter.id === oldId));
|
|
1474
|
+
checkCount++;
|
|
1475
|
+
const old = snapshot.find((memory) => memory.frontmatter.id === oldId);
|
|
1476
|
+
return {
|
|
1477
|
+
supersededId: oldId,
|
|
1478
|
+
confidence: 0.99,
|
|
1479
|
+
reason: "city changed",
|
|
1480
|
+
supersededPath: old?.path ?? "",
|
|
1481
|
+
supersededCreated: "2026-08-09T00:00:00.000Z",
|
|
1482
|
+
supersededTags: old?.frontmatter.tags ?? [],
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
assert.equal(snapshot.some((memory) => memory.frontmatter.id === oldId), false);
|
|
1486
|
+
checkCount++;
|
|
1487
|
+
return null;
|
|
1488
|
+
};
|
|
1489
|
+
const graphCaps = {
|
|
1490
|
+
entityGraph: false,
|
|
1491
|
+
timeGraph: false,
|
|
1492
|
+
causalGraph: false,
|
|
1493
|
+
multiGraphMemory: false,
|
|
1494
|
+
graphWriteSessionAdjacency: false,
|
|
1495
|
+
};
|
|
1496
|
+
await orchestrator.persistExtraction(
|
|
1497
|
+
extractionResult({
|
|
1498
|
+
category: "fact",
|
|
1499
|
+
content: "Alice lives in Austin",
|
|
1500
|
+
confidence: 0.95,
|
|
1501
|
+
tags: [],
|
|
1502
|
+
entityRef: "person:alice",
|
|
1503
|
+
}),
|
|
1504
|
+
storage,
|
|
1505
|
+
null,
|
|
1506
|
+
undefined,
|
|
1507
|
+
undefined,
|
|
1508
|
+
undefined,
|
|
1509
|
+
undefined,
|
|
1510
|
+
graphCaps,
|
|
1511
|
+
);
|
|
1512
|
+
oldId = (await storage.readAllMemories()).find((memory) => memory.content.includes("Austin"))?.frontmatter.id ?? "";
|
|
1513
|
+
assert.notEqual(oldId, "");
|
|
1514
|
+
const result = extractionResult({
|
|
1515
|
+
category: "fact",
|
|
1516
|
+
content: "Alice lives in New York",
|
|
1517
|
+
confidence: 0.95,
|
|
1518
|
+
tags: [],
|
|
1519
|
+
entityRef: "person:alice",
|
|
1520
|
+
});
|
|
1521
|
+
result.facts.push({
|
|
1522
|
+
category: "fact",
|
|
1523
|
+
content: "Alice works in New York",
|
|
1524
|
+
confidence: 0.95,
|
|
1525
|
+
tags: [],
|
|
1526
|
+
entityRef: "person:alice",
|
|
1527
|
+
});
|
|
1528
|
+
await orchestrator.persistExtraction(
|
|
1529
|
+
result,
|
|
1530
|
+
storage,
|
|
1531
|
+
null,
|
|
1532
|
+
undefined,
|
|
1533
|
+
undefined,
|
|
1534
|
+
undefined,
|
|
1535
|
+
undefined,
|
|
1536
|
+
graphCaps,
|
|
1537
|
+
);
|
|
1538
|
+
assert.equal(checkCount, 3);
|
|
1539
|
+
} finally {
|
|
1540
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
test("supersedeMemory does not overwrite an already-superseded cold memory", async () => {
|
|
1545
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-supersede-cold-cas-"));
|
|
1546
|
+
try {
|
|
1547
|
+
const orchestrator = new Orchestrator(baseConfig(memoryDir, { tombstonesEnabled: false })) as unknown as {
|
|
1548
|
+
getStorage: (namespace: string) => Promise<StorageManager>;
|
|
1549
|
+
};
|
|
1550
|
+
const storage = await orchestrator.getStorage("default");
|
|
1551
|
+
await storage.ensureDirectories();
|
|
1552
|
+
|
|
1553
|
+
const { id } = await storage.writeMemory("preference", "Alice prefers tea", { source: "test" });
|
|
1554
|
+
const hot = (await storage.readAllMemories()).find((memory) => memory.frontmatter.id === id);
|
|
1555
|
+
assert.ok(hot);
|
|
1556
|
+
const moved = await storage.migrateMemoryToTier(hot, "cold");
|
|
1557
|
+
assert.equal(moved.changed, true);
|
|
1558
|
+
|
|
1559
|
+
assert.equal(await storage.supersedeMemory(id, "replacement-one", "first replacement"), true);
|
|
1560
|
+
assert.equal(await storage.supersedeMemory(id, "replacement-two", "duplicate replacement"), false);
|
|
1561
|
+
|
|
1562
|
+
const cold = (await storage.readAllColdMemories()).find((memory) => memory.frontmatter.id === id);
|
|
1563
|
+
assert.ok(cold);
|
|
1564
|
+
assert.equal(cold.frontmatter.status, "superseded");
|
|
1565
|
+
assert.equal(cold.frontmatter.supersededBy, "replacement-one");
|
|
1566
|
+
} finally {
|
|
1567
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
1568
|
+
}
|
|
1569
|
+
});
|
|
1570
|
+
test("supersedeMemory skips an inactive hot duplicate and updates the active cold copy", async () => {
|
|
1571
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-supersede-duplicate-tier-"));
|
|
1572
|
+
try {
|
|
1573
|
+
const orchestrator = new Orchestrator(baseConfig(memoryDir, { tombstonesEnabled: false })) as unknown as {
|
|
1574
|
+
getStorage: (namespace: string) => Promise<StorageManager>;
|
|
1575
|
+
};
|
|
1576
|
+
const storage = await orchestrator.getStorage("default");
|
|
1577
|
+
await storage.ensureDirectories();
|
|
1578
|
+
const { id } = await storage.writeMemory("fact", "Alice lives in Austin", { source: "test" });
|
|
1579
|
+
const hot = (await storage.readAllMemories()).find((memory) => memory.frontmatter.id === id);
|
|
1580
|
+
assert.ok(hot);
|
|
1581
|
+
assert.equal((await storage.migrateMemoryToTier(hot, "cold")).changed, true);
|
|
1582
|
+
const cold = (await storage.readAllColdMemories()).find((memory) => memory.frontmatter.id === id);
|
|
1583
|
+
assert.ok(cold);
|
|
1584
|
+
|
|
1585
|
+
const hotPath = storage.buildTierMemoryPath(cold, "hot");
|
|
1586
|
+
await mkdir(path.dirname(hotPath), { recursive: true });
|
|
1587
|
+
await writeFile(hotPath, await readFile(cold.path));
|
|
1588
|
+
const hotDuplicate = await storage.readMemoryByPath(hotPath);
|
|
1589
|
+
assert.ok(hotDuplicate);
|
|
1590
|
+
await storage.writeMemoryFrontmatter(hotDuplicate, {
|
|
1591
|
+
status: "superseded",
|
|
1592
|
+
supersededBy: "older-replacement",
|
|
1593
|
+
});
|
|
1594
|
+
|
|
1595
|
+
assert.equal(await storage.supersedeMemory(id, "replacement", "city changed"), true);
|
|
1596
|
+
const updatedCold = (await storage.readAllColdMemories()).find((memory) => memory.frontmatter.id === id);
|
|
1597
|
+
assert.ok(updatedCold);
|
|
1598
|
+
assert.equal(updatedCold.frontmatter.status, "superseded");
|
|
1599
|
+
assert.equal(updatedCold.frontmatter.supersededBy, "replacement");
|
|
1600
|
+
const unchangedHot = await storage.readMemoryByPath(hotPath);
|
|
1601
|
+
assert.equal(unchangedHot?.frontmatter.supersededBy, "older-replacement");
|
|
1602
|
+
} finally {
|
|
1603
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
1604
|
+
}
|
|
1605
|
+
});
|
|
1606
|
+
test("committed cold supersession returns true when correction audit fails", async () => {
|
|
1607
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-supersede-cold-audit-failure-"));
|
|
1608
|
+
try {
|
|
1609
|
+
const orchestrator = new Orchestrator(baseConfig(memoryDir, { tombstonesEnabled: false })) as unknown as {
|
|
1610
|
+
getStorage: (namespace: string) => Promise<StorageManager>;
|
|
1611
|
+
};
|
|
1612
|
+
const storage = await orchestrator.getStorage("default");
|
|
1613
|
+
await storage.ensureDirectories();
|
|
1614
|
+
const { id } = await storage.writeMemory("fact", "Alice lives in Austin", {
|
|
1615
|
+
entityRef: "person:alice",
|
|
1616
|
+
source: "test",
|
|
1617
|
+
});
|
|
1618
|
+
const hot = (await storage.readAllMemories()).find((memory) => memory.frontmatter.id === id);
|
|
1619
|
+
assert.ok(hot);
|
|
1620
|
+
assert.equal((await storage.migrateMemoryToTier(hot, "cold")).changed, true);
|
|
1621
|
+
|
|
1622
|
+
storage.writeSealedMemory = async () => {
|
|
1623
|
+
throw new Error("correction audit failed");
|
|
1624
|
+
};
|
|
1625
|
+
|
|
1626
|
+
assert.equal(await storage.supersedeMemory(id, "replacement", "city changed"), true);
|
|
1627
|
+
const cold = (await storage.readAllColdMemories()).find((memory) => memory.frontmatter.id === id);
|
|
1628
|
+
assert.ok(cold);
|
|
1629
|
+
assert.equal(cold.frontmatter.status, "superseded");
|
|
1630
|
+
assert.equal(cold.frontmatter.supersededBy, "replacement");
|
|
1631
|
+
} finally {
|
|
1632
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
1633
|
+
}
|
|
1634
|
+
});
|
|
1635
|
+
|
|
1636
|
+
test("failed cold supersession mutation returns false and preserves the active file", async () => {
|
|
1637
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-supersede-cold-mutation-failure-"));
|
|
1638
|
+
try {
|
|
1639
|
+
const orchestrator = new Orchestrator(baseConfig(memoryDir, { tombstonesEnabled: false })) as unknown as {
|
|
1640
|
+
getStorage: (namespace: string) => Promise<StorageManager>;
|
|
1641
|
+
};
|
|
1642
|
+
const storage = await orchestrator.getStorage("default");
|
|
1643
|
+
await storage.ensureDirectories();
|
|
1644
|
+
const { id } = await storage.writeMemory("fact", "Alice lives in Austin", {
|
|
1645
|
+
entityRef: "person:alice",
|
|
1646
|
+
source: "test",
|
|
1647
|
+
});
|
|
1648
|
+
const hot = (await storage.readAllMemories()).find((memory) => memory.frontmatter.id === id);
|
|
1649
|
+
assert.ok(hot);
|
|
1650
|
+
assert.equal((await storage.migrateMemoryToTier(hot, "cold")).changed, true);
|
|
1651
|
+
const storageInternals = storage as unknown as {
|
|
1652
|
+
writeTombstoneBlockedFrontmatter: (...args: readonly unknown[]) => Promise<void>;
|
|
1653
|
+
};
|
|
1654
|
+
storageInternals.writeTombstoneBlockedFrontmatter = async () => {
|
|
1655
|
+
throw new Error("cold mutation failed");
|
|
1656
|
+
};
|
|
1657
|
+
|
|
1658
|
+
assert.equal(await storage.supersedeMemory(id, "replacement", "city changed"), false);
|
|
1659
|
+
const cold = (await storage.readAllColdMemories()).find((memory) => memory.frontmatter.id === id);
|
|
1660
|
+
assert.ok(cold);
|
|
1661
|
+
assert.equal(cold.frontmatter.status, "active");
|
|
1662
|
+
assert.equal(cold.frontmatter.supersededBy, undefined);
|
|
1663
|
+
} finally {
|
|
1664
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
1665
|
+
}
|
|
1666
|
+
});
|
|
1667
|
+
|
|
1668
|
+
test("anchor-only contradiction verifies an active cold anchor through deferred resolve", async () => {
|
|
1669
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-contradiction-cold-anchor-"));
|
|
1670
|
+
try {
|
|
1671
|
+
const orchestrator = new Orchestrator(baseConfig(memoryDir, {
|
|
1672
|
+
tombstonesEnabled: false,
|
|
1673
|
+
contradictionAutoResolve: true,
|
|
1674
|
+
})) as unknown as {
|
|
1675
|
+
qmd: { isAvailable: () => boolean };
|
|
1676
|
+
getStorage: (namespace: string) => Promise<StorageManager>;
|
|
1677
|
+
persistExtraction: (
|
|
1678
|
+
result: ExtractionResult,
|
|
1679
|
+
storage: StorageManager,
|
|
1680
|
+
threadId: null,
|
|
1681
|
+
sourceContext?: undefined,
|
|
1682
|
+
baseNamespace?: undefined,
|
|
1683
|
+
scopeProfileWritePlan?: undefined,
|
|
1684
|
+
sourceText?: undefined,
|
|
1685
|
+
graphCaps?: GraphConstructionCapabilitySet,
|
|
1686
|
+
) => Promise<unknown>;
|
|
1687
|
+
contradictionLinkingCoordinator: ContradictionLinkingCoordinator;
|
|
1688
|
+
};
|
|
1689
|
+
orchestrator.qmd = { isAvailable: () => false };
|
|
1690
|
+
let coldAnchorVerified = false;
|
|
1691
|
+
let detected: {
|
|
1692
|
+
supersededId: string;
|
|
1693
|
+
confidence: number;
|
|
1694
|
+
reason: string;
|
|
1695
|
+
supersededPath: string;
|
|
1696
|
+
supersededCreated: string;
|
|
1697
|
+
supersededTags: string[];
|
|
1698
|
+
} | null = null;
|
|
1699
|
+
const storage = await orchestrator.getStorage("default");
|
|
1700
|
+
await storage.ensureDirectories();
|
|
1701
|
+
let oldId = "";
|
|
1702
|
+
const graphCaps = {
|
|
1703
|
+
entityGraph: false,
|
|
1704
|
+
timeGraph: false,
|
|
1705
|
+
causalGraph: false,
|
|
1706
|
+
graphWriteSessionAdjacency: false,
|
|
1707
|
+
};
|
|
1708
|
+
let supersedeCalls = 0;
|
|
1709
|
+
const supersedeMemory = storage.supersedeMemory.bind(storage);
|
|
1710
|
+
storage.supersedeMemory = async (...args: Parameters<StorageManager["supersedeMemory"]>) => {
|
|
1711
|
+
supersedeCalls++;
|
|
1712
|
+
return supersedeMemory(...args);
|
|
1713
|
+
};
|
|
1714
|
+
orchestrator.contradictionLinkingCoordinator.checkForContradiction = async (...args: unknown[]) => {
|
|
1715
|
+
const anchor = args[3];
|
|
1716
|
+
const snapshot =
|
|
1717
|
+
anchor && typeof anchor === "object" && "storageSnapshot" in anchor
|
|
1718
|
+
? ((anchor as { storageSnapshot?: MemoryFile[] }).storageSnapshot ?? [])
|
|
1719
|
+
: [];
|
|
1720
|
+
if (!oldId) return null;
|
|
1721
|
+
const old = snapshot.find((memory) => memory.frontmatter.id === oldId);
|
|
1722
|
+
assert.ok(old);
|
|
1723
|
+
assert.ok(old.path.includes(`${path.sep}cold${path.sep}`));
|
|
1724
|
+
coldAnchorVerified = true;
|
|
1725
|
+
detected = {
|
|
1726
|
+
supersededId: oldId,
|
|
1727
|
+
confidence: 0.99,
|
|
1728
|
+
reason: "city changed",
|
|
1729
|
+
supersededPath: old.path,
|
|
1730
|
+
supersededCreated: "2026-08-09T00:00:00.000Z",
|
|
1731
|
+
supersededTags: old.frontmatter.tags ?? [],
|
|
1732
|
+
};
|
|
1733
|
+
return detected;
|
|
1734
|
+
};
|
|
1735
|
+
await orchestrator.persistExtraction(
|
|
1736
|
+
extractionResult({
|
|
1737
|
+
category: "fact",
|
|
1738
|
+
content: "Alice lives in Austin",
|
|
1739
|
+
confidence: 0.95,
|
|
1740
|
+
tags: [],
|
|
1741
|
+
entityRef: "person:alice",
|
|
1742
|
+
}),
|
|
1743
|
+
storage,
|
|
1744
|
+
null,
|
|
1745
|
+
undefined,
|
|
1746
|
+
undefined,
|
|
1747
|
+
undefined,
|
|
1748
|
+
undefined,
|
|
1749
|
+
{
|
|
1750
|
+
entityGraph: false,
|
|
1751
|
+
timeGraph: false,
|
|
1752
|
+
causalGraph: false,
|
|
1753
|
+
multiGraphMemory: false,
|
|
1754
|
+
graphWriteSessionAdjacency: false,
|
|
1755
|
+
} as unknown as GraphConstructionCapabilitySet,
|
|
1756
|
+
);
|
|
1757
|
+
const old = (await storage.readAllMemories()).find((memory) => memory.content.includes("Austin"));
|
|
1758
|
+
assert.ok(old);
|
|
1759
|
+
oldId = old.frontmatter.id;
|
|
1760
|
+
const moved = await storage.migrateMemoryToTier(old, "cold");
|
|
1761
|
+
assert.equal(moved.changed, true);
|
|
1762
|
+
|
|
1763
|
+
await orchestrator.persistExtraction(
|
|
1764
|
+
extractionResult({
|
|
1765
|
+
category: "fact",
|
|
1766
|
+
content: "Alice lives in New York",
|
|
1767
|
+
confidence: 0.95,
|
|
1768
|
+
tags: [],
|
|
1769
|
+
entityRef: "person:alice",
|
|
1770
|
+
}),
|
|
1771
|
+
storage,
|
|
1772
|
+
null,
|
|
1773
|
+
undefined,
|
|
1774
|
+
undefined,
|
|
1775
|
+
undefined,
|
|
1776
|
+
undefined,
|
|
1777
|
+
{
|
|
1778
|
+
entityGraph: false,
|
|
1779
|
+
timeGraph: false,
|
|
1780
|
+
causalGraph: false,
|
|
1781
|
+
multiGraphMemory: false,
|
|
1782
|
+
graphWriteSessionAdjacency: false,
|
|
1783
|
+
} as unknown as GraphConstructionCapabilitySet,
|
|
1784
|
+
);
|
|
1785
|
+
assert.equal(coldAnchorVerified, true);
|
|
1786
|
+
const cold = (await storage.readAllColdMemories()).find((memory) => memory.frontmatter.id === oldId);
|
|
1787
|
+
assert.ok(cold);
|
|
1788
|
+
assert.equal(cold.frontmatter.status, "superseded");
|
|
1789
|
+
assert.equal(cold.frontmatter.supersededBy !== undefined, true);
|
|
1790
|
+
} finally {
|
|
1791
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
|
|
1795
|
+
test("anchor-only contradiction flows through deferred resolve and supersedes the old memory", async () => {
|
|
1796
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-contradiction-deferred-"));
|
|
1797
|
+
try {
|
|
1798
|
+
const config = baseConfig(memoryDir) as PluginConfig;
|
|
1799
|
+
const old = {
|
|
1800
|
+
path: "/synthetic/default/old.md",
|
|
1801
|
+
content: "Alice lives in Austin",
|
|
1802
|
+
frontmatter: {
|
|
1803
|
+
id: "old",
|
|
1804
|
+
category: "fact",
|
|
1805
|
+
created: "2026-08-01T00:00:00.000Z",
|
|
1806
|
+
updated: "2026-08-01T00:00:00.000Z",
|
|
1807
|
+
source: "test",
|
|
1808
|
+
confidence: 0.9,
|
|
1809
|
+
confidenceTier: "explicit",
|
|
1810
|
+
tags: [],
|
|
1811
|
+
status: "active",
|
|
1812
|
+
entityRef: "person:alice",
|
|
1813
|
+
structuredAttributes: { city: "Austin" },
|
|
1814
|
+
},
|
|
1815
|
+
} as unknown as MemoryFile;
|
|
1816
|
+
const storage = {
|
|
1817
|
+
readAllMemories: async () => [old],
|
|
1818
|
+
getMemoryById: async (id: string) => (id === "old" ? old : null),
|
|
1819
|
+
supersedeMemory: async (id: string) => {
|
|
1820
|
+
if (id !== "old") return false;
|
|
1821
|
+
old.frontmatter.status = "superseded";
|
|
1822
|
+
return true;
|
|
1823
|
+
},
|
|
1824
|
+
} as never as StorageManager;
|
|
1825
|
+
const extraction = {
|
|
1826
|
+
verifyContradiction: async () => ({
|
|
1827
|
+
isContradiction: true,
|
|
1828
|
+
confidence: 0.99,
|
|
1829
|
+
reasoning: "city changed",
|
|
1830
|
+
whichIsNewer: "second",
|
|
1831
|
+
}),
|
|
1832
|
+
} as unknown as ExtractionEngine;
|
|
1833
|
+
const coordinator = new ContradictionLinkingCoordinator({
|
|
1834
|
+
getConfig: () => config,
|
|
1835
|
+
isSearchAvailable: () => true,
|
|
1836
|
+
searchAcrossNamespaces: async () => [],
|
|
1837
|
+
extractMemoryIdsFromResults: () => [],
|
|
1838
|
+
namespaceFromPath: () => "default",
|
|
1839
|
+
storageForNamespace: async () => storage,
|
|
1840
|
+
getExtraction: () => extraction,
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1843
|
+
const contradiction = await coordinator.checkForContradiction(
|
|
1844
|
+
"Alice lives in New York",
|
|
1845
|
+
"fact",
|
|
1846
|
+
"default",
|
|
1847
|
+
{ entityRef: "person:alice", structuredAttributes: { city: "New York" } },
|
|
1848
|
+
);
|
|
1849
|
+
assert.equal(contradiction?.supersededId, "old");
|
|
1850
|
+
|
|
1851
|
+
await coordinator.applyDeferredContradictionResolve(contradiction, storage, "new", false);
|
|
1852
|
+
assert.equal(old.frontmatter.status, "superseded");
|
|
1853
|
+
} finally {
|
|
1854
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
1855
|
+
}
|
|
1856
|
+
});
|
|
1857
|
+
async function assertQmdCandidateStatusIsNotSupersedable(
|
|
1858
|
+
status: "pending_review" | "rejected" | "quarantined" | "active",
|
|
1859
|
+
options: { archivedAt?: string; archivePath?: string; omitStatus?: boolean } = {},
|
|
1860
|
+
): Promise<void> {
|
|
1861
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), `remnic-contradiction-qmd-${status}-`));
|
|
1862
|
+
try {
|
|
1863
|
+
const config = baseConfig(memoryDir) as PluginConfig;
|
|
1864
|
+
const candidate = {
|
|
1865
|
+
path: options.archivePath
|
|
1866
|
+
? path.join(memoryDir, options.archivePath)
|
|
1867
|
+
: `/synthetic/default/${status}.md`,
|
|
1868
|
+
content: "Alice lives in Austin",
|
|
1869
|
+
frontmatter: {
|
|
1870
|
+
id: `candidate-${status}`,
|
|
1871
|
+
category: "fact",
|
|
1872
|
+
created: "2026-08-01T00:00:00.000Z",
|
|
1873
|
+
updated: "2026-08-01T00:00:00.000Z",
|
|
1874
|
+
source: "test",
|
|
1875
|
+
confidence: 0.9,
|
|
1876
|
+
confidenceTier: "explicit",
|
|
1877
|
+
tags: [],
|
|
1878
|
+
...(options.omitStatus ? {} : { status }),
|
|
1879
|
+
...(options.archivedAt ? { archivedAt: options.archivedAt } : {}),
|
|
1880
|
+
},
|
|
1881
|
+
} as unknown as MemoryFile;
|
|
1882
|
+
const storage = {
|
|
1883
|
+
dir: memoryDir,
|
|
1884
|
+
readAllMemories: async () => [],
|
|
1885
|
+
readAllColdMemories: async () => [],
|
|
1886
|
+
getMemoryById: async (id: string) => (id === candidate.frontmatter.id ? candidate : null),
|
|
1887
|
+
} as never as StorageManager;
|
|
1888
|
+
let verificationCalls = 0;
|
|
1889
|
+
const extraction = {
|
|
1890
|
+
verifyContradiction: async () => {
|
|
1891
|
+
verificationCalls++;
|
|
1892
|
+
return {
|
|
1893
|
+
isContradiction: true,
|
|
1894
|
+
confidence: 0.99,
|
|
1895
|
+
reasoning: "city changed",
|
|
1896
|
+
whichIsNewer: "second",
|
|
1897
|
+
};
|
|
1898
|
+
},
|
|
1899
|
+
} as unknown as ExtractionEngine;
|
|
1900
|
+
const coordinator = new ContradictionLinkingCoordinator({
|
|
1901
|
+
getConfig: () => config,
|
|
1902
|
+
isSearchAvailable: () => true,
|
|
1903
|
+
searchAcrossNamespaces: async () => [
|
|
1904
|
+
{
|
|
1905
|
+
docid: candidate.path,
|
|
1906
|
+
path: candidate.path,
|
|
1907
|
+
snippet: candidate.content,
|
|
1908
|
+
score: 0.99,
|
|
1909
|
+
},
|
|
1910
|
+
],
|
|
1911
|
+
extractMemoryIdsFromResults: () => [candidate.frontmatter.id],
|
|
1912
|
+
namespaceFromPath: () => "default",
|
|
1913
|
+
storageForNamespace: async () => storage,
|
|
1914
|
+
getExtraction: () => extraction,
|
|
1915
|
+
});
|
|
1916
|
+
|
|
1917
|
+
const contradiction = await coordinator.checkForContradiction(
|
|
1918
|
+
"Alice lives in New York",
|
|
1919
|
+
"fact",
|
|
1920
|
+
"default",
|
|
1921
|
+
);
|
|
1922
|
+
|
|
1923
|
+
assert.equal(contradiction, null);
|
|
1924
|
+
assert.equal(verificationCalls, 0);
|
|
1925
|
+
} finally {
|
|
1926
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
test("QMD contradiction candidates in pending_review are not eligible for supersession", async () => {
|
|
1931
|
+
await assertQmdCandidateStatusIsNotSupersedable("pending_review");
|
|
1932
|
+
});
|
|
1933
|
+
|
|
1934
|
+
test("QMD contradiction candidates in rejected are not eligible for supersession", async () => {
|
|
1935
|
+
await assertQmdCandidateStatusIsNotSupersedable("rejected");
|
|
1936
|
+
});
|
|
1937
|
+
|
|
1938
|
+
test("QMD contradiction candidates in quarantined are not eligible for supersession", async () => {
|
|
1939
|
+
await assertQmdCandidateStatusIsNotSupersedable("quarantined");
|
|
1940
|
+
});
|
|
1941
|
+
|
|
1942
|
+
test("QMD active candidates with archivedAt are not eligible for supersession", async () => {
|
|
1943
|
+
await assertQmdCandidateStatusIsNotSupersedable("active", {
|
|
1944
|
+
archivedAt: "2026-08-08T00:00:00.000Z",
|
|
1945
|
+
});
|
|
1946
|
+
});
|
|
1947
|
+
|
|
1948
|
+
test("QMD candidates under archive paths are not eligible without raw status", async () => {
|
|
1949
|
+
await assertQmdCandidateStatusIsNotSupersedable("active", {
|
|
1950
|
+
archivePath: "archive/archived.md",
|
|
1951
|
+
omitStatus: true,
|
|
1952
|
+
});
|
|
1953
|
+
});
|