@remnic/core 9.6.0 → 9.6.2
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-B_sc1w_k.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-FKCRGLRD.js → chunk-4WRKKRTD.js} +266 -160
- package/dist/chunk-4WRKKRTD.js.map +1 -0
- package/dist/{chunk-LQHDIS7L.js → chunk-54BGIBCR.js} +2 -2
- package/dist/{chunk-PVUJ6WVV.js → chunk-5MH5NF3U.js} +5 -5
- package/dist/{chunk-PCLIWBGD.js → chunk-7ARV32PI.js} +8 -6
- package/dist/chunk-7ARV32PI.js.map +1 -0
- 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-TLUDFPZL.js → chunk-CAZ3Z5M6.js} +2 -2
- package/dist/{chunk-RGK27ELN.js → chunk-DEKIXXIG.js} +2 -2
- package/dist/{chunk-2KK5WNQB.js → chunk-DQ42IQSK.js} +2 -2
- package/dist/{chunk-UFUKHGZD.js → chunk-F4I5NYBI.js} +89 -33
- package/dist/chunk-F4I5NYBI.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-LADFWYHY.js → chunk-GOEV7CYF.js} +6 -6
- package/dist/chunk-GOEV7CYF.js.map +1 -0
- 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-4GNEDXDI.js → chunk-M67TLMP6.js} +2 -2
- package/dist/chunk-M67TLMP6.js.map +1 -0
- package/dist/{chunk-MC4FJXPA.js → chunk-MMACM5G2.js} +2 -2
- 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-XCL57M6Q.js → chunk-RRKPLLCC.js} +21 -20
- package/dist/chunk-RRKPLLCC.js.map +1 -0
- package/dist/{chunk-72WG5QKN.js → chunk-SDAXPXER.js} +2 -2
- package/dist/{chunk-4LV23CHK.js → chunk-VHJARF5R.js} +2 -2
- package/dist/{chunk-H2DXDQCQ.js → chunk-VRJG3HIZ.js} +46 -42
- package/dist/chunk-VRJG3HIZ.js.map +1 -0
- package/dist/{chunk-6MMONPHM.js → chunk-W2AHSCKY.js} +2 -2
- package/dist/{chunk-P454KXD6.js → chunk-YAWJMR3Y.js} +21 -21
- 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-CnRsWREp.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-NRLVXPpH.d.ts} +40 -15
- 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 +44 -44
- 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/transfer/types.d.ts +12 -12
- 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 +11 -3
- 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 +7 -11
- 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-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
- /package/dist/{chunk-P454KXD6.js.map → chunk-YAWJMR3Y.js.map} +0 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 1 connector-provenance tests.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that a server-resolved `sourceConnector` identity threads through
|
|
5
|
+
* the memory_store and suggestion_submit write paths — from the MCP request
|
|
6
|
+
* boundary, through the operation handler (where it overrides any client-
|
|
7
|
+
* supplied value), down to YAML frontmatter on disk.
|
|
8
|
+
*
|
|
9
|
+
* All fixtures are synthetic — no real user data.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import test from "node:test";
|
|
17
|
+
|
|
18
|
+
import { EngramMcpServer } from "./access-mcp.js";
|
|
19
|
+
import { memoryStoreOperation } from "./access-operations.js";
|
|
20
|
+
import type {
|
|
21
|
+
EngramAccessMemoryStoreRequest,
|
|
22
|
+
EngramAccessService,
|
|
23
|
+
EngramAccessWriteResponse,
|
|
24
|
+
} from "./access-service.js";
|
|
25
|
+
import { StorageManager } from "./storage.js";
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Mock service that captures memoryStore calls
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
function makeCaptureService(): {
|
|
32
|
+
captured: EngramAccessMemoryStoreRequest[];
|
|
33
|
+
service: EngramAccessService;
|
|
34
|
+
} {
|
|
35
|
+
const captured: EngramAccessMemoryStoreRequest[] = [];
|
|
36
|
+
const service = {
|
|
37
|
+
memoryStore: (
|
|
38
|
+
req: EngramAccessMemoryStoreRequest,
|
|
39
|
+
): Promise<EngramAccessWriteResponse> => {
|
|
40
|
+
captured.push(req);
|
|
41
|
+
return Promise.resolve({
|
|
42
|
+
schemaVersion: 1,
|
|
43
|
+
operation: "memory_store",
|
|
44
|
+
namespace: "default",
|
|
45
|
+
dryRun: false,
|
|
46
|
+
accepted: true,
|
|
47
|
+
queued: false,
|
|
48
|
+
status: "stored",
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
} as unknown as EngramAccessService;
|
|
52
|
+
return { captured, service };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function memoryStoreRequest(
|
|
56
|
+
overrides: Record<string, unknown> = {},
|
|
57
|
+
): { jsonrpc: "2.0"; id: number; method: "tools/call"; params: Record<string, unknown> } {
|
|
58
|
+
return {
|
|
59
|
+
jsonrpc: "2.0",
|
|
60
|
+
id: 1,
|
|
61
|
+
method: "tools/call",
|
|
62
|
+
params: {
|
|
63
|
+
name: "engram.memory_store",
|
|
64
|
+
arguments: {
|
|
65
|
+
content: "The user prefers dark mode for all coding tools",
|
|
66
|
+
schemaVersion: 1,
|
|
67
|
+
idempotencyKey: `key-${overrides.idempotencyKey ?? Math.random().toString(36).slice(2)}`,
|
|
68
|
+
...overrides,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Tests 1–3: MCP layer threads sourceConnector to the service
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
test("memory_store via MCP with sourceConnector 'chatgpt' → service receives it", async () => {
|
|
79
|
+
const { captured, service } = makeCaptureService();
|
|
80
|
+
const server = new EngramMcpServer(service, { principal: "test" });
|
|
81
|
+
|
|
82
|
+
await server.handleRequest(memoryStoreRequest(), {
|
|
83
|
+
sourceConnector: "chatgpt",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
assert.equal(captured.length, 1, "memoryStore should have been called once");
|
|
87
|
+
assert.equal(
|
|
88
|
+
captured[0]!.sourceConnector,
|
|
89
|
+
"chatgpt",
|
|
90
|
+
"service should receive the server-resolved sourceConnector",
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("memory_store via MCP with sourceConnector 'codex-cli' → service receives it", async () => {
|
|
95
|
+
const { captured, service } = makeCaptureService();
|
|
96
|
+
const server = new EngramMcpServer(service, { principal: "test" });
|
|
97
|
+
|
|
98
|
+
await server.handleRequest(memoryStoreRequest(), {
|
|
99
|
+
sourceConnector: "codex-cli",
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
assert.equal(captured.length, 1);
|
|
103
|
+
assert.equal(captured[0]!.sourceConnector, "codex-cli");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("memory_store via MCP with no sourceConnector (operator) → service receives undefined", async () => {
|
|
107
|
+
const { captured, service } = makeCaptureService();
|
|
108
|
+
const server = new EngramMcpServer(service, { principal: "test" });
|
|
109
|
+
|
|
110
|
+
await server.handleRequest(memoryStoreRequest(), {});
|
|
111
|
+
|
|
112
|
+
assert.equal(captured.length, 1);
|
|
113
|
+
assert.equal(
|
|
114
|
+
captured[0]!.sourceConnector,
|
|
115
|
+
undefined,
|
|
116
|
+
"operator calls should not carry a sourceConnector",
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Test 4: client-supplied sourceConnector in args is IGNORED
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
test("client-supplied sourceConnector in args → IGNORED, server value wins", async () => {
|
|
125
|
+
const captured: EngramAccessMemoryStoreRequest[] = [];
|
|
126
|
+
const service = {
|
|
127
|
+
memoryStore: (req: EngramAccessMemoryStoreRequest) => {
|
|
128
|
+
captured.push(req);
|
|
129
|
+
return Promise.resolve({
|
|
130
|
+
schemaVersion: 1,
|
|
131
|
+
operation: "memory_store",
|
|
132
|
+
namespace: "default",
|
|
133
|
+
dryRun: false,
|
|
134
|
+
accepted: true,
|
|
135
|
+
queued: false,
|
|
136
|
+
status: "stored",
|
|
137
|
+
} satisfies EngramAccessWriteResponse);
|
|
138
|
+
},
|
|
139
|
+
} as unknown as EngramAccessService;
|
|
140
|
+
|
|
141
|
+
// Call the handler directly, simulating a malicious client that managed to
|
|
142
|
+
// inject sourceConnector: "evil" into the parsed input. The handler must
|
|
143
|
+
// override it with the server-resolved ctx.sourceConnector.
|
|
144
|
+
await memoryStoreOperation.spec.handler(
|
|
145
|
+
{
|
|
146
|
+
content: "The user prefers vim over emacs",
|
|
147
|
+
schemaVersion: 1,
|
|
148
|
+
idempotencyKey: "evil-key",
|
|
149
|
+
sourceConnector: "evil",
|
|
150
|
+
} as unknown as Parameters<typeof memoryStoreOperation.spec.handler>[0],
|
|
151
|
+
{
|
|
152
|
+
service,
|
|
153
|
+
sourceConnector: "chatgpt",
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
assert.equal(captured.length, 1);
|
|
158
|
+
assert.equal(
|
|
159
|
+
captured[0]!.sourceConnector,
|
|
160
|
+
"chatgpt",
|
|
161
|
+
"server-resolved sourceConnector must override client-supplied value",
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Tests 5–6: StorageManager.writeMemory persists sourceConnector to frontmatter
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
test("StorageManager.writeMemory with sourceConnector 'omp' → frontmatter contains it", async () => {
|
|
170
|
+
StorageManager.clearAllStaticCaches();
|
|
171
|
+
const baseDir = await mkdtemp(path.join(os.tmpdir(), "prov-connector-"));
|
|
172
|
+
try {
|
|
173
|
+
const storage = new StorageManager(baseDir);
|
|
174
|
+
await storage.ensureDirectories();
|
|
175
|
+
|
|
176
|
+
const { id } = await storage.writeMemory(
|
|
177
|
+
"fact",
|
|
178
|
+
"The user works from the Pacific time zone",
|
|
179
|
+
{ sourceConnector: "omp" },
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const memory = await storage.getMemoryById(id);
|
|
183
|
+
assert.ok(memory, "getMemoryById must find the just-written memory");
|
|
184
|
+
assert.equal(
|
|
185
|
+
memory!.frontmatter.sourceConnector,
|
|
186
|
+
"omp",
|
|
187
|
+
"frontmatter should contain the sourceConnector",
|
|
188
|
+
);
|
|
189
|
+
} finally {
|
|
190
|
+
StorageManager.clearAllStaticCaches();
|
|
191
|
+
await rm(baseDir, { recursive: true, force: true });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("StorageManager.writeMemory without sourceConnector → frontmatter omits it", async () => {
|
|
196
|
+
StorageManager.clearAllStaticCaches();
|
|
197
|
+
const baseDir = await mkdtemp(path.join(os.tmpdir(), "prov-connector-"));
|
|
198
|
+
try {
|
|
199
|
+
const storage = new StorageManager(baseDir);
|
|
200
|
+
await storage.ensureDirectories();
|
|
201
|
+
|
|
202
|
+
const { id } = await storage.writeMemory(
|
|
203
|
+
"fact",
|
|
204
|
+
"The user enjoys hiking on weekends",
|
|
205
|
+
{},
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const memory = await storage.getMemoryById(id);
|
|
209
|
+
assert.ok(memory, "getMemoryById must find the just-written memory");
|
|
210
|
+
assert.equal(
|
|
211
|
+
memory!.frontmatter.sourceConnector,
|
|
212
|
+
undefined,
|
|
213
|
+
"frontmatter should omit sourceConnector when not provided",
|
|
214
|
+
);
|
|
215
|
+
} finally {
|
|
216
|
+
StorageManager.clearAllStaticCaches();
|
|
217
|
+
await rm(baseDir, { recursive: true, force: true });
|
|
218
|
+
}
|
|
219
|
+
});
|
|
@@ -245,6 +245,111 @@ test("semantic dedup: normalizes malformed numeric options before lookup", async
|
|
|
245
245
|
assert.equal(clampedLowThreshold.action, "skip");
|
|
246
246
|
});
|
|
247
247
|
|
|
248
|
+
// ── Connector-aware semantic dedup (PR #1852 review finding on 7e0eb1a0) ──────
|
|
249
|
+
//
|
|
250
|
+
// A provenance-bearing candidate (sourceConnector present) may be suppressed
|
|
251
|
+
// only by a near-duplicate from the SAME connector. A different or absent
|
|
252
|
+
// neighbor connector must NOT suppress the write. Operator/unattributed
|
|
253
|
+
// writes (no candidate connector) preserve the original behavior.
|
|
254
|
+
|
|
255
|
+
test("semantic dedup connector-aware: skips same-connector paraphrase", async () => {
|
|
256
|
+
// Candidate from chatgpt; top neighbor is also chatgpt at 0.96 → skip.
|
|
257
|
+
const decision = await decideSemanticDedup(
|
|
258
|
+
"tabs are preferred by the user for indentation",
|
|
259
|
+
makeLookup([{ id: "existing-pref", score: 0.96, sourceConnector: "chatgpt" }]),
|
|
260
|
+
{ ...DEFAULT_OPTS, sourceConnector: "chatgpt" },
|
|
261
|
+
);
|
|
262
|
+
assert.equal(decision.action, "skip");
|
|
263
|
+
if (decision.action === "skip") {
|
|
264
|
+
assert.equal(decision.reason, "near_duplicate");
|
|
265
|
+
assert.equal(decision.topId, "existing-pref");
|
|
266
|
+
assert.equal(decision.topScore, 0.96);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("semantic dedup connector-aware: keeps different-connector paraphrase (B vs A)", async () => {
|
|
271
|
+
// Candidate from chatgpt; only neighbor is codex at 0.96 → must NOT skip.
|
|
272
|
+
const decision = await decideSemanticDedup(
|
|
273
|
+
"tabs are preferred by the user for indentation",
|
|
274
|
+
makeLookup([{ id: "codex-pref", score: 0.96, sourceConnector: "codex" }]),
|
|
275
|
+
{ ...DEFAULT_OPTS, sourceConnector: "chatgpt" },
|
|
276
|
+
);
|
|
277
|
+
assert.equal(decision.action, "keep");
|
|
278
|
+
assert.equal(decision.reason, "connector_mismatch");
|
|
279
|
+
if (decision.action === "keep") {
|
|
280
|
+
assert.equal(decision.topId, "codex-pref");
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("semantic dedup connector-aware: keeps provenance candidate when neighbor connector absent", async () => {
|
|
285
|
+
// Candidate from chatgpt; neighbor has no connector at 0.97 → must NOT skip.
|
|
286
|
+
const decision = await decideSemanticDedup(
|
|
287
|
+
"tabs are preferred by the user for indentation",
|
|
288
|
+
makeLookup([{ id: "operator-pref", score: 0.97 }]),
|
|
289
|
+
{ ...DEFAULT_OPTS, sourceConnector: "chatgpt" },
|
|
290
|
+
);
|
|
291
|
+
assert.equal(decision.action, "keep");
|
|
292
|
+
assert.equal(decision.reason, "connector_mismatch");
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("semantic dedup connector-aware: operator (no candidate connector) preserves skip regardless of neighbor connector", async () => {
|
|
296
|
+
// No candidate connector → existing behavior: skip on any near-duplicate,
|
|
297
|
+
// even one tagged with a different connector.
|
|
298
|
+
const decision = await decideSemanticDedup(
|
|
299
|
+
"tabs are preferred by the user for indentation",
|
|
300
|
+
makeLookup([{ id: "codex-pref", score: 0.96, sourceConnector: "codex" }]),
|
|
301
|
+
DEFAULT_OPTS,
|
|
302
|
+
);
|
|
303
|
+
assert.equal(decision.action, "skip");
|
|
304
|
+
if (decision.action === "skip") {
|
|
305
|
+
assert.equal(decision.topId, "codex-pref");
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("semantic dedup connector-aware: same-connector dedup survives when a different-connector hit scores higher", async () => {
|
|
310
|
+
// Candidate from chatgpt; codex neighbor scores 0.98 (higher) but a chatgpt
|
|
311
|
+
// neighbor also clears threshold at 0.94 → skip against the chatgpt hit,
|
|
312
|
+
// not the codex one. This preserves same-connector dedup.
|
|
313
|
+
const decision = await decideSemanticDedup(
|
|
314
|
+
"tabs are preferred by the user for indentation",
|
|
315
|
+
makeLookup([
|
|
316
|
+
{ id: "codex-pref", score: 0.98, sourceConnector: "codex" },
|
|
317
|
+
{ id: "chatgpt-pref", score: 0.94, sourceConnector: "chatgpt" },
|
|
318
|
+
]),
|
|
319
|
+
{ ...DEFAULT_OPTS, sourceConnector: "chatgpt" },
|
|
320
|
+
);
|
|
321
|
+
assert.equal(decision.action, "skip");
|
|
322
|
+
if (decision.action === "skip") {
|
|
323
|
+
assert.equal(decision.topId, "chatgpt-pref");
|
|
324
|
+
assert.equal(decision.topScore, 0.94);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("semantic dedup connector-aware: keeps when candidate connector set but no same-connector neighbor clears threshold", async () => {
|
|
329
|
+
// Candidate from chatgpt; codex neighbor at 0.97 clears threshold, but the
|
|
330
|
+
// only chatgpt neighbor is at 0.80 (below) → keep (connector_mismatch).
|
|
331
|
+
const decision = await decideSemanticDedup(
|
|
332
|
+
"tabs are preferred by the user for indentation",
|
|
333
|
+
makeLookup([
|
|
334
|
+
{ id: "codex-pref", score: 0.97, sourceConnector: "codex" },
|
|
335
|
+
{ id: "chatgpt-unrelated", score: 0.8, sourceConnector: "chatgpt" },
|
|
336
|
+
]),
|
|
337
|
+
{ ...DEFAULT_OPTS, sourceConnector: "chatgpt" },
|
|
338
|
+
);
|
|
339
|
+
assert.equal(decision.action, "keep");
|
|
340
|
+
assert.equal(decision.reason, "connector_mismatch");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("semantic dedup connector-aware: empty/whitespace candidate connector treated as operator", async () => {
|
|
344
|
+
// A whitespace-only candidate connector is absent → operator behavior → skip.
|
|
345
|
+
const wsDecision = await decideSemanticDedup(
|
|
346
|
+
"tabs are preferred by the user for indentation",
|
|
347
|
+
makeLookup([{ id: "codex-pref", score: 0.96, sourceConnector: "codex" }]),
|
|
348
|
+
{ ...DEFAULT_OPTS, sourceConnector: " " },
|
|
349
|
+
);
|
|
350
|
+
assert.equal(wsDecision.action, "skip");
|
|
351
|
+
});
|
|
352
|
+
|
|
248
353
|
test("semantic dedup: candidates=0 short-circuits without calling lookup", async () => {
|
|
249
354
|
let called = 0;
|
|
250
355
|
const decision = await decideSemanticDedup(
|
package/src/dedup/semantic.ts
CHANGED
|
@@ -23,6 +23,13 @@ export interface SemanticDedupHit {
|
|
|
23
23
|
score: number;
|
|
24
24
|
/** Optional source path, purely informational. */
|
|
25
25
|
path?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Source connector of the existing neighbor memory's provenance, used by
|
|
28
|
+
* the connector-aware skip gate (PR #1852 review finding on 7e0eb1a0).
|
|
29
|
+
* Absent when the neighbor is operator/unattributed or its frontmatter
|
|
30
|
+
* could not be read.
|
|
31
|
+
*/
|
|
32
|
+
sourceConnector?: string;
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
/**
|
|
@@ -45,6 +52,15 @@ export interface SemanticDedupOptions {
|
|
|
45
52
|
threshold: number;
|
|
46
53
|
/** How many nearest neighbors to compare against. */
|
|
47
54
|
candidates: number;
|
|
55
|
+
/**
|
|
56
|
+
* Source connector of the CANDIDATE fact being evaluated (its provenance).
|
|
57
|
+
* When present, a near-duplicate skip is permitted only against a neighbor
|
|
58
|
+
* carrying the SAME connector; a different or absent neighbor connector
|
|
59
|
+
* must NOT suppress the write (PR #1852 review finding on 7e0eb1a0).
|
|
60
|
+
* Absent for operator / unattributed writes, which preserve the original
|
|
61
|
+
* behavior (skip on any near-duplicate regardless of neighbor connector).
|
|
62
|
+
*/
|
|
63
|
+
sourceConnector?: string;
|
|
48
64
|
}
|
|
49
65
|
|
|
50
66
|
export type SemanticDedupDecision =
|
|
@@ -54,7 +70,8 @@ export type SemanticDedupDecision =
|
|
|
54
70
|
| "disabled"
|
|
55
71
|
| "backend_unavailable"
|
|
56
72
|
| "no_candidates"
|
|
57
|
-
| "no_near_duplicate"
|
|
73
|
+
| "no_near_duplicate"
|
|
74
|
+
| "connector_mismatch";
|
|
58
75
|
topScore?: number;
|
|
59
76
|
topId?: string;
|
|
60
77
|
}
|
|
@@ -135,8 +152,21 @@ export async function decideSemanticDedup(
|
|
|
135
152
|
return { action: "keep", reason: "no_candidates" };
|
|
136
153
|
}
|
|
137
154
|
|
|
155
|
+
// Resolve the candidate's provenance connector. An empty/whitespace
|
|
156
|
+
// value is treated as absent (operator / unattributed).
|
|
157
|
+
const candidateConnector =
|
|
158
|
+
typeof options.sourceConnector === "string" &&
|
|
159
|
+
options.sourceConnector.trim().length > 0
|
|
160
|
+
? options.sourceConnector.trim()
|
|
161
|
+
: undefined;
|
|
162
|
+
|
|
138
163
|
// Defensive: callers ought to return sorted, but don't trust it.
|
|
139
164
|
let top: SemanticDedupHit | undefined;
|
|
165
|
+
// Best same-connector neighbor — only tracked when the candidate is
|
|
166
|
+
// provenance-bearing. Tracked in the same pass so a connector-aware skip
|
|
167
|
+
// matches the best neighbor from the SAME connector even when a higher-
|
|
168
|
+
// scoring neighbor from a DIFFERENT connector would otherwise win.
|
|
169
|
+
let sameConnectorTop: SemanticDedupHit | undefined;
|
|
140
170
|
for (const hit of hits) {
|
|
141
171
|
if (
|
|
142
172
|
!hit ||
|
|
@@ -150,12 +180,50 @@ export async function decideSemanticDedup(
|
|
|
150
180
|
if (!top || hit.score > top.score) {
|
|
151
181
|
top = hit;
|
|
152
182
|
}
|
|
183
|
+
if (
|
|
184
|
+
candidateConnector !== undefined &&
|
|
185
|
+
typeof hit.sourceConnector === "string" &&
|
|
186
|
+
hit.sourceConnector.trim() === candidateConnector &&
|
|
187
|
+
(!sameConnectorTop || hit.score > sameConnectorTop.score)
|
|
188
|
+
) {
|
|
189
|
+
sameConnectorTop = hit;
|
|
190
|
+
}
|
|
153
191
|
}
|
|
154
192
|
if (!top) {
|
|
155
193
|
return { action: "keep", reason: "no_near_duplicate" };
|
|
156
194
|
}
|
|
157
195
|
|
|
158
196
|
if (top.score >= threshold) {
|
|
197
|
+
// Connector-aware skip gate (PR #1852 review finding on 7e0eb1a0):
|
|
198
|
+
// A provenance-bearing candidate may be suppressed ONLY by a near-
|
|
199
|
+
// duplicate from the SAME connector. A different or absent neighbor
|
|
200
|
+
// connector must NOT suppress the write — otherwise a connector B
|
|
201
|
+
// paraphrase would be dropped against connector A's memory before the
|
|
202
|
+
// write ever happens. When the candidate carries no connector (operator
|
|
203
|
+
// / unattributed), preserve the original behavior and skip on any match.
|
|
204
|
+
if (candidateConnector !== undefined) {
|
|
205
|
+
const match =
|
|
206
|
+
sameConnectorTop !== undefined && sameConnectorTop.score >= threshold
|
|
207
|
+
? sameConnectorTop
|
|
208
|
+
: undefined;
|
|
209
|
+
if (match) {
|
|
210
|
+
return {
|
|
211
|
+
action: "skip",
|
|
212
|
+
reason: "near_duplicate",
|
|
213
|
+
topScore: match.score,
|
|
214
|
+
topId: match.id,
|
|
215
|
+
topPath: match.path,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
// Best overall hit cleared threshold but came from a different or
|
|
219
|
+
// absent connector; no same-connector near-duplicate exists. Keep.
|
|
220
|
+
return {
|
|
221
|
+
action: "keep",
|
|
222
|
+
reason: "connector_mismatch",
|
|
223
|
+
topScore: top.score,
|
|
224
|
+
topId: top.id,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
159
227
|
return {
|
|
160
228
|
action: "skip",
|
|
161
229
|
reason: "near_duplicate",
|
package/src/explicit-capture.ts
CHANGED
|
@@ -13,6 +13,7 @@ export type ExplicitCaptureInput = {
|
|
|
13
13
|
namespace?: string;
|
|
14
14
|
tags?: string[];
|
|
15
15
|
entityRef?: string;
|
|
16
|
+
sourceConnector?: string;
|
|
16
17
|
ttl?: string;
|
|
17
18
|
sourceReason?: string;
|
|
18
19
|
};
|
|
@@ -24,6 +25,7 @@ export type ValidExplicitCapture = {
|
|
|
24
25
|
namespace?: string;
|
|
25
26
|
tags: string[];
|
|
26
27
|
entityRef?: string;
|
|
28
|
+
sourceConnector?: string;
|
|
27
29
|
expiresAt?: string;
|
|
28
30
|
sourceReason?: string;
|
|
29
31
|
/**
|
|
@@ -371,8 +373,9 @@ export function validateExplicitCaptureInput(
|
|
|
371
373
|
namespace: asTrimmed(input.namespace),
|
|
372
374
|
tags: Array.from(new Set((input.tags ?? []).map((tag) => tag.trim()).filter(Boolean))),
|
|
373
375
|
entityRef: asTrimmed(input.entityRef),
|
|
374
|
-
expiresAt,
|
|
375
376
|
sourceReason: asTrimmed(input.sourceReason),
|
|
377
|
+
sourceConnector: input.sourceConnector,
|
|
378
|
+
expiresAt,
|
|
376
379
|
};
|
|
377
380
|
}
|
|
378
381
|
|
|
@@ -410,7 +413,14 @@ async function findDuplicateExplicitCapture(
|
|
|
410
413
|
const status = memory.frontmatter.status ?? "active";
|
|
411
414
|
if (status !== "active") return false;
|
|
412
415
|
if (memory.frontmatter.category !== candidate.category) return false;
|
|
413
|
-
|
|
416
|
+
if (normalizeCaptureContent(memory.content) !== normalizedCandidate) return false;
|
|
417
|
+
// Connector-aware dedup: same content from different connectors is NOT
|
|
418
|
+
// a duplicate. Operator (undefined) and connector-tagged memories are
|
|
419
|
+
// always distinct. Normalize empty strings to undefined.
|
|
420
|
+
const existingConnector = memory.frontmatter.sourceConnector?.trim() || undefined;
|
|
421
|
+
const newConnector = candidate.sourceConnector?.trim() || undefined;
|
|
422
|
+
if (existingConnector !== newConnector) return false;
|
|
423
|
+
return true;
|
|
414
424
|
});
|
|
415
425
|
return match?.frontmatter.id ?? null;
|
|
416
426
|
}
|
|
@@ -438,6 +448,7 @@ export async function persistExplicitCapture(
|
|
|
438
448
|
entityRef: candidate.entityRef,
|
|
439
449
|
expiresAt: candidate.expiresAt,
|
|
440
450
|
source: source === "inline" ? "explicit-inline" : "explicit",
|
|
451
|
+
...(candidate.sourceConnector ? { sourceConnector: candidate.sourceConnector } : {}),
|
|
441
452
|
});
|
|
442
453
|
// #1522: catalog touch handled at the storage chokepoint — the StorageManager's
|
|
443
454
|
// post-write hook records the namespace touch automatically.
|
|
@@ -492,6 +503,7 @@ async function findQueuedExplicitCaptureDuplicate(
|
|
|
492
503
|
orchestrator: Orchestrator,
|
|
493
504
|
namespace: string | undefined,
|
|
494
505
|
content: string,
|
|
506
|
+
sourceConnector?: string,
|
|
495
507
|
): Promise<string | null> {
|
|
496
508
|
const storage = await orchestrator.getStorage(namespace);
|
|
497
509
|
const existing = await storage.readAllMemories();
|
|
@@ -500,7 +512,11 @@ async function findQueuedExplicitCaptureDuplicate(
|
|
|
500
512
|
const status = memory.frontmatter.status ?? "active";
|
|
501
513
|
if (status !== "pending_review") return false;
|
|
502
514
|
if (!(memory.frontmatter.tags ?? []).includes("queued-review")) return false;
|
|
503
|
-
|
|
515
|
+
if (normalizeCaptureContent(memory.content) !== normalized) return false;
|
|
516
|
+
const existingConnector = memory.frontmatter.sourceConnector?.trim() || undefined;
|
|
517
|
+
const newConnector = sourceConnector?.trim() || undefined;
|
|
518
|
+
if (existingConnector !== newConnector) return false;
|
|
519
|
+
return true;
|
|
504
520
|
});
|
|
505
521
|
return match?.frontmatter.id ?? null;
|
|
506
522
|
}
|
|
@@ -520,7 +536,7 @@ export async function queueExplicitCaptureForReview(
|
|
|
520
536
|
? requestedNamespace
|
|
521
537
|
: resolveExplicitCaptureReviewNamespace(orchestrator, requestedNamespace);
|
|
522
538
|
const content = buildExplicitCaptureReviewContent(input, reason);
|
|
523
|
-
const duplicateOf = await findQueuedExplicitCaptureDuplicate(orchestrator, queueNamespace, content);
|
|
539
|
+
const duplicateOf = await findQueuedExplicitCaptureDuplicate(orchestrator, queueNamespace, content, input.sourceConnector);
|
|
524
540
|
if (duplicateOf) {
|
|
525
541
|
return { id: duplicateOf, duplicateOf };
|
|
526
542
|
}
|
|
@@ -536,6 +552,7 @@ export async function queueExplicitCaptureForReview(
|
|
|
536
552
|
tags: Array.from(new Set([...EXPLICIT_CAPTURE_REVIEW_TAGS, ...requestedTags])),
|
|
537
553
|
entityRef: sanitizeReviewMetadata(input.entityRef),
|
|
538
554
|
source: source === "inline" ? "explicit-inline-review" : "explicit-review",
|
|
555
|
+
...(input.sourceConnector ? { sourceConnector: input.sourceConnector } : {}),
|
|
539
556
|
});
|
|
540
557
|
try {
|
|
541
558
|
const created = await storage.getMemoryById(id);
|