@remnic/core 9.3.751 → 9.3.752
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 +5 -5
- package/dist/access-boundary.js +29 -29
- package/dist/access-cli.js +88 -88
- package/dist/access-http.d.ts +5 -5
- package/dist/access-http.js +35 -35
- package/dist/access-mcp.d.ts +5 -5
- package/dist/access-mcp.js +34 -34
- package/dist/access-operations-batch.js +31 -31
- package/dist/access-operations.d.ts +5 -5
- package/dist/access-operations.js +33 -33
- package/dist/access-recall-surface.d.ts +363 -0
- package/dist/access-recall-surface.js +128 -0
- package/dist/access-schema.js +7 -7
- package/dist/{access-service-DmAEOJdJ.d.ts → access-service-C2cOHQvg.d.ts} +7 -26
- package/dist/access-service.d.ts +5 -5
- package/dist/access-service.js +28 -28
- package/dist/access-surface-catalog.d.ts +5 -5
- 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/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +4 -4
- 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/buffer.js +2 -2
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/{capsule-crypto-7FJQINUR.js → capsule-crypto-CZJSLEFG.js} +3 -3
- package/dist/{catalog-BpOcwHdE.d.ts → catalog-D_FvzTM0.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 +9 -9
- package/dist/causal-retrieval.js +2 -2
- package/dist/causal-trajectory-graph.d.ts +1 -1
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-2NLLXCJG.js → chunk-2BFLDH5F.js} +2 -2
- package/dist/{chunk-UU32EAIY.js → chunk-2DPCAW2T.js} +5 -5
- package/dist/{chunk-5PZCDUJ6.js → chunk-2U4E34Y3.js} +4 -4
- package/dist/{chunk-JRZV3767.js → chunk-33NX5BLR.js} +4 -4
- package/dist/{chunk-X7Y7WX73.js → chunk-437S3G37.js} +2 -2
- package/dist/{chunk-HAZVVOYT.js → chunk-5DXUE7GB.js} +2 -2
- package/dist/{chunk-4DLFJJOQ.js → chunk-5JAT4ZJD.js} +4 -4
- package/dist/{chunk-RS25QOKZ.js → chunk-64SE4X5G.js} +2 -2
- package/dist/{chunk-QRRBXD24.js → chunk-67JRXV6Z.js} +7 -7
- package/dist/{chunk-WA7FECSG.js → chunk-6ROCH5JV.js} +7 -7
- package/dist/{chunk-5QANTTY2.js → chunk-BIAMJUUJ.js} +4 -4
- package/dist/{chunk-IKHKHADN.js → chunk-C4LNKQ3M.js} +5 -5
- package/dist/{chunk-B35L45HZ.js → chunk-DC5OBZS3.js} +2 -2
- package/dist/{chunk-ORAK3LNZ.js → chunk-ER3CCWK2.js} +2 -2
- package/dist/{chunk-SD34EK4Z.js → chunk-FEELYTRT.js} +5 -5
- package/dist/{chunk-KQAFEZQX.js → chunk-FLVL4L3L.js} +2 -2
- package/dist/{chunk-WIWPSQYU.js → chunk-FXRFFYWD.js} +4 -4
- package/dist/{chunk-W3M34XT4.js → chunk-GECEETQ3.js} +4 -4
- package/dist/{chunk-WWSXVOGY.js → chunk-GFGYHLON.js} +2 -2
- package/dist/{chunk-BJMBJZ2Y.js → chunk-GWTFLF2P.js} +59 -59
- package/dist/chunk-GWTFLF2P.js.map +1 -0
- package/dist/{chunk-WOX2UW4K.js → chunk-IBPEVGP5.js} +2 -2
- package/dist/{chunk-JZ7P26GF.js → chunk-IQWFWSCP.js} +4 -4
- package/dist/{chunk-6SXNQFHB.js → chunk-J2JB6BHT.js} +8 -8
- package/dist/{chunk-K2JM4DZZ.js → chunk-JHZ3LEM7.js} +2 -2
- package/dist/{chunk-YGGXUNS4.js → chunk-JNKSFIAY.js} +45 -45
- package/dist/{chunk-CGZWGVQF.js → chunk-K5CWCJKJ.js} +6 -6
- package/dist/{chunk-TPLE6SNY.js → chunk-KFNG72W7.js} +7 -7
- package/dist/{chunk-4NWIGAIC.js → chunk-KT2VQLP7.js} +8 -8
- package/dist/{chunk-6FNZ3NMS.js → chunk-L3VSTJ2V.js} +2 -2
- package/dist/{chunk-ARV3AUOM.js → chunk-M5QKGHCR.js} +2 -2
- package/dist/{chunk-E4PFDD3N.js → chunk-MEPWWSTD.js} +4 -4
- package/dist/{chunk-V5RVMULT.js → chunk-MLDAD27I.js} +7 -7
- package/dist/{chunk-K2R3DEEH.js → chunk-MTN2G4DT.js} +2 -2
- package/dist/{chunk-DBSMLRTO.js → chunk-MW7UUXTA.js} +5 -5
- package/dist/{chunk-OSMBC3JU.js → chunk-OKOF7ITI.js} +6 -6
- package/dist/{chunk-DBRVU5PR.js → chunk-OVMLS7TN.js} +3 -3
- package/dist/{chunk-NNL4LJAD.js → chunk-QMMCMV2L.js} +5 -5
- package/dist/{chunk-RNWWAS5V.js → chunk-QRM3K3XT.js} +5 -5
- package/dist/{chunk-HM6UJPPO.js → chunk-R7TGDLV3.js} +4 -4
- package/dist/{chunk-EEKDRAWX.js → chunk-RBYV3WSB.js} +4 -4
- package/dist/{chunk-LOPZ2DWR.js → chunk-RRRZ3CLN.js} +4 -4
- package/dist/{chunk-7TWA7DKP.js → chunk-S5UYWK3O.js} +6 -6
- package/dist/{chunk-QOGXUNWL.js → chunk-SC5AEZLU.js} +9 -9
- package/dist/{chunk-SNEXVX3U.js → chunk-UFNQKZV7.js} +15 -15
- package/dist/{chunk-W3BFT5OJ.js → chunk-V4ZPLQLC.js} +2 -2
- package/dist/{chunk-SQNED75R.js → chunk-V5UV7BOA.js} +1 -1
- package/dist/{chunk-RWZB6MAY.js → chunk-VQL22SHO.js} +891 -1002
- package/dist/chunk-VQL22SHO.js.map +1 -0
- package/dist/{chunk-UETBEHBA.js → chunk-VX2RYM2K.js} +2232 -1942
- package/dist/chunk-VX2RYM2K.js.map +1 -0
- package/dist/{chunk-K5LLP4CY.js → chunk-XKAOT4ZB.js} +4 -4
- package/dist/{chunk-ZBVQ4ZM4.js → chunk-YGNQG4Q6.js} +8 -8
- package/dist/{chunk-EZ25VE3G.js → chunk-YNDLCWXS.js} +4 -4
- package/dist/{chunk-HYI2GP3F.js → chunk-ZGGI3COG.js} +9 -9
- package/dist/{cli-lmf5RGyQ.d.ts → cli-DFUU9WYu.d.ts} +3 -3
- package/dist/cli.d.ts +6 -6
- package/dist/cli.js +60 -60
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +6 -6
- 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 +5 -5
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +7 -7
- 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/contradiction/index.js +2 -2
- package/dist/{contradiction-scan-3FJYWS3G.js → contradiction-scan-KO4WWCS4.js} +3 -3
- package/dist/contradiction-scan-KO4WWCS4.js.map +1 -0
- 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/dashboard-runtime.js +2 -2
- package/dist/day-summary.d.ts +1 -1
- package/dist/day-summary.js +2 -2
- 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/embedding-fallback.js +2 -2
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +5 -5
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +4 -4
- package/dist/explicit-capture.js +2 -2
- 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/extraction.js +4 -4
- package/dist/fallback-llm.d.ts +1 -1
- 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/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +9 -9
- package/dist/index.js +142 -142
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/engine.js +3 -3
- 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/local-llm.js +2 -2
- package/dist/local-model-endpoint.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 +5 -5
- 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 +10 -10
- 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 +6 -6
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +17 -17
- package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
- package/dist/orchestration/compression-guideline-coordinator.js +2 -2
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +10 -10
- package/dist/{orchestrator-D7bb8ZGp.d.ts → orchestrator-BhxMxtQJ.d.ts} +27 -106
- package/dist/orchestrator.d.ts +4 -4
- package/dist/orchestrator.js +86 -86
- 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-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-renderer.d.ts +1 -1
- package/dist/recall-xray.d.ts +1 -1
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +1 -1
- 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/routing/store.js +2 -2
- package/dist/schemas.d.ts +22 -22
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/embed-helper.js +2 -2
- 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 +3 -3
- 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-CTfbVwCA.d.ts → semantic-consolidation-BrsoFeMP.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +7 -7
- 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/shared-context/manager.js +2 -2
- package/dist/signal.d.ts +1 -1
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +3 -3
- package/dist/summarizer.d.ts +1 -1
- package/dist/summarizer.js +3 -3
- 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/transfer/backup.js +3 -3
- package/dist/transfer/capsule-export.js +3 -3
- package/dist/transfer/capsule-import.js +3 -3
- 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-D4NYDtXI.d.ts → types-BfI4uJeb.d.ts} +1 -1
- package/dist/types.d.ts +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-recall-surface.ts +1552 -0
- package/src/access-service.ts +29 -1219
- package/src/orchestration/recall-entry.ts +290 -0
- package/src/orchestration/session-context.ts +407 -0
- package/src/orchestrator.ts +125 -462
- package/dist/chunk-BJMBJZ2Y.js.map +0 -1
- package/dist/chunk-RWZB6MAY.js.map +0 -1
- package/dist/chunk-UETBEHBA.js.map +0 -1
- /package/dist/{capsule-crypto-7FJQINUR.js.map → access-recall-surface.js.map} +0 -0
- /package/dist/{contradiction-scan-3FJYWS3G.js.map → capsule-crypto-CZJSLEFG.js.map} +0 -0
- /package/dist/{chunk-2NLLXCJG.js.map → chunk-2BFLDH5F.js.map} +0 -0
- /package/dist/{chunk-UU32EAIY.js.map → chunk-2DPCAW2T.js.map} +0 -0
- /package/dist/{chunk-5PZCDUJ6.js.map → chunk-2U4E34Y3.js.map} +0 -0
- /package/dist/{chunk-JRZV3767.js.map → chunk-33NX5BLR.js.map} +0 -0
- /package/dist/{chunk-X7Y7WX73.js.map → chunk-437S3G37.js.map} +0 -0
- /package/dist/{chunk-HAZVVOYT.js.map → chunk-5DXUE7GB.js.map} +0 -0
- /package/dist/{chunk-4DLFJJOQ.js.map → chunk-5JAT4ZJD.js.map} +0 -0
- /package/dist/{chunk-RS25QOKZ.js.map → chunk-64SE4X5G.js.map} +0 -0
- /package/dist/{chunk-QRRBXD24.js.map → chunk-67JRXV6Z.js.map} +0 -0
- /package/dist/{chunk-WA7FECSG.js.map → chunk-6ROCH5JV.js.map} +0 -0
- /package/dist/{chunk-5QANTTY2.js.map → chunk-BIAMJUUJ.js.map} +0 -0
- /package/dist/{chunk-IKHKHADN.js.map → chunk-C4LNKQ3M.js.map} +0 -0
- /package/dist/{chunk-B35L45HZ.js.map → chunk-DC5OBZS3.js.map} +0 -0
- /package/dist/{chunk-ORAK3LNZ.js.map → chunk-ER3CCWK2.js.map} +0 -0
- /package/dist/{chunk-SD34EK4Z.js.map → chunk-FEELYTRT.js.map} +0 -0
- /package/dist/{chunk-KQAFEZQX.js.map → chunk-FLVL4L3L.js.map} +0 -0
- /package/dist/{chunk-WIWPSQYU.js.map → chunk-FXRFFYWD.js.map} +0 -0
- /package/dist/{chunk-W3M34XT4.js.map → chunk-GECEETQ3.js.map} +0 -0
- /package/dist/{chunk-WWSXVOGY.js.map → chunk-GFGYHLON.js.map} +0 -0
- /package/dist/{chunk-WOX2UW4K.js.map → chunk-IBPEVGP5.js.map} +0 -0
- /package/dist/{chunk-JZ7P26GF.js.map → chunk-IQWFWSCP.js.map} +0 -0
- /package/dist/{chunk-6SXNQFHB.js.map → chunk-J2JB6BHT.js.map} +0 -0
- /package/dist/{chunk-K2JM4DZZ.js.map → chunk-JHZ3LEM7.js.map} +0 -0
- /package/dist/{chunk-YGGXUNS4.js.map → chunk-JNKSFIAY.js.map} +0 -0
- /package/dist/{chunk-CGZWGVQF.js.map → chunk-K5CWCJKJ.js.map} +0 -0
- /package/dist/{chunk-TPLE6SNY.js.map → chunk-KFNG72W7.js.map} +0 -0
- /package/dist/{chunk-4NWIGAIC.js.map → chunk-KT2VQLP7.js.map} +0 -0
- /package/dist/{chunk-6FNZ3NMS.js.map → chunk-L3VSTJ2V.js.map} +0 -0
- /package/dist/{chunk-ARV3AUOM.js.map → chunk-M5QKGHCR.js.map} +0 -0
- /package/dist/{chunk-E4PFDD3N.js.map → chunk-MEPWWSTD.js.map} +0 -0
- /package/dist/{chunk-V5RVMULT.js.map → chunk-MLDAD27I.js.map} +0 -0
- /package/dist/{chunk-K2R3DEEH.js.map → chunk-MTN2G4DT.js.map} +0 -0
- /package/dist/{chunk-DBSMLRTO.js.map → chunk-MW7UUXTA.js.map} +0 -0
- /package/dist/{chunk-OSMBC3JU.js.map → chunk-OKOF7ITI.js.map} +0 -0
- /package/dist/{chunk-DBRVU5PR.js.map → chunk-OVMLS7TN.js.map} +0 -0
- /package/dist/{chunk-NNL4LJAD.js.map → chunk-QMMCMV2L.js.map} +0 -0
- /package/dist/{chunk-RNWWAS5V.js.map → chunk-QRM3K3XT.js.map} +0 -0
- /package/dist/{chunk-HM6UJPPO.js.map → chunk-R7TGDLV3.js.map} +0 -0
- /package/dist/{chunk-EEKDRAWX.js.map → chunk-RBYV3WSB.js.map} +0 -0
- /package/dist/{chunk-LOPZ2DWR.js.map → chunk-RRRZ3CLN.js.map} +0 -0
- /package/dist/{chunk-7TWA7DKP.js.map → chunk-S5UYWK3O.js.map} +0 -0
- /package/dist/{chunk-QOGXUNWL.js.map → chunk-SC5AEZLU.js.map} +0 -0
- /package/dist/{chunk-SNEXVX3U.js.map → chunk-UFNQKZV7.js.map} +0 -0
- /package/dist/{chunk-W3BFT5OJ.js.map → chunk-V4ZPLQLC.js.map} +0 -0
- /package/dist/{chunk-SQNED75R.js.map → chunk-V5UV7BOA.js.map} +0 -0
- /package/dist/{chunk-K5LLP4CY.js.map → chunk-XKAOT4ZB.js.map} +0 -0
- /package/dist/{chunk-ZBVQ4ZM4.js.map → chunk-YGNQG4Q6.js.map} +0 -0
- /package/dist/{chunk-EZ25VE3G.js.map → chunk-YNDLCWXS.js.map} +0 -0
- /package/dist/{chunk-HYI2GP3F.js.map → chunk-ZGGI3COG.js.map} +0 -0
package/src/access-service.ts
CHANGED
|
@@ -251,6 +251,8 @@ import {
|
|
|
251
251
|
import { formatProfileTraceAscii } from "./profiling.js";
|
|
252
252
|
import { resolveAccessSetupCapabilities, resolveGraphConstructionCapabilities, resolveIndexingCapabilities } from "./capabilities.js";
|
|
253
253
|
import { resolveRecallEnhancementCapabilities } from "./capabilities.js";
|
|
254
|
+
import { AccessRecallSurface } from "./access-recall-surface.js";
|
|
255
|
+
import { selfDeps } from "./orchestration/self-deps.js";
|
|
254
256
|
|
|
255
257
|
export class EngramAccessInputError extends Error {}
|
|
256
258
|
|
|
@@ -1305,6 +1307,18 @@ export class EngramAccessService {
|
|
|
1305
1307
|
private readonly budget: CrossNamespaceBudget;
|
|
1306
1308
|
private readonly auditAdapter: AccessAuditAdapter | null;
|
|
1307
1309
|
|
|
1310
|
+
/** AccessRecallSurface (access-service decomposition). Lazy; selfDeps live wiring. */
|
|
1311
|
+
private _accessRecallSurface: AccessRecallSurface | undefined;
|
|
1312
|
+
|
|
1313
|
+
private get accessRecallSurface(): AccessRecallSurface {
|
|
1314
|
+
if (!this._accessRecallSurface) {
|
|
1315
|
+
this._accessRecallSurface = new AccessRecallSurface(
|
|
1316
|
+
selfDeps<ConstructorParameters<typeof AccessRecallSurface>[0]>(this),
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
return this._accessRecallSurface;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1308
1322
|
constructor(private readonly orchestrator: Orchestrator) {
|
|
1309
1323
|
this.idempotency = new AccessIdempotencyStore(orchestrator.config.memoryDir);
|
|
1310
1324
|
const accessCaps = resolveAccessSetupCapabilities(orchestrator.config); // #1566 Cluster B
|
|
@@ -2097,83 +2111,9 @@ export class EngramAccessService {
|
|
|
2097
2111
|
*/
|
|
2098
2112
|
rawExcerptsSuppressed?: boolean;
|
|
2099
2113
|
}): Promise<EngramAccessRecallResponse> {
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
const namespace = options.snapshot.namespace
|
|
2103
|
-
? this.resolveNamespace(options.snapshot.namespace)
|
|
2104
|
-
: this.orchestrator.config.defaultNamespace;
|
|
2105
|
-
const sourcesUsed = Array.from(
|
|
2106
|
-
new Set(options.snapshot.results.map((result) => result.servedBy)),
|
|
2107
|
-
);
|
|
2108
|
-
const snapshotForSerialization: LastRecallSnapshot = {
|
|
2109
|
-
sessionKey: options.sessionKey ?? "",
|
|
2110
|
-
recordedAt: new Date(options.snapshot.capturedAt).toISOString(),
|
|
2111
|
-
queryHash: createHash("sha256").update(options.query).digest("hex"),
|
|
2112
|
-
queryLen: options.query.length,
|
|
2113
|
-
memoryIds,
|
|
2114
|
-
namespace,
|
|
2115
|
-
recallNamespaces: [namespace],
|
|
2116
|
-
traceId: options.snapshot.traceId,
|
|
2117
|
-
plannerMode: options.normalizedMode,
|
|
2118
|
-
requestedMode:
|
|
2119
|
-
options.requestedMode && options.requestedMode !== "auto"
|
|
2120
|
-
? options.requestedMode
|
|
2121
|
-
: undefined,
|
|
2122
|
-
sourcesUsed,
|
|
2123
|
-
budgetsApplied: {
|
|
2124
|
-
appliedTopK: memoryIds.length,
|
|
2125
|
-
recallBudgetChars: options.snapshot.budget.chars,
|
|
2126
|
-
maxMemoryTokens: this.orchestrator.config.maxMemoryTokens,
|
|
2127
|
-
finalContextChars: options.snapshot.budget.used,
|
|
2128
|
-
},
|
|
2129
|
-
latencyMs: Date.now() - options.startedAt,
|
|
2130
|
-
resultPaths,
|
|
2131
|
-
};
|
|
2132
|
-
const results = await this.serializeRecallResults(
|
|
2133
|
-
snapshotForSerialization,
|
|
2134
|
-
options.disclosure,
|
|
2135
|
-
{
|
|
2136
|
-
query: options.query,
|
|
2137
|
-
...(options.sessionKey ? { sessionKey: options.sessionKey } : {}),
|
|
2138
|
-
...(options.rawExcerptNamespace
|
|
2139
|
-
? { rawExcerptNamespace: options.rawExcerptNamespace }
|
|
2140
|
-
: {}),
|
|
2141
|
-
...(options.rawExcerptSessionIds !== undefined
|
|
2142
|
-
? { rawExcerptSessionIds: options.rawExcerptSessionIds }
|
|
2143
|
-
: {}),
|
|
2144
|
-
...(options.rawExcerptsSuppressed
|
|
2145
|
-
? { rawExcerptsSuppressed: options.rawExcerptsSuppressed }
|
|
2146
|
-
: {}),
|
|
2147
|
-
},
|
|
2114
|
+
return this.accessRecallSurface.buildRecallResponseFromXraySnapshot(
|
|
2115
|
+
options,
|
|
2148
2116
|
);
|
|
2149
|
-
const context = results
|
|
2150
|
-
.map((result) => {
|
|
2151
|
-
const content =
|
|
2152
|
-
typeof result.content === "string" && result.content.length > 0
|
|
2153
|
-
? result.content
|
|
2154
|
-
: "";
|
|
2155
|
-
return content || result.preview;
|
|
2156
|
-
})
|
|
2157
|
-
.filter((text) => text.length > 0)
|
|
2158
|
-
.join("\n\n");
|
|
2159
|
-
|
|
2160
|
-
return {
|
|
2161
|
-
query: options.query,
|
|
2162
|
-
...(options.sessionKey ? { sessionKey: options.sessionKey } : {}),
|
|
2163
|
-
namespace,
|
|
2164
|
-
context,
|
|
2165
|
-
count: memoryIds.length,
|
|
2166
|
-
memoryIds,
|
|
2167
|
-
results,
|
|
2168
|
-
recordedAt: snapshotForSerialization.recordedAt,
|
|
2169
|
-
traceId: options.snapshot.traceId,
|
|
2170
|
-
plannerMode: options.normalizedMode,
|
|
2171
|
-
fallbackUsed: sourcesUsed.some((source) => source !== "hybrid"),
|
|
2172
|
-
sourcesUsed,
|
|
2173
|
-
disclosure: options.disclosure,
|
|
2174
|
-
budgetsApplied: snapshotForSerialization.budgetsApplied,
|
|
2175
|
-
latencyMs: snapshotForSerialization.latencyMs,
|
|
2176
|
-
};
|
|
2177
2117
|
}
|
|
2178
2118
|
|
|
2179
2119
|
private async serializeRecallResults(
|
|
@@ -2214,198 +2154,11 @@ export class EngramAccessService {
|
|
|
2214
2154
|
}
|
|
2215
2155
|
| null = null,
|
|
2216
2156
|
): Promise<EngramAccessMemorySummary[]> {
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
const recallNamespaces = Array.from(
|
|
2222
|
-
new Set(
|
|
2223
|
-
[
|
|
2224
|
-
namespace,
|
|
2225
|
-
...(Array.isArray(snapshot.recallNamespaces)
|
|
2226
|
-
? snapshot.recallNamespaces.map((ns) => this.resolveNamespace(ns))
|
|
2227
|
-
: []),
|
|
2228
|
-
this.orchestrator.config.defaultNamespace,
|
|
2229
|
-
this.orchestrator.config.sharedNamespace,
|
|
2230
|
-
...(this.orchestrator.config.namespacePolicies ?? []).map((p) => p.name),
|
|
2231
|
-
].filter((ns): ns is string => typeof ns === "string" && ns.length > 0),
|
|
2232
|
-
),
|
|
2157
|
+
return this.accessRecallSurface.serializeRecallResults(
|
|
2158
|
+
snapshot,
|
|
2159
|
+
disclosure,
|
|
2160
|
+
rawContext,
|
|
2233
2161
|
);
|
|
2234
|
-
const results: EngramAccessMemorySummary[] = [];
|
|
2235
|
-
const seen = new Set<string>();
|
|
2236
|
-
const collectionNamespaceFromPrefix = (collectionPrefix: string): string | null => {
|
|
2237
|
-
const baseCollection = this.orchestrator.config.qmdCollection;
|
|
2238
|
-
if (collectionPrefix === baseCollection) return this.orchestrator.config.defaultNamespace;
|
|
2239
|
-
const namespaceSuffix = collectionPrefix.startsWith(`${baseCollection}--`)
|
|
2240
|
-
? collectionPrefix.slice(baseCollection.length + 2)
|
|
2241
|
-
: "";
|
|
2242
|
-
if (!namespaceSuffix) return null;
|
|
2243
|
-
|
|
2244
|
-
const decoded = namespaceIdentityFromToken(namespaceSuffix);
|
|
2245
|
-
if (decoded !== null) return decoded || this.orchestrator.config.defaultNamespace;
|
|
2246
|
-
if (namespaceSuffix.startsWith("ns--")) {
|
|
2247
|
-
const legacyNamespace = namespaceSuffix.slice("ns--".length).trim();
|
|
2248
|
-
return legacyNamespace || null;
|
|
2249
|
-
}
|
|
2250
|
-
return null;
|
|
2251
|
-
};
|
|
2252
|
-
const readResultPath = async (
|
|
2253
|
-
memoryPath: string,
|
|
2254
|
-
): Promise<{ memory: MemoryFile; baseDir: string } | null> => {
|
|
2255
|
-
const parts = qmdCollectionPathParts(memoryPath);
|
|
2256
|
-
const coldCollection =
|
|
2257
|
-
this.orchestrator.config.qmdColdCollection ?? "openclaw-engram-cold";
|
|
2258
|
-
if (parts && parts.collection === coldCollection) {
|
|
2259
|
-
const storages: Array<{ storage: StorageManager; dir: string }> = [];
|
|
2260
|
-
const seenStorageDirs = new Set<string>();
|
|
2261
|
-
const addStorage = (candidateStorage: StorageManager): void => {
|
|
2262
|
-
const candidateDir = nodePath.resolve(candidateStorage.dir);
|
|
2263
|
-
if (seenStorageDirs.has(candidateDir)) return;
|
|
2264
|
-
seenStorageDirs.add(candidateDir);
|
|
2265
|
-
storages.push({ storage: candidateStorage, dir: candidateDir });
|
|
2266
|
-
};
|
|
2267
|
-
addStorage(storage);
|
|
2268
|
-
for (const recallNamespace of recallNamespaces) {
|
|
2269
|
-
try {
|
|
2270
|
-
addStorage(await this.orchestrator.getStorage(recallNamespace));
|
|
2271
|
-
} catch {
|
|
2272
|
-
continue;
|
|
2273
|
-
}
|
|
2274
|
-
}
|
|
2275
|
-
for (const candidateStorage of storages) {
|
|
2276
|
-
try {
|
|
2277
|
-
const coldRoot = nodePath.join(candidateStorage.dir, "cold");
|
|
2278
|
-
for (const candidatePath of qmdResultPathCandidates(
|
|
2279
|
-
coldRoot,
|
|
2280
|
-
parts.relativePath,
|
|
2281
|
-
)) {
|
|
2282
|
-
const memory =
|
|
2283
|
-
await candidateStorage.storage.readMemoryByPath(candidatePath);
|
|
2284
|
-
if (memory) return { memory, baseDir: candidateStorage.dir };
|
|
2285
|
-
}
|
|
2286
|
-
} catch (err) {
|
|
2287
|
-
if (err instanceof SecureStoreLockedError) throw err;
|
|
2288
|
-
continue;
|
|
2289
|
-
}
|
|
2290
|
-
}
|
|
2291
|
-
return null;
|
|
2292
|
-
}
|
|
2293
|
-
|
|
2294
|
-
const collectionNamespace = parts
|
|
2295
|
-
? collectionNamespaceFromPrefix(parts.collection)
|
|
2296
|
-
: null;
|
|
2297
|
-
|
|
2298
|
-
if (parts && collectionNamespace) {
|
|
2299
|
-
try {
|
|
2300
|
-
const collectionStorage =
|
|
2301
|
-
await this.orchestrator.getStorage(collectionNamespace);
|
|
2302
|
-
for (const candidate of qmdResultPathCandidates(
|
|
2303
|
-
collectionStorage.dir,
|
|
2304
|
-
parts.relativePath,
|
|
2305
|
-
)) {
|
|
2306
|
-
const memory = await collectionStorage.readMemoryByPath(candidate);
|
|
2307
|
-
if (memory) return { memory, baseDir: collectionStorage.dir };
|
|
2308
|
-
}
|
|
2309
|
-
return null;
|
|
2310
|
-
} catch (err) {
|
|
2311
|
-
if (err instanceof SecureStoreLockedError) throw err;
|
|
2312
|
-
return null;
|
|
2313
|
-
}
|
|
2314
|
-
}
|
|
2315
|
-
|
|
2316
|
-
if (nodePath.isAbsolute(memoryPath)) {
|
|
2317
|
-
const ownerStorage = await this.storageForAbsoluteRecallPath(
|
|
2318
|
-
memoryPath,
|
|
2319
|
-
namespace,
|
|
2320
|
-
recallNamespaces,
|
|
2321
|
-
);
|
|
2322
|
-
if (!ownerStorage) return null;
|
|
2323
|
-
for (const candidate of qmdResultPathCandidates(
|
|
2324
|
-
ownerStorage.dir,
|
|
2325
|
-
memoryPath,
|
|
2326
|
-
)) {
|
|
2327
|
-
const memory = await ownerStorage.storage.readMemoryByPath(candidate);
|
|
2328
|
-
if (memory) return { memory, baseDir: ownerStorage.dir };
|
|
2329
|
-
}
|
|
2330
|
-
return null;
|
|
2331
|
-
}
|
|
2332
|
-
|
|
2333
|
-
for (const candidate of qmdResultPathCandidates(storageDir, memoryPath)) {
|
|
2334
|
-
const memory = await storage.readMemoryByPath(candidate);
|
|
2335
|
-
if (memory) return { memory, baseDir: storageDir };
|
|
2336
|
-
}
|
|
2337
|
-
return null;
|
|
2338
|
-
};
|
|
2339
|
-
|
|
2340
|
-
// Pre-fetch raw excerpts once when `disclosure === "raw"` so we don't
|
|
2341
|
-
// hit the LCM archive per-result (issue #677 PR 2/4). Excerpts are
|
|
2342
|
-
// attached to the first result; per-result attribution is reserved
|
|
2343
|
-
// for a future PR if/when the LCM index can be joined to memory ids.
|
|
2344
|
-
// Coerce `null` (non-raw disclosure) to `undefined` so the optional
|
|
2345
|
-
// serializer field is never explicitly `null`.
|
|
2346
|
-
// Namespace for the LCM `${namespace}:${sessionKey}` prefix: prefer the
|
|
2347
|
-
// caller-supplied READ-AUTHORIZATION-GATED `rawExcerptNamespace` (#1505
|
|
2348
|
-
// thread 2f7) so raw disclosure honours the same read gate as normal recall
|
|
2349
|
-
// + `lcmSearch` and never attaches `<principal>-project-*` overlay rows the
|
|
2350
|
-
// gate excludes. Fall back to the snapshot's resolved namespace only when no
|
|
2351
|
-
// gated namespace was threaded (sessionless / legacy callers) — unchanged.
|
|
2352
|
-
const rawExcerptsResult =
|
|
2353
|
-
rawContext?.rawExcerptsSuppressed === true
|
|
2354
|
-
? // Implicit raw recall with NO readable LCM namespace (#1505 thread
|
|
2355
|
-
// NBHWz): emit empty excerpts rather than falling back to the
|
|
2356
|
-
// write/overlay `namespace` the read gate excludes.
|
|
2357
|
-
[]
|
|
2358
|
-
: await this.fetchRawExcerpts(
|
|
2359
|
-
disclosure,
|
|
2360
|
-
rawContext
|
|
2361
|
-
? {
|
|
2362
|
-
query: rawContext.query,
|
|
2363
|
-
...(rawContext.sessionKey
|
|
2364
|
-
? { sessionKey: rawContext.sessionKey }
|
|
2365
|
-
: {}),
|
|
2366
|
-
namespace: rawContext.rawExcerptNamespace ?? namespace,
|
|
2367
|
-
...(rawContext.rawExcerptSessionIds !== undefined
|
|
2368
|
-
? { lcmSessionIds: rawContext.rawExcerptSessionIds }
|
|
2369
|
-
: {}),
|
|
2370
|
-
}
|
|
2371
|
-
: null,
|
|
2372
|
-
);
|
|
2373
|
-
const rawExcerpts = rawExcerptsResult ?? undefined;
|
|
2374
|
-
|
|
2375
|
-
for (const memoryPath of snapshot.resultPaths ?? []) {
|
|
2376
|
-
if (!memoryPath || seen.has(memoryPath)) continue;
|
|
2377
|
-
const resolved = await readResultPath(memoryPath);
|
|
2378
|
-
if (!resolved) continue;
|
|
2379
|
-
const { memory, baseDir } = resolved;
|
|
2380
|
-
seen.add(memoryPath);
|
|
2381
|
-
results.push(
|
|
2382
|
-
this.serializeMemorySummary(
|
|
2383
|
-
memory,
|
|
2384
|
-
baseDir,
|
|
2385
|
-
disclosure,
|
|
2386
|
-
// Attach the (possibly empty) raw excerpts to the first raw
|
|
2387
|
-
// result; subsequent results do not duplicate the array.
|
|
2388
|
-
results.length === 0 ? rawExcerpts : undefined,
|
|
2389
|
-
),
|
|
2390
|
-
);
|
|
2391
|
-
}
|
|
2392
|
-
|
|
2393
|
-
if (results.length > 0) return results;
|
|
2394
|
-
|
|
2395
|
-
for (const memoryId of snapshot.memoryIds) {
|
|
2396
|
-
const memory = await storage.getMemoryById(memoryId);
|
|
2397
|
-
if (!memory || seen.has(memory.path)) continue;
|
|
2398
|
-
seen.add(memory.path);
|
|
2399
|
-
results.push(
|
|
2400
|
-
this.serializeMemorySummary(
|
|
2401
|
-
memory,
|
|
2402
|
-
storageDir,
|
|
2403
|
-
disclosure,
|
|
2404
|
-
results.length === 0 ? rawExcerpts : undefined,
|
|
2405
|
-
),
|
|
2406
|
-
);
|
|
2407
|
-
}
|
|
2408
|
-
return results;
|
|
2409
2162
|
}
|
|
2410
2163
|
|
|
2411
2164
|
private async storageForAbsoluteRecallPath(
|
|
@@ -2460,19 +2213,6 @@ export class EngramAccessService {
|
|
|
2460
2213
|
return matches[0] ?? null;
|
|
2461
2214
|
}
|
|
2462
2215
|
|
|
2463
|
-
/**
|
|
2464
|
-
* Fetch raw transcript excerpts from the LCM archive for `disclosure ===
|
|
2465
|
-
* "raw"` recalls (issue #677 PR 2/4). Returns `null` for non-raw recall
|
|
2466
|
-
* depths, an empty array when LCM is disabled / not initialized / has no
|
|
2467
|
-
* matches, and an array of LCM-side excerpts otherwise. Errors are
|
|
2468
|
-
* swallowed and treated as "no excerpts" so a failing LCM never breaks
|
|
2469
|
-
* the recall response.
|
|
2470
|
-
*
|
|
2471
|
-
* Namespace handling: LCM archival prefixes non-default-namespace
|
|
2472
|
-
* sessions with `${namespace}:${sessionKey}` (see `observe()` around
|
|
2473
|
-
* line 2498), so the lookup must mirror that prefix or raw recalls in
|
|
2474
|
-
* non-default namespaces miss their own excerpts.
|
|
2475
|
-
*/
|
|
2476
2216
|
private async fetchRawExcerpts(
|
|
2477
2217
|
disclosure: RecallDisclosure,
|
|
2478
2218
|
context: {
|
|
@@ -2493,63 +2233,10 @@ export class EngramAccessService {
|
|
|
2493
2233
|
lcmSessionIds?: string[];
|
|
2494
2234
|
} | null,
|
|
2495
2235
|
): Promise<EngramAccessMemorySummary["rawExcerpts"] | null> {
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
// across every archived session in the LCM store and would return
|
|
2501
|
-
// excerpts from unrelated sessions (potentially crossing namespaces
|
|
2502
|
-
// via their `${namespace}:${sessionKey}` prefix encoding). Treat a
|
|
2503
|
-
// missing sessionKey as "no excerpts" — callers asking for raw
|
|
2504
|
-
// disclosure outside a session get an empty list, not a leak.
|
|
2505
|
-
if (!context.sessionKey) return [];
|
|
2506
|
-
const lcm = this.orchestrator.lcmEngine;
|
|
2507
|
-
if (!lcm || !lcm.enabled) return [];
|
|
2508
|
-
try {
|
|
2509
|
-
const legacyKey =
|
|
2510
|
-
context.namespace &&
|
|
2511
|
-
context.namespace !== this.orchestrator.config.defaultNamespace
|
|
2512
|
-
? `${context.namespace}:${context.sessionKey}`
|
|
2513
|
-
: context.sessionKey;
|
|
2514
|
-
const lcmSessionIds =
|
|
2515
|
-
context.lcmSessionIds !== undefined
|
|
2516
|
-
? context.lcmSessionIds
|
|
2517
|
-
: [legacyKey];
|
|
2518
|
-
// Cap the excerpt fanout so recall responses stay bounded. Five matches
|
|
2519
|
-
// is enough to anchor the model in the raw transcript without ballooning
|
|
2520
|
-
// token spend; raw is meant as the escape hatch, not the default. The cap
|
|
2521
|
-
// is applied across the MERGED result set so adding fallback keys never
|
|
2522
|
-
// inflates the excerpt budget.
|
|
2523
|
-
const limit = 5;
|
|
2524
|
-
const seenRows = new Set<string>();
|
|
2525
|
-
const excerpts: NonNullable<EngramAccessMemorySummary["rawExcerpts"]> = [];
|
|
2526
|
-
const settledRows = await Promise.allSettled(
|
|
2527
|
-
lcmSessionIds.map(async (lcmSessionKey) =>
|
|
2528
|
-
lcm.searchContextFull(context.query, limit, lcmSessionKey),
|
|
2529
|
-
),
|
|
2530
|
-
);
|
|
2531
|
-
for (const result of settledRows) {
|
|
2532
|
-
if (excerpts.length >= limit) break;
|
|
2533
|
-
if (result.status !== "fulfilled") continue;
|
|
2534
|
-
for (const r of result.value) {
|
|
2535
|
-
const dedupeKey = `${r.session_id} ${r.turn_index}`;
|
|
2536
|
-
if (seenRows.has(dedupeKey)) continue;
|
|
2537
|
-
seenRows.add(dedupeKey);
|
|
2538
|
-
excerpts.push({
|
|
2539
|
-
turnIndex: r.turn_index,
|
|
2540
|
-
role: r.role,
|
|
2541
|
-
content: r.content,
|
|
2542
|
-
sessionId: r.session_id,
|
|
2543
|
-
});
|
|
2544
|
-
if (excerpts.length >= limit) break;
|
|
2545
|
-
}
|
|
2546
|
-
}
|
|
2547
|
-
return excerpts;
|
|
2548
|
-
} catch {
|
|
2549
|
-
// CLAUDE.md rule 13: never let an external subsystem (LCM/SQLite)
|
|
2550
|
-
// crash the primary recall flow.
|
|
2551
|
-
return [];
|
|
2552
|
-
}
|
|
2236
|
+
return this.accessRecallSurface.fetchRawExcerpts(
|
|
2237
|
+
disclosure,
|
|
2238
|
+
context,
|
|
2239
|
+
);
|
|
2553
2240
|
}
|
|
2554
2241
|
|
|
2555
2242
|
private async handleIdempotentWrite<T extends { idempotencyReplay?: boolean }>(options: {
|
|
@@ -3267,453 +2954,9 @@ export class EngramAccessService {
|
|
|
3267
2954
|
response: EngramAccessRecallResponse;
|
|
3268
2955
|
budgetRecordPrincipal: string | null;
|
|
3269
2956
|
}> {
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
// pre-#677 callers see unchanged behavior. Reject explicitly invalid
|
|
3273
|
-
// string values per CLAUDE.md rule 51 (do not silently fall back).
|
|
3274
|
-
const callerProvidedDisclosure =
|
|
3275
|
-
request.disclosure !== undefined && request.disclosure !== null;
|
|
3276
|
-
const requestedDisclosure: RecallDisclosure = (() => {
|
|
3277
|
-
if (!callerProvidedDisclosure) {
|
|
3278
|
-
return DEFAULT_RECALL_DISCLOSURE;
|
|
3279
|
-
}
|
|
3280
|
-
if (!isRecallDisclosure(request.disclosure)) {
|
|
3281
|
-
throw new EngramAccessInputError(
|
|
3282
|
-
`disclosure must be one of: chunk, section, raw (got: ${String(request.disclosure)})`,
|
|
3283
|
-
);
|
|
3284
|
-
}
|
|
3285
|
-
return request.disclosure;
|
|
3286
|
-
})();
|
|
3287
|
-
// Attach any coding context shipped with the recall request BEFORE
|
|
3288
|
-
// namespace resolution so the overlay applies to this recall (issue #569).
|
|
3289
|
-
if (request.codingContext !== undefined && request.sessionKey) {
|
|
3290
|
-
this.setCodingContext({
|
|
3291
|
-
sessionKey: request.sessionKey,
|
|
3292
|
-
codingContext: request.codingContext,
|
|
3293
|
-
});
|
|
3294
|
-
}
|
|
3295
|
-
// Auto-resolve coding context from cwd/projectTag when no explicit
|
|
3296
|
-
// codingContext was supplied (issue #569 wiring). This allows Claude
|
|
3297
|
-
// Code hooks and OpenClaw connectors to get project-scoped memory
|
|
3298
|
-
// transparently.
|
|
3299
|
-
if (request.codingContext === undefined && request.sessionKey) {
|
|
3300
|
-
await this.maybeAttachCodingContext(request.sessionKey, {
|
|
3301
|
-
cwd: request.cwd,
|
|
3302
|
-
projectTag: request.projectTag,
|
|
3303
|
-
});
|
|
3304
|
-
}
|
|
3305
|
-
const authenticatedPrincipal = request.authenticatedPrincipal?.trim();
|
|
3306
|
-
const namespaceOverride = this.resolveRecallNamespace(
|
|
3307
|
-
request.namespace,
|
|
3308
|
-
request.sessionKey,
|
|
3309
|
-
authenticatedPrincipal,
|
|
3310
|
-
);
|
|
3311
|
-
const namespace = namespaceOverride ?? this.orchestrator.config.defaultNamespace;
|
|
3312
|
-
// Normalize mode early so that no_recall / invalid modes skip budget
|
|
3313
|
-
// accounting (Codex P1: budget recorded before mode validation).
|
|
3314
|
-
const mode = this.normalizeRecallMode(request.mode);
|
|
3315
|
-
const maybePrincipal = this.resolveRequestPrincipal(request.sessionKey, authenticatedPrincipal);
|
|
3316
|
-
if (resolveNamespaceCapabilities(this.orchestrator.config).namespaces && !maybePrincipal) {
|
|
3317
|
-
throw new EngramAccessInputError(
|
|
3318
|
-
"authentication required: namespaces are enabled and no principal was supplied",
|
|
3319
|
-
);
|
|
3320
|
-
}
|
|
3321
|
-
const principal = maybePrincipal ?? "default";
|
|
3322
|
-
const principalNamespace = defaultNamespaceForPrincipal(principal, this.orchestrator.config);
|
|
3323
|
-
const profileCodingContext =
|
|
3324
|
-
request.sessionKey && typeof this.orchestrator.getCodingContextForSession === "function"
|
|
3325
|
-
? this.orchestrator.getCodingContextForSession(request.sessionKey)
|
|
3326
|
-
: null;
|
|
3327
|
-
const profileCodingOverlay =
|
|
3328
|
-
!namespaceOverride &&
|
|
3329
|
-
profileCodingContext &&
|
|
3330
|
-
resolveNamespaceCapabilities(this.orchestrator.config).namespaces &&
|
|
3331
|
-
this.orchestrator.config.codingMode?.projectScope
|
|
3332
|
-
? resolveCodingNamespaceOverlay(
|
|
3333
|
-
profileCodingContext,
|
|
3334
|
-
this.orchestrator.config.codingMode,
|
|
3335
|
-
this.orchestrator.config.defaultNamespace,
|
|
3336
|
-
)
|
|
3337
|
-
: null;
|
|
3338
|
-
const profilePlan = namespaceOverride
|
|
3339
|
-
? null
|
|
3340
|
-
: resolveScopeProfilePlan({
|
|
3341
|
-
config: this.orchestrator.config,
|
|
3342
|
-
principal,
|
|
3343
|
-
codingContext: profileCodingContext,
|
|
3344
|
-
codingOverlay: profileCodingOverlay,
|
|
3345
|
-
});
|
|
3346
|
-
// Skip budget checks for modes that never perform a cross-namespace read.
|
|
3347
|
-
const modeSkipsBudget = mode === "no_recall";
|
|
3348
|
-
// Derive the full set of namespaces the orchestrator will actually search.
|
|
3349
|
-
// When no explicit override is provided, `recallNamespacesForPrincipal()` may
|
|
3350
|
-
// expand to shared / policy-default namespaces. Budget must be checked
|
|
3351
|
-
// against every cross-namespace entry in the effective set so that omitting
|
|
3352
|
-
// `namespace` cannot bypass the limiter (Cursor/Codex review feedback).
|
|
3353
|
-
//
|
|
3354
|
-
const legacyRecallNamespaces = Array.isArray(this.orchestrator.config.defaultRecallNamespaces)
|
|
3355
|
-
? recallNamespacesForPrincipal(principal, this.orchestrator.config)
|
|
3356
|
-
: [];
|
|
3357
|
-
const effectiveNamespaces = namespaceOverride
|
|
3358
|
-
? [namespaceOverride]
|
|
3359
|
-
: profilePlan
|
|
3360
|
-
? expandScopeProfileReadNamespaces({
|
|
3361
|
-
profilePlan,
|
|
3362
|
-
principalSelfNamespace: profilePlan.baseNamespace,
|
|
3363
|
-
config: this.orchestrator.config,
|
|
3364
|
-
principal,
|
|
3365
|
-
codingOverlay: profileCodingOverlay,
|
|
3366
|
-
legacyRecallNamespaces,
|
|
3367
|
-
})
|
|
3368
|
-
: legacyRecallNamespaces;
|
|
3369
|
-
const budgetPrincipalNamespace = profilePlan?.baseNamespace ?? principalNamespace;
|
|
3370
|
-
let budgetDecision: BudgetDecision;
|
|
3371
|
-
let recordBudgetAfterSuccess = false;
|
|
3372
|
-
if (modeSkipsBudget) {
|
|
3373
|
-
budgetDecision = {
|
|
3374
|
-
allowed: true as const,
|
|
3375
|
-
reason: "allowed-same-namespace" as const,
|
|
3376
|
-
count: 0,
|
|
3377
|
-
limit: {
|
|
3378
|
-
softLimit: this.orchestrator.config.recallCrossNamespaceBudgetSoftLimit ?? 10,
|
|
3379
|
-
hardLimit: this.orchestrator.config.recallCrossNamespaceBudgetHardLimit ?? 30,
|
|
3380
|
-
windowMs: this.orchestrator.config.recallCrossNamespaceBudgetWindowMs ?? 60_000,
|
|
3381
|
-
},
|
|
3382
|
-
};
|
|
3383
|
-
} else {
|
|
3384
|
-
// Peek at every effective namespace to determine whether ANY would be
|
|
3385
|
-
// cross-namespace WITHOUT recording side effects (Cursor review:
|
|
3386
|
-
// multi-count bug). Record a single budget event only when at least
|
|
3387
|
-
// one effective namespace differs from the principal's self namespace,
|
|
3388
|
-
// and only after recall succeeds so retried transient failures do not
|
|
3389
|
-
// consume budget multiple times before a successful response can be
|
|
3390
|
-
// cached behind the request idempotency key.
|
|
3391
|
-
let anyCrossNamespace = false;
|
|
3392
|
-
let denied: BudgetDecision | null = null;
|
|
3393
|
-
let crossNamespaceDecision: BudgetDecision | null = null;
|
|
3394
|
-
for (const ns of effectiveNamespaces) {
|
|
3395
|
-
const peek = this.budget.peek({
|
|
3396
|
-
principal,
|
|
3397
|
-
principalNamespace: budgetPrincipalNamespace,
|
|
3398
|
-
queryNamespace: ns,
|
|
3399
|
-
});
|
|
3400
|
-
if (peek.reason !== "allowed-same-namespace") {
|
|
3401
|
-
anyCrossNamespace = true;
|
|
3402
|
-
crossNamespaceDecision ??= peek;
|
|
3403
|
-
}
|
|
3404
|
-
if (!peek.allowed) {
|
|
3405
|
-
denied = peek;
|
|
3406
|
-
break;
|
|
3407
|
-
}
|
|
3408
|
-
}
|
|
3409
|
-
if (denied) {
|
|
3410
|
-
// The peek projected a denial — deny without recording so the
|
|
3411
|
-
// bucket is not inflated by rejected attempts.
|
|
3412
|
-
budgetDecision = denied;
|
|
3413
|
-
} else if (anyCrossNamespace) {
|
|
3414
|
-
budgetDecision = crossNamespaceDecision ?? {
|
|
3415
|
-
allowed: true as const,
|
|
3416
|
-
reason: "allowed-under-soft" as const,
|
|
3417
|
-
count: 0,
|
|
3418
|
-
limit: {
|
|
3419
|
-
softLimit: this.orchestrator.config.recallCrossNamespaceBudgetSoftLimit ?? 10,
|
|
3420
|
-
hardLimit: this.orchestrator.config.recallCrossNamespaceBudgetHardLimit ?? 30,
|
|
3421
|
-
windowMs: this.orchestrator.config.recallCrossNamespaceBudgetWindowMs ?? 60_000,
|
|
3422
|
-
},
|
|
3423
|
-
};
|
|
3424
|
-
recordBudgetAfterSuccess = true;
|
|
3425
|
-
} else {
|
|
3426
|
-
budgetDecision = {
|
|
3427
|
-
allowed: true as const,
|
|
3428
|
-
reason: "allowed-same-namespace" as const,
|
|
3429
|
-
count: 0,
|
|
3430
|
-
limit: {
|
|
3431
|
-
softLimit: this.orchestrator.config.recallCrossNamespaceBudgetSoftLimit ?? 10,
|
|
3432
|
-
hardLimit: this.orchestrator.config.recallCrossNamespaceBudgetHardLimit ?? 30,
|
|
3433
|
-
windowMs: this.orchestrator.config.recallCrossNamespaceBudgetWindowMs ?? 60_000,
|
|
3434
|
-
},
|
|
3435
|
-
};
|
|
3436
|
-
}
|
|
3437
|
-
if (!budgetDecision.allowed) {
|
|
3438
|
-
throw new EngramAccessInputError(
|
|
3439
|
-
`recall denied: cross-namespace budget exceeded (${budgetDecision.count}/${budgetDecision.limit.hardLimit} in ${budgetDecision.limit.windowMs}ms window)`,
|
|
3440
|
-
);
|
|
3441
|
-
}
|
|
3442
|
-
// Prune expired principal buckets to prevent unbounded Map growth from
|
|
3443
|
-
// high-cardinality / transient principals (Codex P2 review feedback).
|
|
3444
|
-
this.budget.gc();
|
|
3445
|
-
}
|
|
3446
|
-
const topK = Number.isFinite(request.topK) ? Math.max(0, Math.floor(request.topK ?? 0)) : undefined;
|
|
3447
|
-
// Issue #680 — historical recall pin. Validate at the input
|
|
3448
|
-
// boundary so a malformed `asOf` is rejected with a structured
|
|
3449
|
-
// 400 instead of silently flooring at NaN inside the orchestrator
|
|
3450
|
-
// (CLAUDE.md rule 51, gotcha #51). Empty / undefined is fine —
|
|
3451
|
-
// means "no pin".
|
|
3452
|
-
let asOf: string | undefined;
|
|
3453
|
-
if (request.asOf !== undefined && request.asOf !== null) {
|
|
3454
|
-
if (typeof request.asOf !== "string" || request.asOf.trim().length === 0) {
|
|
3455
|
-
throw new EngramAccessInputError(
|
|
3456
|
-
"asOf must be a non-empty ISO 8601 timestamp string",
|
|
3457
|
-
);
|
|
3458
|
-
}
|
|
3459
|
-
const parsed = Date.parse(request.asOf);
|
|
3460
|
-
if (!Number.isFinite(parsed)) {
|
|
3461
|
-
throw new EngramAccessInputError(
|
|
3462
|
-
`asOf must be a parseable ISO 8601 timestamp (got: "${request.asOf}")`,
|
|
3463
|
-
);
|
|
3464
|
-
}
|
|
3465
|
-
asOf = request.asOf;
|
|
3466
|
-
}
|
|
3467
|
-
const recallOptions: RecallInvocationOptions = {
|
|
3468
|
-
namespace: namespaceOverride,
|
|
3469
|
-
topK,
|
|
3470
|
-
mode,
|
|
3471
|
-
...(authenticatedPrincipal ? { principalOverride: authenticatedPrincipal } : {}),
|
|
3472
|
-
...(asOf !== undefined ? { asOf } : {}),
|
|
3473
|
-
...(request.includeLowConfidence === true ? { includeLowConfidence: true } : {}),
|
|
3474
|
-
};
|
|
3475
|
-
const startedAt = Date.now();
|
|
3476
|
-
const context = await this.orchestrator.recall(query, request.sessionKey, recallOptions);
|
|
3477
|
-
const snapshot = request.sessionKey
|
|
3478
|
-
? this.orchestrator.lastRecall.get(request.sessionKey)
|
|
3479
|
-
: null;
|
|
3480
|
-
const effectiveNamespace = snapshot?.namespace
|
|
3481
|
-
? this.resolveNamespace(snapshot.namespace)
|
|
3482
|
-
: namespace;
|
|
3483
|
-
// Auto-escalation policy (issue #677 PR 4/4). When the operator
|
|
3484
|
-
// configured `recallDisclosureEscalation: "auto"` AND the caller
|
|
3485
|
-
// did not explicitly choose a disclosure level AND recall produced
|
|
3486
|
-
// a low-confidence result set (proxied by fill ratio: results
|
|
3487
|
-
// returned / topK requested), we escalate the default `chunk`
|
|
3488
|
-
// shape to `section` so the LLM gets richer context to compensate
|
|
3489
|
-
// for ambiguous retrieval. Manual mode and explicit caller
|
|
3490
|
-
// disclosure both bypass the policy. Documented in
|
|
3491
|
-
// `recall-disclosure-escalation.ts` and unit-tested there.
|
|
3492
|
-
// Confidence-proxy denominator: priority order is
|
|
3493
|
-
// 1. `snapshot.budgetsApplied.appliedTopK` (ALWAYS wins) — this is
|
|
3494
|
-
// the limit the orchestrator actually applied after planner /
|
|
3495
|
-
// minimal-mode / section-cap narrowing. Codex P1 rounds 2+3
|
|
3496
|
-
// on #705 emphasize that even a caller's explicit `request.topK`
|
|
3497
|
-
// is wrong when the orchestrator caps below it (e.g. topK=50
|
|
3498
|
-
// but appliedTopK=3 makes a 2-hit recall actually 0.67, not
|
|
3499
|
-
// 0.04).
|
|
3500
|
-
// 2. The caller's explicit `topK` when the snapshot lacks
|
|
3501
|
-
// `budgetsApplied` (early-return paths, error cases).
|
|
3502
|
-
// 3. Config `qmdMaxResults` as a last-resort fallback.
|
|
3503
|
-
// Floor at observed-results so the ratio stays in [0, 1] even if
|
|
3504
|
-
// any of the signals drifts below the actual hit count.
|
|
3505
|
-
const resultsReturned = snapshot?.memoryIds?.length ?? 0;
|
|
3506
|
-
const appliedTopK = snapshot?.budgetsApplied?.appliedTopK;
|
|
3507
|
-
const configMaxResults =
|
|
3508
|
-
typeof this.orchestrator.config.qmdMaxResults === "number" &&
|
|
3509
|
-
Number.isFinite(this.orchestrator.config.qmdMaxResults) &&
|
|
3510
|
-
this.orchestrator.config.qmdMaxResults > 0
|
|
3511
|
-
? this.orchestrator.config.qmdMaxResults
|
|
3512
|
-
: 0;
|
|
3513
|
-
const topKDenominator =
|
|
3514
|
-
typeof appliedTopK === "number" &&
|
|
3515
|
-
Number.isFinite(appliedTopK) &&
|
|
3516
|
-
appliedTopK > 0
|
|
3517
|
-
? Math.max(appliedTopK, resultsReturned)
|
|
3518
|
-
: typeof topK === "number" && topK > 0
|
|
3519
|
-
? Math.max(topK, resultsReturned)
|
|
3520
|
-
: Math.max(configMaxResults, resultsReturned, 1);
|
|
3521
|
-
// When the recall produced no snapshot (sessionless / namespace
|
|
3522
|
-
// mismatch / early-return path), there is no confidence signal to
|
|
3523
|
-
// base escalation on. Pass `undefined` so the helper takes its
|
|
3524
|
-
// `no-top-k-confidence` branch instead of computing 0/N=0 and
|
|
3525
|
-
// forcing auto-escalation on every sessionless caller (Codex P2
|
|
3526
|
-
// review on PR #705).
|
|
3527
|
-
const topKConfidence =
|
|
3528
|
-
snapshot && topKDenominator > 0
|
|
3529
|
-
? Math.min(1, resultsReturned / topKDenominator)
|
|
3530
|
-
: undefined;
|
|
3531
|
-
const escalationDecision = decideDisclosureEscalation({
|
|
3532
|
-
mode: this.orchestrator.config.recallDisclosureEscalation,
|
|
3533
|
-
threshold: this.orchestrator.config.recallDisclosureEscalationThreshold,
|
|
3534
|
-
originalDisclosure: requestedDisclosure,
|
|
3535
|
-
callerProvidedDisclosure,
|
|
3536
|
-
topKConfidence,
|
|
3537
|
-
});
|
|
3538
|
-
const disclosure = escalationDecision.effective;
|
|
3539
|
-
// Gate the raw-excerpt LCM read with the SAME read-authorization namespace
|
|
3540
|
-
// `lcmSearch` + the in-prompt LCM sections use (#1505 thread 2f7), so
|
|
3541
|
-
// `disclosure: "raw"` never attaches `<principal>-project-*` overlay rows
|
|
3542
|
-
// when the principal can WRITE but not READ its self base (or
|
|
3543
|
-
// `defaultRecallNamespaces` omits `self`). Computed ONLY for raw disclosure:
|
|
3544
|
-
// it is the sole consumer, and resolving the overlay on every chunk/section
|
|
3545
|
-
// recall would be wasted work — keeping non-raw recall byte-for-byte
|
|
3546
|
-
// unchanged.
|
|
3547
|
-
// Trim the sessionKey to match what `orchestrator.recall(...)` already does
|
|
3548
|
-
// (`request.sessionKey?.trim() || undefined`) and what the x-ray raw-excerpt
|
|
3549
|
-
// path uses (cursor "Raw excerpt key not trimmed"). A whitespace-padded key
|
|
3550
|
-
// otherwise drives recall under one identity but resolves the raw-excerpt
|
|
3551
|
-
// overlay namespace + LCM `session_id` under a DIFFERENT (untrimmed) prefix,
|
|
3552
|
-
// so excerpts are gated/queried inconsistently with recall and the x-ray path.
|
|
3553
|
-
const trimmedSessionKey = request.sessionKey?.trim() || undefined;
|
|
3554
|
-
const rawExcerptNamespace =
|
|
3555
|
-
disclosure === "raw"
|
|
3556
|
-
? this.resolveRawExcerptReadNamespace(
|
|
3557
|
-
request.namespace,
|
|
3558
|
-
trimmedSessionKey,
|
|
3559
|
-
authenticatedPrincipal,
|
|
3560
|
-
)
|
|
3561
|
-
: undefined;
|
|
3562
|
-
// `undefined` for an IMPLICIT raw recall means NO readable LCM namespace
|
|
3563
|
-
// exists (restrictive `default` READ policy, no readable overlay/self) —
|
|
3564
|
-
// suppress excerpts rather than fall back to the write/overlay namespace the
|
|
3565
|
-
// read gate excludes (#1505 thread NBHWz). An EXPLICIT namespace always
|
|
3566
|
-
// resolves (or throws) above, so suppression only applies to the implicit
|
|
3567
|
-
// path.
|
|
3568
|
-
const hasExplicitNamespace =
|
|
3569
|
-
typeof request.namespace === "string" &&
|
|
3570
|
-
request.namespace.trim().length > 0;
|
|
3571
|
-
const rawExcerptsSuppressed =
|
|
3572
|
-
disclosure === "raw" &&
|
|
3573
|
-
!hasExplicitNamespace &&
|
|
3574
|
-
rawExcerptNamespace === undefined;
|
|
3575
|
-
// Ordered, read-authorized LCM read key SET (#1505 fallback unification) so
|
|
3576
|
-
// raw disclosure finds excerpts a branch-scoped session archived at
|
|
3577
|
-
// project/root scope — exactly as recall + `lcmSearch` do. Only with a
|
|
3578
|
-
// concrete sessionKey; already read-gated.
|
|
3579
|
-
const rawExcerptSessionIds =
|
|
3580
|
-
disclosure === "raw" && rawExcerptNamespace && trimmedSessionKey
|
|
3581
|
-
? this.resolveLcmReadSessionIds(
|
|
3582
|
-
request.namespace,
|
|
3583
|
-
rawExcerptNamespace,
|
|
3584
|
-
trimmedSessionKey,
|
|
3585
|
-
authenticatedPrincipal,
|
|
3586
|
-
)
|
|
3587
|
-
: undefined;
|
|
3588
|
-
let results = await this.serializeRecallResults(snapshot, disclosure, {
|
|
3589
|
-
query,
|
|
3590
|
-
sessionKey: trimmedSessionKey,
|
|
3591
|
-
...(rawExcerptNamespace ? { rawExcerptNamespace } : {}),
|
|
3592
|
-
...(rawExcerptSessionIds !== undefined ? { rawExcerptSessionIds } : {}),
|
|
3593
|
-
...(rawExcerptsSuppressed ? { rawExcerptsSuppressed } : {}),
|
|
3594
|
-
});
|
|
3595
|
-
|
|
3596
|
-
// Tag filter (issue #689). Applied post-recall, post-serialization so
|
|
3597
|
-
// the actual frontmatter tags are already loaded onto each result. When
|
|
3598
|
-
// `tags` is absent or empty the filter is a no-op; an invalid `tagMatch`
|
|
3599
|
-
// throws via `parseTagMatch` (CLAUDE.md rule 51).
|
|
3600
|
-
const filterTags = normalizeTags(request.tags);
|
|
3601
|
-
let tagMatchMode: TagMatchMode | undefined;
|
|
3602
|
-
try {
|
|
3603
|
-
tagMatchMode = parseTagMatch(request.tagMatch);
|
|
3604
|
-
} catch (err) {
|
|
3605
|
-
throw new EngramAccessInputError(
|
|
3606
|
-
err instanceof Error ? err.message : String(err),
|
|
3607
|
-
);
|
|
3608
|
-
}
|
|
3609
|
-
let effectiveContext = context;
|
|
3610
|
-
if (filterTags && filterTags.length > 0) {
|
|
3611
|
-
const beforeIds = results.map((r) => r.id);
|
|
3612
|
-
const { results: admitted } = applyTagFilter(results, {
|
|
3613
|
-
tags: filterTags,
|
|
3614
|
-
tagMatch: tagMatchMode,
|
|
3615
|
-
});
|
|
3616
|
-
results = admitted;
|
|
3617
|
-
// Codex P1: `context` was generated by orchestrator.recall(...)
|
|
3618
|
-
// BEFORE the tag filter ran, so it can contain memories that don't
|
|
3619
|
-
// match the requested tags. Surfaces consuming `context` (the
|
|
3620
|
-
// prompt-injection string) would leak excluded content into the
|
|
3621
|
-
// LLM. When the filter actually drops any result, rebuild context
|
|
3622
|
-
// from the admitted set so excluded content is unreachable through
|
|
3623
|
-
// any field of the response. The rebuilt context concatenates each
|
|
3624
|
-
// admitted result's available text (full content at section/raw
|
|
3625
|
-
// disclosure, otherwise the preview) — a different wire format
|
|
3626
|
-
// than the orchestrator's native context, but a strict subset
|
|
3627
|
-
// safe to inject.
|
|
3628
|
-
const admittedIds = new Set(results.map((r) => r.id));
|
|
3629
|
-
const droppedAny = beforeIds.some((id) => !admittedIds.has(id));
|
|
3630
|
-
if (droppedAny) {
|
|
3631
|
-
effectiveContext = results
|
|
3632
|
-
.map((r) => {
|
|
3633
|
-
const content =
|
|
3634
|
-
typeof (r as { content?: unknown }).content === "string"
|
|
3635
|
-
? ((r as { content?: string }).content ?? "")
|
|
3636
|
-
: "";
|
|
3637
|
-
const preview =
|
|
3638
|
-
typeof (r as { preview?: unknown }).preview === "string"
|
|
3639
|
-
? ((r as { preview?: string }).preview ?? "")
|
|
3640
|
-
: "";
|
|
3641
|
-
return content || preview;
|
|
3642
|
-
})
|
|
3643
|
-
.filter((s) => s.length > 0)
|
|
3644
|
-
.join("\n\n");
|
|
3645
|
-
}
|
|
3646
|
-
}
|
|
3647
|
-
const filteredMemoryIds = filterTags && filterTags.length > 0
|
|
3648
|
-
? results.map((r) => r.id)
|
|
3649
|
-
: (snapshot?.memoryIds ?? []);
|
|
3650
|
-
const debug = await this.buildRecallDebug(
|
|
3651
|
-
snapshot,
|
|
3652
|
-
effectiveNamespace,
|
|
3653
|
-
request.includeDebug === true,
|
|
3654
|
-
request.sessionKey,
|
|
2957
|
+
return this.accessRecallSurface.executeRecall(
|
|
2958
|
+
request,
|
|
3655
2959
|
);
|
|
3656
|
-
|
|
3657
|
-
// Fire-and-forget audit recording. Must never block or crash recall.
|
|
3658
|
-
let auditAnomalies: AccessAuditResult["anomalies"] | undefined;
|
|
3659
|
-
if (this.auditAdapter) {
|
|
3660
|
-
try {
|
|
3661
|
-
const resolvedAgentId = principal ?? "__anonymous__";
|
|
3662
|
-
const auditEntry = {
|
|
3663
|
-
ts: new Date().toISOString(),
|
|
3664
|
-
sessionKey: request.sessionKey ?? "",
|
|
3665
|
-
agentId: resolvedAgentId,
|
|
3666
|
-
trigger: "access-surface",
|
|
3667
|
-
queryText: query,
|
|
3668
|
-
candidateMemoryIds: snapshot?.memoryIds ?? [],
|
|
3669
|
-
// Audit must reflect what was actually injected, not what
|
|
3670
|
-
// recall produced before the tag filter. Using `context`
|
|
3671
|
-
// (pre-filter) overstates injectedChars and can leak content
|
|
3672
|
-
// from excluded memories into the audit summary (cursor
|
|
3673
|
-
// Medium on PR #712).
|
|
3674
|
-
summary: effectiveContext.slice(0, 200) || null,
|
|
3675
|
-
injectedChars: effectiveContext.length,
|
|
3676
|
-
toggleState: "enabled" as const,
|
|
3677
|
-
latencyMs: Date.now() - startedAt,
|
|
3678
|
-
plannerMode: snapshot?.plannerMode ?? mode,
|
|
3679
|
-
requestedMode: mode,
|
|
3680
|
-
fallbackUsed: snapshot?.fallbackUsed ?? false,
|
|
3681
|
-
};
|
|
3682
|
-
const auditResult = await this.auditAdapter.record(
|
|
3683
|
-
resolvedAgentId || "__anonymous__",
|
|
3684
|
-
auditEntry,
|
|
3685
|
-
);
|
|
3686
|
-
auditAnomalies = auditResult.anomalies;
|
|
3687
|
-
} catch {
|
|
3688
|
-
// Audit failures must never crash the recall path.
|
|
3689
|
-
}
|
|
3690
|
-
}
|
|
3691
|
-
|
|
3692
|
-
return {
|
|
3693
|
-
response: {
|
|
3694
|
-
query,
|
|
3695
|
-
sessionKey: request.sessionKey,
|
|
3696
|
-
namespace: effectiveNamespace,
|
|
3697
|
-
context: effectiveContext,
|
|
3698
|
-
count: filterTags && filterTags.length > 0
|
|
3699
|
-
? results.length
|
|
3700
|
-
: (snapshot?.memoryIds.length ?? results.length),
|
|
3701
|
-
memoryIds: filteredMemoryIds,
|
|
3702
|
-
results,
|
|
3703
|
-
recordedAt: snapshot?.recordedAt,
|
|
3704
|
-
traceId: snapshot?.traceId,
|
|
3705
|
-
plannerMode: snapshot?.plannerMode ?? mode,
|
|
3706
|
-
fallbackUsed: snapshot?.fallbackUsed ?? false,
|
|
3707
|
-
sourcesUsed: snapshot?.sourcesUsed ?? [],
|
|
3708
|
-
disclosure,
|
|
3709
|
-
budgetsApplied: snapshot?.budgetsApplied,
|
|
3710
|
-
auditAnomalies,
|
|
3711
|
-
budgetWarning: budgetDecision.reason === "warn-over-soft" ? budgetDecision : undefined,
|
|
3712
|
-
latencyMs: snapshot?.latencyMs ?? (Date.now() - startedAt),
|
|
3713
|
-
debug,
|
|
3714
|
-
},
|
|
3715
|
-
budgetRecordPrincipal: recordBudgetAfterSuccess ? principal : null,
|
|
3716
|
-
};
|
|
3717
2960
|
}
|
|
3718
2961
|
|
|
3719
2962
|
async recallExplain(
|
|
@@ -3814,15 +3057,6 @@ export class EngramAccessService {
|
|
|
3814
3057
|
return toRecallExplainJson(snapshot);
|
|
3815
3058
|
}
|
|
3816
3059
|
|
|
3817
|
-
/**
|
|
3818
|
-
* Recall X-ray (issue #570). Runs a recall with `xrayCapture: true`
|
|
3819
|
-
* and returns the resulting snapshot as structured JSON so every
|
|
3820
|
-
* surface (CLI / HTTP / MCP) gets the same payload. Namespace scope
|
|
3821
|
-
* is enforced before the recall fires (CLAUDE.md rule 42 — read and
|
|
3822
|
-
* write paths must resolve through the same namespace layer) so an
|
|
3823
|
-
* unauthorized principal cannot capture an x-ray for a namespace it
|
|
3824
|
-
* cannot read.
|
|
3825
|
-
*/
|
|
3826
3060
|
async recallXray(request: {
|
|
3827
3061
|
query: string;
|
|
3828
3062
|
sessionKey?: string;
|
|
@@ -3867,433 +3101,9 @@ export class EngramAccessService {
|
|
|
3867
3101
|
snapshot?: RecallXraySnapshot;
|
|
3868
3102
|
recall?: EngramAccessRecallResponse;
|
|
3869
3103
|
}> {
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
// input with an explicit error rather than silently producing
|
|
3874
|
-
// an empty snapshot.
|
|
3875
|
-
throw new Error("recallXray: query is required and must be non-empty");
|
|
3876
|
-
}
|
|
3877
|
-
// Validate disclosure UP FRONT — before recall executes, before
|
|
3878
|
-
// the xray queue mutex is acquired, before namespace resolution.
|
|
3879
|
-
// A bad value should fail fast rather than after we've burned
|
|
3880
|
-
// cycles on an irreversible recall (Cursor Medium review on PR
|
|
3881
|
-
// #699).
|
|
3882
|
-
if (
|
|
3883
|
-
request.disclosure !== undefined &&
|
|
3884
|
-
!isRecallDisclosure(request.disclosure)
|
|
3885
|
-
) {
|
|
3886
|
-
throw new EngramAccessInputError(
|
|
3887
|
-
`recallXray: disclosure must be one of: chunk, section, raw (got: ${String(request.disclosure)})`,
|
|
3888
|
-
);
|
|
3889
|
-
}
|
|
3890
|
-
|
|
3891
|
-
const namespacesEnabled = resolveNamespaceCapabilities(this.orchestrator.config).namespaces;
|
|
3892
|
-
const requestedNamespace = request.namespace?.trim()
|
|
3893
|
-
? this.resolveNamespace(request.namespace)
|
|
3894
|
-
: undefined;
|
|
3895
|
-
const authenticatedPrincipal = request.authenticatedPrincipal?.trim();
|
|
3896
|
-
const principal =
|
|
3897
|
-
authenticatedPrincipal
|
|
3898
|
-
|| resolvePrincipal(request.sessionKey, this.orchestrator.config);
|
|
3899
|
-
|
|
3900
|
-
if (requestedNamespace) {
|
|
3901
|
-
if (
|
|
3902
|
-
!canReadNamespace(
|
|
3903
|
-
principal,
|
|
3904
|
-
requestedNamespace,
|
|
3905
|
-
this.orchestrator.config,
|
|
3906
|
-
)
|
|
3907
|
-
) {
|
|
3908
|
-
return { snapshotFound: false };
|
|
3909
|
-
}
|
|
3910
|
-
} else if (
|
|
3911
|
-
namespacesEnabled
|
|
3912
|
-
&& !authenticatedPrincipal
|
|
3913
|
-
&& !request.sessionKey?.trim()
|
|
3914
|
-
) {
|
|
3915
|
-
// Namespaces enabled but no identity supplied — reject rather
|
|
3916
|
-
// than scanning the global namespace (CLAUDE.md rule 48:
|
|
3917
|
-
// least-privileged default).
|
|
3918
|
-
return { snapshotFound: false };
|
|
3919
|
-
}
|
|
3920
|
-
|
|
3921
|
-
// Optional `--budget` override must be a positive integer. Invalid
|
|
3922
|
-
// values throw rather than silently defaulting (CLAUDE.md rule 51).
|
|
3923
|
-
let budgetOverride: number | undefined;
|
|
3924
|
-
if (request.budget !== undefined && request.budget !== null) {
|
|
3925
|
-
const parsed =
|
|
3926
|
-
typeof request.budget === "number"
|
|
3927
|
-
? request.budget
|
|
3928
|
-
: Number(request.budget);
|
|
3929
|
-
if (
|
|
3930
|
-
!Number.isFinite(parsed)
|
|
3931
|
-
|| parsed <= 0
|
|
3932
|
-
|| !Number.isInteger(parsed)
|
|
3933
|
-
) {
|
|
3934
|
-
throw new Error(
|
|
3935
|
-
`recallXray: budget expects a positive integer; got ${JSON.stringify(request.budget)}`,
|
|
3936
|
-
);
|
|
3937
|
-
}
|
|
3938
|
-
budgetOverride = parsed;
|
|
3939
|
-
}
|
|
3940
|
-
const mode = this.normalizeRecallMode(request.mode);
|
|
3941
|
-
const disclosure = request.disclosure ?? DEFAULT_RECALL_DISCLOSURE;
|
|
3942
|
-
|
|
3943
|
-
// Serialize x-ray invocations behind a per-service mutex so the
|
|
3944
|
-
// per-process `getLastXraySnapshot()` slot cannot be clobbered by
|
|
3945
|
-
// a concurrent capturing call before this caller reads it back.
|
|
3946
|
-
// Budget and principal are now threaded through
|
|
3947
|
-
// `RecallInvocationOptions`, so global config mutation is gone
|
|
3948
|
-
// (CLAUDE.md rule 47: no shared mutable state across async
|
|
3949
|
-
// boundaries). The mutex stays only for the snapshot-slot
|
|
3950
|
-
// ordering guarantee.
|
|
3951
|
-
const previousQueue = this.xrayQueue;
|
|
3952
|
-
let release: () => void = () => {};
|
|
3953
|
-
this.xrayQueue = new Promise<void>((resolve) => {
|
|
3954
|
-
release = resolve;
|
|
3955
|
-
});
|
|
3956
|
-
await previousQueue;
|
|
3957
|
-
const recallStartedAt = Date.now();
|
|
3958
|
-
|
|
3959
|
-
const recallSessionKey = request.sessionKey?.trim() || undefined;
|
|
3960
|
-
let xrayResponse: {
|
|
3961
|
-
snapshotFound: boolean;
|
|
3962
|
-
snapshot?: RecallXraySnapshot;
|
|
3963
|
-
} = { snapshotFound: false };
|
|
3964
|
-
|
|
3965
|
-
try {
|
|
3966
|
-
// Clear any prior snapshot so a capture failure surfaces as
|
|
3967
|
-
// `{snapshotFound: false}` rather than returning stale data
|
|
3968
|
-
// from an earlier call on the same orchestrator.
|
|
3969
|
-
this.orchestrator.clearLastXraySnapshot();
|
|
3970
|
-
await this.orchestrator.recall(query, recallSessionKey, {
|
|
3971
|
-
xrayCapture: true,
|
|
3972
|
-
...(requestedNamespace ? { namespace: requestedNamespace } : {}),
|
|
3973
|
-
...(budgetOverride !== undefined
|
|
3974
|
-
? { budgetCharsOverride: budgetOverride }
|
|
3975
|
-
: {}),
|
|
3976
|
-
...(mode !== undefined ? { mode } : {}),
|
|
3977
|
-
// When the caller supplies an authenticated principal, forward
|
|
3978
|
-
// it via the dedicated override channel so orchestrator-side
|
|
3979
|
-
// ACL decisions use the SAME principal the access-surface
|
|
3980
|
-
// pre-check above authorized. Threading an
|
|
3981
|
-
// `authenticatedPrincipal` through `sessionKey` would be wrong:
|
|
3982
|
-
// `resolvePrincipal(sessionKey)` only maps configured raw
|
|
3983
|
-
// session keys and otherwise collapses to `"default"`, which
|
|
3984
|
-
// in namespace-enabled deployments produces false denials /
|
|
3985
|
-
// wrong-scope serving despite the pre-check passing
|
|
3986
|
-
// (CLAUDE.md rule 42).
|
|
3987
|
-
...(authenticatedPrincipal
|
|
3988
|
-
? { principalOverride: authenticatedPrincipal }
|
|
3989
|
-
: {}),
|
|
3990
|
-
...(request.currentContextScopes !== undefined
|
|
3991
|
-
? { currentContextScopes: request.currentContextScopes }
|
|
3992
|
-
: {}),
|
|
3993
|
-
});
|
|
3994
|
-
|
|
3995
|
-
const rawSnapshot = this.orchestrator.getLastXraySnapshot();
|
|
3996
|
-
// Re-check namespace after capture: the recall may have served
|
|
3997
|
-
// from a different namespace than the caller requested. Drop
|
|
3998
|
-
// the snapshot rather than leak cross-tenant data (CLAUDE.md
|
|
3999
|
-
// rules 42 + 47). The comparison is strict so a snapshot whose
|
|
4000
|
-
// namespace is `undefined` cannot bypass the scope the caller
|
|
4001
|
-
// asked for.
|
|
4002
|
-
const namespaceMismatch =
|
|
4003
|
-
requestedNamespace !== undefined &&
|
|
4004
|
-
rawSnapshot?.namespace !== requestedNamespace;
|
|
4005
|
-
if (!rawSnapshot) {
|
|
4006
|
-
xrayResponse = { snapshotFound: false };
|
|
4007
|
-
} else if (namespaceMismatch) {
|
|
4008
|
-
xrayResponse = { snapshotFound: false };
|
|
4009
|
-
} else {
|
|
4010
|
-
// Tag filter (issue #689). Mirrors `recall()` semantics — applied
|
|
4011
|
-
// post-capture by reading each result's frontmatter tags and
|
|
4012
|
-
// dropping non-matching results. Filter activity surfaces as a
|
|
4013
|
-
// `tag-filter` entry in `snapshot.filters` so X-ray consumers can
|
|
4014
|
-
// see the "considered → admitted" delta.
|
|
4015
|
-
let snapshot = rawSnapshot;
|
|
4016
|
-
const xrayFilterTags = normalizeTags(request.tags);
|
|
4017
|
-
let xrayTagMatch: TagMatchMode | undefined;
|
|
4018
|
-
try {
|
|
4019
|
-
xrayTagMatch = parseTagMatch(request.tagMatch);
|
|
4020
|
-
} catch (err) {
|
|
4021
|
-
throw new EngramAccessInputError(
|
|
4022
|
-
err instanceof Error ? err.message : String(err),
|
|
4023
|
-
);
|
|
4024
|
-
}
|
|
4025
|
-
if (xrayFilterTags && xrayFilterTags.length > 0) {
|
|
4026
|
-
const namespace = snapshot.namespace
|
|
4027
|
-
? this.resolveNamespace(snapshot.namespace)
|
|
4028
|
-
: this.orchestrator.config.defaultNamespace;
|
|
4029
|
-
const tagsByIndex = await Promise.all(
|
|
4030
|
-
snapshot.results.map(async (result) => {
|
|
4031
|
-
try {
|
|
4032
|
-
const storage = await this.orchestrator.getStorage(namespace);
|
|
4033
|
-
const memory = await storage.readMemoryByPath(result.path);
|
|
4034
|
-
const t = memory?.frontmatter?.tags;
|
|
4035
|
-
// Normalize identically to the recall path
|
|
4036
|
-
// (`normalizeProjectionTags`): trim and drop empty strings
|
|
4037
|
-
// so X-ray tag matching stays consistent with the recall
|
|
4038
|
-
// surface. Without this, a frontmatter tag like " draft "
|
|
4039
|
-
// would match in recall but not in X-ray (cursor review).
|
|
4040
|
-
return Array.isArray(t) ? normalizeProjectionTags(t) : [];
|
|
4041
|
-
} catch {
|
|
4042
|
-
return [];
|
|
4043
|
-
}
|
|
4044
|
-
}),
|
|
4045
|
-
);
|
|
4046
|
-
const tagged = snapshot.results.map((result, index) => ({
|
|
4047
|
-
result,
|
|
4048
|
-
tags: tagsByIndex[index] ?? [],
|
|
4049
|
-
}));
|
|
4050
|
-
const { results: admittedTagged, trace } = applyTagFilter(tagged, {
|
|
4051
|
-
tags: xrayFilterTags,
|
|
4052
|
-
tagMatch: xrayTagMatch,
|
|
4053
|
-
});
|
|
4054
|
-
const admittedResults = admittedTagged.map((entry) => entry.result);
|
|
4055
|
-
const filters = trace ? [...snapshot.filters, trace] : snapshot.filters;
|
|
4056
|
-
snapshot = { ...snapshot, results: admittedResults, filters };
|
|
4057
|
-
}
|
|
4058
|
-
// Decorate per-result disclosure + token estimate when the caller
|
|
4059
|
-
// wired a depth knob (issue #677 PR 3/4 — codex review on #699
|
|
4060
|
-
// flagged that the renderer's per-disclosure summary stays empty
|
|
4061
|
-
// until callers populate these fields). Estimate tokens from
|
|
4062
|
-
// the actual rendered payload at the requested depth so the
|
|
4063
|
-
// summary reflects real spend; chunk uses the preview, section
|
|
4064
|
-
// and raw use full content. Best-effort only — a missing
|
|
4065
|
-
// memory or read failure is silently dropped (CLAUDE.md rule 13).
|
|
4066
|
-
if (request.disclosure !== undefined) {
|
|
4067
|
-
// Disclosure already validated up front; pin to the narrowed
|
|
4068
|
-
// type here. Re-validation inside the queue would be dead code.
|
|
4069
|
-
const disclosure: RecallDisclosure = request.disclosure;
|
|
4070
|
-
const namespace = snapshot.namespace
|
|
4071
|
-
? this.resolveNamespace(snapshot.namespace)
|
|
4072
|
-
: this.orchestrator.config.defaultNamespace;
|
|
4073
|
-
// Pre-fetch raw excerpts ONCE so the first raw-disclosure
|
|
4074
|
-
// result's token estimate includes the LCM-side excerpt spend
|
|
4075
|
-
// that `shapeMemorySummary` actually attaches in the recall
|
|
4076
|
-
// response. Without this, raw recalls systematically
|
|
4077
|
-
// undercounted spend on the first result (Cursor Medium review
|
|
4078
|
-
// on PR #699). Excerpts are scoped to the same session +
|
|
4079
|
-
// namespace as the recall.
|
|
4080
|
-
// Trim sessionKey to match what `orchestrator.recall(...)`
|
|
4081
|
-
// already does (`request.sessionKey?.trim() || undefined`),
|
|
4082
|
-
// otherwise a whitespace-padded key drives recall under one
|
|
4083
|
-
// identity but probes LCM under a different prefix and
|
|
4084
|
-
// misses stored excerpts (Cursor Low review on PR #699).
|
|
4085
|
-
const trimmedSessionKey = request.sessionKey?.trim() || undefined;
|
|
4086
|
-
// Read-authorization-gated namespace for the raw-excerpt LCM lookup
|
|
4087
|
-
// (#1505 thread 2f7). NOT `snapshot.namespace` (the write/overlay
|
|
4088
|
-
// namespace), which would attach `<principal>-project-*` overlay rows
|
|
4089
|
-
// when the principal can WRITE but not READ its self base. Mirrors the
|
|
4090
|
-
// recall + `lcmSearch` read gate. Resolved ONLY for raw disclosure (its
|
|
4091
|
-
// sole consumer); the `namespace` above is still used for the
|
|
4092
|
-
// memory-FILE reads below (a separate, snapshot-scoped read), so non-raw
|
|
4093
|
-
// x-ray decoration stays byte-for-byte unchanged.
|
|
4094
|
-
const rawExcerptNamespace =
|
|
4095
|
-
disclosure === "raw"
|
|
4096
|
-
? this.resolveRawExcerptReadNamespace(
|
|
4097
|
-
request.namespace,
|
|
4098
|
-
trimmedSessionKey,
|
|
4099
|
-
authenticatedPrincipal,
|
|
4100
|
-
)
|
|
4101
|
-
: namespace;
|
|
4102
|
-
// `undefined` for an IMPLICIT raw recall means NO readable LCM namespace
|
|
4103
|
-
// exists (restrictive `default` READ policy, no readable overlay/self)
|
|
4104
|
-
// — suppress excerpts rather than fall back to the write/overlay
|
|
4105
|
-
// namespace the read gate excludes (#1505 thread NBHWz).
|
|
4106
|
-
const xrayHasExplicitNamespace =
|
|
4107
|
-
typeof request.namespace === "string" &&
|
|
4108
|
-
request.namespace.trim().length > 0;
|
|
4109
|
-
const rawExcerptsSuppressed =
|
|
4110
|
-
disclosure === "raw" &&
|
|
4111
|
-
!xrayHasExplicitNamespace &&
|
|
4112
|
-
rawExcerptNamespace === undefined;
|
|
4113
|
-
// Ordered, read-authorized LCM read key SET (#1505 fallback
|
|
4114
|
-
// unification) so raw disclosure finds excerpts a branch-scoped session
|
|
4115
|
-
// archived at project/root scope — exactly as recall + `lcmSearch` do.
|
|
4116
|
-
// Only meaningful with a concrete sessionKey + a readable namespace;
|
|
4117
|
-
// already read-gated so no unauthorized overlay key is present.
|
|
4118
|
-
const rawExcerptSessionIds =
|
|
4119
|
-
disclosure === "raw" && trimmedSessionKey && rawExcerptNamespace
|
|
4120
|
-
? this.resolveLcmReadSessionIds(
|
|
4121
|
-
request.namespace,
|
|
4122
|
-
rawExcerptNamespace,
|
|
4123
|
-
trimmedSessionKey,
|
|
4124
|
-
authenticatedPrincipal,
|
|
4125
|
-
)
|
|
4126
|
-
: undefined;
|
|
4127
|
-
const rawExcerpts =
|
|
4128
|
-
disclosure === "raw" && !rawExcerptsSuppressed
|
|
4129
|
-
? await this.fetchRawExcerpts(disclosure, {
|
|
4130
|
-
query,
|
|
4131
|
-
...(trimmedSessionKey ? { sessionKey: trimmedSessionKey } : {}),
|
|
4132
|
-
...(rawExcerptNamespace
|
|
4133
|
-
? { namespace: rawExcerptNamespace }
|
|
4134
|
-
: {}),
|
|
4135
|
-
...(rawExcerptSessionIds !== undefined
|
|
4136
|
-
? { lcmSessionIds: rawExcerptSessionIds }
|
|
4137
|
-
: {}),
|
|
4138
|
-
})
|
|
4139
|
-
: disclosure === "raw"
|
|
4140
|
-
? []
|
|
4141
|
-
: null;
|
|
4142
|
-
const rawExcerptText =
|
|
4143
|
-
rawExcerpts && rawExcerpts.length > 0
|
|
4144
|
-
? rawExcerpts.map((e) => e.content).join("\n")
|
|
4145
|
-
: "";
|
|
4146
|
-
// Pre-load every memory in parallel so we can:
|
|
4147
|
-
// (a) re-attribute raw excerpts to the *first readable* result
|
|
4148
|
-
// rather than always to index 0 (Cursor Low review on PR
|
|
4149
|
-
// #699: a missing/unreadable result[0] orphaned the excerpt
|
|
4150
|
-
// budget); and
|
|
4151
|
-
// (b) include the metadata fields `shapeMemorySummary` actually
|
|
4152
|
-
// emits at every depth (id, path, category, status, created,
|
|
4153
|
-
// updated, tags, entityRef) in the token estimate, so the
|
|
4154
|
-
// summary reflects real spend rather than only payload-body
|
|
4155
|
-
// spend (Cursor Low review on PR #699).
|
|
4156
|
-
const memoryByIndex = await Promise.all(
|
|
4157
|
-
snapshot.results.map(async (result) => {
|
|
4158
|
-
try {
|
|
4159
|
-
const storage = await this.orchestrator.getStorage(namespace);
|
|
4160
|
-
return await storage.readMemoryByPath(result.path);
|
|
4161
|
-
} catch {
|
|
4162
|
-
return null;
|
|
4163
|
-
}
|
|
4164
|
-
}),
|
|
4165
|
-
);
|
|
4166
|
-
const firstReadableIndex = memoryByIndex.findIndex((m) => m !== null);
|
|
4167
|
-
const baseDir =
|
|
4168
|
-
(await this.orchestrator.getStorage(namespace)).dir;
|
|
4169
|
-
const decorated = snapshot.results.map((result, index) => {
|
|
4170
|
-
const memory = memoryByIndex[index];
|
|
4171
|
-
if (!memory) {
|
|
4172
|
-
// Unreadable result: attach the disclosure tag anyway so
|
|
4173
|
-
// the per-disclosure summary classifies it correctly,
|
|
4174
|
-
// but skip the token estimate since we don't have the
|
|
4175
|
-
// content to measure. Without the disclosure tag the
|
|
4176
|
-
// result silently flows into the `unspecified` bucket
|
|
4177
|
-
// even though the caller explicitly requested a depth
|
|
4178
|
-
// (Cursor Low review on PR #699).
|
|
4179
|
-
return { ...result, disclosure };
|
|
4180
|
-
}
|
|
4181
|
-
// Build a representative shaped summary so the estimate
|
|
4182
|
-
// counts every field `shapeMemorySummary` actually emits.
|
|
4183
|
-
// The serialized JSON form is a close-enough proxy for the
|
|
4184
|
-
// wire payload size.
|
|
4185
|
-
const shaped = shapeMemorySummary(
|
|
4186
|
-
memory,
|
|
4187
|
-
baseDir,
|
|
4188
|
-
disclosure,
|
|
4189
|
-
disclosure === "raw" &&
|
|
4190
|
-
index === firstReadableIndex &&
|
|
4191
|
-
rawExcerpts &&
|
|
4192
|
-
rawExcerpts.length > 0
|
|
4193
|
-
? rawExcerpts
|
|
4194
|
-
: undefined,
|
|
4195
|
-
);
|
|
4196
|
-
return {
|
|
4197
|
-
...result,
|
|
4198
|
-
disclosure,
|
|
4199
|
-
estimatedTokens: estimateRecallTokens(JSON.stringify(shaped)),
|
|
4200
|
-
};
|
|
4201
|
-
});
|
|
4202
|
-
// Edge case: every result was unreadable but rawExcerpts
|
|
4203
|
-
// still has content — credit that spend to result[0] rather
|
|
4204
|
-
// than dropping it on the floor. Without this, the raw row
|
|
4205
|
-
// in the per-disclosure summary under-reports spend whenever
|
|
4206
|
-
// every memory file is missing/unreadable.
|
|
4207
|
-
if (
|
|
4208
|
-
disclosure === "raw" &&
|
|
4209
|
-
firstReadableIndex === -1 &&
|
|
4210
|
-
rawExcerptText.length > 0 &&
|
|
4211
|
-
decorated.length > 0
|
|
4212
|
-
) {
|
|
4213
|
-
decorated[0] = {
|
|
4214
|
-
...decorated[0]!,
|
|
4215
|
-
disclosure,
|
|
4216
|
-
estimatedTokens: estimateRecallTokens(rawExcerptText),
|
|
4217
|
-
};
|
|
4218
|
-
}
|
|
4219
|
-
const decoratedSnapshot = { ...snapshot, results: decorated };
|
|
4220
|
-
xrayResponse = {
|
|
4221
|
-
snapshotFound: true,
|
|
4222
|
-
snapshot: decoratedSnapshot,
|
|
4223
|
-
};
|
|
4224
|
-
} else {
|
|
4225
|
-
xrayResponse = {
|
|
4226
|
-
snapshotFound: true,
|
|
4227
|
-
snapshot,
|
|
4228
|
-
};
|
|
4229
|
-
}
|
|
4230
|
-
}
|
|
4231
|
-
} finally {
|
|
4232
|
-
release();
|
|
4233
|
-
}
|
|
4234
|
-
|
|
4235
|
-
if (
|
|
4236
|
-
request.includeRecall === true &&
|
|
4237
|
-
xrayResponse.snapshotFound === true &&
|
|
4238
|
-
xrayResponse.snapshot
|
|
4239
|
-
) {
|
|
4240
|
-
// Same read-authorization-gated raw-excerpt namespace the recall path uses
|
|
4241
|
-
// (#1505 thread 2f7), so the includeRecall x-ray path can't leak overlay
|
|
4242
|
-
// transcript rows via raw disclosure. Resolved ONLY for raw disclosure (the
|
|
4243
|
-
// sole consumer) so non-raw x-ray recall stays byte-for-byte unchanged. The
|
|
4244
|
-
// ordered LCM read key SET (#1505 fallback unification) adds the coding read
|
|
4245
|
-
// fallbacks so a branch-scoped session also finds excerpts at project/root
|
|
4246
|
-
// scope.
|
|
4247
|
-
const xrayRawExcerptNamespace =
|
|
4248
|
-
disclosure === "raw"
|
|
4249
|
-
? this.resolveRawExcerptReadNamespace(
|
|
4250
|
-
request.namespace,
|
|
4251
|
-
recallSessionKey,
|
|
4252
|
-
authenticatedPrincipal,
|
|
4253
|
-
)
|
|
4254
|
-
: undefined;
|
|
4255
|
-
// `undefined` for an IMPLICIT raw recall means NO readable LCM namespace
|
|
4256
|
-
// exists — suppress excerpts rather than fall back to the write/overlay
|
|
4257
|
-
// namespace the read gate excludes (#1505 thread NBHWz).
|
|
4258
|
-
const xrayHasExplicitNamespace =
|
|
4259
|
-
typeof request.namespace === "string" &&
|
|
4260
|
-
request.namespace.trim().length > 0;
|
|
4261
|
-
const xrayRawExcerptsSuppressed =
|
|
4262
|
-
disclosure === "raw" &&
|
|
4263
|
-
!xrayHasExplicitNamespace &&
|
|
4264
|
-
xrayRawExcerptNamespace === undefined;
|
|
4265
|
-
const xrayRawExcerptSessionIds =
|
|
4266
|
-
disclosure === "raw" && xrayRawExcerptNamespace && recallSessionKey
|
|
4267
|
-
? this.resolveLcmReadSessionIds(
|
|
4268
|
-
request.namespace,
|
|
4269
|
-
xrayRawExcerptNamespace,
|
|
4270
|
-
recallSessionKey,
|
|
4271
|
-
authenticatedPrincipal,
|
|
4272
|
-
)
|
|
4273
|
-
: undefined;
|
|
4274
|
-
return {
|
|
4275
|
-
...xrayResponse,
|
|
4276
|
-
recall: await this.buildRecallResponseFromXraySnapshot({
|
|
4277
|
-
query,
|
|
4278
|
-
sessionKey: recallSessionKey,
|
|
4279
|
-
snapshot: xrayResponse.snapshot,
|
|
4280
|
-
disclosure,
|
|
4281
|
-
startedAt: recallStartedAt,
|
|
4282
|
-
requestedMode: request.mode,
|
|
4283
|
-
normalizedMode: mode,
|
|
4284
|
-
...(xrayRawExcerptNamespace
|
|
4285
|
-
? { rawExcerptNamespace: xrayRawExcerptNamespace }
|
|
4286
|
-
: {}),
|
|
4287
|
-
...(xrayRawExcerptSessionIds !== undefined
|
|
4288
|
-
? { rawExcerptSessionIds: xrayRawExcerptSessionIds }
|
|
4289
|
-
: {}),
|
|
4290
|
-
...(xrayRawExcerptsSuppressed
|
|
4291
|
-
? { rawExcerptsSuppressed: xrayRawExcerptsSuppressed }
|
|
4292
|
-
: {}),
|
|
4293
|
-
}),
|
|
4294
|
-
};
|
|
4295
|
-
}
|
|
4296
|
-
return xrayResponse;
|
|
3104
|
+
return this.accessRecallSurface.recallXray(
|
|
3105
|
+
request,
|
|
3106
|
+
);
|
|
4297
3107
|
}
|
|
4298
3108
|
// Sequence lock for `recallXray` — see comment inside the method.
|
|
4299
3109
|
// Lives on the instance so every x-ray call on the same service
|