@remnic/core 9.29.0 → 9.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-admin-ops-surface.d.ts +11 -11
- package/dist/access-admin-ops-surface.js +19 -18
- package/dist/access-authorization-probe.d.ts +11 -11
- package/dist/access-authorization-probe.js +20 -19
- package/dist/access-boundary.d.ts +11 -11
- package/dist/access-boundary.js +19 -18
- package/dist/access-citation-namespace.js +2 -2
- package/dist/access-cli.js +45 -44
- package/dist/access-cli.js.map +1 -1
- package/dist/access-health-types.d.ts +1 -1
- package/dist/access-http-lcm-compaction.d.ts +11 -11
- package/dist/access-http.d.ts +11 -11
- package/dist/access-http.js +25 -24
- package/dist/access-identity-continuity-surface.d.ts +8 -8
- package/dist/access-identity-continuity-surface.js +19 -18
- package/dist/access-lcm-surface.d.ts +10 -10
- package/dist/access-lcm-surface.js +19 -18
- package/dist/access-mcp.d.ts +11 -11
- package/dist/access-mcp.js +23 -22
- package/dist/access-memory-search-fanout.d.ts +2 -2
- package/dist/{access-namespace-preflight-DCXBkDp0.d.ts → access-namespace-preflight-VfXssvoI.d.ts} +1 -1
- package/dist/access-namespace-preflight.d.ts +3 -3
- package/dist/access-namespace-preflight.js +19 -18
- package/dist/access-observe-write-surface.d.ts +10 -10
- package/dist/access-observe-write-surface.js +19 -18
- package/dist/access-operations-batch.js +20 -19
- package/dist/access-operations.d.ts +11 -11
- package/dist/access-operations.js +22 -21
- package/dist/access-recall-concurrency.d.ts +11 -11
- package/dist/access-recall-concurrency.js +19 -18
- package/dist/access-recall-response.d.ts +10 -10
- package/dist/access-recall-response.js +19 -18
- package/dist/access-recall-surface.d.ts +10 -10
- package/dist/access-recall-surface.js +19 -18
- package/dist/access-schema.d.ts +7 -7
- package/dist/access-schema.js +3 -3
- package/dist/{access-service-DvPW_M5j.d.ts → access-service-B5RwVsTV.d.ts} +7 -7
- package/dist/access-service.d.ts +11 -11
- package/dist/access-service.js +19 -18
- package/dist/access-surface-catalog.d.ts +11 -11
- package/dist/access-wearables-meetings-surface.d.ts +4 -3
- 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 +3 -3
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +8 -8
- package/dist/briefing.d.ts +3 -2
- package/dist/briefing.js +10 -9
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +3 -2
- package/dist/bulk-import/index.d.ts +3 -3
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/{catalog-BIVgftWu.d.ts → catalog-V7inUQdp.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +11 -10
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/causal-trajectory-graph.d.ts +1 -1
- package/dist/{chunk-KUJQHXPH.js → chunk-2EUVEDF5.js} +2 -2
- package/dist/{chunk-2XC73SHY.js → chunk-4WKA6I65.js} +20 -20
- package/dist/{chunk-2XC73SHY.js.map → chunk-4WKA6I65.js.map} +1 -1
- package/dist/{chunk-AIK5W3IW.js → chunk-5TFP4CUL.js} +68 -2
- package/dist/chunk-5TFP4CUL.js.map +1 -0
- package/dist/{chunk-EVX44I7C.js → chunk-5W4ZLBEZ.js} +9 -9
- package/dist/{chunk-P3MH72CI.js → chunk-62U4J2GN.js} +2 -2
- package/dist/{chunk-YGHMSDLK.js → chunk-66772WBP.js} +2 -2
- package/dist/{chunk-EP5BVCRI.js → chunk-6BNE62PM.js} +2 -2
- package/dist/{chunk-AAP5T6QG.js → chunk-6DNMUYEE.js} +2 -2
- package/dist/{chunk-SG265CVF.js → chunk-6FUPCJUL.js} +7 -7
- package/dist/{chunk-WZ7UB7XQ.js → chunk-6U7F52DE.js} +19 -19
- package/dist/{chunk-E4OCKU67.js → chunk-7XC2HX75.js} +2 -2
- package/dist/{chunk-OVRQLAYR.js → chunk-AZ5BTEKX.js} +2 -2
- package/dist/{chunk-2HNUMF5V.js → chunk-CZRYLSSW.js} +3 -3
- package/dist/chunk-FBUE6PD3.js +1769 -0
- package/dist/chunk-FBUE6PD3.js.map +1 -0
- package/dist/{chunk-WONS4RZ2.js → chunk-FDAMS4PK.js} +2 -2
- package/dist/{chunk-WL5EBRDH.js → chunk-FFAZ2XFU.js} +9 -8
- package/dist/chunk-FFAZ2XFU.js.map +1 -0
- package/dist/{chunk-Q6VFVAKZ.js → chunk-GIBMDSJ2.js} +2 -2
- package/dist/{chunk-ZXAGJFHJ.js → chunk-H5CPS5JP.js} +3 -3
- package/dist/{chunk-NBDCZWP4.js → chunk-I5NLEENJ.js} +2 -2
- package/dist/{chunk-DRBXHRXE.js → chunk-IB26HVAJ.js} +2 -2
- package/dist/{chunk-XEQ7OT7U.js → chunk-IC6CBHCA.js} +3 -3
- package/dist/{chunk-CMRQ34J5.js → chunk-JCBWCYEJ.js} +2 -2
- package/dist/{chunk-3PDOVUHC.js → chunk-KXB4VXHY.js} +2 -2
- package/dist/{chunk-G6LBWZRF.js → chunk-LBOD4U5B.js} +7 -7
- package/dist/{chunk-ZGOQTOYA.js → chunk-LGHKJABJ.js} +3 -3
- package/dist/{chunk-T3GCXULH.js → chunk-M3C2V3PV.js} +10 -10
- package/dist/{chunk-OFHFH655.js → chunk-MJCGLM25.js} +8 -8
- package/dist/{chunk-JDZRYH2N.js → chunk-NHWB6HG2.js} +2 -2
- package/dist/{chunk-TC3VNKGX.js → chunk-NM2WP3B7.js} +4 -4
- package/dist/{chunk-IMA2EWLV.js → chunk-P5PXWTW7.js} +2 -2
- package/dist/chunk-PH3ZWL3W.js +786 -0
- package/dist/chunk-PH3ZWL3W.js.map +1 -0
- package/dist/{chunk-QMBNMPQM.js → chunk-PUS63Q3G.js} +24 -22
- package/dist/{chunk-QMBNMPQM.js.map → chunk-PUS63Q3G.js.map} +1 -1
- package/dist/{chunk-2QMKMVHH.js → chunk-QNWXCJ76.js} +2 -2
- package/dist/{chunk-FCDWWCLX.js → chunk-RGIWINDN.js} +427 -795
- package/dist/chunk-RGIWINDN.js.map +1 -0
- package/dist/{chunk-CSMCNOPH.js → chunk-SPJXKAVF.js} +3 -3
- package/dist/{chunk-4PBVXX7L.js → chunk-TH2LSRMG.js} +2 -2
- package/dist/{chunk-ESBQWC3U.js → chunk-VI2JQPNQ.js} +2 -2
- package/dist/{chunk-GGUBDXRR.js → chunk-W5EG6LE3.js} +2 -2
- package/dist/{chunk-DEBHXHJR.js → chunk-WIPEXG6R.js} +2 -2
- package/dist/{chunk-SH6YO3LT.js → chunk-WVO4G37W.js} +2 -2
- package/dist/{chunk-VVIW3PTA.js → chunk-YTQNQ4KB.js} +14 -14
- package/dist/{chunk-PKRIQL3V.js → chunk-YTSNAUXN.js} +2 -2
- package/dist/{chunk-QAOGGNE3.js → chunk-ZPSVBSOF.js} +4 -4
- package/dist/{cli-CxoSOiG0.d.ts → cli-C4GbETPC.d.ts} +5 -5
- package/dist/cli.d.ts +13 -13
- package/dist/cli.js +36 -35
- package/dist/compounding/engine.d.ts +3 -2
- package/dist/compounding/engine.js +10 -9
- 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 +3 -3
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +10 -9
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +10 -9
- package/dist/consolidation-provenance-check.d.ts +3 -2
- package/dist/consolidation-undo.d.ts +3 -2
- package/dist/contradiction/index.d.ts +3 -2
- 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/corpus-watermark.d.ts +1 -1
- package/dist/corpus-watermark.js +12 -11
- 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 +3 -2
- package/dist/entity-retrieval.js +10 -9
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +52 -10
- package/dist/explicit-capture.js +13 -1
- package/dist/extraction-error-classification.d.ts +1 -1
- 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-liveness.d.ts +1 -1
- package/dist/extraction-source-grounding-rules.d.ts +1 -1
- package/dist/extraction-source-grounding.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/{forget-HRPLVB3W.js → forget-4UY2EOKH.js} +3 -3
- package/dist/graph-dashboard-diff.d.ts +1 -1
- package/dist/graph-dashboard-key.d.ts +1 -1
- package/dist/graph-dashboard-parser.d.ts +1 -1
- package/dist/graph-edge-reinforcement.d.ts +1 -1
- package/dist/graph-snapshot.d.ts +1 -1
- package/dist/graph.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/importers/index.d.ts +1 -1
- package/dist/in-flight-reads.d.ts +1 -1
- package/dist/index.d.ts +21 -21
- package/dist/index.js +47 -45
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/local-model-endpoint.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +10 -9
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +2 -2
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +10 -9
- package/dist/maintenance/rebuild-memory-projection.d.ts +3 -2
- package/dist/maintenance/rebuild-memory-projection.js +11 -10
- package/dist/{maintenance-Cb-PjxQd.d.ts → maintenance-CL30NWtc.d.ts} +3 -3
- package/dist/mcp-memory-inspector-app.d.ts +11 -11
- 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 +3 -2
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +4 -3
- package/dist/namespaces/migrate.js +11 -10
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/storage.d.ts +4 -3
- package/dist/namespaces/storage.js +10 -9
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/offline-sync-impression-drain.d.ts +1 -1
- package/dist/offline-sync-impression-drain.js +5 -5
- package/dist/offline-sync.js +2 -2
- package/dist/operator-doctor-corpus.d.ts +1 -1
- package/dist/operator-doctor-corpus.js +13 -12
- package/dist/operator-doctor-replica.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +4 -3
- package/dist/operator-toolkit.js +18 -17
- package/dist/orchestration/compression-guideline-coordinator.d.ts +3 -2
- package/dist/orchestration/maintenance.d.ts +5 -4
- package/dist/orchestration/maintenance.js +15 -14
- package/dist/{orchestrator-5GKqb_HL.d.ts → orchestrator-CKTCRwWV.d.ts} +7 -7
- package/dist/orchestrator.d.ts +8 -8
- package/dist/orchestrator.js +45 -44
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/{pipeline-3Qi1TpRo.d.ts → pipeline-B7wj4rRZ.d.ts} +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/proactive-contention.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/{purge-YSGX5BOG.js → purge-533FZLS4.js} +16 -9
- package/dist/purge-533FZLS4.js.map +1 -0
- package/dist/qmd-preflight.d.ts +2 -2
- package/dist/{qmd-D9JqzzRX.d.ts → qmd-q9tg4dHp.d.ts} +1 -1
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +2 -2
- package/dist/recall-concurrency-config.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-memory-map.d.ts +1 -1
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-state.js +2 -2
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-timings.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray.d.ts +1 -1
- package/dist/replay/normalizers/chatgpt.d.ts +1 -1
- package/dist/replay/normalizers/claude.d.ts +1 -1
- package/dist/replay/normalizers/openclaw.d.ts +1 -1
- package/dist/replay/normalizers/shared.d.ts +1 -1
- package/dist/replay/runner.d.ts +1 -1
- package/dist/replay/types.d.ts +1 -1
- package/dist/replica-divergence.d.ts +1 -1
- package/dist/replica-peers-config.d.ts +1 -1
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +4 -4
- package/dist/retrieval-agents.d.ts +2 -2
- 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/salvage-envelope.d.ts +1 -1
- package/dist/schemas.d.ts +22 -22
- package/dist/{scope-profiles-C4g6_UdP.d.ts → scope-profiles-CWDyglPM.d.ts} +1 -1
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/index.d.ts +1 -1
- 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/secure-store/index.js +2 -2
- package/dist/{semantic-consolidation-BrxqnzTW.d.ts → semantic-consolidation-BUE3deLn.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +11 -10
- package/dist/semantic-rule-promotion.js +10 -9
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +10 -9
- package/dist/{service-Bi1JgxdP.d.ts → service-CXgM0bQG.d.ts} +2 -2
- 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-CcH6yPk2.d.ts → storage-DysJq-XH.d.ts} +163 -27
- package/dist/storage.d.ts +3 -2
- package/dist/storage.js +12 -10
- package/dist/summarizer.d.ts +1 -1
- package/dist/summarizer.js +3 -3
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/summary-snapshot.js +2 -2
- package/dist/temporal-supersession.d.ts +3 -2
- package/dist/temporal-timeline-recall.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 +3 -2
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/types.d.ts +12 -12
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-Dg0w9ADr.d.ts → types-aPPzhnjT.d.ts} +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/utils/serialize-mutations.d.ts +11 -7
- package/dist/utils/serialize-mutations.js +1 -1
- package/dist/verified-recall.js +10 -9
- package/dist/write-envelope.d.ts +1 -1
- package/package.json +2 -2
- package/src/access-observe-write-surface.ts +1 -1
- package/src/access-service-coding-write.test.ts +40 -0
- package/src/explicit-capture.test.ts +1110 -1
- package/src/explicit-capture.ts +534 -135
- package/src/maintenance/purge.test.ts +157 -1
- package/src/maintenance/purge.ts +15 -9
- package/src/secure-store/secure-fs.ts +78 -1
- package/src/storage/content-hash-index.ts +43 -17
- package/src/storage/entity-canonical-id-migration.ts +151 -23
- package/src/storage/tombstone-blocked-capture-index.ts +562 -0
- package/src/storage/tombstone-blocked-capture-mutation.ts +284 -0
- package/src/storage/tombstone-blocked-capture-sync.ts +872 -0
- package/src/storage-fact-hash-index-defer.test.ts +2900 -57
- package/src/storage.ts +266 -265
- package/src/testing/subjects/tombstone-blocked-capture.test.ts +181 -0
- package/src/utils/serialize-mutations.test.ts +4 -1
- package/src/utils/serialize-mutations.ts +26 -27
- package/dist/chunk-55MDBZLC.js +0 -490
- package/dist/chunk-55MDBZLC.js.map +0 -1
- package/dist/chunk-AIK5W3IW.js.map +0 -1
- package/dist/chunk-FCDWWCLX.js.map +0 -1
- package/dist/chunk-WL5EBRDH.js.map +0 -1
- package/dist/purge-YSGX5BOG.js.map +0 -1
- /package/dist/{chunk-KUJQHXPH.js.map → chunk-2EUVEDF5.js.map} +0 -0
- /package/dist/{chunk-EVX44I7C.js.map → chunk-5W4ZLBEZ.js.map} +0 -0
- /package/dist/{chunk-P3MH72CI.js.map → chunk-62U4J2GN.js.map} +0 -0
- /package/dist/{chunk-YGHMSDLK.js.map → chunk-66772WBP.js.map} +0 -0
- /package/dist/{chunk-EP5BVCRI.js.map → chunk-6BNE62PM.js.map} +0 -0
- /package/dist/{chunk-AAP5T6QG.js.map → chunk-6DNMUYEE.js.map} +0 -0
- /package/dist/{chunk-SG265CVF.js.map → chunk-6FUPCJUL.js.map} +0 -0
- /package/dist/{chunk-WZ7UB7XQ.js.map → chunk-6U7F52DE.js.map} +0 -0
- /package/dist/{chunk-E4OCKU67.js.map → chunk-7XC2HX75.js.map} +0 -0
- /package/dist/{chunk-OVRQLAYR.js.map → chunk-AZ5BTEKX.js.map} +0 -0
- /package/dist/{chunk-2HNUMF5V.js.map → chunk-CZRYLSSW.js.map} +0 -0
- /package/dist/{chunk-WONS4RZ2.js.map → chunk-FDAMS4PK.js.map} +0 -0
- /package/dist/{chunk-Q6VFVAKZ.js.map → chunk-GIBMDSJ2.js.map} +0 -0
- /package/dist/{chunk-ZXAGJFHJ.js.map → chunk-H5CPS5JP.js.map} +0 -0
- /package/dist/{chunk-NBDCZWP4.js.map → chunk-I5NLEENJ.js.map} +0 -0
- /package/dist/{chunk-DRBXHRXE.js.map → chunk-IB26HVAJ.js.map} +0 -0
- /package/dist/{chunk-XEQ7OT7U.js.map → chunk-IC6CBHCA.js.map} +0 -0
- /package/dist/{chunk-CMRQ34J5.js.map → chunk-JCBWCYEJ.js.map} +0 -0
- /package/dist/{chunk-3PDOVUHC.js.map → chunk-KXB4VXHY.js.map} +0 -0
- /package/dist/{chunk-G6LBWZRF.js.map → chunk-LBOD4U5B.js.map} +0 -0
- /package/dist/{chunk-ZGOQTOYA.js.map → chunk-LGHKJABJ.js.map} +0 -0
- /package/dist/{chunk-T3GCXULH.js.map → chunk-M3C2V3PV.js.map} +0 -0
- /package/dist/{chunk-OFHFH655.js.map → chunk-MJCGLM25.js.map} +0 -0
- /package/dist/{chunk-JDZRYH2N.js.map → chunk-NHWB6HG2.js.map} +0 -0
- /package/dist/{chunk-TC3VNKGX.js.map → chunk-NM2WP3B7.js.map} +0 -0
- /package/dist/{chunk-IMA2EWLV.js.map → chunk-P5PXWTW7.js.map} +0 -0
- /package/dist/{chunk-2QMKMVHH.js.map → chunk-QNWXCJ76.js.map} +0 -0
- /package/dist/{chunk-CSMCNOPH.js.map → chunk-SPJXKAVF.js.map} +0 -0
- /package/dist/{chunk-4PBVXX7L.js.map → chunk-TH2LSRMG.js.map} +0 -0
- /package/dist/{chunk-ESBQWC3U.js.map → chunk-VI2JQPNQ.js.map} +0 -0
- /package/dist/{chunk-GGUBDXRR.js.map → chunk-W5EG6LE3.js.map} +0 -0
- /package/dist/{chunk-DEBHXHJR.js.map → chunk-WIPEXG6R.js.map} +0 -0
- /package/dist/{chunk-SH6YO3LT.js.map → chunk-WVO4G37W.js.map} +0 -0
- /package/dist/{chunk-VVIW3PTA.js.map → chunk-YTQNQ4KB.js.map} +0 -0
- /package/dist/{chunk-PKRIQL3V.js.map → chunk-YTSNAUXN.js.map} +0 -0
- /package/dist/{chunk-QAOGGNE3.js.map → chunk-ZPSVBSOF.js.map} +0 -0
- /package/dist/{forget-HRPLVB3W.js.map → forget-4UY2EOKH.js.map} +0 -0
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
|
|
4
|
+
import type { Orchestrator } from "./orchestrator.js";
|
|
5
|
+
import type { StorageManager } from "./storage.js";
|
|
6
|
+
import type { PluginConfig } from "./types.js";
|
|
7
|
+
import type { SealedMemoryEnvelope } from "./write-envelope.js";
|
|
4
8
|
import {
|
|
9
|
+
InlineExplicitCaptureProcessor,
|
|
5
10
|
parseInlineExplicitCaptureNotes,
|
|
11
|
+
persistExplicitCapture,
|
|
12
|
+
queueExplicitCaptureForReview,
|
|
6
13
|
validateExplicitCaptureInput,
|
|
7
14
|
} from "./explicit-capture.js";
|
|
8
15
|
|
|
@@ -24,7 +31,7 @@ test("inline explicit capture rejects malformed confidence values", () => {
|
|
|
24
31
|
assert.throws(
|
|
25
32
|
() => validateExplicitCaptureInput(note),
|
|
26
33
|
/confidence must be a finite number/,
|
|
27
|
-
`${confidenceLine} should be rejected
|
|
34
|
+
`${confidenceLine} should be rejected`
|
|
28
35
|
);
|
|
29
36
|
}
|
|
30
37
|
});
|
|
@@ -38,3 +45,1105 @@ test("inline explicit capture defaults omitted confidence", () => {
|
|
|
38
45
|
const validated = validateExplicitCaptureInput(parseSingleNote(""));
|
|
39
46
|
assert.equal(validated.confidence, 0.95);
|
|
40
47
|
});
|
|
48
|
+
|
|
49
|
+
type StoredMemory = {
|
|
50
|
+
frontmatter: {
|
|
51
|
+
id: string;
|
|
52
|
+
category: string;
|
|
53
|
+
status?: string;
|
|
54
|
+
tags?: string[];
|
|
55
|
+
sourceConnector?: string;
|
|
56
|
+
blockedBy?: string;
|
|
57
|
+
};
|
|
58
|
+
content: string;
|
|
59
|
+
};
|
|
60
|
+
function createInlineCaptureProcessorProbe(
|
|
61
|
+
options: {
|
|
62
|
+
tombstoneBlocked?: boolean;
|
|
63
|
+
authoritativeFactHashMiss?: boolean;
|
|
64
|
+
tombstoneBlockedCaptureIndexHit?: boolean;
|
|
65
|
+
failWrites?: boolean;
|
|
66
|
+
failPrimaryWrites?: boolean;
|
|
67
|
+
duplicateReadDelayMs?: number;
|
|
68
|
+
lockBusyAttempts?: number;
|
|
69
|
+
} = {}
|
|
70
|
+
) {
|
|
71
|
+
const envelopes: SealedMemoryEnvelope[] = [];
|
|
72
|
+
const lifecycleEvents: Array<{ eventType: string; actor: string }> = [];
|
|
73
|
+
const maintenanceReasons: string[] = [];
|
|
74
|
+
const requestedNamespaces: Array<string | undefined> = [];
|
|
75
|
+
const memories: StoredMemory[] = [];
|
|
76
|
+
const coldMemories: StoredMemory[] = [];
|
|
77
|
+
const blockedIndexArguments: Array<{
|
|
78
|
+
content: string;
|
|
79
|
+
category: string;
|
|
80
|
+
sourceConnector?: string;
|
|
81
|
+
}> = [];
|
|
82
|
+
let readAllCalls = 0;
|
|
83
|
+
let activeReadAllCalls = 0;
|
|
84
|
+
let maxConcurrentReadAllCalls = 0;
|
|
85
|
+
let nextId = 1;
|
|
86
|
+
let writeTail = Promise.resolve();
|
|
87
|
+
let lockBusyAttemptsRemaining = options.lockBusyAttempts ?? 0;
|
|
88
|
+
const storage = {
|
|
89
|
+
readAllMemories: async () => {
|
|
90
|
+
readAllCalls += 1;
|
|
91
|
+
activeReadAllCalls += 1;
|
|
92
|
+
maxConcurrentReadAllCalls = Math.max(maxConcurrentReadAllCalls, activeReadAllCalls);
|
|
93
|
+
if (options.duplicateReadDelayMs !== undefined) {
|
|
94
|
+
await new Promise((resolve) => setTimeout(resolve, options.duplicateReadDelayMs));
|
|
95
|
+
}
|
|
96
|
+
activeReadAllCalls -= 1;
|
|
97
|
+
return memories.map((memory) => ({
|
|
98
|
+
...memory,
|
|
99
|
+
frontmatter: { ...memory.frontmatter },
|
|
100
|
+
}));
|
|
101
|
+
},
|
|
102
|
+
readAllColdMemories: async () =>
|
|
103
|
+
coldMemories.map((memory) => ({
|
|
104
|
+
...memory,
|
|
105
|
+
frontmatter: { ...memory.frontmatter },
|
|
106
|
+
})),
|
|
107
|
+
writeSealedMemory: async (envelope: SealedMemoryEnvelope) => {
|
|
108
|
+
if (options.failWrites || (options.failPrimaryWrites && envelope.source === "explicit-inline")) {
|
|
109
|
+
throw new Error("simulated sealed write failure");
|
|
110
|
+
}
|
|
111
|
+
const id = `memory-${nextId++}`;
|
|
112
|
+
envelopes.push(envelope);
|
|
113
|
+
memories.push({
|
|
114
|
+
frontmatter: {
|
|
115
|
+
id,
|
|
116
|
+
category: envelope.category,
|
|
117
|
+
...(options.tombstoneBlocked ? { status: "pending_review", blockedBy: "tombstone-1" } : {}),
|
|
118
|
+
tags: [...envelope.tags],
|
|
119
|
+
sourceConnector: envelope.sourceConnector,
|
|
120
|
+
},
|
|
121
|
+
content: envelope.content,
|
|
122
|
+
});
|
|
123
|
+
return { id, tombstoneBlocked: options.tombstoneBlocked === true };
|
|
124
|
+
},
|
|
125
|
+
withTombstoneBlockedCaptureWriteLock: async <T>(operation: () => Promise<T>): Promise<T> => {
|
|
126
|
+
const previous = writeTail;
|
|
127
|
+
let release!: () => void;
|
|
128
|
+
writeTail = new Promise<void>((resolve) => {
|
|
129
|
+
release = resolve;
|
|
130
|
+
});
|
|
131
|
+
await previous;
|
|
132
|
+
try {
|
|
133
|
+
if (lockBusyAttemptsRemaining > 0) {
|
|
134
|
+
lockBusyAttemptsRemaining -= 1;
|
|
135
|
+
throw new Error("tombstone-blocked capture write lock remained busy");
|
|
136
|
+
}
|
|
137
|
+
return await operation();
|
|
138
|
+
} finally {
|
|
139
|
+
release();
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
...(options.authoritativeFactHashMiss
|
|
143
|
+
? {
|
|
144
|
+
hasFactContentHash: async () => false,
|
|
145
|
+
isFactContentHashAuthoritative: () => true,
|
|
146
|
+
}
|
|
147
|
+
: {}),
|
|
148
|
+
...(options.tombstoneBlockedCaptureIndexHit !== undefined
|
|
149
|
+
? {
|
|
150
|
+
checkTombstoneBlockedExplicitCapture: async (content: string, category: string, sourceConnector?: string) => {
|
|
151
|
+
blockedIndexArguments.push({ content, category, sourceConnector });
|
|
152
|
+
return {
|
|
153
|
+
has: options.tombstoneBlockedCaptureIndexHit === true,
|
|
154
|
+
authoritative: true,
|
|
155
|
+
};
|
|
156
|
+
},
|
|
157
|
+
hasTombstoneBlockedExplicitCapture: async (content: string, category: string, sourceConnector?: string) => {
|
|
158
|
+
blockedIndexArguments.push({ content, category, sourceConnector });
|
|
159
|
+
return options.tombstoneBlockedCaptureIndexHit === true;
|
|
160
|
+
},
|
|
161
|
+
isTombstoneBlockedExplicitCaptureIndexAuthoritative: async () => true,
|
|
162
|
+
}
|
|
163
|
+
: {}),
|
|
164
|
+
appendMemoryLifecycleEvents: async (events: Array<{ eventType: string; actor: string }>) => {
|
|
165
|
+
lifecycleEvents.push(...events);
|
|
166
|
+
},
|
|
167
|
+
getMemoryById: async (id: string) => memories.find((memory) => memory.frontmatter.id === id) ?? null,
|
|
168
|
+
writeMemoryFrontmatter: async (memory: StoredMemory, update: { status?: string }) => {
|
|
169
|
+
memory.frontmatter = { ...memory.frontmatter, ...update };
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
const orchestrator = {
|
|
173
|
+
config: {
|
|
174
|
+
captureMode: "hybrid",
|
|
175
|
+
defaultNamespace: "default",
|
|
176
|
+
memoryDir: "",
|
|
177
|
+
namespacePolicies: [],
|
|
178
|
+
namespacesEnabled: true,
|
|
179
|
+
sharedNamespace: "shared",
|
|
180
|
+
} as unknown as PluginConfig,
|
|
181
|
+
getStorage: async (namespace?: string) => {
|
|
182
|
+
requestedNamespaces.push(namespace);
|
|
183
|
+
return storage;
|
|
184
|
+
},
|
|
185
|
+
requestQmdMaintenanceForTool: (reason: string) => {
|
|
186
|
+
maintenanceReasons.push(reason);
|
|
187
|
+
},
|
|
188
|
+
} as unknown as Orchestrator;
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
envelopes,
|
|
192
|
+
lifecycleEvents,
|
|
193
|
+
maintenanceReasons,
|
|
194
|
+
memories,
|
|
195
|
+
coldMemories,
|
|
196
|
+
storage,
|
|
197
|
+
readAllCalls: () => readAllCalls,
|
|
198
|
+
maxConcurrentReadAllCalls: () => maxConcurrentReadAllCalls,
|
|
199
|
+
blockedIndexArguments: () => blockedIndexArguments,
|
|
200
|
+
orchestrator,
|
|
201
|
+
processor: new InlineExplicitCaptureProcessor(orchestrator, { sourceConnector: "openclaw" }),
|
|
202
|
+
requestedNamespaces,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
test("inline capture processor persists an authorized inline note once and strips it from hybrid replay", async () => {
|
|
207
|
+
const probe = createInlineCaptureProcessorProbe();
|
|
208
|
+
const content = [
|
|
209
|
+
"Keep the visible turn.",
|
|
210
|
+
"<memory_note>",
|
|
211
|
+
"content: The deployment plan should use a rolling rollout.",
|
|
212
|
+
"category: decision",
|
|
213
|
+
"namespace: another-principal",
|
|
214
|
+
"</memory_note>",
|
|
215
|
+
].join("\n");
|
|
216
|
+
const request = {
|
|
217
|
+
captureMode: "hybrid" as const,
|
|
218
|
+
content,
|
|
219
|
+
dedupeKeys: ["message-1"],
|
|
220
|
+
namespace: "principal-project",
|
|
221
|
+
namespacePreResolved: true,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const first = await probe.processor.process(request);
|
|
225
|
+
const replay = await probe.processor.process(request);
|
|
226
|
+
|
|
227
|
+
assert.equal(first.content, "Keep the visible turn.");
|
|
228
|
+
assert.equal(first.accepted, 1);
|
|
229
|
+
assert.equal(first.queued, 0);
|
|
230
|
+
assert.equal(replay.processed, 0);
|
|
231
|
+
assert.equal(probe.envelopes.length, 1);
|
|
232
|
+
assert.equal(probe.envelopes[0]?.source, "explicit-inline");
|
|
233
|
+
assert.equal(probe.envelopes[0]?.sourceConnector, "openclaw");
|
|
234
|
+
assert.deepEqual(probe.requestedNamespaces, ["principal-project"]);
|
|
235
|
+
assert.deepEqual(probe.maintenanceReasons, ["inline.memory_note"]);
|
|
236
|
+
assert.equal(probe.lifecycleEvents[0]?.eventType, "explicit_capture_accepted");
|
|
237
|
+
assert.equal(probe.lifecycleEvents[0]?.actor, "inline.memory_note");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("inline capture processor serializes overlapping duplicate deliveries", async () => {
|
|
241
|
+
const probe = createInlineCaptureProcessorProbe();
|
|
242
|
+
const request = {
|
|
243
|
+
captureMode: "hybrid" as const,
|
|
244
|
+
content: [
|
|
245
|
+
"<memory_note>",
|
|
246
|
+
"content: Overlapping deliveries must persist one inline capture.",
|
|
247
|
+
"category: fact",
|
|
248
|
+
"</memory_note>",
|
|
249
|
+
].join("\n"),
|
|
250
|
+
dedupeKeys: ["overlapping-delivery"],
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const [first, second] = await Promise.all([probe.processor.process(request), probe.processor.process(request)]);
|
|
254
|
+
|
|
255
|
+
assert.equal(first.processed + second.processed, 1);
|
|
256
|
+
assert.equal(first.accepted + second.accepted, 1);
|
|
257
|
+
assert.equal(first.duplicates + second.duplicates, 1);
|
|
258
|
+
assert.equal(first.queued + second.queued, 0);
|
|
259
|
+
assert.equal(probe.envelopes.length, 1);
|
|
260
|
+
assert.equal(probe.lifecycleEvents.length, 1);
|
|
261
|
+
assert.deepEqual(probe.maintenanceReasons, ["inline.memory_note"]);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("persistExplicitCapture serializes duplicate checks across concurrent calls", async () => {
|
|
265
|
+
const probe = createInlineCaptureProcessorProbe();
|
|
266
|
+
const candidate = {
|
|
267
|
+
content: "Concurrent persistence must produce one durable capture.",
|
|
268
|
+
category: "fact" as const,
|
|
269
|
+
confidence: 0.9,
|
|
270
|
+
tags: [],
|
|
271
|
+
};
|
|
272
|
+
const [first, second] = await Promise.all([
|
|
273
|
+
persistExplicitCapture(probe.orchestrator, candidate, "memory_store"),
|
|
274
|
+
persistExplicitCapture(probe.orchestrator, candidate, "memory_store"),
|
|
275
|
+
]);
|
|
276
|
+
assert.equal(Number(first.duplicateOf !== undefined) + Number(second.duplicateOf !== undefined), 1);
|
|
277
|
+
assert.equal(probe.envelopes.length, 1);
|
|
278
|
+
assert.equal(probe.lifecycleEvents.length, 1);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("inline capture processor hashes the effective authorized input for replay dedupe", async () => {
|
|
282
|
+
const probe = createInlineCaptureProcessorProbe({ tombstoneBlocked: true });
|
|
283
|
+
const request = {
|
|
284
|
+
captureMode: "hybrid" as const,
|
|
285
|
+
dedupeKeys: ["authorized-delivery"],
|
|
286
|
+
namespace: "principal-project",
|
|
287
|
+
namespacePreResolved: true,
|
|
288
|
+
};
|
|
289
|
+
const first = await probe.processor.process({
|
|
290
|
+
...request,
|
|
291
|
+
content: [
|
|
292
|
+
"<memory_note>",
|
|
293
|
+
"content: An authorized namespace must control replay identity.",
|
|
294
|
+
"category: fact",
|
|
295
|
+
"namespace: untrusted-inline-value",
|
|
296
|
+
"</memory_note>",
|
|
297
|
+
].join("\n"),
|
|
298
|
+
});
|
|
299
|
+
const replay = await probe.processor.process({
|
|
300
|
+
...request,
|
|
301
|
+
content: [
|
|
302
|
+
"<memory_note>",
|
|
303
|
+
"content: An authorized namespace must control replay identity.",
|
|
304
|
+
"category: fact",
|
|
305
|
+
"</memory_note>",
|
|
306
|
+
].join("\n"),
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
assert.equal(first.queued, 1);
|
|
310
|
+
assert.equal(replay.processed, 0);
|
|
311
|
+
assert.equal(probe.envelopes.length, 1);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("inline capture processor normalizes omitted namespaces to the effective default", async () => {
|
|
315
|
+
const probe = createInlineCaptureProcessorProbe({ tombstoneBlocked: true });
|
|
316
|
+
const request = {
|
|
317
|
+
captureMode: "hybrid" as const,
|
|
318
|
+
dedupeKeys: ["default-namespace-delivery"],
|
|
319
|
+
};
|
|
320
|
+
const first = await probe.processor.process({
|
|
321
|
+
...request,
|
|
322
|
+
content: [
|
|
323
|
+
"<memory_note>",
|
|
324
|
+
"content: Default namespace replay identity must be stable.",
|
|
325
|
+
"category: fact",
|
|
326
|
+
"</memory_note>",
|
|
327
|
+
].join("\n"),
|
|
328
|
+
});
|
|
329
|
+
const replay = await probe.processor.process({
|
|
330
|
+
...request,
|
|
331
|
+
content: [
|
|
332
|
+
"<memory_note>",
|
|
333
|
+
"content: Default namespace replay identity must be stable.",
|
|
334
|
+
"category: fact",
|
|
335
|
+
"namespace: default",
|
|
336
|
+
"</memory_note>",
|
|
337
|
+
].join("\n"),
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
assert.equal(first.queued, 1);
|
|
341
|
+
assert.equal(replay.processed, 0);
|
|
342
|
+
assert.equal(probe.envelopes.length, 1);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("inline capture processor routes tombstone-blocked captures to review", async () => {
|
|
346
|
+
const probe = createInlineCaptureProcessorProbe({ tombstoneBlocked: true });
|
|
347
|
+
const result = await probe.processor.process({
|
|
348
|
+
captureMode: "hybrid",
|
|
349
|
+
content: [
|
|
350
|
+
"<memory_note>",
|
|
351
|
+
"content: A tombstone-blocked capture must not be reported active.",
|
|
352
|
+
"category: fact",
|
|
353
|
+
"</memory_note>",
|
|
354
|
+
].join("\n"),
|
|
355
|
+
dedupeKeys: ["message-tombstone-blocked"],
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
assert.equal(result.accepted, 0);
|
|
359
|
+
assert.equal(result.queued, 1);
|
|
360
|
+
assert.deepEqual(probe.maintenanceReasons, ["inline.memory_note.review"]);
|
|
361
|
+
assert.equal(probe.lifecycleEvents[0]?.eventType, "explicit_capture_queued");
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("inline capture processor queues invalid complete markup and never leaves it for hybrid extraction", async () => {
|
|
365
|
+
const probe = createInlineCaptureProcessorProbe();
|
|
366
|
+
const content = [
|
|
367
|
+
"Keep only this visible turn.",
|
|
368
|
+
"<memory_note>",
|
|
369
|
+
"content: The rollout should use review before production.",
|
|
370
|
+
"confidence: invalid",
|
|
371
|
+
"namespace: another-principal",
|
|
372
|
+
"</memory_note>",
|
|
373
|
+
].join("\n");
|
|
374
|
+
|
|
375
|
+
const result = await probe.processor.process({
|
|
376
|
+
captureMode: "hybrid",
|
|
377
|
+
content,
|
|
378
|
+
dedupeKeys: ["message-2"],
|
|
379
|
+
namespace: "principal-project",
|
|
380
|
+
namespacePreResolved: true,
|
|
381
|
+
});
|
|
382
|
+
const disabled = await probe.processor.process({
|
|
383
|
+
captureMode: "implicit",
|
|
384
|
+
content,
|
|
385
|
+
dedupeKeys: ["message-3"],
|
|
386
|
+
namespace: "principal-project",
|
|
387
|
+
namespacePreResolved: true,
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
assert.equal(result.content, "Keep only this visible turn.");
|
|
391
|
+
assert.equal(result.accepted, 0);
|
|
392
|
+
assert.equal(result.queued, 1);
|
|
393
|
+
assert.equal(probe.envelopes.length, 1);
|
|
394
|
+
assert.equal(probe.envelopes[0]?.source, "explicit-inline-review");
|
|
395
|
+
assert.match(probe.envelopes[0]?.content ?? "", /confidence must be a finite number/);
|
|
396
|
+
assert.equal(probe.memories[0]?.frontmatter.status, "pending_review");
|
|
397
|
+
assert.deepEqual(probe.maintenanceReasons, ["inline.memory_note.review"]);
|
|
398
|
+
assert.equal(probe.lifecycleEvents[0]?.eventType, "explicit_capture_queued");
|
|
399
|
+
assert.equal(probe.lifecycleEvents[0]?.actor, "inline.memory_note");
|
|
400
|
+
assert.equal(disabled.content, content);
|
|
401
|
+
assert.equal(disabled.processed, 0);
|
|
402
|
+
assert.equal(probe.envelopes.length, 1);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("inline capture review fallback serializes duplicate checks across processor instances", async () => {
|
|
406
|
+
const probe = createInlineCaptureProcessorProbe({ duplicateReadDelayMs: 15 });
|
|
407
|
+
const secondProcessor = new InlineExplicitCaptureProcessor(probe.orchestrator, {
|
|
408
|
+
sourceConnector: "openclaw",
|
|
409
|
+
});
|
|
410
|
+
const request = {
|
|
411
|
+
captureMode: "hybrid" as const,
|
|
412
|
+
content: [
|
|
413
|
+
"<memory_note>",
|
|
414
|
+
"content: Concurrent invalid captures must queue one review.",
|
|
415
|
+
"confidence: invalid",
|
|
416
|
+
"category: fact",
|
|
417
|
+
"</memory_note>",
|
|
418
|
+
].join("\n"),
|
|
419
|
+
namespace: "principal-project",
|
|
420
|
+
namespacePreResolved: true,
|
|
421
|
+
};
|
|
422
|
+
const [first, second] = await Promise.all([
|
|
423
|
+
probe.processor.process({ ...request, dedupeKeys: ["review-fallback-1"] }),
|
|
424
|
+
secondProcessor.process({ ...request, dedupeKeys: ["review-fallback-2"] }),
|
|
425
|
+
]);
|
|
426
|
+
|
|
427
|
+
assert.equal(first.queued + second.queued, 1);
|
|
428
|
+
assert.equal(first.duplicates + second.duplicates, 1);
|
|
429
|
+
assert.equal(probe.envelopes.length, 1);
|
|
430
|
+
assert.equal(probe.lifecycleEvents.length, 1);
|
|
431
|
+
assert.equal(probe.maxConcurrentReadAllCalls(), 1);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("inline capture review fallback isolates authorized review namespaces", async () => {
|
|
435
|
+
const primary = createInlineCaptureProcessorProbe();
|
|
436
|
+
const secondary = createInlineCaptureProcessorProbe();
|
|
437
|
+
const config = primary.orchestrator.config as PluginConfig;
|
|
438
|
+
config.namespacePolicies = [
|
|
439
|
+
{ name: "review-a", readPrincipals: ["*"], writePrincipals: ["*"] },
|
|
440
|
+
{ name: "review-b", readPrincipals: ["*"], writePrincipals: ["*"] },
|
|
441
|
+
];
|
|
442
|
+
const primaryStorage = primary.storage;
|
|
443
|
+
const secondaryStorage = secondary.storage;
|
|
444
|
+
primary.orchestrator.getStorage = async (namespace?: string) =>
|
|
445
|
+
(namespace === "review-b" ? secondaryStorage : primaryStorage) as unknown as StorageManager;
|
|
446
|
+
const secondProcessor = new InlineExplicitCaptureProcessor(primary.orchestrator, {
|
|
447
|
+
sourceConnector: "openclaw",
|
|
448
|
+
});
|
|
449
|
+
const content = [
|
|
450
|
+
"<memory_note>",
|
|
451
|
+
"content: The same unsupported note must stay isolated by review principal.",
|
|
452
|
+
"category: fact",
|
|
453
|
+
"namespace: unsupported-inline-namespace",
|
|
454
|
+
"</memory_note>",
|
|
455
|
+
].join("\n");
|
|
456
|
+
const first = await primary.processor.process({
|
|
457
|
+
captureMode: "hybrid",
|
|
458
|
+
content,
|
|
459
|
+
dedupeKeys: ["review-root-a"],
|
|
460
|
+
reviewNamespace: "review-a",
|
|
461
|
+
reviewNamespacePreResolved: true,
|
|
462
|
+
});
|
|
463
|
+
const second = await secondProcessor.process({
|
|
464
|
+
captureMode: "hybrid",
|
|
465
|
+
content,
|
|
466
|
+
dedupeKeys: ["review-root-b"],
|
|
467
|
+
reviewNamespace: "review-b",
|
|
468
|
+
reviewNamespacePreResolved: true,
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
assert.equal(first.queued, 1);
|
|
472
|
+
assert.equal(second.queued, 1);
|
|
473
|
+
assert.equal(primary.memories.length, 1);
|
|
474
|
+
assert.equal(secondary.memories.length, 1);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test("inline capture review fallback locks the resolved review namespace storage", async () => {
|
|
478
|
+
const probe = createInlineCaptureProcessorProbe();
|
|
479
|
+
const queueStorage = probe.storage;
|
|
480
|
+
const defaultStorage = {
|
|
481
|
+
...queueStorage,
|
|
482
|
+
withTombstoneBlockedCaptureWriteLock: async () => {
|
|
483
|
+
throw new Error("default review lock must not be used");
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
probe.orchestrator.getStorage = async (namespace?: string) =>
|
|
487
|
+
(namespace === "review-a" ? queueStorage : defaultStorage) as unknown as StorageManager;
|
|
488
|
+
|
|
489
|
+
const result = await queueExplicitCaptureForReview(
|
|
490
|
+
probe.orchestrator,
|
|
491
|
+
{
|
|
492
|
+
content: "A review fallback must lock its own namespace.",
|
|
493
|
+
category: "fact",
|
|
494
|
+
namespace: "unsupported-inline-namespace",
|
|
495
|
+
},
|
|
496
|
+
"inline",
|
|
497
|
+
new Error("invalid capture"),
|
|
498
|
+
{ resolvedNamespace: "review-a" }
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
assert.equal(result.duplicateOf, undefined);
|
|
502
|
+
assert.equal(probe.memories.length, 1);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test("inline capture routes unsupported namespaces to the authorized default review scope", async () => {
|
|
506
|
+
const probe = createInlineCaptureProcessorProbe();
|
|
507
|
+
const result = await probe.processor.process({
|
|
508
|
+
captureMode: "hybrid",
|
|
509
|
+
content: [
|
|
510
|
+
"<memory_note>",
|
|
511
|
+
"content: An unsupported namespace must still produce a review item.",
|
|
512
|
+
"category: fact",
|
|
513
|
+
"namespace: unsupported-inline-namespace",
|
|
514
|
+
"</memory_note>",
|
|
515
|
+
].join("\n"),
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
assert.equal(result.queued, 1);
|
|
519
|
+
assert.equal(result.failed, 0);
|
|
520
|
+
assert.equal(probe.envelopes[0]?.source, "explicit-inline-review");
|
|
521
|
+
assert.equal(probe.memories.length, 1);
|
|
522
|
+
assert.ok(probe.requestedNamespaces.every((namespace) => namespace === "default"));
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
test("inline capture review fallback replays within one authorized review namespace", async () => {
|
|
527
|
+
const probe = createInlineCaptureProcessorProbe();
|
|
528
|
+
const config = probe.orchestrator.config as PluginConfig;
|
|
529
|
+
config.namespacePolicies = [{ name: "review-a", readPrincipals: ["*"], writePrincipals: ["*"] }];
|
|
530
|
+
const content = [
|
|
531
|
+
"<memory_note>",
|
|
532
|
+
"content: A same-principal invalid note must queue once.",
|
|
533
|
+
"category: fact",
|
|
534
|
+
"namespace: unsupported-inline-namespace",
|
|
535
|
+
"</memory_note>",
|
|
536
|
+
].join("\n");
|
|
537
|
+
const first = await probe.processor.process({
|
|
538
|
+
captureMode: "hybrid",
|
|
539
|
+
content,
|
|
540
|
+
dedupeKeys: ["delivery-a"],
|
|
541
|
+
reviewNamespace: "review-a",
|
|
542
|
+
reviewNamespacePreResolved: true,
|
|
543
|
+
});
|
|
544
|
+
const restarted = new InlineExplicitCaptureProcessor(probe.orchestrator, {
|
|
545
|
+
sourceConnector: "openclaw",
|
|
546
|
+
});
|
|
547
|
+
const second = await restarted.process({
|
|
548
|
+
captureMode: "hybrid",
|
|
549
|
+
content,
|
|
550
|
+
dedupeKeys: ["delivery-b"],
|
|
551
|
+
reviewNamespace: "review-a",
|
|
552
|
+
reviewNamespacePreResolved: true,
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
assert.equal(first.queued, 1);
|
|
556
|
+
assert.equal(second.duplicates, 1);
|
|
557
|
+
assert.equal(probe.memories.length, 1);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
test("inline capture processor rejects pre-resolved input without a resolved namespace", async () => {
|
|
561
|
+
const probe = createInlineCaptureProcessorProbe();
|
|
562
|
+
const content = [
|
|
563
|
+
"<memory_note>",
|
|
564
|
+
"content: A caller must authorize this namespace before writing.",
|
|
565
|
+
"category: fact",
|
|
566
|
+
"namespace: another-principal",
|
|
567
|
+
"</memory_note>",
|
|
568
|
+
].join("\n");
|
|
569
|
+
|
|
570
|
+
await assert.rejects(
|
|
571
|
+
probe.processor.process({
|
|
572
|
+
captureMode: "hybrid",
|
|
573
|
+
content,
|
|
574
|
+
dedupeKeys: ["message-untrusted-namespace"],
|
|
575
|
+
namespacePreResolved: true,
|
|
576
|
+
}),
|
|
577
|
+
/namespacePreResolved requires a resolved namespace/
|
|
578
|
+
);
|
|
579
|
+
assert.equal(probe.envelopes.length, 0);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
test("inline capture processor bounds non-finite dedupe limits", async () => {
|
|
583
|
+
const probe = createInlineCaptureProcessorProbe();
|
|
584
|
+
const processor = new InlineExplicitCaptureProcessor(probe.orchestrator, {
|
|
585
|
+
maxDedupeKeys: Number.POSITIVE_INFINITY,
|
|
586
|
+
sourceConnector: "openclaw",
|
|
587
|
+
});
|
|
588
|
+
const contentFor = (index: number) =>
|
|
589
|
+
[
|
|
590
|
+
"<memory_note>",
|
|
591
|
+
`content: A bounded dedupe capture item number ${index}.`,
|
|
592
|
+
"category: fact",
|
|
593
|
+
"</memory_note>",
|
|
594
|
+
].join("\n");
|
|
595
|
+
|
|
596
|
+
for (let index = 0; index <= 1024; index += 1) {
|
|
597
|
+
await processor.process({
|
|
598
|
+
captureMode: "hybrid",
|
|
599
|
+
content: contentFor(index),
|
|
600
|
+
dedupeKeys: [`message-${index}`],
|
|
601
|
+
});
|
|
602
|
+
probe.memories.length = 0;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const replay = await processor.process({
|
|
606
|
+
captureMode: "hybrid",
|
|
607
|
+
content: contentFor(0),
|
|
608
|
+
dedupeKeys: ["message-0"],
|
|
609
|
+
});
|
|
610
|
+
assert.equal(replay.processed, 1);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
test("inline capture processor keeps a replay key for fractional dedupe limits", async () => {
|
|
614
|
+
const probe = createInlineCaptureProcessorProbe();
|
|
615
|
+
const processor = new InlineExplicitCaptureProcessor(probe.orchestrator, {
|
|
616
|
+
maxDedupeKeys: 0.5,
|
|
617
|
+
sourceConnector: "openclaw",
|
|
618
|
+
});
|
|
619
|
+
const request = {
|
|
620
|
+
captureMode: "hybrid" as const,
|
|
621
|
+
content: [
|
|
622
|
+
"<memory_note>",
|
|
623
|
+
"content: A fractional dedupe limit must retain this replay key.",
|
|
624
|
+
"category: fact",
|
|
625
|
+
"</memory_note>",
|
|
626
|
+
].join("\n"),
|
|
627
|
+
dedupeKeys: ["fractional-limit-message"],
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
await processor.process(request);
|
|
631
|
+
const replay = await processor.process(request);
|
|
632
|
+
assert.equal(replay.processed, 0);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
test("inline capture processor scopes replay keys to the authorized namespace", async () => {
|
|
636
|
+
const probe = createInlineCaptureProcessorProbe();
|
|
637
|
+
const content = [
|
|
638
|
+
"<memory_note>",
|
|
639
|
+
"content: A scoped replay key must not suppress another namespace.",
|
|
640
|
+
"category: fact",
|
|
641
|
+
"</memory_note>",
|
|
642
|
+
].join("\n");
|
|
643
|
+
const request = {
|
|
644
|
+
captureMode: "hybrid" as const,
|
|
645
|
+
content,
|
|
646
|
+
dedupeKeys: ["shared-delivery"],
|
|
647
|
+
namespacePreResolved: true,
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
const first = await probe.processor.process({ ...request, namespace: "principal-one" });
|
|
651
|
+
const second = await probe.processor.process({ ...request, namespace: "principal-two" });
|
|
652
|
+
|
|
653
|
+
assert.equal(first.processed, 1);
|
|
654
|
+
assert.equal(second.processed, 1);
|
|
655
|
+
assert.ok(probe.requestedNamespaces.includes("principal-one"));
|
|
656
|
+
assert.ok(probe.requestedNamespaces.includes("principal-two"));
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test("inline capture processor scopes replay keys to the source connector", async () => {
|
|
660
|
+
const probe = createInlineCaptureProcessorProbe({ tombstoneBlocked: true });
|
|
661
|
+
const request = {
|
|
662
|
+
captureMode: "hybrid" as const,
|
|
663
|
+
content: [
|
|
664
|
+
"<memory_note>",
|
|
665
|
+
"content: A replay key must not cross an authenticated connector boundary.",
|
|
666
|
+
"category: fact",
|
|
667
|
+
"</memory_note>",
|
|
668
|
+
].join("\n"),
|
|
669
|
+
dedupeKeys: ["shared-delivery"],
|
|
670
|
+
namespace: "principal-one",
|
|
671
|
+
namespacePreResolved: true,
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
const first = await probe.processor.process({ ...request, sourceConnector: "connector-one" });
|
|
675
|
+
const second = await probe.processor.process({ ...request, sourceConnector: "connector-two" });
|
|
676
|
+
|
|
677
|
+
assert.equal(first.queued, 1);
|
|
678
|
+
assert.equal(second.queued, 1);
|
|
679
|
+
assert.equal(probe.envelopes.length, 2);
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
test("inline capture processor canonicalizes reordered note fields for replay dedupe", async () => {
|
|
683
|
+
const probe = createInlineCaptureProcessorProbe({ tombstoneBlocked: true });
|
|
684
|
+
const first = await probe.processor.process({
|
|
685
|
+
captureMode: "hybrid",
|
|
686
|
+
content: [
|
|
687
|
+
"<memory_note>",
|
|
688
|
+
"content: Reordered note fields must share one replay identity.",
|
|
689
|
+
"category: fact",
|
|
690
|
+
"tags: release, capture",
|
|
691
|
+
"</memory_note>",
|
|
692
|
+
].join("\n"),
|
|
693
|
+
dedupeKeys: ["reordered-delivery"],
|
|
694
|
+
});
|
|
695
|
+
const replay = await probe.processor.process({
|
|
696
|
+
captureMode: "hybrid",
|
|
697
|
+
content: [
|
|
698
|
+
"<memory_note>",
|
|
699
|
+
"tags: capture, release",
|
|
700
|
+
"category: fact",
|
|
701
|
+
"content: reordered note fields must share ONE replay identity.",
|
|
702
|
+
"</memory_note>",
|
|
703
|
+
].join("\n"),
|
|
704
|
+
dedupeKeys: ["reordered-delivery"],
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
assert.equal(first.queued, 1);
|
|
708
|
+
assert.equal(replay.processed, 0);
|
|
709
|
+
assert.equal(probe.envelopes.length, 1);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
test("inline capture processor reports failed review fallback without claiming capture handling", async () => {
|
|
713
|
+
const probe = createInlineCaptureProcessorProbe({ failWrites: true });
|
|
714
|
+
const result = await probe.processor.process({
|
|
715
|
+
captureMode: "hybrid",
|
|
716
|
+
content: [
|
|
717
|
+
"Keep this visible turn.",
|
|
718
|
+
"<memory_note>",
|
|
719
|
+
"content: This note must remain retryable after both writes fail.",
|
|
720
|
+
"category: fact",
|
|
721
|
+
"</memory_note>",
|
|
722
|
+
].join("\n"),
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
assert.equal(result.processed, 0);
|
|
726
|
+
assert.equal(result.accepted, 0);
|
|
727
|
+
assert.equal(result.queued, 0);
|
|
728
|
+
assert.equal(result.failed, 1);
|
|
729
|
+
assert.equal(result.content, "Keep this visible turn.");
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
test("inline capture does not queue review after a validated persistence failure", async () => {
|
|
733
|
+
const probe = createInlineCaptureProcessorProbe({ failPrimaryWrites: true });
|
|
734
|
+
const result = await probe.processor.process({
|
|
735
|
+
captureMode: "hybrid",
|
|
736
|
+
content: [
|
|
737
|
+
"Keep this visible turn.",
|
|
738
|
+
"<memory_note>",
|
|
739
|
+
"content: A validated note must remain retryable after primary persistence fails.",
|
|
740
|
+
"category: fact",
|
|
741
|
+
"</memory_note>",
|
|
742
|
+
].join("\n"),
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
assert.equal(result.processed, 0);
|
|
746
|
+
assert.equal(result.accepted, 0);
|
|
747
|
+
assert.equal(result.queued, 0);
|
|
748
|
+
assert.equal(result.failed, 1);
|
|
749
|
+
assert.equal(probe.envelopes.length, 0);
|
|
750
|
+
assert.equal(probe.memories.length, 0);
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
test("inline capture defers bounded lock contention for a later retry", async () => {
|
|
754
|
+
const probe = createInlineCaptureProcessorProbe({ lockBusyAttempts: 2 });
|
|
755
|
+
const request = {
|
|
756
|
+
captureMode: "hybrid" as const,
|
|
757
|
+
content: [
|
|
758
|
+
"Keep this visible turn.",
|
|
759
|
+
"<memory_note>",
|
|
760
|
+
"content: A busy capture lock must remain retryable, not become review state.",
|
|
761
|
+
"category: fact",
|
|
762
|
+
"</memory_note>",
|
|
763
|
+
].join("\n"),
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
const deferred = await probe.processor.process(request);
|
|
767
|
+
assert.equal(deferred.processed, 0);
|
|
768
|
+
assert.equal(deferred.queued, 0);
|
|
769
|
+
assert.equal(deferred.failed, 1);
|
|
770
|
+
assert.equal(probe.envelopes.length, 0);
|
|
771
|
+
|
|
772
|
+
const retry = await probe.processor.process(request);
|
|
773
|
+
assert.equal(retry.accepted, 1);
|
|
774
|
+
assert.equal(retry.queued, 0);
|
|
775
|
+
assert.equal(retry.failed, 0);
|
|
776
|
+
assert.equal(probe.envelopes.length, 1);
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
test("inline capture processor derives a replay key without delivery metadata", async () => {
|
|
780
|
+
const probe = createInlineCaptureProcessorProbe({ tombstoneBlocked: true });
|
|
781
|
+
const request = {
|
|
782
|
+
captureMode: "hybrid" as const,
|
|
783
|
+
content: [
|
|
784
|
+
"<memory_note>",
|
|
785
|
+
"content: An id-less replay must not create a second review capture.",
|
|
786
|
+
"category: fact",
|
|
787
|
+
"</memory_note>",
|
|
788
|
+
].join("\n"),
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
const first = await probe.processor.process(request);
|
|
792
|
+
const replay = await probe.processor.process(request);
|
|
793
|
+
|
|
794
|
+
assert.equal(first.queued, 1);
|
|
795
|
+
assert.equal(replay.processed, 0);
|
|
796
|
+
assert.equal(probe.envelopes.length, 1);
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
test("inline capture processor does not let a fallback suppress a new delivery after durable removal", async () => {
|
|
800
|
+
const probe = createInlineCaptureProcessorProbe({ tombstoneBlocked: true });
|
|
801
|
+
const first = await probe.processor.process({
|
|
802
|
+
captureMode: "hybrid",
|
|
803
|
+
content: [
|
|
804
|
+
"<memory_note>",
|
|
805
|
+
"content: A fallback must not suppress a new delivery after durable removal.",
|
|
806
|
+
"category: fact",
|
|
807
|
+
"tags: original-tag",
|
|
808
|
+
"</memory_note>",
|
|
809
|
+
].join("\n"),
|
|
810
|
+
dedupeKeys: ["first-delivery"],
|
|
811
|
+
});
|
|
812
|
+
probe.memories.length = 0;
|
|
813
|
+
const replay = await probe.processor.process({
|
|
814
|
+
captureMode: "hybrid",
|
|
815
|
+
content: [
|
|
816
|
+
"<memory_note>",
|
|
817
|
+
"content: a fallback must not suppress a NEW delivery after durable removal.",
|
|
818
|
+
"category: fact",
|
|
819
|
+
"tags: replay-tag",
|
|
820
|
+
"</memory_note>",
|
|
821
|
+
].join("\n"),
|
|
822
|
+
dedupeKeys: ["second-delivery"],
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
assert.equal(first.queued, 1);
|
|
826
|
+
assert.equal(replay.queued, 1);
|
|
827
|
+
assert.equal(probe.envelopes.length, 2);
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
test("inline capture processor queues complete notes with empty content", async () => {
|
|
831
|
+
for (const noteFields of [["category: fact"], ["content: |", "category: fact"]]) {
|
|
832
|
+
const probe = createInlineCaptureProcessorProbe();
|
|
833
|
+
const result = await probe.processor.process({
|
|
834
|
+
captureMode: "hybrid",
|
|
835
|
+
content: ["Keep this visible text.", "<memory_note>", ...noteFields, "</memory_note>"].join("\n"),
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
assert.equal(result.content, "Keep this visible text.");
|
|
839
|
+
assert.equal(result.processed, 1);
|
|
840
|
+
assert.equal(result.queued, 1);
|
|
841
|
+
assert.equal(probe.envelopes.length, 1);
|
|
842
|
+
assert.match(probe.envelopes[0]?.content ?? "", /\[empty explicit capture\]/);
|
|
843
|
+
assert.equal(probe.lifecycleEvents[0]?.eventType, "explicit_capture_queued");
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
test("inline capture processor deduplicates tombstone review captures after restart", async () => {
|
|
848
|
+
const probe = createInlineCaptureProcessorProbe({
|
|
849
|
+
tombstoneBlocked: true,
|
|
850
|
+
authoritativeFactHashMiss: true,
|
|
851
|
+
});
|
|
852
|
+
const request = {
|
|
853
|
+
captureMode: "hybrid" as const,
|
|
854
|
+
content: [
|
|
855
|
+
"<memory_note>",
|
|
856
|
+
"content: Tombstone-blocked captures must survive process restarts.",
|
|
857
|
+
"category: fact",
|
|
858
|
+
"</memory_note>",
|
|
859
|
+
].join("\n"),
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
const first = await probe.processor.process(request);
|
|
863
|
+
const restarted = new InlineExplicitCaptureProcessor(probe.orchestrator, {
|
|
864
|
+
sourceConnector: "openclaw",
|
|
865
|
+
});
|
|
866
|
+
const replay = await restarted.process(request);
|
|
867
|
+
|
|
868
|
+
assert.equal(first.queued, 1);
|
|
869
|
+
assert.equal(replay.duplicates, 1);
|
|
870
|
+
assert.equal(replay.queued, 0);
|
|
871
|
+
assert.equal(probe.envelopes.length, 1);
|
|
872
|
+
assert.equal(probe.lifecycleEvents.length, 1);
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
test("inline capture queues safe unsupported namespaces through the authorized review scope", async () => {
|
|
876
|
+
const probe = createInlineCaptureProcessorProbe();
|
|
877
|
+
const result = await probe.processor.process({
|
|
878
|
+
captureMode: "hybrid",
|
|
879
|
+
reviewNamespace: "default",
|
|
880
|
+
reviewNamespacePreResolved: true,
|
|
881
|
+
content: [
|
|
882
|
+
"<memory_note>",
|
|
883
|
+
"content: A safe but unconfigured namespace must remain reviewable.",
|
|
884
|
+
"category: fact",
|
|
885
|
+
"namespace: unconfigured-inline",
|
|
886
|
+
"</memory_note>",
|
|
887
|
+
].join("\n"),
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
assert.equal(result.queued, 1);
|
|
891
|
+
assert.equal(probe.envelopes.length, 1);
|
|
892
|
+
assert.match(probe.envelopes[0]?.content ?? "", /Requested namespace: unconfigured-inline/);
|
|
893
|
+
assert.ok(probe.requestedNamespaces.every((namespace) => namespace === "default"));
|
|
894
|
+
});
|
|
895
|
+
test("inline capture allows a corrected namespace after unsupported review fallback", async () => {
|
|
896
|
+
const probe = createInlineCaptureProcessorProbe();
|
|
897
|
+
const content = "A corrected namespace must not inherit the unsupported fallback replay key.";
|
|
898
|
+
const unsupported = await probe.processor.process({
|
|
899
|
+
captureMode: "hybrid",
|
|
900
|
+
content: [
|
|
901
|
+
"<memory_note>",
|
|
902
|
+
`content: ${content}`,
|
|
903
|
+
"category: fact",
|
|
904
|
+
"namespace: unsupported-inline-namespace",
|
|
905
|
+
"</memory_note>",
|
|
906
|
+
].join("\n"),
|
|
907
|
+
});
|
|
908
|
+
const corrected = await probe.processor.process({
|
|
909
|
+
captureMode: "hybrid",
|
|
910
|
+
namespace: "default",
|
|
911
|
+
namespacePreResolved: true,
|
|
912
|
+
content: [
|
|
913
|
+
"<memory_note>",
|
|
914
|
+
`content: ${content}`,
|
|
915
|
+
"category: fact",
|
|
916
|
+
"</memory_note>",
|
|
917
|
+
].join("\n"),
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
assert.equal(unsupported.queued, 1);
|
|
921
|
+
assert.equal(corrected.accepted, 1);
|
|
922
|
+
assert.equal(probe.envelopes.length, 2);
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
test("inline capture review fallback scans the default root when namespaces are disabled", async () => {
|
|
926
|
+
const probe = createInlineCaptureProcessorProbe();
|
|
927
|
+
(probe.orchestrator.config as PluginConfig).namespacesEnabled = false;
|
|
928
|
+
const request = {
|
|
929
|
+
captureMode: "hybrid" as const,
|
|
930
|
+
content: [
|
|
931
|
+
"<memory_note>",
|
|
932
|
+
"content: A default-root review must deduplicate when namespaces are disabled.",
|
|
933
|
+
"category: fact",
|
|
934
|
+
"confidence: invalid",
|
|
935
|
+
"</memory_note>",
|
|
936
|
+
].join("\n"),
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
const first = await probe.processor.process(request);
|
|
940
|
+
const restarted = new InlineExplicitCaptureProcessor(probe.orchestrator, {
|
|
941
|
+
sourceConnector: "openclaw",
|
|
942
|
+
});
|
|
943
|
+
const second = await restarted.process(request);
|
|
944
|
+
|
|
945
|
+
assert.equal(first.queued, 1);
|
|
946
|
+
assert.equal(second.duplicates, 1);
|
|
947
|
+
assert.equal(probe.envelopes.length, 1);
|
|
948
|
+
assert.ok(probe.requestedNamespaces.includes(undefined));
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
test("inline capture accepts corrected metadata after queuing a validation failure", async () => {
|
|
952
|
+
const probe = createInlineCaptureProcessorProbe();
|
|
953
|
+
const content = "Corrected metadata must not be suppressed by an earlier review.";
|
|
954
|
+
const invalid = await probe.processor.process({
|
|
955
|
+
captureMode: "hybrid",
|
|
956
|
+
dedupeKeys: ["invalid-delivery"],
|
|
957
|
+
content: ["<memory_note>", `content: ${content}`, "category: fact", "confidence: abc", "</memory_note>"].join("\n"),
|
|
958
|
+
});
|
|
959
|
+
const corrected = await probe.processor.process({
|
|
960
|
+
captureMode: "hybrid",
|
|
961
|
+
dedupeKeys: ["corrected-delivery"],
|
|
962
|
+
content: ["<memory_note>", `content: ${content}`, "category: fact", "confidence: 0.8", "</memory_note>"].join("\n"),
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
assert.equal(invalid.queued, 1);
|
|
966
|
+
assert.equal(corrected.accepted, 1);
|
|
967
|
+
assert.equal(probe.envelopes.length, 2);
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
test("inline capture processes a corrected sibling after queuing invalid metadata", async () => {
|
|
971
|
+
const probe = createInlineCaptureProcessorProbe();
|
|
972
|
+
const content = "A corrected sibling must survive the invalid review fallback.";
|
|
973
|
+
const result = await probe.processor.process({
|
|
974
|
+
captureMode: "hybrid",
|
|
975
|
+
dedupeKeys: ["shared-delivery"],
|
|
976
|
+
content: [
|
|
977
|
+
"<memory_note>",
|
|
978
|
+
`content: ${content}`,
|
|
979
|
+
"category: fact",
|
|
980
|
+
"confidence: invalid",
|
|
981
|
+
"</memory_note>",
|
|
982
|
+
"<memory_note>",
|
|
983
|
+
`content: ${content}`,
|
|
984
|
+
"category: fact",
|
|
985
|
+
"confidence: 0.8",
|
|
986
|
+
"</memory_note>",
|
|
987
|
+
].join("\n"),
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
assert.equal(result.queued, 1);
|
|
991
|
+
assert.equal(result.accepted, 1);
|
|
992
|
+
assert.equal(probe.envelopes.length, 2);
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
test("inline capture deduplicates an invalid review replay", async () => {
|
|
996
|
+
const probe = createInlineCaptureProcessorProbe();
|
|
997
|
+
const request = {
|
|
998
|
+
captureMode: "hybrid" as const,
|
|
999
|
+
dedupeKeys: ["invalid-review-delivery"],
|
|
1000
|
+
content: [
|
|
1001
|
+
"<memory_note>",
|
|
1002
|
+
"content: An invalid review replay should queue only once.",
|
|
1003
|
+
"category: fact",
|
|
1004
|
+
"confidence: invalid",
|
|
1005
|
+
"</memory_note>",
|
|
1006
|
+
].join("\n"),
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
const first = await probe.processor.process(request);
|
|
1010
|
+
const replay = await probe.processor.process(request);
|
|
1011
|
+
|
|
1012
|
+
assert.equal(first.queued, 1);
|
|
1013
|
+
assert.equal(replay.processed, 0);
|
|
1014
|
+
assert.equal(probe.envelopes.length, 1);
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
test("inline capture keeps invalid notes distinct across delivery keys", async () => {
|
|
1018
|
+
const probe = createInlineCaptureProcessorProbe();
|
|
1019
|
+
const content = [
|
|
1020
|
+
"<memory_note>",
|
|
1021
|
+
"content: The same invalid note may arrive as a new delivery.",
|
|
1022
|
+
"category: fact",
|
|
1023
|
+
"confidence: invalid",
|
|
1024
|
+
"</memory_note>",
|
|
1025
|
+
].join("\n");
|
|
1026
|
+
const first = await probe.processor.process({
|
|
1027
|
+
captureMode: "hybrid",
|
|
1028
|
+
dedupeKeys: ["invalid-delivery-a"],
|
|
1029
|
+
content,
|
|
1030
|
+
});
|
|
1031
|
+
probe.memories.length = 0;
|
|
1032
|
+
const second = await probe.processor.process({
|
|
1033
|
+
captureMode: "hybrid",
|
|
1034
|
+
dedupeKeys: ["invalid-delivery-b"],
|
|
1035
|
+
content,
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
assert.equal(first.queued, 1);
|
|
1039
|
+
assert.equal(second.queued, 1);
|
|
1040
|
+
assert.equal(probe.envelopes.length, 2);
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
test("explicit capture preserves tombstone-blocked status for duplicate pending review rows", async () => {
|
|
1044
|
+
const probe = createInlineCaptureProcessorProbe({
|
|
1045
|
+
tombstoneBlocked: true,
|
|
1046
|
+
authoritativeFactHashMiss: true,
|
|
1047
|
+
});
|
|
1048
|
+
const candidate = validateExplicitCaptureInput({
|
|
1049
|
+
content: "A duplicate pending review capture must remain queued.",
|
|
1050
|
+
category: "fact",
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
const first = await persistExplicitCapture(probe.orchestrator, candidate, "memory_store");
|
|
1054
|
+
|
|
1055
|
+
const duplicate = await persistExplicitCapture(probe.orchestrator, candidate, "memory_store");
|
|
1056
|
+
|
|
1057
|
+
assert.equal(first.tombstoneBlocked, true);
|
|
1058
|
+
assert.equal(duplicate.duplicateOf, first.id);
|
|
1059
|
+
assert.equal(duplicate.tombstoneBlocked, true);
|
|
1060
|
+
assert.equal(probe.envelopes.length, 1);
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
test("explicit capture preserves duplicateOf when a blocked write coalesces", async () => {
|
|
1064
|
+
const probe = createInlineCaptureProcessorProbe({
|
|
1065
|
+
tombstoneBlocked: true,
|
|
1066
|
+
authoritativeFactHashMiss: true,
|
|
1067
|
+
});
|
|
1068
|
+
const originalWrite = probe.storage.writeSealedMemory;
|
|
1069
|
+
probe.storage.writeSealedMemory = async (envelope) => {
|
|
1070
|
+
const result = await originalWrite(envelope);
|
|
1071
|
+
return { ...result, duplicateOf: result.id };
|
|
1072
|
+
};
|
|
1073
|
+
const candidate = validateExplicitCaptureInput({
|
|
1074
|
+
content: "A coalesced blocked write must remain a duplicate.",
|
|
1075
|
+
category: "fact",
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
const result = await persistExplicitCapture(probe.orchestrator, candidate, "memory_store");
|
|
1079
|
+
|
|
1080
|
+
assert.equal(result.duplicateOf, result.id);
|
|
1081
|
+
assert.equal(result.tombstoneBlocked, true);
|
|
1082
|
+
assert.equal(probe.lifecycleEvents.length, 0);
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
test("explicit capture confirms blocked duplicates in the cold tier", async () => {
|
|
1086
|
+
const probe = createInlineCaptureProcessorProbe({
|
|
1087
|
+
tombstoneBlocked: true,
|
|
1088
|
+
authoritativeFactHashMiss: true,
|
|
1089
|
+
});
|
|
1090
|
+
const candidate = validateExplicitCaptureInput({
|
|
1091
|
+
content: "A cold blocked capture must still deduplicate.",
|
|
1092
|
+
category: "fact",
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
const first = await persistExplicitCapture(probe.orchestrator, candidate, "memory_store");
|
|
1096
|
+
const cold = probe.memories.shift();
|
|
1097
|
+
if (!cold) throw new Error("expected the first capture to persist");
|
|
1098
|
+
probe.coldMemories.push(cold);
|
|
1099
|
+
const duplicate = await persistExplicitCapture(probe.orchestrator, candidate, "memory_store");
|
|
1100
|
+
|
|
1101
|
+
assert.equal(duplicate.duplicateOf, first.id);
|
|
1102
|
+
assert.equal(duplicate.tombstoneBlocked, true);
|
|
1103
|
+
assert.equal(probe.envelopes.length, 1);
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
test("explicit capture keeps the authoritative fact-hash miss fast path", async () => {
|
|
1107
|
+
const probe = createInlineCaptureProcessorProbe({
|
|
1108
|
+
authoritativeFactHashMiss: true,
|
|
1109
|
+
tombstoneBlockedCaptureIndexHit: false,
|
|
1110
|
+
});
|
|
1111
|
+
const candidate = validateExplicitCaptureInput({
|
|
1112
|
+
content: "A novel fact should not scan the corpus after an indexed miss.",
|
|
1113
|
+
category: "fact",
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
const result = await persistExplicitCapture(probe.orchestrator, candidate, "memory_store");
|
|
1117
|
+
|
|
1118
|
+
assert.equal(result.duplicateOf, undefined);
|
|
1119
|
+
assert.deepEqual(probe.blockedIndexArguments(), [
|
|
1120
|
+
{
|
|
1121
|
+
content: candidate.content,
|
|
1122
|
+
category: "fact",
|
|
1123
|
+
sourceConnector: undefined,
|
|
1124
|
+
},
|
|
1125
|
+
]);
|
|
1126
|
+
assert.equal(probe.envelopes.length, 1);
|
|
1127
|
+
assert.equal(probe.readAllCalls(), 0);
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
test("explicit capture scans active rows when the blocked index is unavailable", async () => {
|
|
1131
|
+
const probe = createInlineCaptureProcessorProbe({ authoritativeFactHashMiss: true });
|
|
1132
|
+
const candidate = validateExplicitCaptureInput({
|
|
1133
|
+
content: "An active duplicate must survive an unavailable blocked index.",
|
|
1134
|
+
category: "fact",
|
|
1135
|
+
});
|
|
1136
|
+
probe.memories.push({
|
|
1137
|
+
frontmatter: {
|
|
1138
|
+
id: "active-existing",
|
|
1139
|
+
category: "fact",
|
|
1140
|
+
status: "active",
|
|
1141
|
+
},
|
|
1142
|
+
content: candidate.content,
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
const result = await persistExplicitCapture(probe.orchestrator, candidate, "memory_store");
|
|
1146
|
+
|
|
1147
|
+
assert.equal(result.duplicateOf, "active-existing");
|
|
1148
|
+
assert.equal(probe.envelopes.length, 0);
|
|
1149
|
+
});
|