@remnic/core 9.3.718 → 9.3.720
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-boundary.d.ts +10 -7
- package/dist/access-boundary.js +18 -17
- package/dist/access-cli.js +39 -37
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +11 -8
- package/dist/access-http.js +25 -24
- package/dist/access-mcp.d.ts +11 -8
- package/dist/access-mcp.js +21 -20
- package/dist/access-operations-batch.js +19 -18
- package/dist/access-operations.d.ts +11 -8
- package/dist/access-operations.js +20 -19
- package/dist/access-schema.d.ts +19 -19
- package/dist/{access-service-CZbnt0t2.d.ts → access-service-Dw-tK1pa.d.ts} +3 -3
- package/dist/access-service.d.ts +11 -8
- package/dist/access-service.js +17 -16
- package/dist/access-surface-catalog.d.ts +10 -7
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +1 -1
- package/dist/adapters/index.js +7 -7
- package/dist/adapters/registry.js +3 -3
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +11 -8
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +4 -4
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +1 -1
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +30 -2
- package/dist/capabilities.js +3 -1
- package/dist/{catalog-CEYlMs9X.d.ts → catalog-B9SPfcsE.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-behavior.js +2 -2
- package/dist/causal-chain.js +2 -2
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +7 -7
- package/dist/causal-retrieval.js +2 -2
- package/dist/causal-trajectory-graph.d.ts +4 -0
- package/dist/causal-trajectory-graph.js +1 -1
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-7EEVXW7X.js → chunk-2RBMGTK4.js} +2 -2
- package/dist/{chunk-L5ASZA2F.js → chunk-3XEGHEQ2.js} +17 -2
- package/dist/chunk-3XEGHEQ2.js.map +1 -0
- package/dist/{chunk-YYOK4GVF.js → chunk-422TIKIH.js} +3 -3
- package/dist/{chunk-KRGC4EDX.js → chunk-5YQN2XYC.js} +2 -2
- package/dist/{chunk-ANI5DUAE.js → chunk-6S5ZI7HB.js} +7 -7
- package/dist/{chunk-JIZGYQYH.js → chunk-ATGRQ2NE.js} +4 -4
- package/dist/chunk-BCDKBDSY.js +173 -0
- package/dist/chunk-BCDKBDSY.js.map +1 -0
- package/dist/{chunk-P5IPFZVH.js → chunk-BGQ7WKMT.js} +5 -5
- package/dist/{chunk-JAKYZGV4.js → chunk-CYJV5AYC.js} +3 -3
- package/dist/{chunk-APUJIMDM.js → chunk-DG3ORVCU.js} +2 -2
- package/dist/{chunk-PBH4JUAB.js → chunk-DVRHLN2O.js} +18 -3
- package/dist/chunk-DVRHLN2O.js.map +1 -0
- package/dist/{chunk-S35FX6MU.js → chunk-H2AGJ5SV.js} +2 -2
- package/dist/{chunk-A66YRSOS.js → chunk-HAPVZ4CO.js} +2 -2
- package/dist/{chunk-4KZGLDJJ.js → chunk-HHSO6VCQ.js} +2 -2
- package/dist/{chunk-2VHMVWPU.js → chunk-HTIL3EJK.js} +2 -2
- package/dist/{chunk-UCJ5CDTK.js → chunk-HU4EBIXO.js} +22 -14
- package/dist/chunk-HU4EBIXO.js.map +1 -0
- package/dist/{chunk-CTOQEZSN.js → chunk-IPJD7WJH.js} +2 -2
- package/dist/{chunk-DIFU33GU.js → chunk-JV7Q226R.js} +2 -2
- package/dist/{chunk-U5H7WHTS.js → chunk-KNN6DAZQ.js} +4 -4
- package/dist/{chunk-3JJWNZTT.js → chunk-L2B3EE7B.js} +102 -42
- package/dist/chunk-L2B3EE7B.js.map +1 -0
- package/dist/{chunk-5RHM43BJ.js → chunk-LLLG5PTE.js} +3 -3
- package/dist/{chunk-F3PVXKHP.js → chunk-M3GBOKUX.js} +22 -16
- package/dist/chunk-M3GBOKUX.js.map +1 -0
- package/dist/{chunk-2PTBLS2S.js → chunk-NBYXOEO7.js} +2 -2
- package/dist/{chunk-URM3HW2R.js → chunk-NE5WFIY6.js} +2 -2
- package/dist/{chunk-REWQAFSA.js → chunk-NERBFQNY.js} +2 -2
- package/dist/{chunk-773DPI6T.js → chunk-NO4DWSQE.js} +10 -10
- package/dist/{chunk-OZ2KQ7HF.js → chunk-O6SEGJNV.js} +2 -2
- package/dist/{chunk-IF362TF6.js → chunk-PMBWVQ7D.js} +2 -2
- package/dist/chunk-PMBWVQ7D.js.map +1 -0
- package/dist/{chunk-OJU73BUX.js → chunk-PPVCZPYA.js} +2 -2
- package/dist/{chunk-S3OPGGZB.js → chunk-RIW3UDDY.js} +2 -2
- package/dist/{chunk-AU7Q3LSC.js → chunk-RKNJBZ55.js} +7 -7
- package/dist/{chunk-EH27LHML.js → chunk-RX7EBFFJ.js} +2 -2
- package/dist/{chunk-2COL35PK.js → chunk-T3XSXAWK.js} +2 -2
- package/dist/{chunk-2KAYTPPT.js → chunk-UUMYB3FR.js} +4 -4
- package/dist/{chunk-72VDBALG.js → chunk-V5EGLYXE.js} +52 -43
- package/dist/chunk-V5EGLYXE.js.map +1 -0
- package/dist/{chunk-LXSG4L55.js → chunk-V7UUIIR7.js} +2 -2
- package/dist/{chunk-HLIR3RJ7.js → chunk-WBHQ5LP5.js} +2 -2
- package/dist/{chunk-YGKUAX2B.js → chunk-XROP4ZHB.js} +8 -8
- package/dist/chunk-XROP4ZHB.js.map +1 -0
- package/dist/{chunk-NFRBADNR.js → chunk-Y4I7SOJN.js} +559 -318
- package/dist/chunk-Y4I7SOJN.js.map +1 -0
- package/dist/{chunk-YTZLBBEB.js → chunk-YMPEGBLL.js} +3 -3
- package/dist/chunk-YN2UN7N3.js +217 -0
- package/dist/chunk-YN2UN7N3.js.map +1 -0
- package/dist/{chunk-YI7RZO7Y.js → chunk-ZE7GJ76H.js} +1 -1
- package/dist/{chunk-74MYJSKB.js → chunk-ZG2WP425.js} +2 -2
- package/dist/{chunk-A5TUPZ3N.js → chunk-ZKUW2MJG.js} +43 -1
- package/dist/chunk-ZKUW2MJG.js.map +1 -0
- package/dist/{chunk-N3DUH6OO.js → chunk-ZW5I3UNP.js} +1 -1
- package/dist/chunk-ZW5I3UNP.js.map +1 -0
- package/dist/{cli-1orcdXOY.d.ts → cli-qcLCJlOD.d.ts} +3 -3
- package/dist/cli.d.ts +9 -6
- package/dist/cli.js +43 -42
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +4 -4
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +4 -4
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +4 -4
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/contradiction/index.d.ts +1 -1
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +4 -4
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +11 -8
- package/dist/extraction-faithfulness.d.ts +1 -1
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/{forget-U66KPDPY.js → forget-OEE5OELK.js} +2 -2
- package/dist/graph-dashboard-diff.d.ts +4 -0
- package/dist/graph-dashboard-key.d.ts +4 -0
- package/dist/graph-dashboard-parser.d.ts +4 -0
- package/dist/{graph-edge-decay-JLOF3YW4.js → graph-edge-decay-QDDC6AVV.js} +2 -2
- package/dist/graph-edge-reinforcement.d.ts +4 -0
- package/dist/graph-snapshot.d.ts +7 -3
- package/dist/graph-snapshot.js +2 -2
- package/dist/graph.d.ts +7 -1
- package/dist/graph.js +1 -1
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +571 -570
- package/dist/index.js +57 -55
- package/dist/index.js.map +1 -1
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/engine.js +4 -4
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/index.js +8 -8
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +4 -4
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -4
- package/dist/maintenance/rebuild-memory-projection.js +5 -5
- package/dist/mcp-memory-inspector-app.d.ts +11 -8
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-worth-outcomes.d.ts +1 -1
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +2 -2
- package/dist/namespaces/migrate.js +9 -9
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +4 -4
- package/dist/namespaces/storage.d.ts +2 -2
- package/dist/namespaces/storage.js +4 -4
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +3 -2
- package/dist/operator-toolkit.js +16 -15
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +6 -6
- package/dist/{orchestrator-kOxredSr.d.ts → orchestrator-Bg5Fnza6.d.ts} +178 -81
- package/dist/orchestrator.d.ts +8 -5
- package/dist/orchestrator.js +31 -29
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +18 -1
- package/dist/recall-xray.js +2 -2
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +2 -2
- package/dist/retrieval-agents.d.ts +1 -1
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +3 -3
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +7 -7
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-B58v3S-a.d.ts → semantic-consolidation-ClOiS6KP.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +5 -5
- package/dist/semantic-rule-promotion.js +4 -4
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +4 -4
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/shared-context/manager.d.ts +1 -1
- package/dist/signal.d.ts +1 -1
- package/dist/storage.d.ts +45 -1
- package/dist/storage.js +3 -3
- package/dist/summarizer.d.ts +1 -1
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/trust-score-stage.d.ts +178 -0
- package/dist/trust-score-stage.js +18 -0
- package/dist/trust-score-stage.js.map +1 -0
- package/dist/trust-score.d.ts +164 -0
- package/dist/trust-score.js +22 -0
- package/dist/trust-score.js.map +1 -0
- package/dist/{types-ACmDOScJ.d.ts → types-BSdUWePh.d.ts} +37 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +4 -4
- package/package.json +2 -2
- package/src/access-service.ts +8 -5
- package/src/capabilities-cluster-b.test.ts +78 -0
- package/src/capabilities.test.ts +11 -1
- package/src/capabilities.ts +58 -0
- package/src/cli/research-status-commands.ts +7 -3
- package/src/cli.ts +11 -6
- package/src/config.ts +51 -0
- package/src/graph-snapshot.ts +3 -3
- package/src/graph.ts +8 -7
- package/src/lifecycle/tombstones.test.ts +242 -1
- package/src/lifecycle/tombstones.ts +157 -70
- package/src/operator-toolkit.ts +24 -7
- package/src/orchestration/tier-migration-coordinator.ts +286 -0
- package/src/orchestrator.ts +331 -222
- package/src/recall-xray.test.ts +62 -0
- package/src/recall-xray.ts +34 -0
- package/src/trust-score-recall-paths.test.ts +157 -0
- package/src/trust-score-stage.test.ts +350 -0
- package/src/trust-score-stage.ts +398 -0
- package/src/trust-score.test.ts +282 -0
- package/src/trust-score.ts +449 -0
- package/src/types.ts +42 -0
- package/dist/chunk-3JJWNZTT.js.map +0 -1
- package/dist/chunk-72VDBALG.js.map +0 -1
- package/dist/chunk-A5TUPZ3N.js.map +0 -1
- package/dist/chunk-F3PVXKHP.js.map +0 -1
- package/dist/chunk-IF362TF6.js.map +0 -1
- package/dist/chunk-L5ASZA2F.js.map +0 -1
- package/dist/chunk-N3DUH6OO.js.map +0 -1
- package/dist/chunk-NFRBADNR.js.map +0 -1
- package/dist/chunk-PBH4JUAB.js.map +0 -1
- package/dist/chunk-UCJ5CDTK.js.map +0 -1
- package/dist/chunk-YGKUAX2B.js.map +0 -1
- /package/dist/{chunk-7EEVXW7X.js.map → chunk-2RBMGTK4.js.map} +0 -0
- /package/dist/{chunk-YYOK4GVF.js.map → chunk-422TIKIH.js.map} +0 -0
- /package/dist/{chunk-KRGC4EDX.js.map → chunk-5YQN2XYC.js.map} +0 -0
- /package/dist/{chunk-ANI5DUAE.js.map → chunk-6S5ZI7HB.js.map} +0 -0
- /package/dist/{chunk-JIZGYQYH.js.map → chunk-ATGRQ2NE.js.map} +0 -0
- /package/dist/{chunk-P5IPFZVH.js.map → chunk-BGQ7WKMT.js.map} +0 -0
- /package/dist/{chunk-JAKYZGV4.js.map → chunk-CYJV5AYC.js.map} +0 -0
- /package/dist/{chunk-APUJIMDM.js.map → chunk-DG3ORVCU.js.map} +0 -0
- /package/dist/{chunk-S35FX6MU.js.map → chunk-H2AGJ5SV.js.map} +0 -0
- /package/dist/{chunk-A66YRSOS.js.map → chunk-HAPVZ4CO.js.map} +0 -0
- /package/dist/{chunk-4KZGLDJJ.js.map → chunk-HHSO6VCQ.js.map} +0 -0
- /package/dist/{chunk-2VHMVWPU.js.map → chunk-HTIL3EJK.js.map} +0 -0
- /package/dist/{chunk-CTOQEZSN.js.map → chunk-IPJD7WJH.js.map} +0 -0
- /package/dist/{chunk-DIFU33GU.js.map → chunk-JV7Q226R.js.map} +0 -0
- /package/dist/{chunk-U5H7WHTS.js.map → chunk-KNN6DAZQ.js.map} +0 -0
- /package/dist/{chunk-5RHM43BJ.js.map → chunk-LLLG5PTE.js.map} +0 -0
- /package/dist/{chunk-2PTBLS2S.js.map → chunk-NBYXOEO7.js.map} +0 -0
- /package/dist/{chunk-URM3HW2R.js.map → chunk-NE5WFIY6.js.map} +0 -0
- /package/dist/{chunk-REWQAFSA.js.map → chunk-NERBFQNY.js.map} +0 -0
- /package/dist/{chunk-773DPI6T.js.map → chunk-NO4DWSQE.js.map} +0 -0
- /package/dist/{chunk-OZ2KQ7HF.js.map → chunk-O6SEGJNV.js.map} +0 -0
- /package/dist/{chunk-OJU73BUX.js.map → chunk-PPVCZPYA.js.map} +0 -0
- /package/dist/{chunk-S3OPGGZB.js.map → chunk-RIW3UDDY.js.map} +0 -0
- /package/dist/{chunk-AU7Q3LSC.js.map → chunk-RKNJBZ55.js.map} +0 -0
- /package/dist/{chunk-EH27LHML.js.map → chunk-RX7EBFFJ.js.map} +0 -0
- /package/dist/{chunk-2COL35PK.js.map → chunk-T3XSXAWK.js.map} +0 -0
- /package/dist/{chunk-2KAYTPPT.js.map → chunk-UUMYB3FR.js.map} +0 -0
- /package/dist/{chunk-LXSG4L55.js.map → chunk-V7UUIIR7.js.map} +0 -0
- /package/dist/{chunk-HLIR3RJ7.js.map → chunk-WBHQ5LP5.js.map} +0 -0
- /package/dist/{chunk-YTZLBBEB.js.map → chunk-YMPEGBLL.js.map} +0 -0
- /package/dist/{chunk-YI7RZO7Y.js.map → chunk-ZE7GJ76H.js.map} +0 -0
- /package/dist/{chunk-74MYJSKB.js.map → chunk-ZG2WP425.js.map} +0 -0
- /package/dist/{forget-U66KPDPY.js.map → forget-OEE5OELK.js.map} +0 -0
- /package/dist/{graph-edge-decay-JLOF3YW4.js.map → graph-edge-decay-QDDC6AVV.js.map} +0 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { parseConfig } from "./config.js";
|
|
5
|
+
import {
|
|
6
|
+
resolveAccessSetupCapabilities,
|
|
7
|
+
type AccessSetupCapabilitySet,
|
|
8
|
+
} from "./capabilities.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parity tests for the access-setup capability set (issue #1566 Cluster B).
|
|
12
|
+
*
|
|
13
|
+
* Every access-setup capability field must project from its `<field>Enabled`
|
|
14
|
+
* config flag, and the default-off semantics for both flags must be preserved.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const ACCESS_FIELD_TO_FLAG: Record<keyof AccessSetupCapabilitySet, string> = {
|
|
18
|
+
recallCrossNamespaceBudget: "recallCrossNamespaceBudgetEnabled",
|
|
19
|
+
recallAuditAnomalyDetection: "recallAuditAnomalyDetectionEnabled",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const ACCESS_FIELDS = Object.keys(ACCESS_FIELD_TO_FLAG) as Array<
|
|
23
|
+
keyof AccessSetupCapabilitySet
|
|
24
|
+
>;
|
|
25
|
+
|
|
26
|
+
test("resolveAccessSetupCapabilities projects every field from its flag (true variant)", () => {
|
|
27
|
+
const overrides: Record<string, boolean> = {};
|
|
28
|
+
for (const flag of Object.values(ACCESS_FIELD_TO_FLAG)) overrides[flag] = true;
|
|
29
|
+
const config = parseConfig(overrides);
|
|
30
|
+
const accessCaps = resolveAccessSetupCapabilities(config);
|
|
31
|
+
|
|
32
|
+
for (const field of ACCESS_FIELDS) {
|
|
33
|
+
const flag = ACCESS_FIELD_TO_FLAG[field];
|
|
34
|
+
assert.equal(
|
|
35
|
+
accessCaps[field],
|
|
36
|
+
(config as unknown as Record<string, boolean>)[flag],
|
|
37
|
+
`accessCaps.${field} must equal config.${flag} (true variant)`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("resolveAccessSetupCapabilities projects every field from its flag (false variant)", () => {
|
|
43
|
+
const overrides: Record<string, boolean> = {};
|
|
44
|
+
for (const flag of Object.values(ACCESS_FIELD_TO_FLAG)) overrides[flag] = false;
|
|
45
|
+
const config = parseConfig(overrides);
|
|
46
|
+
const accessCaps = resolveAccessSetupCapabilities(config);
|
|
47
|
+
|
|
48
|
+
for (const field of ACCESS_FIELDS) {
|
|
49
|
+
assert.equal(
|
|
50
|
+
accessCaps[field],
|
|
51
|
+
false,
|
|
52
|
+
`accessCaps.${field} must be false when its flag is false`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("resolveAccessSetupCapabilities preserves default-off semantics when flags are undefined", () => {
|
|
58
|
+
// parseConfig with no overrides exercises the documented defaults. Both
|
|
59
|
+
// access-setup flags default to false.
|
|
60
|
+
const config = parseConfig({});
|
|
61
|
+
const accessCaps = resolveAccessSetupCapabilities(config);
|
|
62
|
+
|
|
63
|
+
assert.equal(
|
|
64
|
+
accessCaps.recallCrossNamespaceBudget,
|
|
65
|
+
false,
|
|
66
|
+
"recallCrossNamespaceBudget must default to false",
|
|
67
|
+
);
|
|
68
|
+
assert.equal(
|
|
69
|
+
accessCaps.recallAuditAnomalyDetection,
|
|
70
|
+
false,
|
|
71
|
+
"recallAuditAnomalyDetection must default to false",
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("resolveAccessSetupCapabilities returns a frozen object", () => {
|
|
76
|
+
const accessCaps = resolveAccessSetupCapabilities(parseConfig({}));
|
|
77
|
+
assert.equal(Object.isFrozen(accessCaps), true, "AccessSetupCapabilitySet must be frozen");
|
|
78
|
+
});
|
package/src/capabilities.test.ts
CHANGED
|
@@ -2,7 +2,12 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
|
|
4
4
|
import { parseConfig } from "./config.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
resolveCapabilities,
|
|
7
|
+
resolveAccessSetupCapabilities,
|
|
8
|
+
type CapabilitySet,
|
|
9
|
+
type AccessSetupCapabilitySet,
|
|
10
|
+
} from "./capabilities.js";
|
|
6
11
|
|
|
7
12
|
/**
|
|
8
13
|
* Characterization tests for the recall-operation CapabilitySet (issue #1523).
|
|
@@ -22,6 +27,7 @@ const FIELD_TO_FLAG: Record<keyof CapabilitySet, string> = {
|
|
|
22
27
|
rerankCache: "rerankCacheEnabled",
|
|
23
28
|
recallDirectAnswer: "recallDirectAnswerEnabled",
|
|
24
29
|
recallMemoryWorthFilter: "recallMemoryWorthFilterEnabled",
|
|
30
|
+
recallTrustScore: "trustScoreEnabled",
|
|
25
31
|
recallMmr: "recallMmrEnabled",
|
|
26
32
|
recallReasoningTraceBoost: "recallReasoningTraceBoostEnabled",
|
|
27
33
|
recallPlannerLlm: "recallPlannerLlmEnabled",
|
|
@@ -30,6 +36,10 @@ const FIELD_TO_FLAG: Record<keyof CapabilitySet, string> = {
|
|
|
30
36
|
graphRecall: "graphRecallEnabled",
|
|
31
37
|
graphAssistInFullMode: "graphAssistInFullModeEnabled",
|
|
32
38
|
graphExpandedIntent: "graphExpandedIntentEnabled",
|
|
39
|
+
// Issue #1566 Cluster C: mixed-operation flags.
|
|
40
|
+
rerank: "rerankEnabled",
|
|
41
|
+
harmonicRetrieval: "harmonicRetrievalEnabled",
|
|
42
|
+
parallelRetrieval: "parallelRetrievalEnabled",
|
|
33
43
|
};
|
|
34
44
|
|
|
35
45
|
const FIELDS = Object.keys(FIELD_TO_FLAG) as Array<keyof CapabilitySet>;
|
package/src/capabilities.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface CapabilitySet {
|
|
|
38
38
|
readonly recallDirectAnswer: boolean;
|
|
39
39
|
/** `recallMemoryWorthFilterEnabled` — Memory-Worth score reweighting. */
|
|
40
40
|
readonly recallMemoryWorthFilter: boolean;
|
|
41
|
+
/** `trustScoreEnabled` — unified TrustScore recall stage (issue #1577). */
|
|
42
|
+
readonly recallTrustScore: boolean;
|
|
41
43
|
/** `recallMmrEnabled` — maximal-marginal-relevance diversification. */
|
|
42
44
|
readonly recallMmr: boolean;
|
|
43
45
|
/** `recallReasoningTraceBoostEnabled` — boost reasoning-trace memories. */
|
|
@@ -54,6 +56,13 @@ export interface CapabilitySet {
|
|
|
54
56
|
readonly graphAssistInFullMode: boolean;
|
|
55
57
|
/** `graphExpandedIntentEnabled` — promote broad-intent asks to graph mode. */
|
|
56
58
|
readonly graphExpandedIntent: boolean;
|
|
59
|
+
// --- Issue #1566 Cluster C: mixed-operation flags (recall + summarizer/CLI/writes) ---
|
|
60
|
+
/** `rerankEnabled` — optional LLM reranking of recall candidates. */
|
|
61
|
+
readonly rerank: boolean;
|
|
62
|
+
/** `harmonicRetrievalEnabled` — abstraction-node harmonic retrieval tier. */
|
|
63
|
+
readonly harmonicRetrieval: boolean;
|
|
64
|
+
/** `parallelRetrievalEnabled` — three-agent parallel retrieval (DirectFact + Contextual + Temporal). */
|
|
65
|
+
readonly parallelRetrieval: boolean;
|
|
57
66
|
}
|
|
58
67
|
|
|
59
68
|
/**
|
|
@@ -72,6 +81,9 @@ export function resolveCapabilities(config: PluginConfig): CapabilitySet {
|
|
|
72
81
|
rerankCache: config.rerankCacheEnabled,
|
|
73
82
|
recallDirectAnswer: config.recallDirectAnswerEnabled,
|
|
74
83
|
recallMemoryWorthFilter: config.recallMemoryWorthFilterEnabled,
|
|
84
|
+
// Issue #1577: TrustScore subsumes the Memory Worth multiplier when on;
|
|
85
|
+
// the orchestrator runs exactly one of the two (mutual exclusion, rule 39).
|
|
86
|
+
recallTrustScore: config.trustScoreEnabled === true,
|
|
75
87
|
recallMmr: config.recallMmrEnabled,
|
|
76
88
|
recallReasoningTraceBoost: config.recallReasoningTraceBoostEnabled,
|
|
77
89
|
recallPlannerLlm: config.recallPlannerLlmEnabled,
|
|
@@ -82,6 +94,10 @@ export function resolveCapabilities(config: PluginConfig): CapabilitySet {
|
|
|
82
94
|
// migrated call sites used (`!== false` = default-on, `=== true` = default-off).
|
|
83
95
|
graphAssistInFullMode: config.graphAssistInFullModeEnabled !== false,
|
|
84
96
|
graphExpandedIntent: config.graphExpandedIntentEnabled === true,
|
|
97
|
+
// Issue #1566 Cluster C: mixed-operation flags resolved once per recall op.
|
|
98
|
+
rerank: config.rerankEnabled,
|
|
99
|
+
harmonicRetrieval: config.harmonicRetrievalEnabled,
|
|
100
|
+
parallelRetrieval: config.parallelRetrievalEnabled,
|
|
85
101
|
});
|
|
86
102
|
}
|
|
87
103
|
|
|
@@ -145,3 +161,45 @@ export function resolveGraphConstructionCapabilities(
|
|
|
145
161
|
graphWriteSessionAdjacency: config.graphWriteSessionAdjacencyEnabled !== false,
|
|
146
162
|
});
|
|
147
163
|
}
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Access-setup capability set (issue #1566 Cluster B).
|
|
167
|
+
//
|
|
168
|
+
// These two flags gate cross-namespace recall budgeting and anomaly-detection
|
|
169
|
+
// auditing. Resolved once at access-service handler entry (and the operator
|
|
170
|
+
// doctor check) rather than re-derived from raw config at each site.
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Frozen projection of access-setup feature gates (issue #1566 Cluster B).
|
|
175
|
+
*
|
|
176
|
+
* Every field is `readonly boolean`. Composition (including the default-off
|
|
177
|
+
* semantics for the optional `recallAuditAnomalyDetectionEnabled`) lives ONLY
|
|
178
|
+
* in {@link resolveAccessSetupCapabilities}.
|
|
179
|
+
*/
|
|
180
|
+
export interface AccessSetupCapabilitySet {
|
|
181
|
+
/** `recallCrossNamespaceBudgetEnabled` — rolling cross-namespace recall budget. */
|
|
182
|
+
readonly recallCrossNamespaceBudget: boolean;
|
|
183
|
+
/** `recallAuditAnomalyDetectionEnabled` — access-audit anomaly detection (default-off). */
|
|
184
|
+
readonly recallAuditAnomalyDetection: boolean;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Resolve the {@link AccessSetupCapabilitySet} from parsed config.
|
|
189
|
+
*
|
|
190
|
+
* Call this ONCE at access-service handler entry and thread the result down.
|
|
191
|
+
*/
|
|
192
|
+
export type AccessSetupConfigProjection = Pick<
|
|
193
|
+
PluginConfig,
|
|
194
|
+
"recallCrossNamespaceBudgetEnabled" | "recallAuditAnomalyDetectionEnabled"
|
|
195
|
+
>;
|
|
196
|
+
|
|
197
|
+
export function resolveAccessSetupCapabilities(
|
|
198
|
+
config: AccessSetupConfigProjection,
|
|
199
|
+
): AccessSetupCapabilitySet {
|
|
200
|
+
return Object.freeze({
|
|
201
|
+
recallCrossNamespaceBudget: config.recallCrossNamespaceBudgetEnabled,
|
|
202
|
+
// Optional/default-off flag: preserve `=== true` semantics.
|
|
203
|
+
recallAuditAnomalyDetection: config.recallAuditAnomalyDetectionEnabled === true,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import type { Orchestrator } from "../orchestrator.js";
|
|
21
|
+
import { resolveCapabilities } from "../capabilities.js";
|
|
21
22
|
import type { CliCommand } from "../cli.js";
|
|
22
23
|
import type { UtilityTelemetryEvent } from "../utility-telemetry.js";
|
|
23
24
|
import {
|
|
@@ -42,6 +43,9 @@ export function registerResearchStatusCommands(
|
|
|
42
43
|
cmd: CliCommand,
|
|
43
44
|
orchestrator: Orchestrator,
|
|
44
45
|
): void {
|
|
46
|
+
// Resolve recall-operation gates once (#1566 Cluster C — harmonicRetrieval is a
|
|
47
|
+
// mixed-op flag read here on the CLI-search path as well as in recall).
|
|
48
|
+
const caps = resolveCapabilities(orchestrator.config);
|
|
45
49
|
cmd
|
|
46
50
|
.command("objective-state-status")
|
|
47
51
|
.description("Show objective-state store status, snapshot counts, and latest stored snapshot")
|
|
@@ -111,7 +115,7 @@ cmd
|
|
|
111
115
|
const status = await runAbstractionNodeStatusCliCommand({
|
|
112
116
|
memoryDir: orchestrator.config.memoryDir,
|
|
113
117
|
abstractionNodeStoreDir: orchestrator.config.abstractionNodeStoreDir,
|
|
114
|
-
harmonicRetrievalEnabled:
|
|
118
|
+
harmonicRetrievalEnabled: caps.harmonicRetrieval,
|
|
115
119
|
abstractionAnchorsEnabled: orchestrator.config.abstractionAnchorsEnabled,
|
|
116
120
|
});
|
|
117
121
|
console.log(JSON.stringify(status, null, 2));
|
|
@@ -125,7 +129,7 @@ cmd
|
|
|
125
129
|
const status = await runCueAnchorStatusCliCommand({
|
|
126
130
|
memoryDir: orchestrator.config.memoryDir,
|
|
127
131
|
abstractionNodeStoreDir: orchestrator.config.abstractionNodeStoreDir,
|
|
128
|
-
harmonicRetrievalEnabled:
|
|
132
|
+
harmonicRetrievalEnabled: caps.harmonicRetrieval,
|
|
129
133
|
abstractionAnchorsEnabled: orchestrator.config.abstractionAnchorsEnabled,
|
|
130
134
|
});
|
|
131
135
|
console.log(JSON.stringify(status, null, 2));
|
|
@@ -147,7 +151,7 @@ cmd
|
|
|
147
151
|
const results = await runHarmonicSearchCliCommand({
|
|
148
152
|
memoryDir: orchestrator.config.memoryDir,
|
|
149
153
|
abstractionNodeStoreDir: orchestrator.config.abstractionNodeStoreDir,
|
|
150
|
-
harmonicRetrievalEnabled:
|
|
154
|
+
harmonicRetrievalEnabled: caps.harmonicRetrieval,
|
|
151
155
|
abstractionAnchorsEnabled: orchestrator.config.abstractionAnchorsEnabled,
|
|
152
156
|
query,
|
|
153
157
|
maxResults: Number.isFinite(maxResults) ? maxResults : 3,
|
package/src/cli.ts
CHANGED
|
@@ -125,6 +125,7 @@ import {
|
|
|
125
125
|
validateEvalBenchmarkPack,
|
|
126
126
|
} from "./evals.js";
|
|
127
127
|
import { analyzeGraphHealth, type GraphHealthReport } from "./graph.js";
|
|
128
|
+
import { resolveGraphConstructionCapabilities } from "./capabilities.js";
|
|
128
129
|
import {
|
|
129
130
|
getCausalTrajectoryStoreStatus,
|
|
130
131
|
type CausalTrajectoryStoreStatus,
|
|
@@ -6360,11 +6361,12 @@ export function registerCli(
|
|
|
6360
6361
|
.option("--repair-guidance", "Include non-destructive repair guidance")
|
|
6361
6362
|
.action(async (...args: unknown[]) => {
|
|
6362
6363
|
const options = (args[0] ?? {}) as Record<string, unknown>;
|
|
6364
|
+
const graphCaps = resolveGraphConstructionCapabilities(orchestrator.config);
|
|
6363
6365
|
const report = await runGraphHealthCliCommand({
|
|
6364
6366
|
memoryDir: orchestrator.config.memoryDir,
|
|
6365
|
-
entityGraphEnabled:
|
|
6366
|
-
timeGraphEnabled:
|
|
6367
|
-
causalGraphEnabled:
|
|
6367
|
+
entityGraphEnabled: graphCaps.entityGraph,
|
|
6368
|
+
timeGraphEnabled: graphCaps.timeGraph,
|
|
6369
|
+
causalGraphEnabled: graphCaps.causalGraph,
|
|
6368
6370
|
includeRepairGuidance: options.repairGuidance === true,
|
|
6369
6371
|
});
|
|
6370
6372
|
console.log(JSON.stringify(report, null, 2));
|
|
@@ -6415,12 +6417,15 @@ export function registerCli(
|
|
|
6415
6417
|
.option("--json", "Emit machine-readable JSON only")
|
|
6416
6418
|
.action(async (...args: unknown[]) => {
|
|
6417
6419
|
const options = (args[0] ?? {}) as Record<string, unknown>;
|
|
6420
|
+
const repairGraphCaps = resolveGraphConstructionCapabilities(orchestrator.config);
|
|
6418
6421
|
const report = await runOperatorRepair({
|
|
6419
6422
|
config: {
|
|
6420
6423
|
memoryDir: orchestrator.config.memoryDir,
|
|
6421
|
-
entityGraphEnabled:
|
|
6422
|
-
timeGraphEnabled:
|
|
6423
|
-
causalGraphEnabled:
|
|
6424
|
+
entityGraphEnabled: repairGraphCaps.entityGraph,
|
|
6425
|
+
timeGraphEnabled: repairGraphCaps.timeGraph,
|
|
6426
|
+
causalGraphEnabled: repairGraphCaps.causalGraph,
|
|
6427
|
+
multiGraphMemoryEnabled: repairGraphCaps.multiGraphMemory,
|
|
6428
|
+
graphWriteSessionAdjacencyEnabled: repairGraphCaps.graphWriteSessionAdjacency,
|
|
6424
6429
|
},
|
|
6425
6430
|
apply: options.apply === true,
|
|
6426
6431
|
dryRun: options.dryRun === true,
|
package/src/config.ts
CHANGED
|
@@ -28,6 +28,7 @@ import type {
|
|
|
28
28
|
SlotBehaviorConfig,
|
|
29
29
|
SlotMismatchMode,
|
|
30
30
|
TriggerMode,
|
|
31
|
+
TrustWeights,
|
|
31
32
|
} from "./types.js";
|
|
32
33
|
import { log } from "./logger.js";
|
|
33
34
|
import { cloneDefaultSessionObserverBands } from "./session-observer-bands.js";
|
|
@@ -2336,6 +2337,27 @@ export function parseConfig(
|
|
|
2336
2337
|
const n = coerceNumber(cfg.recallMemoryWorthHalfLifeMs);
|
|
2337
2338
|
return n !== undefined && n >= 0 ? n : 0;
|
|
2338
2339
|
})(),
|
|
2340
|
+
// Unified TrustScore recall stage (issue #1577). Default off: the stage
|
|
2341
|
+
// is absent and ranking is byte-identical to the pre-feature pipeline
|
|
2342
|
+
// (rule 39). Flip the master gate via #1574 ablation evidence only.
|
|
2343
|
+
trustScoreEnabled: coerceBool(cfg.trustScoreEnabled) ?? false,
|
|
2344
|
+
// Epistemic rendering ships behind its own gate so scoring can land first.
|
|
2345
|
+
trustScoreEpistemicRendering: coerceBool(cfg.trustScoreEpistemicRendering) ?? false,
|
|
2346
|
+
// Quarantine defaults on (only effective under the master gate); keeps
|
|
2347
|
+
// hard negatives out of injection but visible in X-ray (rule 34).
|
|
2348
|
+
trustScoreQuarantine: coerceBool(cfg.trustScoreQuarantine) ?? true,
|
|
2349
|
+
trustScoreMinMultiplier: (() => {
|
|
2350
|
+
const n = coerceNumber(cfg.trustScoreMinMultiplier);
|
|
2351
|
+
return n !== undefined && n >= 0 && n <= 1 ? n : 0.5;
|
|
2352
|
+
})(),
|
|
2353
|
+
trustScoreMaxMultiplier: (() => {
|
|
2354
|
+
const n = coerceNumber(cfg.trustScoreMaxMultiplier);
|
|
2355
|
+
return n !== undefined && n >= 1 && n <= 4 ? n : 1.25;
|
|
2356
|
+
})(),
|
|
2357
|
+
// Per-component weights. Accept a nested object from config; invalid
|
|
2358
|
+
// entries (non-finite, outside [0,1]) are dropped by resolveTrustWeights,
|
|
2359
|
+
// which falls back to the documented defaults (rule 51).
|
|
2360
|
+
trustScoreWeights: parseTrustScoreWeights(cfg.trustScoreWeights),
|
|
2339
2361
|
// Memory Linking (Phase 3A)
|
|
2340
2362
|
memoryLinkingEnabled: cfg.memoryLinkingEnabled === true, // Off by default initially
|
|
2341
2363
|
// Conversation Threading (Phase 3B)
|
|
@@ -4639,3 +4661,32 @@ function buildRecallPipelineConfig(cfg: Record<string, unknown>): RecallPipeline
|
|
|
4639
4661
|
|
|
4640
4662
|
return { recallBudgetChars, pipeline };
|
|
4641
4663
|
}
|
|
4664
|
+
|
|
4665
|
+
/**
|
|
4666
|
+
* Parse the `trustScoreWeights` config value into a {@link TrustWeights}
|
|
4667
|
+
* object. Accepts a plain object of per-component numbers; non-finite or
|
|
4668
|
+
* out-of-`[0,1]` entries are dropped (the scorer falls back to defaults via
|
|
4669
|
+
* `resolveTrustWeights`). A non-object value yields `{}` → all defaults.
|
|
4670
|
+
* Rule 51: invalid weight rejected, never silently clamped to an extreme.
|
|
4671
|
+
*/
|
|
4672
|
+
function parseTrustScoreWeights(value: unknown): TrustWeights {
|
|
4673
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
4674
|
+
const src = value as Record<string, unknown>;
|
|
4675
|
+
const out: TrustWeights = {};
|
|
4676
|
+
const keys: Array<keyof TrustWeights> = [
|
|
4677
|
+
"memoryWorth",
|
|
4678
|
+
"provenance",
|
|
4679
|
+
"faithfulness",
|
|
4680
|
+
"corroboration",
|
|
4681
|
+
"contradiction",
|
|
4682
|
+
"domainCalibration",
|
|
4683
|
+
"feedback",
|
|
4684
|
+
"recency",
|
|
4685
|
+
];
|
|
4686
|
+
for (const key of keys) {
|
|
4687
|
+
const raw = src[key as string];
|
|
4688
|
+
if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 0 || raw > 1) continue;
|
|
4689
|
+
(out as Record<string, number>)[key as string] = raw;
|
|
4690
|
+
}
|
|
4691
|
+
return out;
|
|
4692
|
+
}
|
package/src/graph-snapshot.ts
CHANGED
|
@@ -132,9 +132,9 @@ export function parseGraphSnapshotSince(raw: string | undefined): number | undef
|
|
|
132
132
|
export async function buildGraphSnapshot(opts: {
|
|
133
133
|
memoryDir: string;
|
|
134
134
|
graphConfig: {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
entityGraph: boolean;
|
|
136
|
+
timeGraph: boolean;
|
|
137
|
+
causalGraph: boolean;
|
|
138
138
|
};
|
|
139
139
|
request: GraphSnapshotRequest;
|
|
140
140
|
loadNode: GraphSnapshotNodeLoader;
|
package/src/graph.ts
CHANGED
|
@@ -15,6 +15,7 @@ import * as path from "node:path";
|
|
|
15
15
|
|
|
16
16
|
import { readEdgeConfidence } from "./graph-edge-reinforcement.js";
|
|
17
17
|
import { emitGraphEvent } from "./graph-events.js";
|
|
18
|
+
import type { GraphConstructionCapabilitySet } from "./capabilities.js";
|
|
18
19
|
|
|
19
20
|
export type GraphType = "entity" | "time" | "causal";
|
|
20
21
|
|
|
@@ -198,12 +199,12 @@ export async function readEdgesStrict(memoryDir: string, type: GraphType): Promi
|
|
|
198
199
|
*/
|
|
199
200
|
export async function readAllEdges(
|
|
200
201
|
memoryDir: string,
|
|
201
|
-
|
|
202
|
+
graphCaps: Pick<GraphConstructionCapabilitySet, "entityGraph" | "timeGraph" | "causalGraph">,
|
|
202
203
|
): Promise<GraphEdge[]> {
|
|
203
204
|
const parts: GraphEdge[][] = await Promise.all([
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
205
|
+
graphCaps.entityGraph ? readEdges(memoryDir, "entity") : Promise.resolve([]),
|
|
206
|
+
graphCaps.timeGraph ? readEdges(memoryDir, "time") : Promise.resolve([]),
|
|
207
|
+
graphCaps.causalGraph ? readEdges(memoryDir, "causal") : Promise.resolve([]),
|
|
207
208
|
]);
|
|
208
209
|
return parts.flat();
|
|
209
210
|
}
|
|
@@ -396,9 +397,9 @@ export class GraphIndex {
|
|
|
396
397
|
return this.edgeCache.allEdges;
|
|
397
398
|
}
|
|
398
399
|
const allEdges = await readAllEdges(this.memoryDir, {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
400
|
+
entityGraph: this.cfg.entityGraphEnabled,
|
|
401
|
+
timeGraph: this.cfg.timeGraphEnabled,
|
|
402
|
+
causalGraph: this.cfg.causalGraphEnabled,
|
|
402
403
|
});
|
|
403
404
|
this.edgeCache = { allEdges, loadedAt: Date.now() };
|
|
404
405
|
return allEdges;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { describe, it } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { mkdtemp, readFile, writeFile, rm, mkdir, appendFile } from "node:fs/promises";
|
|
3
|
+
import { mkdtemp, readFile, writeFile, rm, mkdir, appendFile, utimes } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { statSync } from "node:fs";
|
|
5
7
|
import path from "node:path";
|
|
6
8
|
import { createHash } from "node:crypto";
|
|
7
9
|
import {
|
|
@@ -345,6 +347,245 @@ describe("TombstoneStore — enabled gate (rule 30)", () => {
|
|
|
345
347
|
});
|
|
346
348
|
});
|
|
347
349
|
|
|
350
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
351
|
+
// Cross-process tombstone write lock (issue #1639)
|
|
352
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* A read-merge-write file io that simulates the secure-store append path
|
|
356
|
+
* (read encrypted → decrypt → concat → re-encrypt → atomic rename). Unlike a
|
|
357
|
+
* raw O_APPEND, this is NOT atomic across processes: two writers that each
|
|
358
|
+
* read the same contents and write back drop the other line — the exact
|
|
359
|
+
* lost-write race the #1639 cross-process lock closes. The setImmediate yield
|
|
360
|
+
* widens the race window so a missing lock would demonstrably lose entries.
|
|
361
|
+
*/
|
|
362
|
+
function makeReadMergeWriteIo() {
|
|
363
|
+
return {
|
|
364
|
+
read: (p: string) => readFile(p, "utf8"),
|
|
365
|
+
append: async (p: string, c: string) => {
|
|
366
|
+
await mkdir(path.dirname(p), { recursive: true });
|
|
367
|
+
let existing = "";
|
|
368
|
+
try {
|
|
369
|
+
existing = await readFile(p, "utf8");
|
|
370
|
+
} catch (err) {
|
|
371
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
|
|
372
|
+
}
|
|
373
|
+
// Yield so two concurrent read-merge-write appends observably overlap
|
|
374
|
+
// when the cross-process lock is absent.
|
|
375
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
376
|
+
await writeFile(p, existing + c, "utf8");
|
|
377
|
+
},
|
|
378
|
+
write: async (p: string, c: string) => {
|
|
379
|
+
await mkdir(path.dirname(p), { recursive: true });
|
|
380
|
+
await writeFile(p, c, "utf8");
|
|
381
|
+
},
|
|
382
|
+
stat: (p: string) => statSync(p),
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
describe("TombstoneStore — cross-process write lock (issue #1639)", () => {
|
|
387
|
+
it("two processes appending concurrently produce no lost entries", async () => {
|
|
388
|
+
const dir = await mkdtemp(path.join(tmpdir(), "tomb-xproc-"));
|
|
389
|
+
const moduleUrl = new URL("./tombstones.ts", import.meta.url).href;
|
|
390
|
+
const entriesPerWorker = 15;
|
|
391
|
+
|
|
392
|
+
// Worker source: plain JS, no nested template literals. Each child imports
|
|
393
|
+
// the store, points it at the shared tombstones.jsonl with a read-merge-
|
|
394
|
+
// write io, and appends N entries. The cross-process lock serializes the
|
|
395
|
+
// appends across the two processes so no entry is dropped.
|
|
396
|
+
const workerSource = [
|
|
397
|
+
"(async () => {",
|
|
398
|
+
"const { TombstoneStore } = await import(process.argv[1]);",
|
|
399
|
+
"const { mkdir, readFile, writeFile } = await import(\"node:fs/promises\");",
|
|
400
|
+
"const { statSync } = await import(\"node:fs\");",
|
|
401
|
+
"const path = await import(\"node:path\");",
|
|
402
|
+
"const { createHash } = await import(\"node:crypto\");",
|
|
403
|
+
"const dir = process.argv[2];",
|
|
404
|
+
"const workerId = Number(process.argv[3]);",
|
|
405
|
+
"const count = Number(process.argv[4]);",
|
|
406
|
+
"const filePath = path.join(dir, \"tombstones.jsonl\");",
|
|
407
|
+
"function hash(c){return createHash(\"sha256\").update(c).digest(\"hex\");}",
|
|
408
|
+
"function normalize(c){return c;}",
|
|
409
|
+
"const io = {",
|
|
410
|
+
" read: (p) => readFile(p, \"utf8\"),",
|
|
411
|
+
" append: async (p, c) => {",
|
|
412
|
+
" await mkdir(path.dirname(p), { recursive: true });",
|
|
413
|
+
" let existing = \"\";",
|
|
414
|
+
" try { existing = await readFile(p, \"utf8\"); } catch (e) { if (e.code !== \"ENOENT\") throw e; }",
|
|
415
|
+
" await new Promise((r) => setImmediate(r));",
|
|
416
|
+
" await writeFile(p, existing + c, \"utf8\");",
|
|
417
|
+
" },",
|
|
418
|
+
" write: async (p, c) => { await mkdir(path.dirname(p), { recursive: true }); await writeFile(p, c, \"utf8\"); },",
|
|
419
|
+
" stat: (p) => statSync(p),",
|
|
420
|
+
"};",
|
|
421
|
+
"const store = new TombstoneStore(filePath, \"default\", { enabled: true, semanticMatch: false, semanticThreshold: 0.9, hashContent: hash, normalizeText: normalize }, io);",
|
|
422
|
+
"await store.load();",
|
|
423
|
+
"for (let i = 0; i < count; i += 1) {",
|
|
424
|
+
" await store.appendTombstone({ reason: \"correction\", createdBy: \"user_correction\", sourceMemoryId: \"w\" + workerId + \"-f\" + i, rawContent: \"worker \" + workerId + \" fact \" + i });",
|
|
425
|
+
"}",
|
|
426
|
+
"})();",
|
|
427
|
+
].join("\n");
|
|
428
|
+
|
|
429
|
+
function runWorker(workerId: number): Promise<void> {
|
|
430
|
+
return new Promise((resolve, reject) => {
|
|
431
|
+
const child = spawn(
|
|
432
|
+
process.execPath,
|
|
433
|
+
["--import", "tsx", "-e", workerSource, moduleUrl, dir, String(workerId), String(entriesPerWorker)],
|
|
434
|
+
{ stdio: ["ignore", "pipe", "pipe"] },
|
|
435
|
+
);
|
|
436
|
+
let stderr = "";
|
|
437
|
+
child.stderr.setEncoding("utf8");
|
|
438
|
+
child.stderr.on("data", (chunk) => { stderr += chunk; });
|
|
439
|
+
child.on("error", reject);
|
|
440
|
+
child.on("close", (code) => {
|
|
441
|
+
if (code === 0) resolve();
|
|
442
|
+
else reject(new Error(`worker ${workerId} exited ${code}: ${stderr}`));
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Two genuinely concurrent processes contending on the same lockfile.
|
|
448
|
+
await Promise.all([runWorker(0), runWorker(1)]);
|
|
449
|
+
|
|
450
|
+
const raw = await readFile(path.join(dir, "tombstones.jsonl"), "utf8");
|
|
451
|
+
const fileLines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
452
|
+
// No lost entries: every line parses and the total is exactly 2 × N.
|
|
453
|
+
assert.equal(fileLines.length, entriesPerWorker * 2);
|
|
454
|
+
for (const line of fileLines) {
|
|
455
|
+
assert.doesNotThrow(() => JSON.parse(line));
|
|
456
|
+
}
|
|
457
|
+
// Every worker entry is present (none dropped by a lost write).
|
|
458
|
+
const sourceIds = new Set(fileLines.map((l) => JSON.parse(l).sourceMemoryId));
|
|
459
|
+
for (let w = 0; w < 2; w += 1) {
|
|
460
|
+
for (let i = 0; i < entriesPerWorker; i += 1) {
|
|
461
|
+
assert.ok(sourceIds.has(`w${w}-f${i}`), `missing w${w}-f${i} — lost write`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
// The advisory lockfile is released after the run (no lingering lock).
|
|
465
|
+
await rm(path.join(dir, "tombstones.lock"), { force: true });
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it("recovers from a stale lock left by a crashed holder", async () => {
|
|
469
|
+
const dir = await mkdtemp(path.join(tmpdir(), "tomb-stale-lock-"));
|
|
470
|
+
const filePath = path.join(dir, "tombstones.jsonl");
|
|
471
|
+
const lockPath = path.join(dir, "tombstones.lock");
|
|
472
|
+
// Seed a lockfile written by a (now-crashed) holder: <pid> <uuid> <iso>.
|
|
473
|
+
await mkdir(dir, { recursive: true });
|
|
474
|
+
await writeFile(
|
|
475
|
+
lockPath,
|
|
476
|
+
`${process.pid} 00000000-0000-0000-0000-000000000000 2000-01-01T00:00:00.000Z\n`,
|
|
477
|
+
"utf8",
|
|
478
|
+
);
|
|
479
|
+
// Make it older than staleMs so the next acquire breaks it.
|
|
480
|
+
const old = new Date(Date.now() - 120_000);
|
|
481
|
+
await utimes(lockPath, old, old);
|
|
482
|
+
|
|
483
|
+
const store = new TombstoneStore(
|
|
484
|
+
filePath,
|
|
485
|
+
"default",
|
|
486
|
+
{ enabled: true, semanticMatch: false, semanticThreshold: 0.9, hashContent: computeHash, normalizeText: normalizeContent, lockStaleMs: 1_000 },
|
|
487
|
+
makeReadMergeWriteIo(),
|
|
488
|
+
);
|
|
489
|
+
// The append must break the stale lock, acquire a fresh one, and succeed.
|
|
490
|
+
const id = await store.appendTombstone({
|
|
491
|
+
reason: "correction",
|
|
492
|
+
createdBy: "user_correction",
|
|
493
|
+
sourceMemoryId: "fact-stale",
|
|
494
|
+
rawContent: "Recovered from a stale lock.",
|
|
495
|
+
});
|
|
496
|
+
assert.match(id, /^tomb-/);
|
|
497
|
+
// The tombstone landed durably.
|
|
498
|
+
const raw = await readFile(filePath, "utf8");
|
|
499
|
+
const lines2 = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
500
|
+
assert.equal(lines2.length, 1);
|
|
501
|
+
// Our fresh lock was released on completion (the stale one was broken).
|
|
502
|
+
await rm(lockPath, { force: true });
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it("rebuild preserves a peer's revocation appended before rebuild acquired the lock (cursor/codex #1639)", async () => {
|
|
506
|
+
// Regression for the rebuild race: rebuild must re-read the log under the
|
|
507
|
+
// lock (ensureFreshAgainstDisk) before computing its payload, otherwise a
|
|
508
|
+
// revocation a peer wrote while rebuild waited for the lock is dropped and
|
|
509
|
+
// the retired fact silently un-revokes (resurrection).
|
|
510
|
+
const dir = await mkdtemp(path.join(tmpdir(), "tomb-rebuild-race-"));
|
|
511
|
+
const filePath = path.join(dir, "tombstones.jsonl");
|
|
512
|
+
const opts = { enabled: true, semanticMatch: false, semanticThreshold: 0.9, hashContent: computeHash, normalizeText: normalizeContent };
|
|
513
|
+
const io = makeReadMergeWriteIo();
|
|
514
|
+
// Store A writes tombstone-1, then goes stale (never sees the peer write).
|
|
515
|
+
const storeA = new TombstoneStore(filePath, "default", opts, io);
|
|
516
|
+
const t1 = await storeA.appendTombstone({
|
|
517
|
+
reason: "correction",
|
|
518
|
+
createdBy: "user_correction",
|
|
519
|
+
sourceMemoryId: "fact-rebuild-1",
|
|
520
|
+
rawContent: "The DB is MySQL.",
|
|
521
|
+
});
|
|
522
|
+
// Peer (store B, same file) revokes t1 durably. Store A's in-memory index
|
|
523
|
+
// is now stale (it still thinks t1 is active).
|
|
524
|
+
const storeB = new TombstoneStore(filePath, "default", opts, io);
|
|
525
|
+
await storeB.revoke(t1, "user_correction");
|
|
526
|
+
// Store A rebuilds from a retired-memory corpus that reconstructs t1.
|
|
527
|
+
// Without the under-lock re-read, A computes existingRevocations from its
|
|
528
|
+
// stale index (empty), overwrites the file with [rebuilt-t1], and the
|
|
529
|
+
// revocation is LOST — t1 is active again (resurrection). With the fix,
|
|
530
|
+
// ensureFreshAgainstDisk reloads the revocation under the lock and rebuild
|
|
531
|
+
// preserves it.
|
|
532
|
+
await storeA.rebuild([
|
|
533
|
+
{
|
|
534
|
+
memoryId: "fact-rebuild-1",
|
|
535
|
+
rawContent: "The DB is MySQL.",
|
|
536
|
+
reason: "correction" as const,
|
|
537
|
+
createdBy: "user_correction" as const,
|
|
538
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
539
|
+
},
|
|
540
|
+
]);
|
|
541
|
+
// The rebuilt file must still contain the revocation (the retired fact
|
|
542
|
+
// stays revoked, not silently un-revoked by the rebuild).
|
|
543
|
+
const raw = await readFile(filePath, "utf8");
|
|
544
|
+
const rebuiltLines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
545
|
+
const hasRevocation = rebuiltLines.some((l) => {
|
|
546
|
+
try { return JSON.parse(l).kind === "revocation"; } catch { return false; }
|
|
547
|
+
});
|
|
548
|
+
assert.ok(hasRevocation, "rebuild dropped the peer's revocation — resurrection risk");
|
|
549
|
+
// And the rebuilt store A must report the fact as revoked (lookup returns null).
|
|
550
|
+
await storeA.load();
|
|
551
|
+
assert.equal(
|
|
552
|
+
storeA.lookup({ namespace: "default", contentHash: computeHash("The DB is MySQL.") }),
|
|
553
|
+
null,
|
|
554
|
+
"revoked fact resurrected after rebuild",
|
|
555
|
+
);
|
|
556
|
+
await rm(path.join(dir, "tombstones.lock"), { force: true });
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it("in-process concurrent appends still serialize under the lock", async () => {
|
|
560
|
+
const dir = await mkdtemp(path.join(tmpdir(), "tomb-inproc-lock-"));
|
|
561
|
+
const filePath = path.join(dir, "tombstones.jsonl");
|
|
562
|
+
const store = new TombstoneStore(
|
|
563
|
+
filePath,
|
|
564
|
+
"default",
|
|
565
|
+
{ enabled: true, semanticMatch: false, semanticThreshold: 0.9, hashContent: computeHash, normalizeText: normalizeContent },
|
|
566
|
+
makeReadMergeWriteIo(),
|
|
567
|
+
);
|
|
568
|
+
const contents = Array.from({ length: 25 }, (_, i) => `locked content ${i}`);
|
|
569
|
+
await Promise.all(
|
|
570
|
+
contents.map((c) =>
|
|
571
|
+
store.appendTombstone({
|
|
572
|
+
reason: "correction",
|
|
573
|
+
createdBy: "user_correction",
|
|
574
|
+
sourceMemoryId: `fact-${c}`,
|
|
575
|
+
rawContent: c,
|
|
576
|
+
}),
|
|
577
|
+
),
|
|
578
|
+
);
|
|
579
|
+
await store.load();
|
|
580
|
+
const raw = await readFile(filePath, "utf8");
|
|
581
|
+
const lines3 = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
582
|
+
// No lost writes despite the read-merge-write io + concurrent appends.
|
|
583
|
+
assert.equal(lines3.length, contents.length);
|
|
584
|
+
assert.equal(store.stats().count, contents.length);
|
|
585
|
+
await rm(path.join(dir, "tombstones.lock"), { force: true });
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
|
|
348
589
|
describe("parseTombstoneLine", () => {
|
|
349
590
|
it("rejects malformed input", () => {
|
|
350
591
|
assert.equal(parseTombstoneLine(""), null);
|