@remnic/core 9.6.1 → 9.6.3
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 +7 -8
- package/dist/access-admin-ops-surface.js +8 -8
- package/dist/access-boundary.d.ts +9 -8
- package/dist/access-boundary.js +9 -9
- package/dist/access-cli.js +40 -40
- package/dist/access-http.d.ts +47 -16
- package/dist/access-http.js +13 -13
- package/dist/access-identity-continuity-surface.d.ts +6 -7
- package/dist/access-identity-continuity-surface.js +8 -8
- package/dist/access-lcm-surface.d.ts +7 -8
- package/dist/access-lcm-surface.js +8 -8
- package/dist/access-mcp.d.ts +13 -8
- package/dist/access-mcp.js +12 -12
- package/dist/access-observe-write-surface.d.ts +7 -8
- package/dist/access-observe-write-surface.js +8 -8
- package/dist/access-operations-batch.js +10 -10
- package/dist/access-operations.d.ts +8 -9
- package/dist/access-operations.js +11 -11
- package/dist/access-recall-surface.d.ts +7 -8
- package/dist/access-recall-surface.js +8 -8
- package/dist/access-schema.d.ts +6 -6
- package/dist/{access-service-CXJaql3A.d.ts → access-service-Bps_9G1t.d.ts} +9 -9
- package/dist/access-service.d.ts +7 -8
- package/dist/access-service.js +8 -8
- package/dist/access-surface-catalog.d.ts +7 -8
- package/dist/action-confidence.d.ts +1 -2
- package/dist/active-memory-bridge.d.ts +1 -2
- package/dist/active-recall.d.ts +1 -2
- package/dist/behavior-learner.d.ts +1 -2
- package/dist/behavior-signals.d.ts +1 -2
- package/dist/bootstrap.d.ts +6 -7
- package/dist/briefing.d.ts +2 -3
- package/dist/briefing.js +3 -3
- package/dist/buffer-surprise-report.d.ts +1 -2
- package/dist/buffer.d.ts +2 -3
- package/dist/buffer.js +1 -1
- package/dist/bulk-import/index.d.ts +3 -3
- package/dist/bulk-import/index.js +2 -2
- package/dist/calibration.d.ts +1 -2
- package/dist/capabilities.d.ts +1 -2
- package/dist/{catalog-C9cu67Ig.d.ts → catalog-9WanSGtt.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -2
- package/dist/causal-consolidation.d.ts +1 -2
- package/dist/causal-consolidation.js +4 -4
- package/dist/causal-trajectory-graph.d.ts +1 -2
- package/dist/{chunk-PNLFN2PZ.js → chunk-23G3T2Q5.js} +2 -2
- package/dist/{chunk-SIRPLF54.js → chunk-2K3OSLHE.js} +2 -2
- package/dist/{chunk-JS2HKKOK.js → chunk-3AC5UYKI.js} +1 -1
- package/dist/{chunk-JS2HKKOK.js.map → chunk-3AC5UYKI.js.map} +1 -1
- package/dist/{chunk-LQHDIS7L.js → chunk-54BGIBCR.js} +2 -2
- package/dist/{chunk-PVUJ6WVV.js → chunk-5MH5NF3U.js} +5 -5
- package/dist/{chunk-Y7UNNAZD.js → chunk-7ZYBDQ7V.js} +2 -2
- package/dist/{chunk-2PRQG7PV.js → chunk-AJVLDJU6.js} +1 -1
- package/dist/chunk-AJVLDJU6.js.map +1 -0
- package/dist/{chunk-LR5DZ56F.js → chunk-AXALUFAH.js} +2 -2
- package/dist/{chunk-AYZJID4S.js → chunk-B5GBWBCZ.js} +2 -2
- package/dist/{chunk-H2DXDQCQ.js → chunk-BT3PTVYZ.js} +48 -44
- package/dist/chunk-BT3PTVYZ.js.map +1 -0
- package/dist/{chunk-TLUDFPZL.js → chunk-CAZ3Z5M6.js} +2 -2
- package/dist/{chunk-P454KXD6.js → chunk-CJPVWAJF.js} +21 -21
- package/dist/{chunk-RGK27ELN.js → chunk-DEKIXXIG.js} +2 -2
- package/dist/{chunk-XCL57M6Q.js → chunk-DPZKMGEP.js} +21 -20
- package/dist/chunk-DPZKMGEP.js.map +1 -0
- package/dist/{chunk-2KK5WNQB.js → chunk-DQ42IQSK.js} +2 -2
- package/dist/{chunk-UFUKHGZD.js → chunk-EIGE4IMQ.js} +89 -33
- package/dist/chunk-EIGE4IMQ.js.map +1 -0
- package/dist/{chunk-HOJZMQ4J.js → chunk-FGDKJJZS.js} +2 -2
- package/dist/{chunk-ABP6QMVM.js → chunk-FQ7HAWWC.js} +2 -2
- package/dist/{chunk-CYWA2CXR.js → chunk-FVTLSFWU.js} +2 -2
- package/dist/{chunk-JJEJJ7RK.js → chunk-FXZTL7F7.js} +2 -2
- package/dist/{chunk-DANV2HTH.js → chunk-GQGDRLDM.js} +3 -3
- package/dist/{chunk-7F7Z6MOS.js → chunk-H4CKS5B2.js} +2 -2
- package/dist/{chunk-SVOHGUMI.js → chunk-H4FHR4UM.js} +174 -197
- package/dist/chunk-H4FHR4UM.js.map +1 -0
- package/dist/{chunk-CKITWVWJ.js → chunk-HSSERO3E.js} +2 -2
- package/dist/{chunk-23JFRB73.js → chunk-JODHPNLN.js} +2 -2
- package/dist/{chunk-DS7MM2ES.js → chunk-JZKR6ZAV.js} +2 -2
- package/dist/{chunk-BEM4D2QG.js → chunk-KJIJNLXT.js} +3 -3
- package/dist/{chunk-JSNVWNGX.js → chunk-KVSVVKHA.js} +2 -2
- package/dist/{chunk-MC4FJXPA.js → chunk-MMACM5G2.js} +2 -2
- package/dist/{chunk-PCLIWBGD.js → chunk-N2RP4K5Q.js} +8 -6
- package/dist/chunk-N2RP4K5Q.js.map +1 -0
- package/dist/{chunk-OUELRE5E.js → chunk-NOUJNHWG.js} +1 -1
- package/dist/chunk-NOUJNHWG.js.map +1 -0
- package/dist/{chunk-3XIY7MDQ.js → chunk-O6YI4CVI.js} +2 -2
- package/dist/{chunk-F53UF7CI.js → chunk-QLVLW7UD.js} +6 -2
- package/dist/{chunk-F53UF7CI.js.map → chunk-QLVLW7UD.js.map} +1 -1
- package/dist/{chunk-LADFWYHY.js → chunk-QMC3BI3K.js} +6 -6
- package/dist/chunk-QMC3BI3K.js.map +1 -0
- package/dist/{chunk-72WG5QKN.js → chunk-SDAXPXER.js} +2 -2
- package/dist/{chunk-FKCRGLRD.js → chunk-T43VKQU3.js} +269 -167
- package/dist/chunk-T43VKQU3.js.map +1 -0
- package/dist/{chunk-4LV23CHK.js → chunk-VHJARF5R.js} +2 -2
- package/dist/{chunk-6MMONPHM.js → chunk-W2AHSCKY.js} +2 -2
- package/dist/{chunk-4GNEDXDI.js → chunk-WULTHJRU.js} +2 -2
- package/dist/chunk-WULTHJRU.js.map +1 -0
- package/dist/{chunk-K5LLP4CY.js → chunk-ZGPDSEKG.js} +20 -9
- package/dist/chunk-ZGPDSEKG.js.map +1 -0
- package/dist/{cli-CqdPA6z9.d.ts → cli-SBWBw_eK.d.ts} +4 -5
- package/dist/cli.d.ts +9 -10
- package/dist/cli.js +32 -32
- package/dist/compounding/engine.d.ts +2 -3
- package/dist/compounding/engine.js +3 -3
- package/dist/compounding/preference-consolidator.d.ts +1 -2
- package/dist/compression-optimizer.d.ts +1 -2
- package/dist/config.d.ts +1 -2
- package/dist/connectors/codex-materialize-runner.d.ts +1 -2
- package/dist/connectors/codex-materialize-runner.js +3 -3
- package/dist/connectors/codex-materialize.d.ts +1 -2
- package/dist/connectors/index.d.ts +1 -2
- package/dist/connectors/index.js +3 -3
- package/dist/consolidation-provenance-check.d.ts +2 -3
- package/dist/consolidation-undo.d.ts +2 -3
- package/dist/contradiction/index.d.ts +3 -4
- package/dist/conversation-index/backend.d.ts +1 -2
- package/dist/conversation-index/chunker.d.ts +1 -2
- package/dist/conversation-index/faiss-adapter.d.ts +1 -2
- package/dist/conversation-index/indexer.d.ts +1 -2
- package/dist/conversation-index/search.d.ts +1 -2
- package/dist/day-summary.d.ts +1 -2
- package/dist/delinearize.d.ts +1 -2
- package/dist/direct-answer-wiring.d.ts +1 -2
- package/dist/direct-answer.d.ts +1 -2
- package/dist/embedding-fallback.d.ts +1 -2
- package/dist/enrichment/index.d.ts +1 -2
- package/dist/entity-retrieval.d.ts +2 -3
- package/dist/entity-retrieval.js +3 -3
- package/dist/entity-schema.d.ts +1 -2
- package/dist/explicit-capture.d.ts +8 -7
- package/dist/explicit-capture.js +1 -1
- package/dist/extraction-faithfulness.d.ts +1 -2
- package/dist/extraction-judge-telemetry.d.ts +1 -2
- package/dist/extraction-judge-training.d.ts +1 -2
- package/dist/extraction-judge.d.ts +1 -2
- package/dist/extraction.d.ts +1 -2
- package/dist/fallback-llm.d.ts +1 -2
- package/dist/graph-dashboard-diff.d.ts +1 -2
- package/dist/graph-dashboard-key.d.ts +1 -2
- package/dist/graph-dashboard-parser.d.ts +1 -2
- package/dist/graph-edge-reinforcement.d.ts +1 -2
- package/dist/graph-snapshot.d.ts +1 -2
- package/dist/graph.d.ts +1 -2
- package/dist/identity-continuity.d.ts +1 -2
- package/dist/importance.d.ts +1 -2
- package/dist/importers/index.d.ts +1 -1
- package/dist/importers/index.js +2 -2
- package/dist/index.d.ts +14 -15
- package/dist/index.js +40 -40
- package/dist/intent.d.ts +1 -2
- package/dist/lcm/engine.d.ts +1 -2
- package/dist/lcm/index.d.ts +1 -2
- package/dist/lcm/tools.d.ts +1 -2
- package/dist/lifecycle.d.ts +1 -2
- package/dist/live-connectors-runner.d.ts +1 -2
- package/dist/local-llm.d.ts +1 -2
- package/dist/local-model-endpoint.d.ts +1 -2
- package/dist/maintenance/memory-governance.d.ts +1 -2
- package/dist/maintenance/memory-governance.js +3 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
- package/dist/maintenance/rebuild-memory-projection.js +4 -4
- package/dist/mcp-memory-inspector-app.d.ts +7 -8
- package/dist/memory-action-policy.d.ts +1 -2
- package/dist/memory-cache.d.ts +1 -2
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -2
- package/dist/memory-projection-store.d.ts +1 -2
- package/dist/memory-provenance.d.ts +1 -2
- package/dist/memory-worth-outcomes.d.ts +2 -3
- package/dist/models-json.d.ts +1 -2
- package/dist/namespaces/migrate.d.ts +3 -4
- package/dist/namespaces/migrate.js +4 -4
- package/dist/namespaces/principal.d.ts +1 -2
- package/dist/namespaces/search.d.ts +1 -2
- package/dist/namespaces/storage.d.ts +3 -4
- package/dist/namespaces/storage.js +3 -3
- package/dist/native-knowledge.d.ts +1 -2
- package/dist/operator-toolkit.d.ts +2 -3
- package/dist/operator-toolkit.js +7 -7
- package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -3
- package/dist/orchestration/maintenance.d.ts +2 -3
- package/dist/orchestration/maintenance.js +5 -5
- package/dist/{orchestrator-CWFrT8AF.d.ts → orchestrator-B0YrUl2H.d.ts} +40 -38
- package/dist/orchestrator.d.ts +6 -7
- package/dist/orchestrator.js +40 -40
- package/dist/patterns-cli.d.ts +1 -2
- package/dist/{pipeline-DmqtvION.d.ts → pipeline-CiD86zIk.d.ts} +1 -1
- package/dist/policy-runtime.d.ts +1 -2
- package/dist/provenance.d.ts +1 -2
- package/dist/{qmd--fnPMK60.d.ts → qmd-Bef7DDuQ.d.ts} +1 -1
- package/dist/qmd-preflight.d.ts +2 -3
- package/dist/qmd-recall-cache.d.ts +1 -2
- package/dist/qmd.d.ts +2 -3
- package/dist/recall-disclosure-escalation.d.ts +1 -2
- package/dist/recall-explain-renderer.d.ts +1 -2
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-planner-llm.d.ts +1 -2
- package/dist/recall-state.d.ts +1 -2
- package/dist/recall-tag-filter.d.ts +1 -2
- package/dist/recall-timings.d.ts +1 -2
- package/dist/recall-xray-cli.d.ts +1 -2
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -2
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +1 -2
- package/dist/recall-xray.js +2 -2
- package/dist/replay/normalizers/chatgpt.d.ts +1 -0
- package/dist/replay/normalizers/chatgpt.js +3 -3
- package/dist/replay/normalizers/claude.d.ts +1 -0
- package/dist/replay/normalizers/claude.js +3 -3
- package/dist/replay/normalizers/openclaw.d.ts +1 -0
- package/dist/replay/normalizers/openclaw.js +3 -3
- package/dist/replay/normalizers/shared.d.ts +1 -0
- package/dist/replay/normalizers/shared.js +2 -2
- package/dist/replay/runner.d.ts +1 -0
- package/dist/replay/runner.js +2 -2
- package/dist/replay/types.d.ts +2 -1
- package/dist/replay/types.js +1 -1
- package/dist/resolve-auth-token.d.ts +1 -2
- package/dist/retrieval-agents.d.ts +2 -3
- package/dist/retrieval-tiers.d.ts +1 -2
- package/dist/routing/engine.d.ts +1 -2
- package/dist/routing/store.d.ts +1 -2
- package/dist/schemas.d.ts +22 -22
- package/dist/search/embed-helper.d.ts +1 -2
- package/dist/search/factory.d.ts +1 -2
- package/dist/search/index.d.ts +1 -2
- package/dist/search/lancedb-backend.d.ts +1 -2
- package/dist/search/meilisearch-backend.d.ts +1 -2
- package/dist/search/noop-backend.d.ts +1 -2
- package/dist/search/orama-backend.d.ts +1 -2
- package/dist/search/port.d.ts +1 -2
- package/dist/search/remote-backend.d.ts +1 -2
- package/dist/{semantic-DJR8_DMQ.d.ts → semantic-TddhF0y7.d.ts} +17 -1
- package/dist/{semantic-consolidation-O5lMZ4LG.d.ts → semantic-consolidation-BTShF2Fo.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -3
- package/dist/semantic-consolidation.js +4 -4
- package/dist/semantic-rule-promotion.js +3 -3
- package/dist/semantic-rule-verifier.d.ts +1 -2
- package/dist/semantic-rule-verifier.js +3 -3
- package/dist/session-observer-bands.d.ts +1 -2
- package/dist/session-observer-state.d.ts +1 -2
- package/dist/shared-context/manager.d.ts +3 -4
- package/dist/signal.d.ts +1 -2
- package/dist/{storage-C1maCNbc.d.ts → storage-DxPDAiU9.d.ts} +5 -1
- package/dist/storage.d.ts +2 -3
- package/dist/storage.js +2 -2
- package/dist/summarizer.d.ts +1 -2
- package/dist/summary-snapshot.d.ts +1 -2
- package/dist/temporal-supersession.d.ts +2 -3
- package/dist/temporal-validity.d.ts +1 -2
- package/dist/threading.d.ts +1 -2
- package/dist/tier-migration.d.ts +2 -3
- package/dist/tier-routing.d.ts +1 -2
- package/dist/topics.d.ts +1 -2
- package/dist/transcript.d.ts +1 -2
- package/dist/trust-score-stage.d.ts +1 -2
- package/dist/trust-score.d.ts +1 -2
- package/dist/{types-CGgcNWCG.d.ts → types-DDUDgd7T.d.ts} +93 -4
- package/dist/types.d.ts +1 -2
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -2
- package/dist/verified-recall.js +3 -3
- package/package.json +2 -2
- package/src/access-admin-ops-surface.ts +1 -0
- package/src/access-boundary.ts +2 -0
- package/src/access-http.test.ts +129 -0
- package/src/access-http.ts +92 -23
- package/src/access-mcp.ts +11 -2
- package/src/access-observe-write-surface.ts +87 -113
- package/src/access-operations-batch.ts +2 -2
- package/src/access-operations.ts +17 -16
- package/src/access-service.ts +13 -5
- package/src/admin/admin-surfaces.ts +2 -0
- package/src/buffer.ts +6 -1
- package/src/bulk-import/types.ts +2 -1
- package/src/coding/architecture-surfaces.test.ts +79 -5
- package/src/coding/architecture-surfaces.ts +7 -1
- package/src/coding/decision-surfaces.test.ts +1 -0
- package/src/coding/decision-surfaces.ts +4 -0
- package/src/connector-provenance-lifecycle.test.ts +618 -0
- package/src/connector-provenance.test.ts +219 -0
- package/src/dedup/semantic.test.ts +105 -0
- package/src/dedup/semantic.ts +69 -1
- package/src/explicit-capture.ts +21 -4
- package/src/orchestration/extraction-persist.ts +154 -6
- package/src/orchestration/extraction-run.ts +138 -228
- package/src/orchestration/persistence-index.ts +40 -5
- package/src/orchestration/turn-ingestion.ts +16 -9
- package/src/orchestrator-connector-dedup.test.ts +1082 -0
- package/src/orchestrator.ts +10 -26
- package/src/replay/types.ts +2 -1
- package/src/storage.ts +426 -553
- package/src/types.ts +11 -2
- package/src/utils/yaml-scalar.ts +20 -0
- package/dist/chunk-2PRQG7PV.js.map +0 -1
- package/dist/chunk-4GNEDXDI.js.map +0 -1
- package/dist/chunk-FKCRGLRD.js.map +0 -1
- package/dist/chunk-H2DXDQCQ.js.map +0 -1
- package/dist/chunk-K5LLP4CY.js.map +0 -1
- package/dist/chunk-LADFWYHY.js.map +0 -1
- package/dist/chunk-OUELRE5E.js.map +0 -1
- package/dist/chunk-PCLIWBGD.js.map +0 -1
- package/dist/chunk-SVOHGUMI.js.map +0 -1
- package/dist/chunk-UFUKHGZD.js.map +0 -1
- package/dist/chunk-XCL57M6Q.js.map +0 -1
- package/dist/types-gb_SNtjR.d.ts +0 -85
- /package/dist/{chunk-PNLFN2PZ.js.map → chunk-23G3T2Q5.js.map} +0 -0
- /package/dist/{chunk-SIRPLF54.js.map → chunk-2K3OSLHE.js.map} +0 -0
- /package/dist/{chunk-LQHDIS7L.js.map → chunk-54BGIBCR.js.map} +0 -0
- /package/dist/{chunk-PVUJ6WVV.js.map → chunk-5MH5NF3U.js.map} +0 -0
- /package/dist/{chunk-Y7UNNAZD.js.map → chunk-7ZYBDQ7V.js.map} +0 -0
- /package/dist/{chunk-LR5DZ56F.js.map → chunk-AXALUFAH.js.map} +0 -0
- /package/dist/{chunk-AYZJID4S.js.map → chunk-B5GBWBCZ.js.map} +0 -0
- /package/dist/{chunk-TLUDFPZL.js.map → chunk-CAZ3Z5M6.js.map} +0 -0
- /package/dist/{chunk-P454KXD6.js.map → chunk-CJPVWAJF.js.map} +0 -0
- /package/dist/{chunk-RGK27ELN.js.map → chunk-DEKIXXIG.js.map} +0 -0
- /package/dist/{chunk-2KK5WNQB.js.map → chunk-DQ42IQSK.js.map} +0 -0
- /package/dist/{chunk-HOJZMQ4J.js.map → chunk-FGDKJJZS.js.map} +0 -0
- /package/dist/{chunk-ABP6QMVM.js.map → chunk-FQ7HAWWC.js.map} +0 -0
- /package/dist/{chunk-CYWA2CXR.js.map → chunk-FVTLSFWU.js.map} +0 -0
- /package/dist/{chunk-JJEJJ7RK.js.map → chunk-FXZTL7F7.js.map} +0 -0
- /package/dist/{chunk-DANV2HTH.js.map → chunk-GQGDRLDM.js.map} +0 -0
- /package/dist/{chunk-7F7Z6MOS.js.map → chunk-H4CKS5B2.js.map} +0 -0
- /package/dist/{chunk-CKITWVWJ.js.map → chunk-HSSERO3E.js.map} +0 -0
- /package/dist/{chunk-23JFRB73.js.map → chunk-JODHPNLN.js.map} +0 -0
- /package/dist/{chunk-DS7MM2ES.js.map → chunk-JZKR6ZAV.js.map} +0 -0
- /package/dist/{chunk-BEM4D2QG.js.map → chunk-KJIJNLXT.js.map} +0 -0
- /package/dist/{chunk-JSNVWNGX.js.map → chunk-KVSVVKHA.js.map} +0 -0
- /package/dist/{chunk-MC4FJXPA.js.map → chunk-MMACM5G2.js.map} +0 -0
- /package/dist/{chunk-3XIY7MDQ.js.map → chunk-O6YI4CVI.js.map} +0 -0
- /package/dist/{chunk-72WG5QKN.js.map → chunk-SDAXPXER.js.map} +0 -0
- /package/dist/{chunk-4LV23CHK.js.map → chunk-VHJARF5R.js.map} +0 -0
- /package/dist/{chunk-6MMONPHM.js.map → chunk-W2AHSCKY.js.map} +0 -0
|
@@ -0,0 +1,1082 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connector-aware dedup regression tests (QOjlB, PR #1852).
|
|
3
|
+
*
|
|
4
|
+
* After a content-hash dedup hit, a readAllMemories() scan verifies the
|
|
5
|
+
* existing fact shares the same sourceConnector. If no same-connector
|
|
6
|
+
* active fact is found, exactDuplicate is set to false so the write
|
|
7
|
+
* proceeds. On scan failure, the write also proceeds (fail open) so an
|
|
8
|
+
* unverifiable hash hit can never silently drop content.
|
|
9
|
+
*
|
|
10
|
+
* Fix location: extraction-persist.ts lines 1585-1607.
|
|
11
|
+
*
|
|
12
|
+
* These tests call persistExtraction directly — the same entry point used
|
|
13
|
+
* by buffer extraction and proactive extraction.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import test from "node:test";
|
|
17
|
+
import assert from "node:assert/strict";
|
|
18
|
+
import os from "node:os";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { mkdtemp, mkdir } from "node:fs/promises";
|
|
21
|
+
|
|
22
|
+
import { parseConfig } from "./config.js";
|
|
23
|
+
import { Orchestrator } from "./orchestrator.js";
|
|
24
|
+
import { ContentHashIndex, type StorageManager } from "./storage.js";
|
|
25
|
+
import type { ExtractionResult, ExtractedFact, MemoryFile } from "./types.js";
|
|
26
|
+
import type { ResolvedScopeProfilePlan } from "./namespaces/scope-profiles.js";
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Types — minimal surface of Orchestrator needed by these tests.
|
|
30
|
+
// persistExtraction is private; cast through unknown to reach it without `any`.
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/** Source-context shape accepted by persistExtraction (connector subset). */
|
|
34
|
+
interface TestSourceContext {
|
|
35
|
+
sourceConnector?: string;
|
|
36
|
+
validAt?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Orchestrator fields the tests touch (all exist on the real instance). */
|
|
40
|
+
interface OrchestratorTestSurface {
|
|
41
|
+
persistExtraction: (
|
|
42
|
+
result: ExtractionResult,
|
|
43
|
+
storage: StorageManager,
|
|
44
|
+
threadId: string | null,
|
|
45
|
+
sourceContext?: TestSourceContext,
|
|
46
|
+
baseNamespace?: string,
|
|
47
|
+
scopeProfileWritePlan?: ResolvedScopeProfilePlan | null,
|
|
48
|
+
) => Promise<string[]>;
|
|
49
|
+
getStorage: (namespace: string) => Promise<StorageManager>;
|
|
50
|
+
contentHashIndex: ContentHashIndex | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Helpers
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
function makeFact(content: string): ExtractedFact {
|
|
58
|
+
return {
|
|
59
|
+
content,
|
|
60
|
+
category: "fact",
|
|
61
|
+
tags: [],
|
|
62
|
+
confidence: 0.9,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function factResult(content: string): ExtractionResult {
|
|
67
|
+
return {
|
|
68
|
+
facts: [makeFact(content)],
|
|
69
|
+
entities: [],
|
|
70
|
+
relationships: [],
|
|
71
|
+
questions: [],
|
|
72
|
+
profileUpdates: [],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function makeDedupOrchestrator(
|
|
77
|
+
overrides: Record<string, unknown> = {},
|
|
78
|
+
): Promise<{
|
|
79
|
+
orchestrator: OrchestratorTestSurface;
|
|
80
|
+
storage: StorageManager;
|
|
81
|
+
memoryDir: string;
|
|
82
|
+
hashIndex: ContentHashIndex;
|
|
83
|
+
}> {
|
|
84
|
+
const memoryDir = await mkdtemp(
|
|
85
|
+
path.join(os.tmpdir(), "remnic-connector-dedup-"),
|
|
86
|
+
);
|
|
87
|
+
const config = parseConfig({
|
|
88
|
+
openaiApiKey: "sk-test",
|
|
89
|
+
memoryDir,
|
|
90
|
+
workspaceDir: path.join(memoryDir, "workspace"),
|
|
91
|
+
qmdEnabled: false,
|
|
92
|
+
embeddingFallbackEnabled: false,
|
|
93
|
+
chunkingEnabled: false,
|
|
94
|
+
multiGraphMemoryEnabled: false,
|
|
95
|
+
factDeduplicationEnabled: true,
|
|
96
|
+
...overrides,
|
|
97
|
+
});
|
|
98
|
+
const orchestrator = new Orchestrator(
|
|
99
|
+
config,
|
|
100
|
+
) as unknown as OrchestratorTestSurface;
|
|
101
|
+
const storage = await orchestrator.getStorage("default");
|
|
102
|
+
await storage.ensureDirectories();
|
|
103
|
+
|
|
104
|
+
// Inject a real ContentHashIndex so the hash-dedup path is active.
|
|
105
|
+
const stateDir = path.join(memoryDir, "state");
|
|
106
|
+
await mkdir(stateDir, { recursive: true });
|
|
107
|
+
const hashIndex = new ContentHashIndex(stateDir);
|
|
108
|
+
await hashIndex.load();
|
|
109
|
+
orchestrator.contentHashIndex = hashIndex;
|
|
110
|
+
|
|
111
|
+
return { orchestrator, storage, memoryDir, hashIndex };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Tests
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
test("connector dedup (QOjlB): same content + same connector dedupes", async () => {
|
|
119
|
+
const { orchestrator, storage } = await makeDedupOrchestrator();
|
|
120
|
+
const body =
|
|
121
|
+
"The deployment pipeline runs canary checks every fifteen minutes before promoting to production.";
|
|
122
|
+
|
|
123
|
+
// First write with connector "chatgpt" — must succeed.
|
|
124
|
+
const ids1 = await orchestrator.persistExtraction(
|
|
125
|
+
factResult(body),
|
|
126
|
+
storage,
|
|
127
|
+
null,
|
|
128
|
+
{ sourceConnector: "chatgpt" },
|
|
129
|
+
);
|
|
130
|
+
assert.equal(ids1.length, 1, "first write must succeed");
|
|
131
|
+
|
|
132
|
+
// Second write: same content, same connector → must dedupe.
|
|
133
|
+
const ids2 = await orchestrator.persistExtraction(
|
|
134
|
+
factResult(body),
|
|
135
|
+
storage,
|
|
136
|
+
null,
|
|
137
|
+
{ sourceConnector: "chatgpt" },
|
|
138
|
+
);
|
|
139
|
+
assert.equal(ids2.length, 0, "same-connector duplicate must be deduped");
|
|
140
|
+
|
|
141
|
+
// Only one copy of the fact on disk.
|
|
142
|
+
const all = await storage.readAllMemories();
|
|
143
|
+
const matching = all.filter((m: MemoryFile) => m.content.includes(body));
|
|
144
|
+
assert.equal(matching.length, 1, "only one memory on disk");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("connector dedup (QOjlB): same content + different connector does NOT dedupe", async () => {
|
|
148
|
+
const { orchestrator, storage } = await makeDedupOrchestrator();
|
|
149
|
+
const body =
|
|
150
|
+
"The staging environment uses isolated feature flags per tenant for safer rollouts.";
|
|
151
|
+
|
|
152
|
+
// First write with connector "chatgpt".
|
|
153
|
+
const ids1 = await orchestrator.persistExtraction(
|
|
154
|
+
factResult(body),
|
|
155
|
+
storage,
|
|
156
|
+
null,
|
|
157
|
+
{ sourceConnector: "chatgpt" },
|
|
158
|
+
);
|
|
159
|
+
assert.equal(ids1.length, 1, "first write must succeed");
|
|
160
|
+
|
|
161
|
+
// Second write: same content, DIFFERENT connector → must NOT dedupe.
|
|
162
|
+
const ids2 = await orchestrator.persistExtraction(
|
|
163
|
+
factResult(body),
|
|
164
|
+
storage,
|
|
165
|
+
null,
|
|
166
|
+
{ sourceConnector: "codex-cli" },
|
|
167
|
+
);
|
|
168
|
+
assert.equal(ids2.length, 1, "cross-connector fact must NOT be deduped");
|
|
169
|
+
|
|
170
|
+
// Two memories on disk, one per connector.
|
|
171
|
+
const all = await storage.readAllMemories();
|
|
172
|
+
const chatgptMems = all.filter(
|
|
173
|
+
(m: MemoryFile) => m.frontmatter.sourceConnector === "chatgpt",
|
|
174
|
+
);
|
|
175
|
+
const codexMems = all.filter(
|
|
176
|
+
(m: MemoryFile) => m.frontmatter.sourceConnector === "codex-cli",
|
|
177
|
+
);
|
|
178
|
+
assert.equal(chatgptMems.length, 1, "one chatgpt memory on disk");
|
|
179
|
+
assert.equal(codexMems.length, 1, "one codex-cli memory on disk");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("connector dedup (QOjlB): stale hash with no matching memory writes through", async () => {
|
|
183
|
+
const { orchestrator, storage, hashIndex } = await makeDedupOrchestrator();
|
|
184
|
+
const body =
|
|
185
|
+
"The QMD index rebuilds incrementally after each extraction batch completes.";
|
|
186
|
+
|
|
187
|
+
// Simulate a stale hash: register the content hash WITHOUT writing any
|
|
188
|
+
// matching memory. The hash says "duplicate" but no memory backs it.
|
|
189
|
+
hashIndex.add(body);
|
|
190
|
+
|
|
191
|
+
// persistExtraction must write through — the connector-aware scan finds
|
|
192
|
+
// no matching memory, so exactDuplicate falls to false.
|
|
193
|
+
const ids = await orchestrator.persistExtraction(
|
|
194
|
+
factResult(body),
|
|
195
|
+
storage,
|
|
196
|
+
null,
|
|
197
|
+
{ sourceConnector: "chatgpt" },
|
|
198
|
+
);
|
|
199
|
+
assert.equal(ids.length, 1, "stale hash with no memory must write through");
|
|
200
|
+
|
|
201
|
+
// Verify the fact landed on disk.
|
|
202
|
+
const all = await storage.readAllMemories();
|
|
203
|
+
const matching = all.filter((m: MemoryFile) => m.content.includes(body));
|
|
204
|
+
assert.equal(matching.length, 1, "fact must be persisted on disk");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("connector dedup (QOjlB): readAllMemories throws → writes through (fail open)", async () => {
|
|
208
|
+
const { orchestrator, storage, hashIndex } = await makeDedupOrchestrator();
|
|
209
|
+
const body =
|
|
210
|
+
"The redaction engine compiles regex patterns defensively to avoid ReDoS.";
|
|
211
|
+
|
|
212
|
+
// Pre-register the hash so hasContentHashDedup returns true, forcing the
|
|
213
|
+
// connector-aware scan to run.
|
|
214
|
+
hashIndex.add(body);
|
|
215
|
+
|
|
216
|
+
// Override readAllMemories to simulate a scan failure. With the test
|
|
217
|
+
// config (no embedding fallback, no graph, no namespaces) the dedup
|
|
218
|
+
// scan at extraction-persist.ts:1592 is the ONLY readAllMemories call
|
|
219
|
+
// during persistExtraction, so this override targets it precisely.
|
|
220
|
+
const originalReadAllMemories = storage.readAllMemories.bind(storage);
|
|
221
|
+
storage.readAllMemories = async (): Promise<MemoryFile[]> => {
|
|
222
|
+
throw new Error("simulated readAllMemories failure");
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// persistExtraction must write through — the scan failure is caught and
|
|
226
|
+
// exactDuplicate falls to false (fail open).
|
|
227
|
+
const ids = await orchestrator.persistExtraction(
|
|
228
|
+
factResult(body),
|
|
229
|
+
storage,
|
|
230
|
+
null,
|
|
231
|
+
{ sourceConnector: "chatgpt" },
|
|
232
|
+
);
|
|
233
|
+
assert.equal(ids.length, 1, "scan failure must fail open to write");
|
|
234
|
+
|
|
235
|
+
// Restore so post-test verification can read memories.
|
|
236
|
+
storage.readAllMemories = originalReadAllMemories;
|
|
237
|
+
const all = await storage.readAllMemories();
|
|
238
|
+
const matching = all.filter((m: MemoryFile) => m.content.includes(body));
|
|
239
|
+
assert.equal(matching.length, 1, "fact must be persisted despite scan failure");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
// Promotion dedup tests (QOjlD, PR #1852)
|
|
244
|
+
//
|
|
245
|
+
// The promotion paths (profile-target at extraction-persist.ts:476-521 and
|
|
246
|
+
// shared at :730-751) apply the same connector-aware dedup after a hash hit.
|
|
247
|
+
// After hasFactContentHash returns true on the TARGET namespace, a
|
|
248
|
+
// readAllMemories() scan checks whether a same-content, same-connector
|
|
249
|
+
// active fact exists there. If not, the promotion write proceeds. Fail
|
|
250
|
+
// open on scan error.
|
|
251
|
+
//
|
|
252
|
+
// Each test pre-writes a fact to the TARGET namespace (via writeMemory,
|
|
253
|
+
// which registers the content hash in the target's fact-hash index), then
|
|
254
|
+
// calls persistExtraction on the SOURCE namespace with a scope-profile
|
|
255
|
+
// plan that auto-promotes "fact" to a userGlobal target.
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
const PROMOTION_TARGET_NS = "user-promotarget";
|
|
259
|
+
|
|
260
|
+
function makePromotionScopePlan(): ResolvedScopeProfilePlan {
|
|
261
|
+
return {
|
|
262
|
+
profileId: "test-promotion",
|
|
263
|
+
profile: {
|
|
264
|
+
readOrder: ["userProject", "userGlobal"],
|
|
265
|
+
writeDefault: "userProject",
|
|
266
|
+
promotionTargets: ["userGlobal"],
|
|
267
|
+
autoPromote: {
|
|
268
|
+
enabled: true,
|
|
269
|
+
targets: ["userGlobal"],
|
|
270
|
+
categories: ["fact"],
|
|
271
|
+
minConfidenceTier: "speculative",
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
baseNamespace: "default",
|
|
275
|
+
writeLayer: "userProject",
|
|
276
|
+
writeNamespace: "default",
|
|
277
|
+
readNamespaces: ["default", PROMOTION_TARGET_NS],
|
|
278
|
+
layers: [
|
|
279
|
+
{
|
|
280
|
+
id: "userProject",
|
|
281
|
+
kind: "user-project",
|
|
282
|
+
namespace: "default",
|
|
283
|
+
readable: true,
|
|
284
|
+
writable: true,
|
|
285
|
+
promotable: false,
|
|
286
|
+
reason: "test",
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
id: "userGlobal",
|
|
290
|
+
kind: "user-global",
|
|
291
|
+
namespace: PROMOTION_TARGET_NS,
|
|
292
|
+
readable: true,
|
|
293
|
+
writable: true,
|
|
294
|
+
promotable: true,
|
|
295
|
+
reason: "test",
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
promotionTargets: [
|
|
299
|
+
{
|
|
300
|
+
target: "userGlobal",
|
|
301
|
+
namespace: PROMOTION_TARGET_NS,
|
|
302
|
+
authorized: true,
|
|
303
|
+
reason: "test",
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
warnings: [],
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function makePromotionOrchestrator(): Promise<{
|
|
311
|
+
orchestrator: OrchestratorTestSurface;
|
|
312
|
+
storage: StorageManager;
|
|
313
|
+
targetStorage: StorageManager;
|
|
314
|
+
}> {
|
|
315
|
+
const dedup = await makeDedupOrchestrator({
|
|
316
|
+
namespacesEnabled: true,
|
|
317
|
+
defaultNamespace: "default",
|
|
318
|
+
sharedNamespace: "shared",
|
|
319
|
+
});
|
|
320
|
+
const targetStorage = await dedup.orchestrator.getStorage(PROMOTION_TARGET_NS);
|
|
321
|
+
await targetStorage.ensureDirectories();
|
|
322
|
+
return {
|
|
323
|
+
orchestrator: dedup.orchestrator,
|
|
324
|
+
storage: dedup.storage,
|
|
325
|
+
targetStorage,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
test("promotion dedup (QOjlD): same content + same connector skips promotion", async () => {
|
|
330
|
+
const { orchestrator, storage, targetStorage } = await makePromotionOrchestrator();
|
|
331
|
+
const scopePlan = makePromotionScopePlan();
|
|
332
|
+
const body = "The Galileo service rotates TLS certificates every ninety days.";
|
|
333
|
+
|
|
334
|
+
// Pre-write the same fact to the TARGET namespace with connector "chatgpt".
|
|
335
|
+
// This registers the content hash in the target's fact-hash index.
|
|
336
|
+
await targetStorage.writeMemory("fact", body, {
|
|
337
|
+
confidence: 0.9,
|
|
338
|
+
sourceConnector: "chatgpt",
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// persistExtraction writes to source (default) and promotes to target.
|
|
342
|
+
const ids = await orchestrator.persistExtraction(
|
|
343
|
+
factResult(body),
|
|
344
|
+
storage,
|
|
345
|
+
null,
|
|
346
|
+
{ sourceConnector: "chatgpt" },
|
|
347
|
+
"default",
|
|
348
|
+
scopePlan,
|
|
349
|
+
);
|
|
350
|
+
assert.equal(ids.length, 1, "source write must succeed");
|
|
351
|
+
|
|
352
|
+
// Target must still have exactly ONE memory — promotion was skipped.
|
|
353
|
+
const targetMems = await targetStorage.readAllMemories();
|
|
354
|
+
assert.equal(
|
|
355
|
+
targetMems.length,
|
|
356
|
+
1,
|
|
357
|
+
"promotion must be skipped for same-connector duplicate",
|
|
358
|
+
);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("promotion dedup (QOjlD): same content + different connector promotes", async () => {
|
|
362
|
+
const { orchestrator, storage, targetStorage } = await makePromotionOrchestrator();
|
|
363
|
+
const scopePlan = makePromotionScopePlan();
|
|
364
|
+
const body = "The staging cluster uses dedicated namespaces per service tier.";
|
|
365
|
+
|
|
366
|
+
// Pre-write with connector "chatgpt".
|
|
367
|
+
await targetStorage.writeMemory("fact", body, {
|
|
368
|
+
confidence: 0.9,
|
|
369
|
+
sourceConnector: "chatgpt",
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// Extract with a DIFFERENT connector — promotion must proceed.
|
|
373
|
+
const ids = await orchestrator.persistExtraction(
|
|
374
|
+
factResult(body),
|
|
375
|
+
storage,
|
|
376
|
+
null,
|
|
377
|
+
{ sourceConnector: "codex-cli" },
|
|
378
|
+
"default",
|
|
379
|
+
scopePlan,
|
|
380
|
+
);
|
|
381
|
+
assert.equal(ids.length, 1, "source write must succeed");
|
|
382
|
+
|
|
383
|
+
// Target must now have TWO memories (pre-write + promoted copy).
|
|
384
|
+
const targetMems = await targetStorage.readAllMemories();
|
|
385
|
+
assert.equal(
|
|
386
|
+
targetMems.length,
|
|
387
|
+
2,
|
|
388
|
+
"promotion must proceed for different-connector content",
|
|
389
|
+
);
|
|
390
|
+
const connectors = targetMems.map(
|
|
391
|
+
(m: MemoryFile) => m.frontmatter.sourceConnector ?? undefined,
|
|
392
|
+
);
|
|
393
|
+
assert.ok(
|
|
394
|
+
connectors.includes("chatgpt"),
|
|
395
|
+
"pre-written chatgpt copy must remain",
|
|
396
|
+
);
|
|
397
|
+
assert.ok(
|
|
398
|
+
connectors.includes("codex-cli"),
|
|
399
|
+
"promoted codex-cli copy must exist",
|
|
400
|
+
);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test("promotion dedup (QOjlD): connector vs operator promotes", async () => {
|
|
404
|
+
const { orchestrator, storage, targetStorage } = await makePromotionOrchestrator();
|
|
405
|
+
const scopePlan = makePromotionScopePlan();
|
|
406
|
+
const body = "The observability stack ingests traces at one-second granularity.";
|
|
407
|
+
|
|
408
|
+
// Pre-write WITH a connector (chatgpt).
|
|
409
|
+
await targetStorage.writeMemory("fact", body, {
|
|
410
|
+
confidence: 0.9,
|
|
411
|
+
sourceConnector: "chatgpt",
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// Extract WITHOUT a sourceConnector (operator write).
|
|
415
|
+
const ids = await orchestrator.persistExtraction(
|
|
416
|
+
factResult(body),
|
|
417
|
+
storage,
|
|
418
|
+
null,
|
|
419
|
+
undefined,
|
|
420
|
+
"default",
|
|
421
|
+
scopePlan,
|
|
422
|
+
);
|
|
423
|
+
assert.equal(ids.length, 1, "source write must succeed");
|
|
424
|
+
|
|
425
|
+
// Connectors differ (chatgpt vs undefined) → promotion must proceed.
|
|
426
|
+
const targetMems = await targetStorage.readAllMemories();
|
|
427
|
+
assert.equal(
|
|
428
|
+
targetMems.length,
|
|
429
|
+
2,
|
|
430
|
+
"promotion must proceed when only one side has a connector",
|
|
431
|
+
);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("promotion dedup (QOjlD): operator vs operator skips promotion", async () => {
|
|
435
|
+
const { orchestrator, storage, targetStorage } = await makePromotionOrchestrator();
|
|
436
|
+
const scopePlan = makePromotionScopePlan();
|
|
437
|
+
const body = "The incident channel escalates pages within five minutes of detection.";
|
|
438
|
+
|
|
439
|
+
// Pre-write WITHOUT a connector (operator write).
|
|
440
|
+
await targetStorage.writeMemory("fact", body, {
|
|
441
|
+
confidence: 0.9,
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// Extract WITHOUT a sourceConnector (operator write).
|
|
445
|
+
const ids = await orchestrator.persistExtraction(
|
|
446
|
+
factResult(body),
|
|
447
|
+
storage,
|
|
448
|
+
null,
|
|
449
|
+
undefined,
|
|
450
|
+
"default",
|
|
451
|
+
scopePlan,
|
|
452
|
+
);
|
|
453
|
+
assert.equal(ids.length, 1, "source write must succeed");
|
|
454
|
+
|
|
455
|
+
// Both undefined → connectors match → promotion must be skipped.
|
|
456
|
+
const targetMems = await targetStorage.readAllMemories();
|
|
457
|
+
assert.equal(
|
|
458
|
+
targetMems.length,
|
|
459
|
+
1,
|
|
460
|
+
"promotion must be skipped for operator-vs-operator duplicate",
|
|
461
|
+
);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test("promotion dedup (QOjlD): readAllMemories throws on target → promotion proceeds (fail open)", async () => {
|
|
465
|
+
const { orchestrator, storage, targetStorage } = await makePromotionOrchestrator();
|
|
466
|
+
const scopePlan = makePromotionScopePlan();
|
|
467
|
+
const body = "The rate limiter resets token buckets every two hundred milliseconds.";
|
|
468
|
+
|
|
469
|
+
// Pre-write the same fact to the target with connector "chatgpt".
|
|
470
|
+
await targetStorage.writeMemory("fact", body, {
|
|
471
|
+
confidence: 0.9,
|
|
472
|
+
sourceConnector: "chatgpt",
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// Force the target's fact-hash index to become authoritative NOW so the
|
|
476
|
+
// subsequent hasFactContentHash call inside persistExtraction does NOT
|
|
477
|
+
// internally call readAllMemories (which we are about to override).
|
|
478
|
+
await targetStorage.isFactContentHashAuthoritative();
|
|
479
|
+
|
|
480
|
+
// Sanity: the hash really is registered before we break the scan.
|
|
481
|
+
assert.ok(
|
|
482
|
+
await targetStorage.hasFactContentHash(body),
|
|
483
|
+
"pre-write must register the content hash in the target index",
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
// Override readAllMemories on the TARGET so the connector-aware scan fails.
|
|
487
|
+
// hasFactContentHash still works (authoritative flag is set), so the code
|
|
488
|
+
// enters the scan block — then readAllMemories throws → fail-open → proceeds.
|
|
489
|
+
const originalReadAllMemories = targetStorage.readAllMemories.bind(targetStorage);
|
|
490
|
+
targetStorage.readAllMemories = async (): Promise<MemoryFile[]> => {
|
|
491
|
+
throw new Error("simulated target readAllMemories failure");
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const ids = await orchestrator.persistExtraction(
|
|
495
|
+
factResult(body),
|
|
496
|
+
storage,
|
|
497
|
+
null,
|
|
498
|
+
{ sourceConnector: "chatgpt" },
|
|
499
|
+
"default",
|
|
500
|
+
scopePlan,
|
|
501
|
+
);
|
|
502
|
+
assert.equal(ids.length, 1, "source write must succeed");
|
|
503
|
+
|
|
504
|
+
// Restore and verify the promotion proceeded (fail open).
|
|
505
|
+
targetStorage.readAllMemories = originalReadAllMemories;
|
|
506
|
+
const targetMems = await targetStorage.readAllMemories();
|
|
507
|
+
assert.equal(
|
|
508
|
+
targetMems.length,
|
|
509
|
+
2,
|
|
510
|
+
"promotion must proceed (fail open) when target scan throws",
|
|
511
|
+
);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test("promotion dedup (QOjlD): cited fact canonicalized before connector-aware dedup", async () => {
|
|
515
|
+
// When inlineSourceAttributionEnabled is true, the orchestrator appends a
|
|
516
|
+
// citation tag to every persisted fact. A second extraction that arrives
|
|
517
|
+
// already carrying that citation must still be deduped: normalizeStoredHashSource
|
|
518
|
+
// strips the citation from the STORED content before the connector-aware scan
|
|
519
|
+
// compares it to the canonical incoming body. Without this, the cited body
|
|
520
|
+
// would never match and the fact would be re-persisted (and re-promoted)
|
|
521
|
+
// on every relay cycle.
|
|
522
|
+
const { orchestrator, storage } = await makeDedupOrchestrator({
|
|
523
|
+
inlineSourceAttributionEnabled: true,
|
|
524
|
+
});
|
|
525
|
+
const rawBody =
|
|
526
|
+
"The deployment manifest pins container digests for reproducible builds.";
|
|
527
|
+
|
|
528
|
+
// First persist: orchestrator appends an inline citation and writes the fact.
|
|
529
|
+
const firstIds = await orchestrator.persistExtraction(
|
|
530
|
+
factResult(rawBody),
|
|
531
|
+
storage,
|
|
532
|
+
null,
|
|
533
|
+
{ sourceConnector: "chatgpt" },
|
|
534
|
+
);
|
|
535
|
+
assert.equal(firstIds.length, 1, "first write must succeed");
|
|
536
|
+
|
|
537
|
+
// Read back the persisted body — it now carries the citation tag.
|
|
538
|
+
const memories = await storage.readAllMemories();
|
|
539
|
+
const firstMemory = memories.find(
|
|
540
|
+
(m: MemoryFile) => m.frontmatter.id === firstIds[0],
|
|
541
|
+
);
|
|
542
|
+
assert.ok(firstMemory, "first memory must be readable");
|
|
543
|
+
assert.notEqual(
|
|
544
|
+
firstMemory.content,
|
|
545
|
+
rawBody,
|
|
546
|
+
"content must have a citation appended",
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
// Second persist: submit the ALREADY-CITED body with the same connector.
|
|
550
|
+
// normalizeStoredHashSource strips the citation before comparing, so the
|
|
551
|
+
// connector-aware scan finds the matching fact and dedupes at the source
|
|
552
|
+
// gate — promotion never fires.
|
|
553
|
+
const secondIds = await orchestrator.persistExtraction(
|
|
554
|
+
factResult(firstMemory.content),
|
|
555
|
+
storage,
|
|
556
|
+
null,
|
|
557
|
+
{ sourceConnector: "chatgpt" },
|
|
558
|
+
);
|
|
559
|
+
assert.equal(
|
|
560
|
+
secondIds.length,
|
|
561
|
+
0,
|
|
562
|
+
"cited duplicate must be deduped after canonicalization",
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
// Only one copy of the fact exists on disk.
|
|
566
|
+
const all = await storage.readAllMemories();
|
|
567
|
+
assert.equal(all.length, 1, "only one memory on disk");
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// ---------------------------------------------------------------------------
|
|
571
|
+
// Thread 5 (QPDE5): procedure body comparison in extraction dedup.
|
|
572
|
+
// The hash is keyed on buildProcedurePersistBody (title + steps), but the
|
|
573
|
+
// connector-aware scan previously compared against canonicalContentForHash
|
|
574
|
+
// (title only). Same-connector procedures with steps were re-persisted.
|
|
575
|
+
// ---------------------------------------------------------------------------
|
|
576
|
+
|
|
577
|
+
function makeProcedureFact(
|
|
578
|
+
title: string,
|
|
579
|
+
steps: Array<{ intent: string; expectedOutcome?: string }>,
|
|
580
|
+
): ExtractedFact {
|
|
581
|
+
return {
|
|
582
|
+
content: title,
|
|
583
|
+
category: "procedure",
|
|
584
|
+
tags: [],
|
|
585
|
+
confidence: 0.9,
|
|
586
|
+
procedureSteps: steps.map((s, i) => ({
|
|
587
|
+
order: i + 1,
|
|
588
|
+
intent: s.intent,
|
|
589
|
+
expectedOutcome: s.expectedOutcome,
|
|
590
|
+
})),
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function procedureResult(
|
|
595
|
+
title: string,
|
|
596
|
+
steps: Array<{ intent: string; expectedOutcome?: string }>,
|
|
597
|
+
): ExtractionResult {
|
|
598
|
+
return {
|
|
599
|
+
facts: [makeProcedureFact(title, steps)],
|
|
600
|
+
entities: [],
|
|
601
|
+
relationships: [],
|
|
602
|
+
questions: [],
|
|
603
|
+
profileUpdates: [],
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
test("connector dedup (QPDE5): same-connector procedure with steps dedupes", async () => {
|
|
608
|
+
const { orchestrator, storage } = await makeDedupOrchestrator();
|
|
609
|
+
// Title must contain a PROCEDURE_TRIGGER_RE phrase ("to deploy").
|
|
610
|
+
const title = "Workflow to deploy the canary service to staging";
|
|
611
|
+
const steps = [
|
|
612
|
+
{ intent: "Build the container image", expectedOutcome: "Image tagged and pushed" },
|
|
613
|
+
{ intent: "Update the Helm chart values", expectedOutcome: "Values file committed" },
|
|
614
|
+
];
|
|
615
|
+
|
|
616
|
+
// First write with connector "chatgpt" — must succeed.
|
|
617
|
+
const ids1 = await orchestrator.persistExtraction(
|
|
618
|
+
procedureResult(title, steps),
|
|
619
|
+
storage,
|
|
620
|
+
null,
|
|
621
|
+
{ sourceConnector: "chatgpt" },
|
|
622
|
+
);
|
|
623
|
+
assert.equal(ids1.length, 1, "first procedure write must succeed");
|
|
624
|
+
|
|
625
|
+
// Second write: same procedure, same connector → must dedupe.
|
|
626
|
+
const ids2 = await orchestrator.persistExtraction(
|
|
627
|
+
procedureResult(title, steps),
|
|
628
|
+
storage,
|
|
629
|
+
null,
|
|
630
|
+
{ sourceConnector: "chatgpt" },
|
|
631
|
+
);
|
|
632
|
+
assert.equal(ids2.length, 0, "same-connector procedure must be deduped");
|
|
633
|
+
|
|
634
|
+
// Only one procedure memory on disk.
|
|
635
|
+
const all = await storage.readAllMemories();
|
|
636
|
+
const procs = all.filter((m: MemoryFile) => m.frontmatter.category === "procedure");
|
|
637
|
+
assert.equal(procs.length, 1, "only one procedure memory on disk");
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
test("connector dedup (QPDE5): same procedure + different connector does NOT dedupe", async () => {
|
|
641
|
+
const { orchestrator, storage } = await makeDedupOrchestrator();
|
|
642
|
+
const title = "Recipe for rotating database credentials securely";
|
|
643
|
+
const steps = [
|
|
644
|
+
{ intent: "Generate new credentials in vault" },
|
|
645
|
+
{ intent: "Update application configuration" },
|
|
646
|
+
{ intent: "Verify connectivity with new credentials" },
|
|
647
|
+
];
|
|
648
|
+
|
|
649
|
+
// First write with connector "chatgpt".
|
|
650
|
+
const ids1 = await orchestrator.persistExtraction(
|
|
651
|
+
procedureResult(title, steps),
|
|
652
|
+
storage,
|
|
653
|
+
null,
|
|
654
|
+
{ sourceConnector: "chatgpt" },
|
|
655
|
+
);
|
|
656
|
+
assert.equal(ids1.length, 1, "first procedure write must succeed");
|
|
657
|
+
|
|
658
|
+
// Second write: same procedure, DIFFERENT connector → must NOT dedupe.
|
|
659
|
+
const ids2 = await orchestrator.persistExtraction(
|
|
660
|
+
procedureResult(title, steps),
|
|
661
|
+
storage,
|
|
662
|
+
null,
|
|
663
|
+
{ sourceConnector: "codex-cli" },
|
|
664
|
+
);
|
|
665
|
+
assert.equal(ids2.length, 1, "cross-connector procedure must NOT be deduped");
|
|
666
|
+
|
|
667
|
+
// Two procedures on disk.
|
|
668
|
+
const all = await storage.readAllMemories();
|
|
669
|
+
const procs = all.filter((m: MemoryFile) => m.frontmatter.category === "procedure");
|
|
670
|
+
assert.equal(procs.length, 2, "two procedure memories on disk");
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
// ---------------------------------------------------------------------------
|
|
674
|
+
// Thread 3 (QO42V): structuredAttributes enrichment in extraction dedup.
|
|
675
|
+
// Facts with structuredAttributes get an appended [Attributes: ...] suffix in
|
|
676
|
+
// the stored body. The connector-aware scan previously compared the enriched
|
|
677
|
+
// stored body against the raw hash key (no suffix), so same-connector enriched
|
|
678
|
+
// facts were re-persisted.
|
|
679
|
+
// ---------------------------------------------------------------------------
|
|
680
|
+
|
|
681
|
+
function makeEnrichedFact(
|
|
682
|
+
content: string,
|
|
683
|
+
attrs: Record<string, string>,
|
|
684
|
+
): ExtractedFact {
|
|
685
|
+
return {
|
|
686
|
+
content,
|
|
687
|
+
category: "fact",
|
|
688
|
+
tags: [],
|
|
689
|
+
confidence: 0.9,
|
|
690
|
+
structuredAttributes: attrs,
|
|
691
|
+
entityRef: "test-entity",
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function enrichedResult(
|
|
696
|
+
content: string,
|
|
697
|
+
attrs: Record<string, string>,
|
|
698
|
+
): ExtractionResult {
|
|
699
|
+
return {
|
|
700
|
+
facts: [makeEnrichedFact(content, attrs)],
|
|
701
|
+
entities: [],
|
|
702
|
+
relationships: [],
|
|
703
|
+
questions: [],
|
|
704
|
+
profileUpdates: [],
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
test("connector dedup (QO42V): same-connector fact with structuredAttributes dedupes", async () => {
|
|
709
|
+
const { orchestrator, storage } = await makeDedupOrchestrator();
|
|
710
|
+
const body = "The primary database runs on PostgreSQL version sixteen.";
|
|
711
|
+
const attrs = { dbEngine: "postgresql", version: "16" };
|
|
712
|
+
|
|
713
|
+
// First write with connector "chatgpt" — must succeed.
|
|
714
|
+
const ids1 = await orchestrator.persistExtraction(
|
|
715
|
+
enrichedResult(body, attrs),
|
|
716
|
+
storage,
|
|
717
|
+
null,
|
|
718
|
+
{ sourceConnector: "chatgpt" },
|
|
719
|
+
);
|
|
720
|
+
assert.equal(ids1.length, 1, "first enriched fact write must succeed");
|
|
721
|
+
|
|
722
|
+
// Second write: same content + attrs, same connector → must dedupe.
|
|
723
|
+
const ids2 = await orchestrator.persistExtraction(
|
|
724
|
+
enrichedResult(body, attrs),
|
|
725
|
+
storage,
|
|
726
|
+
null,
|
|
727
|
+
{ sourceConnector: "chatgpt" },
|
|
728
|
+
);
|
|
729
|
+
assert.equal(ids2.length, 0, "same-connector enriched fact must be deduped");
|
|
730
|
+
|
|
731
|
+
// Only one memory on disk.
|
|
732
|
+
const all = await storage.readAllMemories();
|
|
733
|
+
const facts = all.filter(
|
|
734
|
+
(m: MemoryFile) => m.frontmatter.category === "fact" && m.frontmatter.status !== "superseded",
|
|
735
|
+
);
|
|
736
|
+
assert.equal(facts.length, 1, "only one active fact memory on disk");
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
// ---------------------------------------------------------------------------
|
|
740
|
+
// Thread 4 (QPAn-): shared promotion short-circuit ignores connector when
|
|
741
|
+
// temporal supersession is off or the fact has no entity/attributes.
|
|
742
|
+
// Uses a scope plan with a serverShared promotion target so the shared
|
|
743
|
+
// promotion path fires and reaches the no-supersession else branch.
|
|
744
|
+
// ---------------------------------------------------------------------------
|
|
745
|
+
|
|
746
|
+
function makeSharedPromotionScopePlan(): ResolvedScopeProfilePlan {
|
|
747
|
+
return {
|
|
748
|
+
profileId: "test-shared-promotion",
|
|
749
|
+
profile: {
|
|
750
|
+
readOrder: ["userProject", "serverShared"],
|
|
751
|
+
writeDefault: "userProject",
|
|
752
|
+
promotionTargets: ["serverShared"],
|
|
753
|
+
autoPromote: {
|
|
754
|
+
enabled: true,
|
|
755
|
+
targets: ["serverShared"],
|
|
756
|
+
categories: ["fact"],
|
|
757
|
+
minConfidenceTier: "speculative",
|
|
758
|
+
},
|
|
759
|
+
},
|
|
760
|
+
baseNamespace: "default",
|
|
761
|
+
writeLayer: "userProject",
|
|
762
|
+
writeNamespace: "default",
|
|
763
|
+
readNamespaces: ["default", "shared"],
|
|
764
|
+
layers: [
|
|
765
|
+
{
|
|
766
|
+
id: "userProject",
|
|
767
|
+
kind: "user-project",
|
|
768
|
+
namespace: "default",
|
|
769
|
+
readable: true,
|
|
770
|
+
writable: true,
|
|
771
|
+
promotable: false,
|
|
772
|
+
reason: "test",
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
id: "serverShared",
|
|
776
|
+
kind: "server-shared",
|
|
777
|
+
namespace: "shared",
|
|
778
|
+
readable: true,
|
|
779
|
+
writable: true,
|
|
780
|
+
promotable: true,
|
|
781
|
+
reason: "test",
|
|
782
|
+
},
|
|
783
|
+
],
|
|
784
|
+
promotionTargets: [
|
|
785
|
+
{
|
|
786
|
+
target: "serverShared",
|
|
787
|
+
namespace: "shared",
|
|
788
|
+
authorized: true,
|
|
789
|
+
reason: "test",
|
|
790
|
+
},
|
|
791
|
+
],
|
|
792
|
+
warnings: [],
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
test("shared promotion (QPAn-): different-connector fact promotes when supersession is off", async () => {
|
|
797
|
+
const { orchestrator, storage } = await makePromotionOrchestrator();
|
|
798
|
+
const scopePlan = makeSharedPromotionScopePlan();
|
|
799
|
+
const body =
|
|
800
|
+
"The backup scheduler retains snapshots for thirty days before pruning.";
|
|
801
|
+
|
|
802
|
+
// Pre-write the fact to the SHARED namespace with connector "chatgpt".
|
|
803
|
+
// No entityRef/structuredAttributes → shared promotion takes the else branch.
|
|
804
|
+
const sharedStorage = await orchestrator.getStorage("shared");
|
|
805
|
+
await sharedStorage.writeMemory("fact", body, {
|
|
806
|
+
confidence: 0.9,
|
|
807
|
+
sourceConnector: "chatgpt",
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
// Extract with a DIFFERENT connector — shared promotion else branch
|
|
811
|
+
// must NOT short-circuit; the different-connector fact gets promoted.
|
|
812
|
+
const ids = await orchestrator.persistExtraction(
|
|
813
|
+
factResult(body),
|
|
814
|
+
storage,
|
|
815
|
+
null,
|
|
816
|
+
{ sourceConnector: "codex-cli" },
|
|
817
|
+
"default",
|
|
818
|
+
scopePlan,
|
|
819
|
+
);
|
|
820
|
+
assert.equal(ids.length, 1, "source write must succeed");
|
|
821
|
+
|
|
822
|
+
// Shared namespace must now have TWO memories (chatgpt + codex-cli).
|
|
823
|
+
const sharedMems = await sharedStorage.readAllMemories();
|
|
824
|
+
const connectors = sharedMems.map(
|
|
825
|
+
(m: MemoryFile) => m.frontmatter.sourceConnector ?? undefined,
|
|
826
|
+
);
|
|
827
|
+
assert.ok(
|
|
828
|
+
connectors.includes("chatgpt"),
|
|
829
|
+
"pre-written chatgpt shared copy must remain",
|
|
830
|
+
);
|
|
831
|
+
assert.ok(
|
|
832
|
+
connectors.includes("codex-cli"),
|
|
833
|
+
"promoted codex-cli shared copy must exist",
|
|
834
|
+
);
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
test("shared promotion (QPAn-): same-connector fact skips promotion when supersession is off", async () => {
|
|
838
|
+
const { orchestrator, storage } = await makePromotionOrchestrator();
|
|
839
|
+
const scopePlan = makeSharedPromotionScopePlan();
|
|
840
|
+
const body =
|
|
841
|
+
"The log aggregator compacts segments every six hours to reclaim disk space.";
|
|
842
|
+
|
|
843
|
+
// Pre-write to shared with connector "chatgpt".
|
|
844
|
+
const sharedStorage = await orchestrator.getStorage("shared");
|
|
845
|
+
await sharedStorage.writeMemory("fact", body, {
|
|
846
|
+
confidence: 0.9,
|
|
847
|
+
sourceConnector: "chatgpt",
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
// Extract with the SAME connector — shared promotion must be skipped.
|
|
851
|
+
const ids = await orchestrator.persistExtraction(
|
|
852
|
+
factResult(body),
|
|
853
|
+
storage,
|
|
854
|
+
null,
|
|
855
|
+
{ sourceConnector: "chatgpt" },
|
|
856
|
+
"default",
|
|
857
|
+
scopePlan,
|
|
858
|
+
);
|
|
859
|
+
assert.equal(ids.length, 1, "source write must succeed");
|
|
860
|
+
|
|
861
|
+
// Shared namespace must still have exactly ONE memory.
|
|
862
|
+
const sharedMems = await sharedStorage.readAllMemories();
|
|
863
|
+
assert.equal(
|
|
864
|
+
sharedMems.length,
|
|
865
|
+
1,
|
|
866
|
+
"same-connector shared promotion must be skipped",
|
|
867
|
+
);
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
// ---------------------------------------------------------------------------
|
|
871
|
+
// Regression: shared-promotion temporal backfill respects connector identity
|
|
872
|
+
// (codex finding: temporal backfill occurred before connector mismatch check).
|
|
873
|
+
// The shared namespace's backfillTemporalBoundsOnDedupHit must NOT patch
|
|
874
|
+
// temporal bounds onto a different-connector fact. Same-connector facts
|
|
875
|
+
// still get backfilled as designed.
|
|
876
|
+
// ---------------------------------------------------------------------------
|
|
877
|
+
|
|
878
|
+
test("shared backfill: different-connector shared fact is NOT temporal-patched", async () => {
|
|
879
|
+
const { orchestrator, storage } = await makeDedupOrchestrator({
|
|
880
|
+
namespacesEnabled: true,
|
|
881
|
+
defaultNamespace: "default",
|
|
882
|
+
sharedNamespace: "shared",
|
|
883
|
+
temporalBiTemporal: true,
|
|
884
|
+
});
|
|
885
|
+
const scopePlan = makeSharedPromotionScopePlan();
|
|
886
|
+
const body =
|
|
887
|
+
"The CDN edge cache invalidates stale assets every forty-five minutes.";
|
|
888
|
+
|
|
889
|
+
// Pre-write a fact to the SHARED namespace with connector "chatgpt"
|
|
890
|
+
// and NO temporal bounds (invalid_at absent).
|
|
891
|
+
const sharedStorage = await orchestrator.getStorage("shared");
|
|
892
|
+
await sharedStorage.writeMemory("fact", body, {
|
|
893
|
+
confidence: 0.9,
|
|
894
|
+
sourceConnector: "chatgpt",
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
// Verify the pre-written fact has no invalid_at.
|
|
898
|
+
const sharedBefore = await sharedStorage.readAllMemories();
|
|
899
|
+
const preFact = sharedBefore.find((m: MemoryFile) => m.frontmatter.sourceConnector === "chatgpt");
|
|
900
|
+
assert.ok(preFact, "pre-written chatgpt fact exists");
|
|
901
|
+
assert.equal(preFact!.frontmatter.invalid_at, undefined, "no invalid_at before extraction");
|
|
902
|
+
|
|
903
|
+
// Extract the SAME content with a DIFFERENT connector and an eventTime
|
|
904
|
+
// expression that resolves a validUntil bound.
|
|
905
|
+
const ids = await orchestrator.persistExtraction(
|
|
906
|
+
{
|
|
907
|
+
facts: [{
|
|
908
|
+
content: body,
|
|
909
|
+
category: "fact" as const,
|
|
910
|
+
tags: [],
|
|
911
|
+
confidence: 0.9,
|
|
912
|
+
eventTime: "until 2025-06",
|
|
913
|
+
}],
|
|
914
|
+
entities: [],
|
|
915
|
+
relationships: [],
|
|
916
|
+
questions: [],
|
|
917
|
+
profileUpdates: [],
|
|
918
|
+
},
|
|
919
|
+
storage,
|
|
920
|
+
null,
|
|
921
|
+
{ sourceConnector: "codex-cli", validAt: "2025-07-01T00:00:00.000Z" },
|
|
922
|
+
"default",
|
|
923
|
+
scopePlan,
|
|
924
|
+
);
|
|
925
|
+
assert.equal(ids.length, 1, "source write must succeed");
|
|
926
|
+
|
|
927
|
+
// The chatgpt shared fact must NOT have been temporal-patched —
|
|
928
|
+
// different connector means no cross-connector backfill.
|
|
929
|
+
const sharedAfter = await sharedStorage.readAllMemories();
|
|
930
|
+
const chatgptFact = sharedAfter.find(
|
|
931
|
+
(m: MemoryFile) => m.frontmatter.sourceConnector === "chatgpt",
|
|
932
|
+
);
|
|
933
|
+
assert.ok(chatgptFact, "chatgpt shared fact still exists");
|
|
934
|
+
assert.equal(
|
|
935
|
+
chatgptFact!.frontmatter.invalid_at,
|
|
936
|
+
undefined,
|
|
937
|
+
"chatgpt fact must NOT get invalid_at from codex-li backfill",
|
|
938
|
+
);
|
|
939
|
+
|
|
940
|
+
// The codex-li fact must have its OWN promoted shared copy with bounds.
|
|
941
|
+
const codexFact = sharedAfter.find(
|
|
942
|
+
(m: MemoryFile) => m.frontmatter.sourceConnector === "codex-cli",
|
|
943
|
+
);
|
|
944
|
+
assert.ok(codexFact, "codex-li shared copy was promoted");
|
|
945
|
+
assert.ok(
|
|
946
|
+
codexFact!.frontmatter.invalid_at,
|
|
947
|
+
"codex-li shared copy has temporal bounds from its own extraction",
|
|
948
|
+
);
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
test("shared backfill: same-connector shared fact IS temporal-patched", async () => {
|
|
952
|
+
const { orchestrator, storage } = await makeDedupOrchestrator({
|
|
953
|
+
namespacesEnabled: true,
|
|
954
|
+
defaultNamespace: "default",
|
|
955
|
+
sharedNamespace: "shared",
|
|
956
|
+
temporalBiTemporal: true,
|
|
957
|
+
});
|
|
958
|
+
const scopePlan = makeSharedPromotionScopePlan();
|
|
959
|
+
const body =
|
|
960
|
+
"The feature flag service polls configuration updates every ninety seconds.";
|
|
961
|
+
|
|
962
|
+
// Pre-write a fact to shared with connector "chatgpt" and NO temporal bounds.
|
|
963
|
+
const sharedStorage = await orchestrator.getStorage("shared");
|
|
964
|
+
await sharedStorage.writeMemory("fact", body, {
|
|
965
|
+
confidence: 0.9,
|
|
966
|
+
sourceConnector: "chatgpt",
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
// Extract SAME content with SAME connector and an eventTime resolving validUntil.
|
|
970
|
+
await orchestrator.persistExtraction(
|
|
971
|
+
{
|
|
972
|
+
facts: [{
|
|
973
|
+
content: body,
|
|
974
|
+
category: "fact" as const,
|
|
975
|
+
tags: [],
|
|
976
|
+
confidence: 0.9,
|
|
977
|
+
eventTime: "until 2025-03",
|
|
978
|
+
}],
|
|
979
|
+
entities: [],
|
|
980
|
+
relationships: [],
|
|
981
|
+
questions: [],
|
|
982
|
+
profileUpdates: [],
|
|
983
|
+
},
|
|
984
|
+
storage,
|
|
985
|
+
null,
|
|
986
|
+
{ sourceConnector: "chatgpt", validAt: "2025-07-01T00:00:00.000Z" },
|
|
987
|
+
"default",
|
|
988
|
+
scopePlan,
|
|
989
|
+
);
|
|
990
|
+
|
|
991
|
+
// Same connector → backfill MUST fire, patching invalid_at onto the
|
|
992
|
+
// existing shared fact.
|
|
993
|
+
const sharedAfter = await sharedStorage.readAllMemories();
|
|
994
|
+
const chatgptFact = sharedAfter.find(
|
|
995
|
+
(m: MemoryFile) => m.frontmatter.sourceConnector === "chatgpt",
|
|
996
|
+
);
|
|
997
|
+
assert.ok(chatgptFact, "chatgpt shared fact exists");
|
|
998
|
+
assert.ok(
|
|
999
|
+
chatgptFact!.frontmatter.invalid_at,
|
|
1000
|
+
"same-connector shared fact must get invalid_at backfilled",
|
|
1001
|
+
);
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
// ---------------------------------------------------------------------------
|
|
1005
|
+
// Regression (cursor #1852): persistence-index backfill must match connector
|
|
1006
|
+
// identity EXACTLY, including undefined. The prior guard only filtered
|
|
1007
|
+
// sourceConnector when truthy — so an operator re-extraction with NO connector
|
|
1008
|
+
// skipped the guard entirely, letting .find() select the first hash/entity
|
|
1009
|
+
// match, which could be a connector-TAGGED duplicate, mutating its temporal
|
|
1010
|
+
// bounds. Fix: exact comparison (undefined === undefined, tagged === tagged).
|
|
1011
|
+
// ---------------------------------------------------------------------------
|
|
1012
|
+
|
|
1013
|
+
test("shared backfill: operator (no-connector) extraction must NOT patch a connector-tagged duplicate", async () => {
|
|
1014
|
+
const { orchestrator, storage } = await makeDedupOrchestrator({
|
|
1015
|
+
namespacesEnabled: true,
|
|
1016
|
+
defaultNamespace: "default",
|
|
1017
|
+
sharedNamespace: "shared",
|
|
1018
|
+
temporalBiTemporal: true,
|
|
1019
|
+
});
|
|
1020
|
+
const scopePlan = makeSharedPromotionScopePlan();
|
|
1021
|
+
const body =
|
|
1022
|
+
"The rate limiter resets its token bucket every two hundred milliseconds.";
|
|
1023
|
+
|
|
1024
|
+
// Pre-write TWO facts to the shared namespace with identical content:
|
|
1025
|
+
// one tagged "chatgpt" and one connectorless. Neither has temporal bounds.
|
|
1026
|
+
const sharedStorage = await orchestrator.getStorage("shared");
|
|
1027
|
+
await sharedStorage.writeMemory("fact", body, {
|
|
1028
|
+
confidence: 0.9,
|
|
1029
|
+
sourceConnector: "chatgpt",
|
|
1030
|
+
});
|
|
1031
|
+
await sharedStorage.writeMemory("fact", body, {
|
|
1032
|
+
confidence: 0.9,
|
|
1033
|
+
// No sourceConnector — connectorless, the only kind eligible for
|
|
1034
|
+
// operator/no-connector backfill.
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
// Verify the tagged fact exists and lacks temporal bounds.
|
|
1038
|
+
const sharedBefore = await sharedStorage.readAllMemories();
|
|
1039
|
+
const taggedBefore = sharedBefore.find(
|
|
1040
|
+
(m: MemoryFile) => m.frontmatter.sourceConnector === "chatgpt",
|
|
1041
|
+
);
|
|
1042
|
+
assert.ok(taggedBefore, "pre-written chatgpt fact exists");
|
|
1043
|
+
assert.equal(taggedBefore!.frontmatter.invalid_at, undefined);
|
|
1044
|
+
|
|
1045
|
+
// Extract the SAME content with NO sourceConnector (operator
|
|
1046
|
+
// re-extraction) and an eventTime that resolves a validUntil bound.
|
|
1047
|
+
const ids = await orchestrator.persistExtraction(
|
|
1048
|
+
{
|
|
1049
|
+
facts: [{
|
|
1050
|
+
content: body,
|
|
1051
|
+
category: "fact" as const,
|
|
1052
|
+
tags: [],
|
|
1053
|
+
confidence: 0.9,
|
|
1054
|
+
eventTime: "until 2025-06",
|
|
1055
|
+
}],
|
|
1056
|
+
entities: [],
|
|
1057
|
+
relationships: [],
|
|
1058
|
+
questions: [],
|
|
1059
|
+
profileUpdates: [],
|
|
1060
|
+
},
|
|
1061
|
+
storage,
|
|
1062
|
+
null,
|
|
1063
|
+
{ validAt: "2025-07-01T00:00:00.000Z" },
|
|
1064
|
+
"default",
|
|
1065
|
+
scopePlan,
|
|
1066
|
+
);
|
|
1067
|
+
assert.ok(ids.length >= 1, "extraction must proceed");
|
|
1068
|
+
|
|
1069
|
+
// The chatgpt-tagged fact must NOT have been temporal-patched.
|
|
1070
|
+
// Operator/no-connector backfill may only select a connectorless
|
|
1071
|
+
// candidate, never a connector-tagged one (cursor #1852).
|
|
1072
|
+
const sharedAfter = await sharedStorage.readAllMemories();
|
|
1073
|
+
const taggedAfter = sharedAfter.find(
|
|
1074
|
+
(m: MemoryFile) => m.frontmatter.sourceConnector === "chatgpt",
|
|
1075
|
+
);
|
|
1076
|
+
assert.ok(taggedAfter, "chatgpt fact still exists");
|
|
1077
|
+
assert.equal(
|
|
1078
|
+
taggedAfter!.frontmatter.invalid_at,
|
|
1079
|
+
undefined,
|
|
1080
|
+
"tagged fact must NOT get invalid_at from connectorless/operator backfill",
|
|
1081
|
+
);
|
|
1082
|
+
});
|