@remnic/core 9.3.751 → 9.3.753
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 +194 -0
- package/dist/access-admin-ops-surface.js +128 -0
- package/dist/access-boundary.d.ts +5 -5
- package/dist/access-boundary.js +30 -30
- package/dist/access-cli.js +92 -92
- package/dist/access-http.d.ts +5 -5
- package/dist/access-http.js +38 -38
- package/dist/access-mcp.d.ts +5 -5
- package/dist/access-mcp.js +35 -35
- package/dist/access-operations-batch.js +32 -32
- package/dist/access-operations.d.ts +11 -11
- package/dist/access-operations.js +34 -34
- package/dist/access-recall-surface.d.ts +363 -0
- package/dist/access-recall-surface.js +128 -0
- package/dist/access-schema.d.ts +116 -116
- package/dist/access-schema.js +8 -8
- package/dist/{access-service-DmAEOJdJ.d.ts → access-service-MitduesD.d.ts} +75 -118
- package/dist/access-service.d.ts +5 -5
- package/dist/access-service.js +33 -29
- 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 +6 -6
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- 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 +3 -3
- package/dist/briefing.js +10 -10
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +3 -3
- 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/capsule-crypto-CZJSLEFG.js.map +1 -0
- package/dist/{catalog-BpOcwHdE.d.ts → catalog-BEmNVRN0.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 +15 -15
- 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-6SXNQFHB.js → chunk-243P7YYV.js} +7 -7
- package/dist/{chunk-2NLLXCJG.js → chunk-2BFLDH5F.js} +2 -2
- package/dist/{chunk-ZBVQ4ZM4.js → chunk-34THARMH.js} +17 -17
- package/dist/{chunk-SNEXVX3U.js → chunk-3HGZX26P.js} +21 -21
- package/dist/{chunk-5PZCDUJ6.js → chunk-3ZIKDH6C.js} +4 -4
- package/dist/{chunk-X7Y7WX73.js → chunk-437S3G37.js} +2 -2
- package/dist/{chunk-RS25QOKZ.js → chunk-64SE4X5G.js} +2 -2
- package/dist/{chunk-JBPKEARU.js → chunk-AU7Q3LSC.js} +4 -4
- package/dist/{chunk-6FNZ3NMS.js → chunk-AXBRVBT6.js} +2 -2
- package/dist/{chunk-W3BFT5OJ.js → chunk-BWOEYBRB.js} +2 -2
- package/dist/{chunk-UU32EAIY.js → chunk-C3IHRWIC.js} +7 -7
- package/dist/{chunk-DBRVU5PR.js → chunk-DKIZ4BUQ.js} +14 -14
- package/dist/{chunk-EEKDRAWX.js → chunk-DS4QM6I7.js} +4 -4
- package/dist/{chunk-WOX2UW4K.js → chunk-E3MRYDUQ.js} +2 -2
- package/dist/{chunk-K2R3DEEH.js → chunk-E6LENGYV.js} +2 -2
- package/dist/{chunk-CGZWGVQF.js → chunk-EEO4YWQ7.js} +9 -9
- package/dist/{chunk-ORAK3LNZ.js → chunk-ER3CCWK2.js} +2 -2
- package/dist/{chunk-TPLE6SNY.js → chunk-GWGWBK5Y.js} +7 -7
- package/dist/{chunk-BJMBJZ2Y.js → chunk-GWTFLF2P.js} +59 -59
- package/dist/chunk-GWTFLF2P.js.map +1 -0
- package/dist/{chunk-IKHKHADN.js → chunk-ITLJATQI.js} +5 -5
- package/dist/{chunk-E4PFDD3N.js → chunk-JES2O5Q6.js} +4 -4
- package/dist/{chunk-QOGXUNWL.js → chunk-JGPURIEH.js} +7 -7
- package/dist/{chunk-YGGXUNS4.js → chunk-KY4WOZ3P.js} +55 -55
- package/dist/{chunk-WA7FECSG.js → chunk-KYHGLDFU.js} +7 -7
- package/dist/{chunk-V5RVMULT.js → chunk-LZ73Y56P.js} +10 -10
- package/dist/{chunk-ARV3AUOM.js → chunk-M5QKGHCR.js} +2 -2
- package/dist/{chunk-SD34EK4Z.js → chunk-N3VE7OCY.js} +4 -4
- package/dist/{chunk-KQAFEZQX.js → chunk-OUXPVWTZ.js} +6 -6
- package/dist/{chunk-NNL4LJAD.js → chunk-OZMBT7N3.js} +5 -5
- package/dist/{chunk-RWZB6MAY.js → chunk-PN5AMCK2.js} +1065 -1176
- package/dist/chunk-PN5AMCK2.js.map +1 -0
- package/dist/{chunk-WWSXVOGY.js → chunk-QIRZI3C3.js} +2 -2
- package/dist/{chunk-B35L45HZ.js → chunk-TVISHYL5.js} +2 -2
- package/dist/{chunk-SQNED75R.js → chunk-V5UV7BOA.js} +1 -1
- package/dist/{chunk-7TWA7DKP.js → chunk-VVSV2QVY.js} +2 -2
- package/dist/{chunk-4NWIGAIC.js → chunk-XOPLMSVV.js} +2 -2
- package/dist/{chunk-K2JM4DZZ.js → chunk-YNBEHUZH.js} +2 -2
- package/dist/{chunk-EZ25VE3G.js → chunk-YNDLCWXS.js} +4 -4
- package/dist/{chunk-UETBEHBA.js → chunk-ZECLLIMX.js} +3442 -3085
- package/dist/chunk-ZECLLIMX.js.map +1 -0
- package/dist/{chunk-QRRBXD24.js → chunk-ZIFPFKB2.js} +7 -7
- package/dist/{chunk-HYI2GP3F.js → chunk-ZMHDSIEX.js} +7 -7
- package/dist/{cli-lmf5RGyQ.d.ts → cli-DqonYBZ0.d.ts} +3 -3
- package/dist/cli.d.ts +6 -6
- package/dist/cli.js +64 -64
- package/dist/compounding/engine.d.ts +3 -3
- package/dist/compounding/engine.js +11 -11
- 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 +11 -11
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +12 -12
- 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-2SODRNUF.js} +3 -3
- package/dist/contradiction-scan-2SODRNUF.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/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +11 -11
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +6 -6
- 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 +3 -3
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/{first-start-migration-4I3VDSYS.js → first-start-migration-DY6YCRC2.js} +2 -2
- package/dist/{forget-WYLR4ZFV.js → forget-OEE5OELK.js} +2 -2
- 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 +1284 -1284
- package/dist/index.js +148 -148
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/engine.js +4 -4
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/index.js +8 -8
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/local-model-endpoint.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +10 -10
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +10 -10
- package/dist/maintenance/rebuild-memory-projection.js +11 -11
- 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 +13 -13
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +1 -1
- package/dist/namespaces/storage.d.ts +2 -2
- package/dist/namespaces/storage.js +12 -12
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +19 -19
- package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +15 -15
- package/dist/{orchestrator-D7bb8ZGp.d.ts → orchestrator-D7ZGOQLX.d.ts} +101 -180
- package/dist/orchestrator.d.ts +4 -4
- package/dist/orchestrator.js +92 -92
- 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 +3 -3
- 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/schemas.d.ts +100 -100
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +1 -1
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +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-CTfbVwCA.d.ts → semantic-consolidation-7Sndms2M.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +13 -13
- package/dist/semantic-rule-promotion.js +10 -10
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +10 -10
- 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 +9 -9
- package/dist/signal.d.ts +1 -1
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +9 -9
- package/dist/summarizer.d.ts +1 -1
- package/dist/summarizer.js +2 -2
- 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/{tier-stats-5XF4UBRQ.js → tier-stats-TG6GUZ6N.js} +4 -4
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/autodetect.js +1 -1
- package/dist/transfer/backup.js +3 -3
- package/dist/transfer/capsule-export.js +4 -4
- package/dist/transfer/capsule-import.js +3 -3
- package/dist/transfer/types.d.ts +110 -110
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-D4NYDtXI.d.ts → types-DTEFxFuL.d.ts} +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +10 -10
- package/package.json +2 -2
- package/src/access-admin-ops-surface.ts +828 -0
- package/src/access-recall-surface.ts +1552 -0
- package/src/access-service.ts +70 -1917
- 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-admin-ops-surface.js.map} +0 -0
- /package/dist/{contradiction-scan-3FJYWS3G.js.map → access-recall-surface.js.map} +0 -0
- /package/dist/{chunk-6SXNQFHB.js.map → chunk-243P7YYV.js.map} +0 -0
- /package/dist/{chunk-2NLLXCJG.js.map → chunk-2BFLDH5F.js.map} +0 -0
- /package/dist/{chunk-ZBVQ4ZM4.js.map → chunk-34THARMH.js.map} +0 -0
- /package/dist/{chunk-SNEXVX3U.js.map → chunk-3HGZX26P.js.map} +0 -0
- /package/dist/{chunk-5PZCDUJ6.js.map → chunk-3ZIKDH6C.js.map} +0 -0
- /package/dist/{chunk-X7Y7WX73.js.map → chunk-437S3G37.js.map} +0 -0
- /package/dist/{chunk-RS25QOKZ.js.map → chunk-64SE4X5G.js.map} +0 -0
- /package/dist/{chunk-JBPKEARU.js.map → chunk-AU7Q3LSC.js.map} +0 -0
- /package/dist/{chunk-6FNZ3NMS.js.map → chunk-AXBRVBT6.js.map} +0 -0
- /package/dist/{chunk-W3BFT5OJ.js.map → chunk-BWOEYBRB.js.map} +0 -0
- /package/dist/{chunk-UU32EAIY.js.map → chunk-C3IHRWIC.js.map} +0 -0
- /package/dist/{chunk-DBRVU5PR.js.map → chunk-DKIZ4BUQ.js.map} +0 -0
- /package/dist/{chunk-EEKDRAWX.js.map → chunk-DS4QM6I7.js.map} +0 -0
- /package/dist/{chunk-WOX2UW4K.js.map → chunk-E3MRYDUQ.js.map} +0 -0
- /package/dist/{chunk-K2R3DEEH.js.map → chunk-E6LENGYV.js.map} +0 -0
- /package/dist/{chunk-CGZWGVQF.js.map → chunk-EEO4YWQ7.js.map} +0 -0
- /package/dist/{chunk-ORAK3LNZ.js.map → chunk-ER3CCWK2.js.map} +0 -0
- /package/dist/{chunk-TPLE6SNY.js.map → chunk-GWGWBK5Y.js.map} +0 -0
- /package/dist/{chunk-IKHKHADN.js.map → chunk-ITLJATQI.js.map} +0 -0
- /package/dist/{chunk-E4PFDD3N.js.map → chunk-JES2O5Q6.js.map} +0 -0
- /package/dist/{chunk-QOGXUNWL.js.map → chunk-JGPURIEH.js.map} +0 -0
- /package/dist/{chunk-YGGXUNS4.js.map → chunk-KY4WOZ3P.js.map} +0 -0
- /package/dist/{chunk-WA7FECSG.js.map → chunk-KYHGLDFU.js.map} +0 -0
- /package/dist/{chunk-V5RVMULT.js.map → chunk-LZ73Y56P.js.map} +0 -0
- /package/dist/{chunk-ARV3AUOM.js.map → chunk-M5QKGHCR.js.map} +0 -0
- /package/dist/{chunk-SD34EK4Z.js.map → chunk-N3VE7OCY.js.map} +0 -0
- /package/dist/{chunk-KQAFEZQX.js.map → chunk-OUXPVWTZ.js.map} +0 -0
- /package/dist/{chunk-NNL4LJAD.js.map → chunk-OZMBT7N3.js.map} +0 -0
- /package/dist/{chunk-WWSXVOGY.js.map → chunk-QIRZI3C3.js.map} +0 -0
- /package/dist/{chunk-B35L45HZ.js.map → chunk-TVISHYL5.js.map} +0 -0
- /package/dist/{chunk-SQNED75R.js.map → chunk-V5UV7BOA.js.map} +0 -0
- /package/dist/{chunk-7TWA7DKP.js.map → chunk-VVSV2QVY.js.map} +0 -0
- /package/dist/{chunk-4NWIGAIC.js.map → chunk-XOPLMSVV.js.map} +0 -0
- /package/dist/{chunk-K2JM4DZZ.js.map → chunk-YNBEHUZH.js.map} +0 -0
- /package/dist/{chunk-EZ25VE3G.js.map → chunk-YNDLCWXS.js.map} +0 -0
- /package/dist/{chunk-QRRBXD24.js.map → chunk-ZIFPFKB2.js.map} +0 -0
- /package/dist/{chunk-HYI2GP3F.js.map → chunk-ZMHDSIEX.js.map} +0 -0
- /package/dist/{first-start-migration-4I3VDSYS.js.map → first-start-migration-DY6YCRC2.js.map} +0 -0
- /package/dist/{forget-WYLR4ZFV.js.map → forget-OEE5OELK.js.map} +0 -0
- /package/dist/{tier-stats-5XF4UBRQ.js.map → tier-stats-TG6GUZ6N.js.map} +0 -0
package/src/access-service.ts
CHANGED
|
@@ -251,6 +251,9 @@ 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 { AccessAdminOpsSurface } from "./access-admin-ops-surface.js";
|
|
255
|
+
import { AccessRecallSurface } from "./access-recall-surface.js";
|
|
256
|
+
import { selfDeps } from "./orchestration/self-deps.js";
|
|
254
257
|
|
|
255
258
|
export class EngramAccessInputError extends Error {}
|
|
256
259
|
|
|
@@ -300,12 +303,12 @@ function qmdResultPathCandidates(
|
|
|
300
303
|
return [...candidates];
|
|
301
304
|
}
|
|
302
305
|
|
|
303
|
-
type AccessProfilingReportRequest = {
|
|
306
|
+
export type AccessProfilingReportRequest = {
|
|
304
307
|
format?: string;
|
|
305
308
|
limit?: number;
|
|
306
309
|
};
|
|
307
310
|
|
|
308
|
-
type AccessProfilingReportResponse = {
|
|
311
|
+
export type AccessProfilingReportResponse = {
|
|
309
312
|
enabled: boolean;
|
|
310
313
|
format?: "ascii" | "json";
|
|
311
314
|
report?: string;
|
|
@@ -858,7 +861,7 @@ export interface EngramAccessOfflineSyncApplyResponse extends OfflineSyncApplyCh
|
|
|
858
861
|
export type EngramAccessActionConfidenceRequest = ActionConfidenceInput;
|
|
859
862
|
export type EngramAccessActionConfidenceResponse = ActionConfidenceResult;
|
|
860
863
|
|
|
861
|
-
async function buildProjectedGovernanceProposedActions(
|
|
864
|
+
export async function buildProjectedGovernanceProposedActions(
|
|
862
865
|
storage: Awaited<ReturnType<Orchestrator["getStorage"]>>,
|
|
863
866
|
projected: NonNullable<Awaited<ReturnType<Awaited<ReturnType<Orchestrator["getStorage"]>>["getProjectedGovernanceRecord"]>>>,
|
|
864
867
|
): Promise<Awaited<ReturnType<typeof readMemoryGovernanceRunArtifact>>["appliedActions"]> {
|
|
@@ -877,7 +880,7 @@ async function buildProjectedGovernanceProposedActions(
|
|
|
877
880
|
return buildProposedActions(reviewQueue, memories);
|
|
878
881
|
}
|
|
879
882
|
|
|
880
|
-
function hasGroupedGovernanceActions(
|
|
883
|
+
export function hasGroupedGovernanceActions(
|
|
881
884
|
grouped?: Awaited<ReturnType<typeof readMemoryGovernanceRunArtifact>>["transitionReport"]["proposed"],
|
|
882
885
|
): boolean {
|
|
883
886
|
if (!grouped) return false;
|
|
@@ -1305,6 +1308,30 @@ export class EngramAccessService {
|
|
|
1305
1308
|
private readonly budget: CrossNamespaceBudget;
|
|
1306
1309
|
private readonly auditAdapter: AccessAuditAdapter | null;
|
|
1307
1310
|
|
|
1311
|
+
/** AccessAdminOpsSurface (access-service decomposition). Lazy; selfDeps live wiring. */
|
|
1312
|
+
private _accessAdminOpsSurface: AccessAdminOpsSurface | undefined;
|
|
1313
|
+
|
|
1314
|
+
private get accessAdminOpsSurface(): AccessAdminOpsSurface {
|
|
1315
|
+
if (!this._accessAdminOpsSurface) {
|
|
1316
|
+
this._accessAdminOpsSurface = new AccessAdminOpsSurface(
|
|
1317
|
+
selfDeps<ConstructorParameters<typeof AccessAdminOpsSurface>[0]>(this),
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
return this._accessAdminOpsSurface;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/** AccessRecallSurface (access-service decomposition). Lazy; selfDeps live wiring. */
|
|
1324
|
+
private _accessRecallSurface: AccessRecallSurface | undefined;
|
|
1325
|
+
|
|
1326
|
+
private get accessRecallSurface(): AccessRecallSurface {
|
|
1327
|
+
if (!this._accessRecallSurface) {
|
|
1328
|
+
this._accessRecallSurface = new AccessRecallSurface(
|
|
1329
|
+
selfDeps<ConstructorParameters<typeof AccessRecallSurface>[0]>(this),
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
return this._accessRecallSurface;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1308
1335
|
constructor(private readonly orchestrator: Orchestrator) {
|
|
1309
1336
|
this.idempotency = new AccessIdempotencyStore(orchestrator.config.memoryDir);
|
|
1310
1337
|
const accessCaps = resolveAccessSetupCapabilities(orchestrator.config); // #1566 Cluster B
|
|
@@ -2097,83 +2124,9 @@ export class EngramAccessService {
|
|
|
2097
2124
|
*/
|
|
2098
2125
|
rawExcerptsSuppressed?: boolean;
|
|
2099
2126
|
}): 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
|
-
},
|
|
2127
|
+
return this.accessRecallSurface.buildRecallResponseFromXraySnapshot(
|
|
2128
|
+
options,
|
|
2148
2129
|
);
|
|
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
2130
|
}
|
|
2178
2131
|
|
|
2179
2132
|
private async serializeRecallResults(
|
|
@@ -2214,198 +2167,11 @@ export class EngramAccessService {
|
|
|
2214
2167
|
}
|
|
2215
2168
|
| null = null,
|
|
2216
2169
|
): 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
|
-
),
|
|
2170
|
+
return this.accessRecallSurface.serializeRecallResults(
|
|
2171
|
+
snapshot,
|
|
2172
|
+
disclosure,
|
|
2173
|
+
rawContext,
|
|
2233
2174
|
);
|
|
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
2175
|
}
|
|
2410
2176
|
|
|
2411
2177
|
private async storageForAbsoluteRecallPath(
|
|
@@ -2460,19 +2226,6 @@ export class EngramAccessService {
|
|
|
2460
2226
|
return matches[0] ?? null;
|
|
2461
2227
|
}
|
|
2462
2228
|
|
|
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
2229
|
private async fetchRawExcerpts(
|
|
2477
2230
|
disclosure: RecallDisclosure,
|
|
2478
2231
|
context: {
|
|
@@ -2493,63 +2246,10 @@ export class EngramAccessService {
|
|
|
2493
2246
|
lcmSessionIds?: string[];
|
|
2494
2247
|
} | null,
|
|
2495
2248
|
): 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
|
-
}
|
|
2249
|
+
return this.accessRecallSurface.fetchRawExcerpts(
|
|
2250
|
+
disclosure,
|
|
2251
|
+
context,
|
|
2252
|
+
);
|
|
2553
2253
|
}
|
|
2554
2254
|
|
|
2555
2255
|
private async handleIdempotentWrite<T extends { idempotencyReplay?: boolean }>(options: {
|
|
@@ -3267,453 +2967,9 @@ export class EngramAccessService {
|
|
|
3267
2967
|
response: EngramAccessRecallResponse;
|
|
3268
2968
|
budgetRecordPrincipal: string | null;
|
|
3269
2969
|
}> {
|
|
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,
|
|
2970
|
+
return this.accessRecallSurface.executeRecall(
|
|
2971
|
+
request,
|
|
3310
2972
|
);
|
|
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,
|
|
3655
|
-
);
|
|
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
2973
|
}
|
|
3718
2974
|
|
|
3719
2975
|
async recallExplain(
|
|
@@ -3814,15 +3070,6 @@ export class EngramAccessService {
|
|
|
3814
3070
|
return toRecallExplainJson(snapshot);
|
|
3815
3071
|
}
|
|
3816
3072
|
|
|
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
3073
|
async recallXray(request: {
|
|
3827
3074
|
query: string;
|
|
3828
3075
|
sessionKey?: string;
|
|
@@ -3867,433 +3114,9 @@ export class EngramAccessService {
|
|
|
3867
3114
|
snapshot?: RecallXraySnapshot;
|
|
3868
3115
|
recall?: EngramAccessRecallResponse;
|
|
3869
3116
|
}> {
|
|
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;
|
|
3117
|
+
return this.accessRecallSurface.recallXray(
|
|
3118
|
+
request,
|
|
3119
|
+
);
|
|
4297
3120
|
}
|
|
4298
3121
|
// Sequence lock for `recallXray` — see comment inside the method.
|
|
4299
3122
|
// Lives on the instance so every x-ray call on the same service
|
|
@@ -5025,97 +3848,11 @@ export class EngramAccessService {
|
|
|
5025
3848
|
}
|
|
5026
3849
|
|
|
5027
3850
|
async reviewQueue(runId?: string, namespace?: string, principal?: string): Promise<EngramAccessReviewQueueResponse> {
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
action: row.action,
|
|
5034
|
-
memoryId: row.memoryId,
|
|
5035
|
-
reasonCode: row.reasonCode,
|
|
5036
|
-
beforeStatus: row.beforeStatus,
|
|
5037
|
-
afterStatus: row.afterStatus,
|
|
5038
|
-
originalPath: row.originalPath,
|
|
5039
|
-
currentPath: row.currentPath,
|
|
5040
|
-
})) as Awaited<
|
|
5041
|
-
ReturnType<typeof readMemoryGovernanceRunArtifact>
|
|
5042
|
-
>["appliedActions"];
|
|
5043
|
-
const projectedProposedActions = await buildProjectedGovernanceProposedActions(storage, projected);
|
|
5044
|
-
const projectedArtifact = await (async () => {
|
|
5045
|
-
try {
|
|
5046
|
-
return await readMemoryGovernanceRunArtifact(storage.dir, projected.runId);
|
|
5047
|
-
} catch {
|
|
5048
|
-
return null;
|
|
5049
|
-
}
|
|
5050
|
-
})();
|
|
5051
|
-
const metrics = projected.metrics as Awaited<ReturnType<typeof readMemoryGovernanceRunArtifact>>["metrics"];
|
|
5052
|
-
const fallbackTransitionReport = {
|
|
5053
|
-
proposed: groupActionsByStatus(projectedProposedActions),
|
|
5054
|
-
applied: groupActionsByStatus(projectedAppliedActions),
|
|
5055
|
-
};
|
|
5056
|
-
const transitionReport = projectedArtifact?.transitionReport
|
|
5057
|
-
? {
|
|
5058
|
-
proposed:
|
|
5059
|
-
hasGroupedGovernanceActions(projectedArtifact.transitionReport.proposed) || projectedProposedActions.length === 0
|
|
5060
|
-
? projectedArtifact.transitionReport.proposed
|
|
5061
|
-
: fallbackTransitionReport.proposed,
|
|
5062
|
-
applied:
|
|
5063
|
-
hasGroupedGovernanceActions(projectedArtifact.transitionReport.applied) || projectedAppliedActions.length === 0
|
|
5064
|
-
? projectedArtifact.transitionReport.applied
|
|
5065
|
-
: fallbackTransitionReport.applied,
|
|
5066
|
-
}
|
|
5067
|
-
: fallbackTransitionReport;
|
|
5068
|
-
const qualityScore = projectedArtifact?.qualityScore ?? metrics?.qualityScore ?? buildQualityScore(metrics?.reviewReasons ?? {
|
|
5069
|
-
exact_duplicate: 0,
|
|
5070
|
-
semantic_duplicate_candidate: 0,
|
|
5071
|
-
disputed_memory: 0,
|
|
5072
|
-
speculative_low_confidence: 0,
|
|
5073
|
-
archive_candidate: 0,
|
|
5074
|
-
explicit_capture_review: 0,
|
|
5075
|
-
malformed_import: 0,
|
|
5076
|
-
});
|
|
5077
|
-
const effectiveMetrics = metrics ? { ...metrics, qualityScore: metrics.qualityScore ?? qualityScore } : metrics;
|
|
5078
|
-
|
|
5079
|
-
return {
|
|
5080
|
-
found: true,
|
|
5081
|
-
namespace: resolvedNamespace,
|
|
5082
|
-
runId: projected.runId,
|
|
5083
|
-
summary: projected.summary as Awaited<ReturnType<typeof readMemoryGovernanceRunArtifact>>["summary"],
|
|
5084
|
-
metrics: effectiveMetrics,
|
|
5085
|
-
qualityScore,
|
|
5086
|
-
reviewQueue: projected.reviewQueueRows.map((row) => ({
|
|
5087
|
-
entryId: row.entryId,
|
|
5088
|
-
memoryId: row.memoryId,
|
|
5089
|
-
path: row.path,
|
|
5090
|
-
reasonCode: row.reasonCode,
|
|
5091
|
-
severity: row.severity,
|
|
5092
|
-
suggestedAction: row.suggestedAction,
|
|
5093
|
-
suggestedStatus: row.suggestedStatus,
|
|
5094
|
-
relatedMemoryIds: row.relatedMemoryIds,
|
|
5095
|
-
})) as Awaited<
|
|
5096
|
-
ReturnType<typeof readMemoryGovernanceRunArtifact>
|
|
5097
|
-
>["reviewQueue"],
|
|
5098
|
-
appliedActions: projectedAppliedActions,
|
|
5099
|
-
transitionReport,
|
|
5100
|
-
report: projected.report,
|
|
5101
|
-
};
|
|
5102
|
-
}
|
|
5103
|
-
|
|
5104
|
-
const resolvedRunId = runId?.trim() || (await listMemoryGovernanceRuns(storage.dir))[0];
|
|
5105
|
-
if (!resolvedRunId) return { found: false, namespace: resolvedNamespace };
|
|
5106
|
-
const artifact = await readMemoryGovernanceRunArtifact(storage.dir, resolvedRunId);
|
|
5107
|
-
return {
|
|
5108
|
-
found: true,
|
|
5109
|
-
namespace: resolvedNamespace,
|
|
5110
|
-
runId: resolvedRunId,
|
|
5111
|
-
summary: artifact.summary,
|
|
5112
|
-
metrics: artifact.metrics,
|
|
5113
|
-
qualityScore: artifact.qualityScore,
|
|
5114
|
-
reviewQueue: artifact.reviewQueue,
|
|
5115
|
-
appliedActions: artifact.appliedActions,
|
|
5116
|
-
transitionReport: artifact.transitionReport,
|
|
5117
|
-
report: artifact.report,
|
|
5118
|
-
};
|
|
3851
|
+
return this.accessAdminOpsSurface.reviewQueue(
|
|
3852
|
+
runId,
|
|
3853
|
+
namespace,
|
|
3854
|
+
principal,
|
|
3855
|
+
);
|
|
5119
3856
|
}
|
|
5120
3857
|
|
|
5121
3858
|
async maintenance(namespace?: string, principal?: string): Promise<EngramAccessMaintenanceResponse> {
|
|
@@ -5200,56 +3937,10 @@ export class EngramAccessService {
|
|
|
5200
3937
|
summaryPath: string;
|
|
5201
3938
|
reportPath: string;
|
|
5202
3939
|
}> {
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
}
|
|
5207
|
-
const resolvedNamespace = this.writableNamespaceFor(
|
|
5208
|
-
request.namespace,
|
|
5209
|
-
undefined,
|
|
5210
|
-
request.authenticatedPrincipal ?? principal,
|
|
3940
|
+
return this.accessAdminOpsSurface.governanceRun(
|
|
3941
|
+
request,
|
|
3942
|
+
principal,
|
|
5211
3943
|
);
|
|
5212
|
-
const storage = await this.orchestrator.getStorage(resolvedNamespace);
|
|
5213
|
-
const mode = request.mode === "apply" ? "apply" : "shadow";
|
|
5214
|
-
const boundedBatchSize =
|
|
5215
|
-
typeof request.batchSize === "number" && Number.isFinite(request.batchSize)
|
|
5216
|
-
? Math.max(1, Math.floor(request.batchSize))
|
|
5217
|
-
: undefined;
|
|
5218
|
-
const result = await runMemoryGovernance({
|
|
5219
|
-
memoryDir: storage.dir,
|
|
5220
|
-
mode,
|
|
5221
|
-
recentDays:
|
|
5222
|
-
typeof request.recentDays === "number" && Number.isFinite(request.recentDays)
|
|
5223
|
-
? Math.max(1, Math.floor(request.recentDays))
|
|
5224
|
-
: undefined,
|
|
5225
|
-
maxMemories:
|
|
5226
|
-
typeof request.maxMemories === "number" && Number.isFinite(request.maxMemories)
|
|
5227
|
-
? Math.max(1, Math.floor(request.maxMemories))
|
|
5228
|
-
: undefined,
|
|
5229
|
-
batchSize: boundedBatchSize,
|
|
5230
|
-
});
|
|
5231
|
-
if (mode === "apply") {
|
|
5232
|
-
try {
|
|
5233
|
-
await this.orchestrator.processEntitySynthesisQueue(
|
|
5234
|
-
resolvedNamespace,
|
|
5235
|
-
Math.min(boundedBatchSize ?? 5, 5),
|
|
5236
|
-
);
|
|
5237
|
-
} catch (error) {
|
|
5238
|
-
log.debug(`governanceRun: entity synthesis refresh failed after governance apply: ${error}`);
|
|
5239
|
-
}
|
|
5240
|
-
}
|
|
5241
|
-
|
|
5242
|
-
return {
|
|
5243
|
-
namespace: resolvedNamespace,
|
|
5244
|
-
runId: result.runId,
|
|
5245
|
-
traceId: result.traceId,
|
|
5246
|
-
mode: result.mode,
|
|
5247
|
-
reviewQueueCount: result.reviewQueue.length,
|
|
5248
|
-
proposedActionCount: result.proposedActions.length,
|
|
5249
|
-
appliedActionCount: result.appliedActions.length,
|
|
5250
|
-
summaryPath: result.summaryPath,
|
|
5251
|
-
reportPath: result.reportPath,
|
|
5252
|
-
};
|
|
5253
3944
|
}
|
|
5254
3945
|
|
|
5255
3946
|
async procedureMiningRun(
|
|
@@ -5415,160 +4106,17 @@ export class EngramAccessService {
|
|
|
5415
4106
|
reason?: string;
|
|
5416
4107
|
retryAfterMs?: number;
|
|
5417
4108
|
}> {
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
sessions: 0,
|
|
5422
|
-
chunks: 0,
|
|
5423
|
-
skipped: 0,
|
|
5424
|
-
skippedSessionKeys: [],
|
|
5425
|
-
embeddedRuns: 0,
|
|
5426
|
-
reason: "disabled",
|
|
5427
|
-
};
|
|
5428
|
-
}
|
|
5429
|
-
|
|
5430
|
-
const hours =
|
|
5431
|
-
typeof request.hours === "number" && Number.isFinite(request.hours)
|
|
5432
|
-
? Math.max(1, Math.floor(request.hours))
|
|
5433
|
-
: 24;
|
|
5434
|
-
|
|
5435
|
-
let sessionKey: string | undefined;
|
|
5436
|
-
if (request.sessionKey !== undefined) {
|
|
5437
|
-
if (typeof request.sessionKey !== "string" || request.sessionKey.trim().length === 0) {
|
|
5438
|
-
throw new EngramAccessInputError("sessionKey must be a non-empty string when provided");
|
|
5439
|
-
}
|
|
5440
|
-
sessionKey = request.sessionKey.trim();
|
|
5441
|
-
}
|
|
5442
|
-
|
|
5443
|
-
if (sessionKey) {
|
|
5444
|
-
const result = await this.orchestrator.updateConversationIndex(
|
|
5445
|
-
sessionKey,
|
|
5446
|
-
hours,
|
|
5447
|
-
{ embed: request.embed },
|
|
5448
|
-
);
|
|
5449
|
-
return {
|
|
5450
|
-
enabled: true,
|
|
5451
|
-
sessionKey,
|
|
5452
|
-
sessions: 1,
|
|
5453
|
-
chunks: result.chunks,
|
|
5454
|
-
skipped: result.skipped ? 1 : 0,
|
|
5455
|
-
skippedSessionKeys: result.skipped ? [sessionKey] : [],
|
|
5456
|
-
embeddedRuns: result.embedded ? 1 : 0,
|
|
5457
|
-
reason: result.reason,
|
|
5458
|
-
retryAfterMs: result.retryAfterMs,
|
|
5459
|
-
};
|
|
5460
|
-
}
|
|
5461
|
-
|
|
5462
|
-
const sessionKeys = await this.orchestrator.transcript.listSessionKeys();
|
|
5463
|
-
let chunks = 0;
|
|
5464
|
-
let skipped = 0;
|
|
5465
|
-
const skippedSessionKeys: string[] = [];
|
|
5466
|
-
let embeddedRuns = 0;
|
|
5467
|
-
|
|
5468
|
-
for (const sessionKey of sessionKeys) {
|
|
5469
|
-
const result = await this.orchestrator.updateConversationIndex(
|
|
5470
|
-
sessionKey,
|
|
5471
|
-
hours,
|
|
5472
|
-
{ embed: request.embed },
|
|
5473
|
-
);
|
|
5474
|
-
chunks += result.chunks;
|
|
5475
|
-
if (result.skipped) {
|
|
5476
|
-
skipped += 1;
|
|
5477
|
-
skippedSessionKeys.push(sessionKey);
|
|
5478
|
-
}
|
|
5479
|
-
if (result.embedded) {
|
|
5480
|
-
embeddedRuns += 1;
|
|
5481
|
-
}
|
|
5482
|
-
}
|
|
5483
|
-
|
|
5484
|
-
return {
|
|
5485
|
-
enabled: true,
|
|
5486
|
-
sessions: sessionKeys.length,
|
|
5487
|
-
chunks,
|
|
5488
|
-
skipped,
|
|
5489
|
-
skippedSessionKeys,
|
|
5490
|
-
embeddedRuns,
|
|
5491
|
-
};
|
|
4109
|
+
return this.accessAdminOpsSurface.conversationIndexUpdate(
|
|
4110
|
+
request,
|
|
4111
|
+
);
|
|
5492
4112
|
}
|
|
5493
4113
|
|
|
5494
4114
|
async profilingReport(
|
|
5495
4115
|
request: AccessProfilingReportRequest = {},
|
|
5496
4116
|
): Promise<AccessProfilingReportResponse> {
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
enabled: false,
|
|
5501
|
-
reason: "disabled",
|
|
5502
|
-
message: "Profiling is disabled. Set profilingEnabled: true in your plugin config to enable.",
|
|
5503
|
-
};
|
|
5504
|
-
}
|
|
5505
|
-
|
|
5506
|
-
const format = request.format ?? "ascii";
|
|
5507
|
-
if (format !== "ascii" && format !== "json") {
|
|
5508
|
-
throw new EngramAccessInputError("format must be one of: ascii, json");
|
|
5509
|
-
}
|
|
5510
|
-
|
|
5511
|
-
const limit = request.limit ?? 5;
|
|
5512
|
-
if (!Number.isInteger(limit) || limit < 1 || limit > 20) {
|
|
5513
|
-
throw new EngramAccessInputError("limit must be an integer between 1 and 20");
|
|
5514
|
-
}
|
|
5515
|
-
|
|
5516
|
-
const traces = profiler.getRecentTraces(limit);
|
|
5517
|
-
const stats = profiler.getStats();
|
|
5518
|
-
const bottleneck = profiler.identifyBottleneck();
|
|
5519
|
-
|
|
5520
|
-
if (format === "json") {
|
|
5521
|
-
return {
|
|
5522
|
-
enabled: true,
|
|
5523
|
-
format,
|
|
5524
|
-
traces,
|
|
5525
|
-
stats,
|
|
5526
|
-
bottleneck,
|
|
5527
|
-
};
|
|
5528
|
-
}
|
|
5529
|
-
|
|
5530
|
-
const lines: string[] = [];
|
|
5531
|
-
lines.push("Engram Profiling Report");
|
|
5532
|
-
lines.push("=".repeat(60));
|
|
5533
|
-
lines.push("");
|
|
5534
|
-
|
|
5535
|
-
type BucketEntry = { count: number; avgMs: number; p50Ms: number; p95Ms: number; maxMs: number };
|
|
5536
|
-
const allBuckets: Array<[string, Record<string, BucketEntry>]> = [
|
|
5537
|
-
["byKind", stats.byKind],
|
|
5538
|
-
["bySpan", stats.bySpan],
|
|
5539
|
-
];
|
|
5540
|
-
const hasStats = allBuckets.some(([, entries]) => Object.keys(entries).length > 0);
|
|
5541
|
-
if (hasStats) {
|
|
5542
|
-
lines.push("Aggregate Stats (all retained traces):");
|
|
5543
|
-
for (const [bucket, entries] of allBuckets) {
|
|
5544
|
-
for (const [key, summary] of Object.entries(entries)) {
|
|
5545
|
-
lines.push(
|
|
5546
|
-
` ${bucket}/${key}: avg=${summary.avgMs}ms p50=${summary.p50Ms}ms p95=${summary.p95Ms}ms max=${summary.maxMs}ms (n=${summary.count})`,
|
|
5547
|
-
);
|
|
5548
|
-
}
|
|
5549
|
-
}
|
|
5550
|
-
lines.push("");
|
|
5551
|
-
}
|
|
5552
|
-
|
|
5553
|
-
if (bottleneck) {
|
|
5554
|
-
lines.push(`Bottleneck: ${bottleneck}`);
|
|
5555
|
-
lines.push("");
|
|
5556
|
-
}
|
|
5557
|
-
|
|
5558
|
-
if (traces.length === 0) {
|
|
5559
|
-
lines.push("No traces recorded yet. Trigger a recall or extraction to see timing data.");
|
|
5560
|
-
} else {
|
|
5561
|
-
for (const trace of traces) {
|
|
5562
|
-
lines.push(formatProfileTraceAscii(trace));
|
|
5563
|
-
lines.push("");
|
|
5564
|
-
}
|
|
5565
|
-
}
|
|
5566
|
-
|
|
5567
|
-
return {
|
|
5568
|
-
enabled: true,
|
|
5569
|
-
format,
|
|
5570
|
-
report: lines.join("\n"),
|
|
5571
|
-
};
|
|
4117
|
+
return this.accessAdminOpsSurface.profilingReport(
|
|
4118
|
+
request,
|
|
4119
|
+
);
|
|
5572
4120
|
}
|
|
5573
4121
|
|
|
5574
4122
|
async trustZoneStatus(namespace?: string, principal?: string): Promise<EngramAccessTrustZoneStatusResponse> {
|
|
@@ -7506,134 +6054,14 @@ export class EngramAccessService {
|
|
|
7506
6054
|
return { explanation };
|
|
7507
6055
|
}
|
|
7508
6056
|
|
|
7509
|
-
/**
|
|
7510
|
-
* Read-only graph snapshot for the admin pane (issue #691 PR 2/5).
|
|
7511
|
-
*
|
|
7512
|
-
* Reads adjacency from the JSONL edge store written by `GraphIndex` and
|
|
7513
|
-
* resolves node metadata via the namespaced storage manager. Namespace
|
|
7514
|
-
* resolution mirrors the read-side path used by `recall` /
|
|
7515
|
-
* `procedureStats`, so multi-principal deployments can't leak edges from
|
|
7516
|
-
* a peer namespace (CLAUDE.md rule 42).
|
|
7517
|
-
*/
|
|
7518
6057
|
async graphSnapshot(
|
|
7519
6058
|
request: GraphSnapshotRequest & { namespace?: string },
|
|
7520
6059
|
authenticatedPrincipal?: string,
|
|
7521
6060
|
): Promise<GraphSnapshotResponse> {
|
|
7522
|
-
|
|
7523
|
-
request
|
|
6061
|
+
return this.accessAdminOpsSurface.graphSnapshot(
|
|
6062
|
+
request,
|
|
7524
6063
|
authenticatedPrincipal,
|
|
7525
6064
|
);
|
|
7526
|
-
const storage = await this.orchestrator.getStorage(namespace);
|
|
7527
|
-
const cfg = this.orchestrator.config;
|
|
7528
|
-
const graphCaps = resolveGraphConstructionCapabilities(cfg);
|
|
7529
|
-
// Canonicalize the storage root once — through `realpath` so that any
|
|
7530
|
-
// symlink in the namespace root path itself is resolved before we
|
|
7531
|
-
// compare children against it. This is required because
|
|
7532
|
-
// `GraphEdge.from` / `to` are JSONL-parsed strings — a malformed edge
|
|
7533
|
-
// with an absolute path, a `..` segment, OR a symlink that resolves
|
|
7534
|
-
// to a file outside the namespace would otherwise read a memory file
|
|
7535
|
-
// from a peer namespace, leaking metadata across tenants
|
|
7536
|
-
// (codex P1 + follow-up on PR #734; CLAUDE.md rule 42).
|
|
7537
|
-
let namespaceRootReal: string;
|
|
7538
|
-
try {
|
|
7539
|
-
namespaceRootReal = await nodeFs.realpath(storage.dir);
|
|
7540
|
-
} catch {
|
|
7541
|
-
// If the namespace root itself doesn't exist on disk yet (fresh
|
|
7542
|
-
// install with no memories), fall back to the lexical resolve so
|
|
7543
|
-
// the snapshot can still return an empty result rather than
|
|
7544
|
-
// throwing. No symlink can resolve through a missing path, so
|
|
7545
|
-
// this fallback is safe — every candidate we see will fail the
|
|
7546
|
-
// realpath step below and surface as `null`.
|
|
7547
|
-
namespaceRootReal = nodePath.resolve(storage.dir);
|
|
7548
|
-
}
|
|
7549
|
-
const namespaceRootWithSep = namespaceRootReal.endsWith(nodePath.sep)
|
|
7550
|
-
? namespaceRootReal
|
|
7551
|
-
: namespaceRootReal + nodePath.sep;
|
|
7552
|
-
const loadNode = async (relPath: string): Promise<GraphSnapshotNodeMetadata | null> => {
|
|
7553
|
-
// `GraphEdge.from` / `to` are storage-relative paths; resolve against
|
|
7554
|
-
// the namespaced storage root so the metadata read honors namespace
|
|
7555
|
-
// boundaries even when the same memory id exists in multiple
|
|
7556
|
-
// namespaces.
|
|
7557
|
-
//
|
|
7558
|
-
// Three-stage guard:
|
|
7559
|
-
// 1. Reject absolute paths up front — only relative endpoints are
|
|
7560
|
-
// ever produced by the writer, so anything else is malformed.
|
|
7561
|
-
// 2. Lexical containment check on the resolved path. This catches
|
|
7562
|
-
// `..` traversals before we touch the filesystem.
|
|
7563
|
-
// 3. `fs.realpath` containment check — resolves symlinks so an
|
|
7564
|
-
// in-namespace path that *points* at an out-of-namespace file
|
|
7565
|
-
// is still rejected. Without this step a symlinked endpoint
|
|
7566
|
-
// could leak a peer namespace's frontmatter.
|
|
7567
|
-
// Bad paths surface a length-only warning (never echo the offending
|
|
7568
|
-
// segments — those would themselves cross namespace boundaries
|
|
7569
|
-
// through the log surface) and fall through to a `null` metadata
|
|
7570
|
-
// result.
|
|
7571
|
-
if (nodePath.isAbsolute(relPath)) {
|
|
7572
|
-
log.warn(
|
|
7573
|
-
`graphSnapshot: rejected absolute edge endpoint (len=${relPath.length}) `
|
|
7574
|
-
+ `outside namespace root`,
|
|
7575
|
-
);
|
|
7576
|
-
return null;
|
|
7577
|
-
}
|
|
7578
|
-
const candidate = nodePath.resolve(namespaceRootReal, relPath);
|
|
7579
|
-
if (candidate !== namespaceRootReal && !candidate.startsWith(namespaceRootWithSep)) {
|
|
7580
|
-
log.warn(
|
|
7581
|
-
`graphSnapshot: rejected traversing edge endpoint (len=${relPath.length}) `
|
|
7582
|
-
+ `outside namespace root`,
|
|
7583
|
-
);
|
|
7584
|
-
return null;
|
|
7585
|
-
}
|
|
7586
|
-
let canonical: string;
|
|
7587
|
-
try {
|
|
7588
|
-
canonical = await nodeFs.realpath(candidate);
|
|
7589
|
-
} catch {
|
|
7590
|
-
// Missing file — `readMemoryByPath` will return null too. We
|
|
7591
|
-
// intentionally still call it so callers see a consistent
|
|
7592
|
-
// "unknown" result rather than special-casing missing edges.
|
|
7593
|
-
canonical = candidate;
|
|
7594
|
-
}
|
|
7595
|
-
if (canonical !== namespaceRootReal && !canonical.startsWith(namespaceRootWithSep)) {
|
|
7596
|
-
log.warn(
|
|
7597
|
-
`graphSnapshot: rejected symlinked edge endpoint (len=${relPath.length}) `
|
|
7598
|
-
+ `that resolved outside namespace root`,
|
|
7599
|
-
);
|
|
7600
|
-
return null;
|
|
7601
|
-
}
|
|
7602
|
-
// Both `canonical` (realpath of candidate) and `namespaceRootReal`
|
|
7603
|
-
// (realpath of storage.dir) are fully resolved here, so the
|
|
7604
|
-
// containment check above is comparing apples-to-apples even when
|
|
7605
|
-
// storage.dir (= storage.baseDir) is itself a symlink to the real
|
|
7606
|
-
// directory. Pass `canonical` — not the pre-realpath `candidate` —
|
|
7607
|
-
// so the storage read also uses the stable real path.
|
|
7608
|
-
const memory = await storage.readMemoryByPath(canonical);
|
|
7609
|
-
if (!memory) return null;
|
|
7610
|
-
const fm = memory.frontmatter;
|
|
7611
|
-
return {
|
|
7612
|
-
category: fm.category ?? "unknown",
|
|
7613
|
-
label: fm.id ?? nodePath.basename(canonical, nodePath.extname(canonical)),
|
|
7614
|
-
updated: fm.updated,
|
|
7615
|
-
};
|
|
7616
|
-
};
|
|
7617
|
-
// Use the realpath-resolved namespace root for the edge-file read so
|
|
7618
|
-
// the JSONL location is stable whether storage.dir is a direct path
|
|
7619
|
-
// or a symlink. namespaceRootReal was computed via `fs.realpath`
|
|
7620
|
-
// above; using it here keeps both the graph-file I/O and the loadNode
|
|
7621
|
-
// containment check on the same resolved base path.
|
|
7622
|
-
return buildGraphSnapshot({
|
|
7623
|
-
memoryDir: namespaceRootReal,
|
|
7624
|
-
graphConfig: {
|
|
7625
|
-
entityGraph: graphCaps.entityGraph,
|
|
7626
|
-
timeGraph: graphCaps.timeGraph,
|
|
7627
|
-
causalGraph: graphCaps.causalGraph,
|
|
7628
|
-
},
|
|
7629
|
-
request: {
|
|
7630
|
-
limit: request.limit,
|
|
7631
|
-
since: request.since,
|
|
7632
|
-
focusNodeId: request.focusNodeId,
|
|
7633
|
-
categories: request.categories,
|
|
7634
|
-
},
|
|
7635
|
-
loadNode,
|
|
7636
|
-
});
|
|
7637
6065
|
}
|
|
7638
6066
|
|
|
7639
6067
|
async memoryFeedback(request: {
|
|
@@ -8330,108 +6758,13 @@ export class EngramAccessService {
|
|
|
8330
6758
|
});
|
|
8331
6759
|
}
|
|
8332
6760
|
|
|
8333
|
-
/**
|
|
8334
|
-
* List capsule archives in the namespace-scoped capsule store.
|
|
8335
|
-
*
|
|
8336
|
-
* MCP uses this access-layer method instead of reading arbitrary paths so
|
|
8337
|
-
* capsule discovery remains bound to the same namespace ACLs as export and
|
|
8338
|
-
* import.
|
|
8339
|
-
*/
|
|
8340
6761
|
async capsuleList(options?: {
|
|
8341
6762
|
namespace?: string;
|
|
8342
6763
|
principal?: string;
|
|
8343
6764
|
}): Promise<EngramAccessCapsuleListResponse> {
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
let dirEntries: import("node:fs").Dirent[];
|
|
8348
|
-
try {
|
|
8349
|
-
const capsulesDirStat = await nodeFs.lstat(capsulesDir);
|
|
8350
|
-
if (capsulesDirStat.isSymbolicLink()) {
|
|
8351
|
-
throw new EngramAccessInputError("capsule list failed: capsule store directory must not be a symlink");
|
|
8352
|
-
}
|
|
8353
|
-
if (!capsulesDirStat.isDirectory()) {
|
|
8354
|
-
throw new EngramAccessInputError("capsule list failed: capsule store path must be a directory");
|
|
8355
|
-
}
|
|
8356
|
-
dirEntries = await nodeFs.readdir(capsulesDir, { withFileTypes: true });
|
|
8357
|
-
} catch (err) {
|
|
8358
|
-
const code = typeof err === "object" && err !== null && "code" in err
|
|
8359
|
-
? (err as { code?: unknown }).code
|
|
8360
|
-
: undefined;
|
|
8361
|
-
if (code === "ENOENT") {
|
|
8362
|
-
return { namespace: resolvedNamespace, capsulesDir, capsules: [] };
|
|
8363
|
-
}
|
|
8364
|
-
throw err;
|
|
8365
|
-
}
|
|
8366
|
-
|
|
8367
|
-
const archiveNames = dirEntries
|
|
8368
|
-
.filter(
|
|
8369
|
-
(entry) =>
|
|
8370
|
-
entry.isFile() &&
|
|
8371
|
-
(entry.name.endsWith(".capsule.json.gz") ||
|
|
8372
|
-
entry.name.endsWith(".capsule.json.gz.enc")),
|
|
8373
|
-
)
|
|
8374
|
-
.map((entry) => entry.name)
|
|
8375
|
-
.sort();
|
|
8376
|
-
|
|
8377
|
-
const capsules: CapsuleListEntry[] = [];
|
|
8378
|
-
for (const archiveName of archiveNames) {
|
|
8379
|
-
const archivePath = nodePath.join(capsulesDir, archiveName);
|
|
8380
|
-
const id = archiveName
|
|
8381
|
-
.replace(/\.capsule\.json\.gz\.enc$/, "")
|
|
8382
|
-
.replace(/\.capsule\.json\.gz$/, "");
|
|
8383
|
-
const manifestPath = nodePath.join(capsulesDir, `${id}.manifest.json`);
|
|
8384
|
-
|
|
8385
|
-
let createdAt: string | null = null;
|
|
8386
|
-
let pluginVersion: string | null = null;
|
|
8387
|
-
let fileCount: number | null = null;
|
|
8388
|
-
let description: string | null = null;
|
|
8389
|
-
let manifestPathOrNull: string | null = manifestPath;
|
|
8390
|
-
|
|
8391
|
-
try {
|
|
8392
|
-
const manifestStat = await nodeFs.lstat(manifestPath);
|
|
8393
|
-
if (manifestStat.isSymbolicLink() || !manifestStat.isFile()) {
|
|
8394
|
-
capsules.push({
|
|
8395
|
-
id,
|
|
8396
|
-
archivePath,
|
|
8397
|
-
manifestPath: manifestPathOrNull,
|
|
8398
|
-
createdAt,
|
|
8399
|
-
pluginVersion,
|
|
8400
|
-
fileCount,
|
|
8401
|
-
description,
|
|
8402
|
-
});
|
|
8403
|
-
continue;
|
|
8404
|
-
}
|
|
8405
|
-
const raw = await nodeFs.readFile(manifestPath, "utf-8");
|
|
8406
|
-
const sidecar = JSON.parse(raw) as Record<string, unknown>;
|
|
8407
|
-
createdAt = typeof sidecar.createdAt === "string" ? sidecar.createdAt : null;
|
|
8408
|
-
pluginVersion = typeof sidecar.pluginVersion === "string" ? sidecar.pluginVersion : null;
|
|
8409
|
-
fileCount = Array.isArray(sidecar.files) ? sidecar.files.length : null;
|
|
8410
|
-
const capsule = sidecar.capsule as Record<string, unknown> | undefined;
|
|
8411
|
-
description = capsule && typeof capsule.description === "string"
|
|
8412
|
-
? capsule.description
|
|
8413
|
-
: null;
|
|
8414
|
-
} catch (err) {
|
|
8415
|
-
const code = typeof err === "object" && err !== null && "code" in err
|
|
8416
|
-
? (err as { code?: unknown }).code
|
|
8417
|
-
: undefined;
|
|
8418
|
-
if (code === "ENOENT") {
|
|
8419
|
-
manifestPathOrNull = null;
|
|
8420
|
-
}
|
|
8421
|
-
}
|
|
8422
|
-
|
|
8423
|
-
capsules.push({
|
|
8424
|
-
id,
|
|
8425
|
-
archivePath,
|
|
8426
|
-
manifestPath: manifestPathOrNull,
|
|
8427
|
-
createdAt,
|
|
8428
|
-
pluginVersion,
|
|
8429
|
-
fileCount,
|
|
8430
|
-
description,
|
|
8431
|
-
});
|
|
8432
|
-
}
|
|
8433
|
-
|
|
8434
|
-
return { namespace: resolvedNamespace, capsulesDir, capsules };
|
|
6765
|
+
return this.accessAdminOpsSurface.capsuleList(
|
|
6766
|
+
options,
|
|
6767
|
+
);
|
|
8435
6768
|
}
|
|
8436
6769
|
|
|
8437
6770
|
/**
|
|
@@ -8674,87 +7007,15 @@ export class EngramAccessService {
|
|
|
8674
7007
|
return getDreamsStatus(storage.dir, windowHours);
|
|
8675
7008
|
}
|
|
8676
7009
|
|
|
8677
|
-
/**
|
|
8678
|
-
* Manually invoke a single Dreams phase pass (PR 4/4).
|
|
8679
|
-
*
|
|
8680
|
-
* Deep-sleep delegates to memory governance (shadow → dry-run, apply → live).
|
|
8681
|
-
* Light-sleep and REM scan the observation ledger and memory corpus
|
|
8682
|
-
* respectively, returning the same telemetry shape as a scheduled run.
|
|
8683
|
-
*/
|
|
8684
7010
|
async dreamsRun(options: {
|
|
8685
7011
|
phase: import("./types.js").DreamsPhase;
|
|
8686
7012
|
dryRun?: boolean;
|
|
8687
7013
|
namespace?: string;
|
|
8688
7014
|
authenticatedPrincipal?: string;
|
|
8689
7015
|
}): Promise<import("./types.js").DreamsRunResult> {
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
if (!validPhases.includes(options.phase)) {
|
|
8693
|
-
throw new EngramAccessInputError(
|
|
8694
|
-
`Invalid phase: ${String(options.phase)}. Must be one of: ${validPhases.join(", ")}`,
|
|
8695
|
-
);
|
|
8696
|
-
}
|
|
8697
|
-
const deepSleep = this.orchestrator.config.dreamsPhases.deepSleep;
|
|
8698
|
-
if (
|
|
8699
|
-
options.phase === "deepSleep" &&
|
|
8700
|
-
deepSleep.enabled === false &&
|
|
8701
|
-
deepSleep.enabledExplicitlySet === true
|
|
8702
|
-
) {
|
|
8703
|
-
throw new EngramAccessInputError(
|
|
8704
|
-
"memory governance is disabled by dreams.phases.deepSleep.enabled=false",
|
|
8705
|
-
);
|
|
8706
|
-
}
|
|
8707
|
-
const dryRun = options.dryRun === true;
|
|
8708
|
-
const resolvedNamespace = this.writableNamespaceFor(
|
|
8709
|
-
options.namespace,
|
|
8710
|
-
undefined,
|
|
8711
|
-
options.authenticatedPrincipal,
|
|
7016
|
+
return this.accessAdminOpsSurface.dreamsRun(
|
|
7017
|
+
options,
|
|
8712
7018
|
);
|
|
8713
|
-
const storage = await this.orchestrator.getStorage(resolvedNamespace);
|
|
8714
|
-
const memoryDir = storage.dir;
|
|
8715
|
-
const phaseRunner = dryRun || options.phase === "deepSleep"
|
|
8716
|
-
? undefined
|
|
8717
|
-
: async (_opts: { memoryDir: string; phase: "lightSleep" | "rem" }) => {
|
|
8718
|
-
if (_opts.phase === "lightSleep") {
|
|
8719
|
-
const result = await this.orchestrator.runLifecyclePolicyNow(storage);
|
|
8720
|
-
return {
|
|
8721
|
-
itemsProcessed: result.memoriesAssessed,
|
|
8722
|
-
notes: `scored ${result.memoriesAssessed} memories`,
|
|
8723
|
-
};
|
|
8724
|
-
}
|
|
8725
|
-
const result = await this.orchestrator.runSemanticConsolidationNow({
|
|
8726
|
-
dryRun: false,
|
|
8727
|
-
storage,
|
|
8728
|
-
});
|
|
8729
|
-
const itemsProcessed = result.clusters.reduce(
|
|
8730
|
-
(sum, cluster) => sum + cluster.memories.length,
|
|
8731
|
-
0,
|
|
8732
|
-
);
|
|
8733
|
-
return {
|
|
8734
|
-
itemsProcessed,
|
|
8735
|
-
notes: `REM consolidation found ${result.clustersFound} clusters`,
|
|
8736
|
-
};
|
|
8737
|
-
};
|
|
8738
|
-
const governanceRunner = options.phase === "deepSleep"
|
|
8739
|
-
? async (_opts: { memoryDir: string; dryRun: boolean }) => {
|
|
8740
|
-
return this.orchestrator.runDeepSleepGovernanceNow({
|
|
8741
|
-
storage,
|
|
8742
|
-
dryRun: _opts.dryRun,
|
|
8743
|
-
});
|
|
8744
|
-
}
|
|
8745
|
-
: undefined;
|
|
8746
|
-
const result = await runDreamsPhase(
|
|
8747
|
-
{ memoryDir, phase: options.phase, dryRun },
|
|
8748
|
-
governanceRunner,
|
|
8749
|
-
phaseRunner,
|
|
8750
|
-
);
|
|
8751
|
-
return {
|
|
8752
|
-
phase: result.phase,
|
|
8753
|
-
dryRun: result.dryRun,
|
|
8754
|
-
durationMs: result.durationMs,
|
|
8755
|
-
itemsProcessed: result.itemsProcessed,
|
|
8756
|
-
notes: result.notes,
|
|
8757
|
-
};
|
|
8758
7019
|
}
|
|
8759
7020
|
|
|
8760
7021
|
// ---------------------------------------------------------------------------
|
|
@@ -8913,12 +7174,6 @@ export class EngramAccessService {
|
|
|
8913
7174
|
return redactSensitive(result);
|
|
8914
7175
|
}
|
|
8915
7176
|
|
|
8916
|
-
/**
|
|
8917
|
-
* Manually promote a memory into one or more authorized targets. Requires
|
|
8918
|
-
* a non-empty reason (audit-logged). Reuses the scope-profile promotion
|
|
8919
|
-
* resolution and `canWriteNamespace` gate — there is no dashboard-only
|
|
8920
|
-
* write path.
|
|
8921
|
-
*/
|
|
8922
7177
|
async adminPromoteMemory(request: {
|
|
8923
7178
|
sourceMemoryId: string;
|
|
8924
7179
|
namespace?: string;
|
|
@@ -8928,111 +7183,9 @@ export class EngramAccessService {
|
|
|
8928
7183
|
reason: string;
|
|
8929
7184
|
actor?: never; // ignored — actor is derived from the authenticated principal
|
|
8930
7185
|
}) {
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
// scope plan (principal self / scope-profile write layer / coding overlay)
|
|
8935
|
-
// instead of falling through to config.defaultNamespace. This matches the
|
|
8936
|
-
// runtime observe/write path.
|
|
8937
|
-
let sourceNamespace: string;
|
|
8938
|
-
if (request.namespace) {
|
|
8939
|
-
sourceNamespace = this.resolveReadableNamespace(
|
|
8940
|
-
request.namespace,
|
|
8941
|
-
request.principal,
|
|
8942
|
-
);
|
|
8943
|
-
} else {
|
|
8944
|
-
const codingContext = request.sessionKey
|
|
8945
|
-
? this.orchestrator.getCodingContextForSession(request.sessionKey) ?? null
|
|
8946
|
-
: null;
|
|
8947
|
-
const plan = resolveScopePlan({
|
|
8948
|
-
config,
|
|
8949
|
-
sessionKey: request.sessionKey,
|
|
8950
|
-
principalOverride: request.principal,
|
|
8951
|
-
codingContext,
|
|
8952
|
-
namespacesEnabled,
|
|
8953
|
-
});
|
|
8954
|
-
sourceNamespace = this.resolveReadableNamespace(
|
|
8955
|
-
plan.baseNamespace,
|
|
8956
|
-
request.principal,
|
|
8957
|
-
);
|
|
8958
|
-
}
|
|
8959
|
-
// Fix OdB0c: thread coding context into the scope-profile plan so
|
|
8960
|
-
// userProject/teamProject promotion targets resolve for project-scoped
|
|
8961
|
-
// sessions (previously hard-coded to null).
|
|
8962
|
-
const codingContext = request.sessionKey
|
|
8963
|
-
? this.orchestrator.getCodingContextForSession(request.sessionKey) ?? null
|
|
8964
|
-
: null;
|
|
8965
|
-
const codingOverlay = codingContext
|
|
8966
|
-
? resolveCodingNamespaceOverlay(
|
|
8967
|
-
codingContext,
|
|
8968
|
-
config.codingMode,
|
|
8969
|
-
config.defaultNamespace,
|
|
8970
|
-
)
|
|
8971
|
-
: null;
|
|
8972
|
-
const scopeProfilePlan = resolveScopeProfilePlan({
|
|
8973
|
-
config,
|
|
8974
|
-
principal: request.principal,
|
|
8975
|
-
codingContext,
|
|
8976
|
-
codingOverlay,
|
|
8977
|
-
});
|
|
8978
|
-
const storage: PromotionStorageProvider = {
|
|
8979
|
-
readMemory: async (namespace, memoryId) => {
|
|
8980
|
-
const resolved = await this.orchestrator.getStorage(namespace);
|
|
8981
|
-
const memory = await resolved.getMemoryById(memoryId);
|
|
8982
|
-
if (!memory) return null;
|
|
8983
|
-
return {
|
|
8984
|
-
category: memory.frontmatter.category,
|
|
8985
|
-
content: memory.content,
|
|
8986
|
-
frontmatter: memory.frontmatter,
|
|
8987
|
-
};
|
|
8988
|
-
},
|
|
8989
|
-
writePromotedMemory: async (namespace, memory) => {
|
|
8990
|
-
const resolved = await this.orchestrator.getStorage(namespace);
|
|
8991
|
-
const { id, tombstoneBlocked } = await resolved.writeMemory(
|
|
8992
|
-
memory.category,
|
|
8993
|
-
memory.content,
|
|
8994
|
-
{
|
|
8995
|
-
confidence: memory.confidence,
|
|
8996
|
-
tags: memory.tags,
|
|
8997
|
-
entityRef: memory.entityRef,
|
|
8998
|
-
source: `admin-promotion:${memory.sourceNamespace}:${memory.reason.slice(0, 120)}`,
|
|
8999
|
-
lineage: memory.lineage,
|
|
9000
|
-
sourceMemoryId: memory.sourceMemoryId,
|
|
9001
|
-
actor: memory.actor,
|
|
9002
|
-
validAt: memory.validAt,
|
|
9003
|
-
},
|
|
9004
|
-
);
|
|
9005
|
-
// #1645: a tombstone-blocked promotion lands pending_review (no active
|
|
9006
|
-
// copy in the target). Report it as a failed promotion so the admin
|
|
9007
|
-
// sees an honest result — the content is queued for review, not
|
|
9008
|
-
// actively promoted. promoteMemory's catch block sanitizes this into
|
|
9009
|
-
// a generic "promotion write failed" audit entry.
|
|
9010
|
-
if (tombstoneBlocked) {
|
|
9011
|
-
throw new Error(
|
|
9012
|
-
"target namespace tombstone-blocked the promoted content (pending_review)",
|
|
9013
|
-
);
|
|
9014
|
-
}
|
|
9015
|
-
return id;
|
|
9016
|
-
},
|
|
9017
|
-
};
|
|
9018
|
-
try {
|
|
9019
|
-
return await promoteMemory({
|
|
9020
|
-
config,
|
|
9021
|
-
sourceMemoryId: request.sourceMemoryId,
|
|
9022
|
-
sourceNamespace,
|
|
9023
|
-
principal: request.principal,
|
|
9024
|
-
targets: request.targets,
|
|
9025
|
-
reason: request.reason,
|
|
9026
|
-
actor: request.principal ?? "admin-console",
|
|
9027
|
-
storage,
|
|
9028
|
-
scopeProfilePlan,
|
|
9029
|
-
});
|
|
9030
|
-
} catch (err) {
|
|
9031
|
-
if (err instanceof AdminPromotionError) {
|
|
9032
|
-
throw new EngramAccessInputError(err.message);
|
|
9033
|
-
}
|
|
9034
|
-
throw err;
|
|
9035
|
-
}
|
|
7186
|
+
return this.accessAdminOpsSurface.adminPromoteMemory(
|
|
7187
|
+
request,
|
|
7188
|
+
);
|
|
9036
7189
|
}
|
|
9037
7190
|
}
|
|
9038
7191
|
|