@remnic/core 9.3.755 → 9.3.757
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 +1 -1
- package/dist/access-admin-ops-surface.js +45 -45
- package/dist/access-audit.js +2 -2
- package/dist/access-boundary.d.ts +1 -1
- package/dist/access-boundary.js +46 -46
- package/dist/access-cli.js +105 -104
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +1 -1
- package/dist/access-http.js +55 -55
- package/dist/access-lcm-surface.d.ts +1 -1
- package/dist/access-lcm-surface.js +45 -45
- package/dist/access-mcp.d.ts +1 -1
- package/dist/access-mcp.js +52 -52
- package/dist/access-observe-write-surface.d.ts +225 -0
- package/dist/access-observe-write-surface.js +128 -0
- package/dist/access-operations-batch.js +49 -49
- package/dist/access-operations.d.ts +5 -5
- package/dist/access-operations.js +51 -51
- package/dist/access-recall-surface.d.ts +1 -1
- package/dist/access-recall-surface.js +45 -45
- package/dist/access-schema.d.ts +70 -70
- package/dist/access-schema.js +4 -4
- package/dist/{access-service-C3jkLi2U.d.ts → access-service-CJwcDoQp.d.ts} +4 -5
- package/dist/access-service.d.ts +1 -1
- package/dist/access-service.js +45 -45
- package/dist/access-surface-catalog.d.ts +1 -1
- package/dist/active-recall.js +1 -1
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/{auto-sync-G4YKBPOU.js → auto-sync-2SOUW4JW.js} +6 -6
- package/dist/briefing.js +12 -12
- package/dist/calibration.js +2 -2
- package/dist/{capsule-crypto-CZJSLEFG.js → capsule-crypto-RAARDY3M.js} +2 -2
- package/dist/causal-behavior.js +2 -2
- package/dist/causal-chain.js +2 -2
- package/dist/causal-consolidation.js +20 -20
- package/dist/causal-retrieval.js +2 -2
- package/dist/causal-trajectory-graph.js +2 -2
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-T6WHI3JW.js → chunk-2NTWULEQ.js} +6 -6
- package/dist/chunk-3I4FUAVJ.js +20 -0
- package/dist/chunk-3I4FUAVJ.js.map +1 -0
- package/dist/{chunk-TH7WMHVK.js → chunk-3NN73CM7.js} +2 -2
- package/dist/{chunk-GCEV6SXP.js → chunk-4L2KHKLY.js} +2300 -2220
- package/dist/chunk-4L2KHKLY.js.map +1 -0
- package/dist/{chunk-TAPSNIMA.js → chunk-5CEJH5ZN.js} +4 -4
- package/dist/{chunk-XBM5DG4O.js → chunk-5PHPNUZN.js} +684 -599
- package/dist/chunk-5PHPNUZN.js.map +1 -0
- package/dist/{chunk-CIDGUFAL.js → chunk-6A76LSX6.js} +2 -2
- package/dist/{chunk-UQJ6YTKR.js → chunk-6DMTYBNY.js} +7 -7
- package/dist/{chunk-S6UCPBPL.js → chunk-6FYRCQ7K.js} +2 -2
- package/dist/{chunk-7XH7VJN4.js → chunk-6T4LTI2F.js} +4 -4
- package/dist/{chunk-PJ7DIEFC.js → chunk-7HJ3XBMD.js} +5 -5
- package/dist/{chunk-TVVEYCNW.js → chunk-7K5Q6COX.js} +4 -4
- package/dist/{chunk-I3UWEZTK.js → chunk-AINK3KDE.js} +2 -2
- package/dist/{chunk-ZQNCHPTZ.js → chunk-CTGK5VK3.js} +2 -2
- package/dist/{chunk-G7LCG56O.js → chunk-CXW6Y7SI.js} +4 -4
- package/dist/{chunk-M5QKGHCR.js → chunk-CYWCKFCL.js} +5 -5
- package/dist/{chunk-T422SYM6.js → chunk-DCWIQFNA.js} +5 -5
- package/dist/{chunk-2BFLDH5F.js → chunk-EEIROWEG.js} +2 -2
- package/dist/{chunk-DS4QM6I7.js → chunk-EEKDRAWX.js} +4 -4
- package/dist/{chunk-OCFITZRV.js → chunk-FBS6V6LJ.js} +4 -4
- package/dist/{chunk-I6IAJCJS.js → chunk-FFPH47IX.js} +7 -7
- package/dist/{chunk-N4AGSRVA.js → chunk-GI7PRVMV.js} +9 -9
- package/dist/{chunk-FQLM6ZUP.js → chunk-GIJDW2YO.js} +3 -3
- package/dist/{chunk-LFKEFIJA.js → chunk-I65IQD7Y.js} +4 -4
- package/dist/{chunk-3DOD6ICJ.js → chunk-IBAU2RZN.js} +6 -6
- package/dist/{chunk-24463PIV.js → chunk-IF6BTZCB.js} +2 -2
- package/dist/{chunk-GMWKWA6N.js → chunk-IR6ATBUI.js} +5 -5
- package/dist/{chunk-TJATYB3F.js → chunk-IZP62CEF.js} +7 -7
- package/dist/{chunk-FLVL4L3L.js → chunk-KTULX4YO.js} +4 -4
- package/dist/{chunk-XCXN3D2U.js → chunk-L5DU7THE.js} +6 -6
- package/dist/{chunk-437S3G37.js → chunk-LNQUAJNC.js} +1 -1
- package/dist/{chunk-LD53WPMU.js → chunk-LQ6JI4VH.js} +4 -4
- package/dist/{chunk-NCNOYM6F.js → chunk-LVMVYLNB.js} +2 -2
- package/dist/{chunk-XD6JVK5P.js → chunk-MQQO56LN.js} +47 -47
- package/dist/{chunk-PJQMISPD.js → chunk-NNL4LJAD.js} +6 -6
- package/dist/{chunk-GGYUR4M2.js → chunk-PHWBKQHK.js} +12 -12
- package/dist/{chunk-TQT3USCG.js → chunk-PMBWVQ7D.js} +2 -2
- package/dist/{chunk-JITAVROY.js → chunk-PXHY2E3S.js} +4 -4
- package/dist/{chunk-GPFXPC2V.js → chunk-QOGXUNWL.js} +8 -8
- package/dist/{chunk-SP42M3XP.js → chunk-QVQG7W2H.js} +7 -7
- package/dist/{chunk-2QSZNTDO.js → chunk-RKNJBZ55.js} +4 -4
- package/dist/{chunk-VL5QHTJI.js → chunk-RQXVVENY.js} +128 -128
- package/dist/{chunk-KKOCFUI5.js → chunk-SUN6XVNI.js} +10 -10
- package/dist/{chunk-FOJY75BL.js → chunk-T3UW6NRQ.js} +13 -13
- package/dist/{chunk-2MR3MFQB.js → chunk-UPSAT4PY.js} +2 -2
- package/dist/{chunk-5J4OL7TJ.js → chunk-VUA4S7P6.js} +4 -4
- package/dist/{chunk-YQGHW5ML.js → chunk-WRFKZEO6.js} +4 -4
- package/dist/{chunk-H6S3DJVE.js → chunk-XKUYMM34.js} +2 -2
- package/dist/{chunk-EHQ5HO5T.js → chunk-XROP4ZHB.js} +4 -4
- package/dist/{chunk-2OZ2JEUP.js → chunk-YTU7WKYN.js} +2 -2
- package/dist/{chunk-V5UV7BOA.js → chunk-ZE7GJ76H.js} +1 -1
- package/dist/{chunk-ER3CCWK2.js → chunk-ZG2WP425.js} +2 -2
- package/dist/{chunk-4LQUYBZM.js → chunk-ZL3Y3ZTQ.js} +7 -7
- package/dist/{cli-BJQ2ahrh.d.ts → cli-cZyANcmY.d.ts} +1 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +72 -72
- package/dist/compounding/engine.js +13 -13
- package/dist/config-record.d.ts +11 -0
- package/dist/config-record.js +8 -0
- package/dist/config.js +1 -1
- package/dist/connectors/codex-materialize-runner.js +13 -13
- package/dist/connectors/index.js +14 -14
- package/dist/consolidation-provenance-check.js +2 -2
- package/dist/contradiction/index.js +6 -6
- package/dist/{contradiction-scan-2SODRNUF.js → contradiction-scan-3FJYWS3G.js} +3 -3
- package/dist/contradiction-scan-3FJYWS3G.js.map +1 -0
- package/dist/conversation-index/backend.js +2 -2
- package/dist/entity-retrieval.js +13 -13
- package/dist/extraction-faithfulness.js +3 -3
- package/dist/extraction-judge.js +3 -3
- package/dist/extraction.js +4 -4
- package/dist/fallback-llm.js +2 -2
- package/dist/{first-start-migration-DY6YCRC2.js → first-start-migration-RYY7PPGF.js} +4 -4
- package/dist/{graph-edge-decay-RXUCJIN4.js → graph-edge-decay-U2GKDAVM.js} +4 -4
- package/dist/graph-edge-decay-U2GKDAVM.js.map +1 -0
- package/dist/graph-snapshot.js +3 -3
- package/dist/graph.js +2 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +162 -158
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +2 -2
- package/dist/maintenance/memory-governance.js +13 -13
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +12 -12
- package/dist/maintenance/rebuild-memory-projection.js +14 -14
- package/dist/mcp-memory-inspector-app.d.ts +1 -1
- package/dist/namespaces/migrate.js +24 -24
- package/dist/namespaces/search.js +10 -10
- package/dist/namespaces/storage.js +14 -14
- package/dist/operator-toolkit.js +32 -32
- package/dist/orchestration/maintenance.js +17 -17
- package/dist/orchestrator.js +107 -106
- package/dist/recall-planner-llm.js +2 -2
- package/dist/resume-bundles.js +2 -2
- package/dist/schemas.d.ts +100 -100
- package/dist/search/factory.js +9 -9
- package/dist/search/index.js +13 -13
- package/dist/search/lancedb-backend.js +2 -2
- package/dist/search/meilisearch-backend.js +2 -2
- package/dist/search/orama-backend.js +2 -2
- package/dist/semantic-consolidation.js +16 -16
- package/dist/semantic-rule-promotion.js +12 -12
- package/dist/semantic-rule-verifier.js +13 -13
- package/dist/shared-context/manager.d.ts +8 -8
- package/dist/storage.d.ts +60 -78
- package/dist/storage.js +19 -11
- package/dist/summarizer.js +4 -4
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +3 -3
- package/dist/transfer/capsule-import.js +3 -3
- package/dist/transfer/types.d.ts +66 -66
- package/dist/verified-recall.js +13 -13
- package/package.json +2 -2
- package/src/access-observe-write-surface.ts +1007 -0
- package/src/access-service.ts +48 -794
- package/src/config-record.ts +25 -0
- package/src/index.ts +1 -0
- package/src/orchestration/self-deps.ts +9 -1
- package/src/storage/entity-store.ts +4 -2
- package/src/storage/memory-read-store.ts +756 -0
- package/src/storage.ts +67 -627
- package/dist/chunk-GCEV6SXP.js.map +0 -1
- package/dist/chunk-XBM5DG4O.js.map +0 -1
- /package/dist/{capsule-crypto-CZJSLEFG.js.map → access-observe-write-surface.js.map} +0 -0
- /package/dist/{auto-sync-G4YKBPOU.js.map → auto-sync-2SOUW4JW.js.map} +0 -0
- /package/dist/{contradiction-scan-2SODRNUF.js.map → capsule-crypto-RAARDY3M.js.map} +0 -0
- /package/dist/{chunk-T6WHI3JW.js.map → chunk-2NTWULEQ.js.map} +0 -0
- /package/dist/{chunk-TH7WMHVK.js.map → chunk-3NN73CM7.js.map} +0 -0
- /package/dist/{chunk-TAPSNIMA.js.map → chunk-5CEJH5ZN.js.map} +0 -0
- /package/dist/{chunk-CIDGUFAL.js.map → chunk-6A76LSX6.js.map} +0 -0
- /package/dist/{chunk-UQJ6YTKR.js.map → chunk-6DMTYBNY.js.map} +0 -0
- /package/dist/{chunk-S6UCPBPL.js.map → chunk-6FYRCQ7K.js.map} +0 -0
- /package/dist/{chunk-7XH7VJN4.js.map → chunk-6T4LTI2F.js.map} +0 -0
- /package/dist/{chunk-PJ7DIEFC.js.map → chunk-7HJ3XBMD.js.map} +0 -0
- /package/dist/{chunk-TVVEYCNW.js.map → chunk-7K5Q6COX.js.map} +0 -0
- /package/dist/{chunk-I3UWEZTK.js.map → chunk-AINK3KDE.js.map} +0 -0
- /package/dist/{chunk-ZQNCHPTZ.js.map → chunk-CTGK5VK3.js.map} +0 -0
- /package/dist/{chunk-G7LCG56O.js.map → chunk-CXW6Y7SI.js.map} +0 -0
- /package/dist/{chunk-M5QKGHCR.js.map → chunk-CYWCKFCL.js.map} +0 -0
- /package/dist/{chunk-T422SYM6.js.map → chunk-DCWIQFNA.js.map} +0 -0
- /package/dist/{chunk-2BFLDH5F.js.map → chunk-EEIROWEG.js.map} +0 -0
- /package/dist/{chunk-DS4QM6I7.js.map → chunk-EEKDRAWX.js.map} +0 -0
- /package/dist/{chunk-OCFITZRV.js.map → chunk-FBS6V6LJ.js.map} +0 -0
- /package/dist/{chunk-I6IAJCJS.js.map → chunk-FFPH47IX.js.map} +0 -0
- /package/dist/{chunk-N4AGSRVA.js.map → chunk-GI7PRVMV.js.map} +0 -0
- /package/dist/{chunk-FQLM6ZUP.js.map → chunk-GIJDW2YO.js.map} +0 -0
- /package/dist/{chunk-LFKEFIJA.js.map → chunk-I65IQD7Y.js.map} +0 -0
- /package/dist/{chunk-3DOD6ICJ.js.map → chunk-IBAU2RZN.js.map} +0 -0
- /package/dist/{chunk-24463PIV.js.map → chunk-IF6BTZCB.js.map} +0 -0
- /package/dist/{chunk-GMWKWA6N.js.map → chunk-IR6ATBUI.js.map} +0 -0
- /package/dist/{chunk-TJATYB3F.js.map → chunk-IZP62CEF.js.map} +0 -0
- /package/dist/{chunk-FLVL4L3L.js.map → chunk-KTULX4YO.js.map} +0 -0
- /package/dist/{chunk-XCXN3D2U.js.map → chunk-L5DU7THE.js.map} +0 -0
- /package/dist/{chunk-437S3G37.js.map → chunk-LNQUAJNC.js.map} +0 -0
- /package/dist/{chunk-LD53WPMU.js.map → chunk-LQ6JI4VH.js.map} +0 -0
- /package/dist/{chunk-NCNOYM6F.js.map → chunk-LVMVYLNB.js.map} +0 -0
- /package/dist/{chunk-XD6JVK5P.js.map → chunk-MQQO56LN.js.map} +0 -0
- /package/dist/{chunk-PJQMISPD.js.map → chunk-NNL4LJAD.js.map} +0 -0
- /package/dist/{chunk-GGYUR4M2.js.map → chunk-PHWBKQHK.js.map} +0 -0
- /package/dist/{chunk-TQT3USCG.js.map → chunk-PMBWVQ7D.js.map} +0 -0
- /package/dist/{chunk-JITAVROY.js.map → chunk-PXHY2E3S.js.map} +0 -0
- /package/dist/{chunk-GPFXPC2V.js.map → chunk-QOGXUNWL.js.map} +0 -0
- /package/dist/{chunk-SP42M3XP.js.map → chunk-QVQG7W2H.js.map} +0 -0
- /package/dist/{chunk-2QSZNTDO.js.map → chunk-RKNJBZ55.js.map} +0 -0
- /package/dist/{chunk-VL5QHTJI.js.map → chunk-RQXVVENY.js.map} +0 -0
- /package/dist/{chunk-KKOCFUI5.js.map → chunk-SUN6XVNI.js.map} +0 -0
- /package/dist/{chunk-FOJY75BL.js.map → chunk-T3UW6NRQ.js.map} +0 -0
- /package/dist/{chunk-2MR3MFQB.js.map → chunk-UPSAT4PY.js.map} +0 -0
- /package/dist/{chunk-5J4OL7TJ.js.map → chunk-VUA4S7P6.js.map} +0 -0
- /package/dist/{chunk-YQGHW5ML.js.map → chunk-WRFKZEO6.js.map} +0 -0
- /package/dist/{chunk-H6S3DJVE.js.map → chunk-XKUYMM34.js.map} +0 -0
- /package/dist/{chunk-EHQ5HO5T.js.map → chunk-XROP4ZHB.js.map} +0 -0
- /package/dist/{chunk-2OZ2JEUP.js.map → chunk-YTU7WKYN.js.map} +0 -0
- /package/dist/{chunk-V5UV7BOA.js.map → chunk-ZE7GJ76H.js.map} +0 -0
- /package/dist/{chunk-ER3CCWK2.js.map → chunk-ZG2WP425.js.map} +0 -0
- /package/dist/{chunk-4LQUYBZM.js.map → chunk-ZL3Y3ZTQ.js.map} +0 -0
- /package/dist/{graph-edge-decay-RXUCJIN4.js.map → config-record.js.map} +0 -0
- /package/dist/{first-start-migration-DY6YCRC2.js.map → first-start-migration-RYY7PPGF.js.map} +0 -0
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access observe/write surface (extracted from access-service.ts;
|
|
3
|
+
* god-file decomposition, #1526 playbook: verbatim move + live selfDeps
|
|
4
|
+
* wiring).
|
|
5
|
+
*
|
|
6
|
+
* Owns the write-side and operational surfaces of the access layer:
|
|
7
|
+
* - runObserve (turn ingestion incl. idempotency + scope resolution)
|
|
8
|
+
* - memoryStore / suggestionSubmit / memoryActionApply
|
|
9
|
+
* - work-layer task/project/board surfaces
|
|
10
|
+
* - briefing and QMD health (global + per-namespace)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { FileCalendarSource, buildBriefing, parseBriefingFocus, parseBriefingWindow } from "./briefing.js";
|
|
15
|
+
import { resolveCompressionCapabilities, resolveNamespaceCapabilities, resolveObjectiveStateCapabilities } from "./capabilities.js";
|
|
16
|
+
import { lcmSessionKeyForNamespace } from "./coding/coding-namespace.js";
|
|
17
|
+
import { type ValidExplicitCapture, persistExplicitCapture, queueExplicitCaptureForReview } from "./explicit-capture.js";
|
|
18
|
+
import { log } from "./logger.js";
|
|
19
|
+
import { recordObjectiveStateSnapshotsFromObservedMessages } from "./objective-state-writers.js";
|
|
20
|
+
import type { Orchestrator } from "./orchestrator.js";
|
|
21
|
+
import { displayErrorDetail } from "./runtime/better-sqlite.js";
|
|
22
|
+
import type { MemoryActionOutcome, MemoryActionType } from "./types.js";
|
|
23
|
+
import { exportWorkBoardMarkdown, exportWorkBoardSnapshot, importWorkBoardSnapshot } from "./work/board.js";
|
|
24
|
+
import { wrapWorkLayerContext } from "./work/boundary.js";
|
|
25
|
+
import { WorkStorage } from "./work/storage.js";
|
|
26
|
+
import {
|
|
27
|
+
ENGRAM_ACCESS_WRITE_SCHEMA_VERSION,
|
|
28
|
+
EngramAccessInputError,
|
|
29
|
+
type CodingScopedWriteInput,
|
|
30
|
+
type EngramAccessBriefingRequest,
|
|
31
|
+
type EngramAccessBriefingResponse,
|
|
32
|
+
type EngramAccessMemoryStoreRequest,
|
|
33
|
+
type EngramAccessObserveRequest,
|
|
34
|
+
type EngramAccessObserveResponse,
|
|
35
|
+
type EngramAccessQmdCollectionState,
|
|
36
|
+
type EngramAccessQmdHealthResponse,
|
|
37
|
+
type EngramAccessSuggestionSubmitRequest,
|
|
38
|
+
type EngramAccessWriteResponse,
|
|
39
|
+
type MemoryScopePlan,
|
|
40
|
+
} from "./access-service.js";
|
|
41
|
+
|
|
42
|
+
export interface AccessObserveWriteSurfaceDeps {
|
|
43
|
+
attachCodingContextAfterScopedWrite(
|
|
44
|
+
request: CodingScopedWriteInput & { namespace?: string; sessionKey?: string },
|
|
45
|
+
): Promise<void>;
|
|
46
|
+
handleIdempotentWrite<T extends { idempotencyReplay?: boolean }>(options: {
|
|
47
|
+
operation: string;
|
|
48
|
+
idempotencyKey?: string;
|
|
49
|
+
requestFingerprint: unknown;
|
|
50
|
+
skip?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Invoked exactly once, immediately before an ACTUAL (non-replay, non-skip)
|
|
53
|
+
* write is committed — atomically with the idempotency miss determination.
|
|
54
|
+
* The HTTP surface uses this to enforce the write rate limit against the
|
|
55
|
+
* real write/miss (and the real resolved namespace), so a namespace-divergent
|
|
56
|
+
* idempotency peek can never let a fresh write skip the quota check (#1434
|
|
57
|
+
* Codex review). It is NOT called on dryRun (skip) or replay, preserving the
|
|
58
|
+
* replay-bypasses-a-full-window behavior.
|
|
59
|
+
*/
|
|
60
|
+
beforeExecute?: () => void | Promise<void>;
|
|
61
|
+
execute: () => Promise<T>;
|
|
62
|
+
}): Promise<T>;
|
|
63
|
+
legacyResponseNamespaceForScope(scope: MemoryScopePlan): string;
|
|
64
|
+
maybeAttachCodingContext(
|
|
65
|
+
sessionKey: string | undefined,
|
|
66
|
+
options: { cwd?: string; projectTag?: string },
|
|
67
|
+
): Promise<void>;
|
|
68
|
+
namespaceQmdHealth(
|
|
69
|
+
searchBackend: string,
|
|
70
|
+
qmdEnabled: boolean,
|
|
71
|
+
namespace: string,
|
|
72
|
+
fallbackCollection: string,
|
|
73
|
+
): Promise<EngramAccessQmdHealthResponse | null>;
|
|
74
|
+
objectiveStateStoreLocationForNamespace(namespace: string): Promise<{
|
|
75
|
+
memoryDir: string;
|
|
76
|
+
objectiveStateStoreDir?: string;
|
|
77
|
+
}>;
|
|
78
|
+
readonly orchestrator: Orchestrator;
|
|
79
|
+
qmdCollectionState(
|
|
80
|
+
searchBackend: string,
|
|
81
|
+
qmdEnabled: boolean,
|
|
82
|
+
collection: string,
|
|
83
|
+
): Promise<EngramAccessQmdCollectionState>;
|
|
84
|
+
qmdProbeAvailable(
|
|
85
|
+
searchBackend: string,
|
|
86
|
+
qmdEnabled: boolean,
|
|
87
|
+
): Promise<boolean>;
|
|
88
|
+
resolveCodingScopedWriteNamespace(
|
|
89
|
+
request: CodingScopedWriteInput & {
|
|
90
|
+
namespace?: string;
|
|
91
|
+
sessionKey?: string;
|
|
92
|
+
authenticatedPrincipal?: string;
|
|
93
|
+
},
|
|
94
|
+
): Promise<string>;
|
|
95
|
+
resolveMemoryScopePlan(
|
|
96
|
+
request: CodingScopedWriteInput & {
|
|
97
|
+
namespace?: string;
|
|
98
|
+
sessionKey?: string;
|
|
99
|
+
authenticatedPrincipal?: string;
|
|
100
|
+
},
|
|
101
|
+
): Promise<MemoryScopePlan>;
|
|
102
|
+
resolveReadableNamespace(namespace: string | undefined, principal?: string): string;
|
|
103
|
+
validateWriteCandidate(
|
|
104
|
+
request: EngramAccessMemoryStoreRequest | EngramAccessSuggestionSubmitRequest,
|
|
105
|
+
namespace: string,
|
|
106
|
+
): ValidExplicitCapture;
|
|
107
|
+
writableNamespaceFor(
|
|
108
|
+
namespace: string | undefined,
|
|
109
|
+
sessionKey: string | undefined,
|
|
110
|
+
authenticatedPrincipal?: string,
|
|
111
|
+
): string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export class AccessObserveWriteSurface {
|
|
115
|
+
constructor(
|
|
116
|
+
private readonly deps: AccessObserveWriteSurfaceDeps,
|
|
117
|
+
) {}
|
|
118
|
+
|
|
119
|
+
async qmdHealth(
|
|
120
|
+
searchBackend: string,
|
|
121
|
+
qmdEnabled: boolean,
|
|
122
|
+
namespace: string,
|
|
123
|
+
collection: string,
|
|
124
|
+
): Promise<EngramAccessQmdHealthResponse> {
|
|
125
|
+
if (searchBackend !== "qmd" || !qmdEnabled) {
|
|
126
|
+
return {
|
|
127
|
+
enabled: qmdEnabled,
|
|
128
|
+
active: false,
|
|
129
|
+
degraded: false,
|
|
130
|
+
mode: searchBackend !== "qmd" ? "not-selected" : "disabled",
|
|
131
|
+
collection,
|
|
132
|
+
collectionState: "skipped",
|
|
133
|
+
installedVersion: null,
|
|
134
|
+
supportedVersion: null,
|
|
135
|
+
supported: null,
|
|
136
|
+
upgradeAvailable: null,
|
|
137
|
+
doctorAvailable: null,
|
|
138
|
+
debugStatus: searchBackend !== "qmd" ? `backend=${searchBackend}` : "backend=disabled",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (resolveNamespaceCapabilities(this.deps.orchestrator.config).namespaces === true) {
|
|
143
|
+
const namespaceHealth = await this.deps.namespaceQmdHealth(searchBackend, qmdEnabled, namespace, collection);
|
|
144
|
+
if (namespaceHealth) return namespaceHealth;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const qmd = this.deps.orchestrator.qmd;
|
|
148
|
+
if (!qmd) {
|
|
149
|
+
return {
|
|
150
|
+
enabled: true,
|
|
151
|
+
active: false,
|
|
152
|
+
degraded: true,
|
|
153
|
+
mode: "fallback",
|
|
154
|
+
collection,
|
|
155
|
+
collectionState: "unknown",
|
|
156
|
+
installedVersion: null,
|
|
157
|
+
supportedVersion: null,
|
|
158
|
+
supported: null,
|
|
159
|
+
upgradeAvailable: null,
|
|
160
|
+
doctorAvailable: null,
|
|
161
|
+
debugStatus: "backend=unavailable",
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const diagnosticAvailable = await this.deps.qmdProbeAvailable(searchBackend, qmdEnabled);
|
|
165
|
+
const operationalAvailable = diagnosticAvailable || qmd.isAvailable();
|
|
166
|
+
const collectionState = diagnosticAvailable
|
|
167
|
+
? await this.deps.qmdCollectionState(searchBackend, qmdEnabled, collection)
|
|
168
|
+
: "unknown";
|
|
169
|
+
const active = operationalAvailable && collectionState !== "missing";
|
|
170
|
+
const degraded =
|
|
171
|
+
searchBackend === "qmd" &&
|
|
172
|
+
qmdEnabled &&
|
|
173
|
+
(!active || !diagnosticAvailable || collectionState === "unknown");
|
|
174
|
+
const debugStatus = qmd.debugStatus();
|
|
175
|
+
const versionStatus =
|
|
176
|
+
"getVersionStatus" in qmd && typeof qmd.getVersionStatus === "function"
|
|
177
|
+
? qmd.getVersionStatus()
|
|
178
|
+
: null;
|
|
179
|
+
const daemonMode =
|
|
180
|
+
"isDaemonMode" in qmd && typeof qmd.isDaemonMode === "function"
|
|
181
|
+
? qmd.isDaemonMode() === true
|
|
182
|
+
: false;
|
|
183
|
+
const mode =
|
|
184
|
+
searchBackend !== "qmd"
|
|
185
|
+
? "not-selected"
|
|
186
|
+
: !qmdEnabled
|
|
187
|
+
? "disabled"
|
|
188
|
+
: !active
|
|
189
|
+
? "fallback"
|
|
190
|
+
: daemonMode
|
|
191
|
+
? "daemon"
|
|
192
|
+
: "cli";
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
enabled: qmdEnabled,
|
|
196
|
+
active,
|
|
197
|
+
degraded,
|
|
198
|
+
mode,
|
|
199
|
+
collection,
|
|
200
|
+
collectionState,
|
|
201
|
+
installedVersion: versionStatus?.installedVersion ?? null,
|
|
202
|
+
supportedVersion: versionStatus?.supportedVersion ?? null,
|
|
203
|
+
supported: versionStatus?.supported ?? null,
|
|
204
|
+
upgradeAvailable: versionStatus?.upgradeAvailable ?? null,
|
|
205
|
+
doctorAvailable: versionStatus?.capabilities?.doctor ?? null,
|
|
206
|
+
debugStatus,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async namespaceQmdHealth(
|
|
211
|
+
searchBackend: string,
|
|
212
|
+
qmdEnabled: boolean,
|
|
213
|
+
namespace: string,
|
|
214
|
+
fallbackCollection: string,
|
|
215
|
+
): Promise<EngramAccessQmdHealthResponse | null> {
|
|
216
|
+
if (searchBackend !== "qmd" || !qmdEnabled) return null;
|
|
217
|
+
const searchHealthForNamespace = (
|
|
218
|
+
this.deps.orchestrator as Orchestrator & {
|
|
219
|
+
searchHealthForNamespace?: (
|
|
220
|
+
namespace: string,
|
|
221
|
+
execution?: { signal?: AbortSignal },
|
|
222
|
+
) => Promise<{
|
|
223
|
+
collection: string;
|
|
224
|
+
available: boolean;
|
|
225
|
+
collectionState: EngramAccessQmdCollectionState;
|
|
226
|
+
debugStatus: string;
|
|
227
|
+
installedVersion: string | null;
|
|
228
|
+
supportedVersion: string | null;
|
|
229
|
+
supported: boolean | null;
|
|
230
|
+
upgradeAvailable: boolean | null;
|
|
231
|
+
doctorAvailable: boolean | null;
|
|
232
|
+
daemonMode: boolean | null;
|
|
233
|
+
}>;
|
|
234
|
+
}
|
|
235
|
+
).searchHealthForNamespace;
|
|
236
|
+
if (typeof searchHealthForNamespace !== "function") return null;
|
|
237
|
+
|
|
238
|
+
const controller = new AbortController();
|
|
239
|
+
const timer = setTimeout(() => controller.abort(), 2_000);
|
|
240
|
+
timer.unref?.();
|
|
241
|
+
try {
|
|
242
|
+
const health = await searchHealthForNamespace.call(this.deps.orchestrator, namespace, {
|
|
243
|
+
signal: controller.signal,
|
|
244
|
+
});
|
|
245
|
+
const active = health.available && health.collectionState !== "missing";
|
|
246
|
+
const degraded = !active || health.collectionState === "unknown";
|
|
247
|
+
const mode =
|
|
248
|
+
!active
|
|
249
|
+
? "fallback"
|
|
250
|
+
: health.daemonMode === true
|
|
251
|
+
? "daemon"
|
|
252
|
+
: "cli";
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
enabled: true,
|
|
256
|
+
active,
|
|
257
|
+
degraded,
|
|
258
|
+
mode,
|
|
259
|
+
collection: health.collection || fallbackCollection,
|
|
260
|
+
collectionState: health.collectionState,
|
|
261
|
+
installedVersion: health.installedVersion,
|
|
262
|
+
supportedVersion: health.supportedVersion,
|
|
263
|
+
supported: health.supported,
|
|
264
|
+
upgradeAvailable: health.upgradeAvailable,
|
|
265
|
+
doctorAvailable: health.doctorAvailable,
|
|
266
|
+
debugStatus: health.debugStatus,
|
|
267
|
+
};
|
|
268
|
+
} catch (error) {
|
|
269
|
+
const detail = displayErrorDetail(error) || "unknown";
|
|
270
|
+
return {
|
|
271
|
+
enabled: true,
|
|
272
|
+
active: false,
|
|
273
|
+
degraded: true,
|
|
274
|
+
mode: "fallback",
|
|
275
|
+
collection: fallbackCollection,
|
|
276
|
+
collectionState: "unknown",
|
|
277
|
+
installedVersion: null,
|
|
278
|
+
supportedVersion: null,
|
|
279
|
+
supported: null,
|
|
280
|
+
upgradeAvailable: null,
|
|
281
|
+
doctorAvailable: null,
|
|
282
|
+
debugStatus: `backend=namespace-unavailable error=${detail}`,
|
|
283
|
+
};
|
|
284
|
+
} finally {
|
|
285
|
+
clearTimeout(timer);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Build a daily context briefing. Gracefully degrades when the OpenAI key
|
|
291
|
+
* or Responses API is unavailable — never throws for LLM-related problems.
|
|
292
|
+
*/
|
|
293
|
+
async briefing(
|
|
294
|
+
request: EngramAccessBriefingRequest,
|
|
295
|
+
): Promise<EngramAccessBriefingResponse> {
|
|
296
|
+
const config = this.deps.orchestrator.config;
|
|
297
|
+
if (!config.briefing.enabled) {
|
|
298
|
+
throw new EngramAccessInputError("briefing is disabled");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const namespace = this.deps.resolveReadableNamespace(request.namespace, request.principal);
|
|
302
|
+
const storage = await this.deps.orchestrator.getStorage(namespace);
|
|
303
|
+
|
|
304
|
+
const token = typeof request.since === "string" && request.since.trim().length > 0
|
|
305
|
+
? request.since.trim()
|
|
306
|
+
: config.briefing.defaultWindow;
|
|
307
|
+
const window = parseBriefingWindow(token);
|
|
308
|
+
if (!window) {
|
|
309
|
+
throw new EngramAccessInputError(`invalid briefing window: ${token}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Validate focus: only treat undefined / empty strings as "no filter".
|
|
313
|
+
// Anything else that parses to null (e.g. "project:", "topic:") is malformed
|
|
314
|
+
// and must be rejected so a templating miss never silently broadens the
|
|
315
|
+
// briefing from a targeted project view to all memories.
|
|
316
|
+
const rawFocus = typeof request.focus === "string" ? request.focus.trim() : "";
|
|
317
|
+
let focus = null;
|
|
318
|
+
if (rawFocus.length > 0) {
|
|
319
|
+
focus = parseBriefingFocus(rawFocus);
|
|
320
|
+
if (!focus) {
|
|
321
|
+
throw new EngramAccessInputError(
|
|
322
|
+
`invalid briefing focus filter: ${request.focus}`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Reject unsupported format values explicitly. Programmatic callers that
|
|
328
|
+
// bypass CLI/MCP pre-validation (which already use validateBriefingFormat)
|
|
329
|
+
// could otherwise send a typo like "jsno" and silently receive a response
|
|
330
|
+
// in the default format, masking the client bug and breaking format-dependent
|
|
331
|
+
// automation. Only undefined / absent format falls through to the default.
|
|
332
|
+
const SUPPORTED_FORMATS = ["markdown", "json"] as const;
|
|
333
|
+
if (
|
|
334
|
+
typeof request.format === "string" &&
|
|
335
|
+
!(SUPPORTED_FORMATS as readonly string[]).includes(request.format)
|
|
336
|
+
) {
|
|
337
|
+
throw new EngramAccessInputError(
|
|
338
|
+
`unsupported briefing format: "${request.format}". Accepted: ${SUPPORTED_FORMATS.join(", ")}.`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
const format: "markdown" | "json" = request.format === "json"
|
|
342
|
+
? "json"
|
|
343
|
+
: request.format === "markdown"
|
|
344
|
+
? "markdown"
|
|
345
|
+
: config.briefing.defaultFormat;
|
|
346
|
+
|
|
347
|
+
const maxFollowups = typeof request.maxFollowups === "number" && Number.isFinite(request.maxFollowups)
|
|
348
|
+
? Math.max(0, Math.min(10, Math.floor(request.maxFollowups)))
|
|
349
|
+
: config.briefing.maxFollowups;
|
|
350
|
+
|
|
351
|
+
const calendarSource = config.briefing.calendarSource
|
|
352
|
+
? new FileCalendarSource(config.briefing.calendarSource)
|
|
353
|
+
: undefined;
|
|
354
|
+
|
|
355
|
+
const result = await buildBriefing({
|
|
356
|
+
storage,
|
|
357
|
+
namespace,
|
|
358
|
+
window,
|
|
359
|
+
focus,
|
|
360
|
+
calendarSource,
|
|
361
|
+
maxFollowups,
|
|
362
|
+
allowLlm: config.briefing.llmFollowups,
|
|
363
|
+
openaiApiKey: config.openaiApiKey,
|
|
364
|
+
openaiBaseUrl: config.openaiBaseUrl,
|
|
365
|
+
model: config.model,
|
|
366
|
+
// Without a direct OpenAI key, route follow-ups through the configured
|
|
367
|
+
// LLM chain (gateway model source or local LLM) — same fallback every
|
|
368
|
+
// other LLM feature uses. A configured key keeps its precedence so
|
|
369
|
+
// existing deployments are unchanged.
|
|
370
|
+
followupGenerator: config.openaiApiKey
|
|
371
|
+
? undefined
|
|
372
|
+
: this.deps.orchestrator.briefingChainFollowupGenerator,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
format,
|
|
377
|
+
window: result.window,
|
|
378
|
+
namespace,
|
|
379
|
+
markdown: result.markdown,
|
|
380
|
+
json: result.json,
|
|
381
|
+
followupsUnavailableReason: result.followupsUnavailableReason,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async memoryStore(
|
|
386
|
+
request: EngramAccessMemoryStoreRequest,
|
|
387
|
+
hooks?: { enforceWriteQuota?: () => void | Promise<void> },
|
|
388
|
+
): Promise<EngramAccessWriteResponse> {
|
|
389
|
+
const namespace = await this.deps.resolveCodingScopedWriteNamespace(request);
|
|
390
|
+
const schemaVersion = request.schemaVersion ?? ENGRAM_ACCESS_WRITE_SCHEMA_VERSION;
|
|
391
|
+
if (schemaVersion !== ENGRAM_ACCESS_WRITE_SCHEMA_VERSION) {
|
|
392
|
+
throw new EngramAccessInputError(`unsupported schemaVersion: ${schemaVersion}`);
|
|
393
|
+
}
|
|
394
|
+
const execute = async (): Promise<EngramAccessWriteResponse> => {
|
|
395
|
+
const candidate = this.deps.validateWriteCandidate(request, namespace);
|
|
396
|
+
if (request.dryRun === true) {
|
|
397
|
+
return {
|
|
398
|
+
schemaVersion: ENGRAM_ACCESS_WRITE_SCHEMA_VERSION,
|
|
399
|
+
operation: "memory_store",
|
|
400
|
+
namespace,
|
|
401
|
+
dryRun: true,
|
|
402
|
+
accepted: true,
|
|
403
|
+
queued: false,
|
|
404
|
+
status: "validated",
|
|
405
|
+
idempotencyKey: request.idempotencyKey?.trim() || undefined,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
const result = await persistExplicitCapture(this.deps.orchestrator, candidate, "memory_store");
|
|
409
|
+
// Seed the session's coding binding ONLY after a real write commits, and
|
|
410
|
+
// only when the namespace came from project scoping (no explicit
|
|
411
|
+
// namespace). This mirrors recall's maybeAttachCodingContext so a LATER
|
|
412
|
+
// bare recall/write on the same session is scoped to the same project —
|
|
413
|
+
// but never binds the session on a dryRun, replay/conflict, quota
|
|
414
|
+
// rejection, or an explicit-namespace write (which bypasses the overlay),
|
|
415
|
+
// since those don't reach this point or aren't project-scoped (Codex review).
|
|
416
|
+
await this.deps.attachCodingContextAfterScopedWrite(request);
|
|
417
|
+
// #1645 (review thread yG-): a tombstone-blocked capture is pending_review
|
|
418
|
+
// (no active copy) — report it as queued_for_review so the HTTP/MCP caller
|
|
419
|
+
// doesn't read it as a successfully stored active memory.
|
|
420
|
+
const blocked = result.tombstoneBlocked === true;
|
|
421
|
+
const response: EngramAccessWriteResponse = {
|
|
422
|
+
schemaVersion: ENGRAM_ACCESS_WRITE_SCHEMA_VERSION,
|
|
423
|
+
operation: "memory_store",
|
|
424
|
+
namespace,
|
|
425
|
+
dryRun: false,
|
|
426
|
+
accepted: true,
|
|
427
|
+
queued: blocked,
|
|
428
|
+
status: blocked ? "queued_for_review" : result.duplicateOf ? "duplicate" : "stored",
|
|
429
|
+
memoryId: result.id,
|
|
430
|
+
duplicateOf: result.duplicateOf,
|
|
431
|
+
idempotencyKey: request.idempotencyKey?.trim() || undefined,
|
|
432
|
+
};
|
|
433
|
+
log.info(
|
|
434
|
+
`access-write op=memory_store namespace=${namespace} dryRun=false status=${response.status} memoryId=${response.memoryId ?? "-"} idempotency=${response.idempotencyKey ? "yes" : "no"}`,
|
|
435
|
+
);
|
|
436
|
+
return response;
|
|
437
|
+
};
|
|
438
|
+
return this.deps.handleIdempotentWrite({
|
|
439
|
+
operation: "memory_store",
|
|
440
|
+
idempotencyKey: request.idempotencyKey,
|
|
441
|
+
requestFingerprint: {
|
|
442
|
+
schemaVersion,
|
|
443
|
+
content: request.content,
|
|
444
|
+
category: request.category,
|
|
445
|
+
confidence: request.confidence,
|
|
446
|
+
namespace,
|
|
447
|
+
tags: request.tags,
|
|
448
|
+
entityRef: request.entityRef,
|
|
449
|
+
ttl: request.ttl,
|
|
450
|
+
sourceReason: request.sourceReason,
|
|
451
|
+
},
|
|
452
|
+
skip: request.dryRun === true,
|
|
453
|
+
beforeExecute: hooks?.enforceWriteQuota,
|
|
454
|
+
execute,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async suggestionSubmit(
|
|
459
|
+
request: EngramAccessSuggestionSubmitRequest,
|
|
460
|
+
hooks?: { enforceWriteQuota?: () => void | Promise<void> },
|
|
461
|
+
): Promise<EngramAccessWriteResponse> {
|
|
462
|
+
const namespace = await this.deps.resolveCodingScopedWriteNamespace(request);
|
|
463
|
+
const schemaVersion = request.schemaVersion ?? ENGRAM_ACCESS_WRITE_SCHEMA_VERSION;
|
|
464
|
+
if (schemaVersion !== ENGRAM_ACCESS_WRITE_SCHEMA_VERSION) {
|
|
465
|
+
throw new EngramAccessInputError(`unsupported schemaVersion: ${schemaVersion}`);
|
|
466
|
+
}
|
|
467
|
+
const execute = async (): Promise<EngramAccessWriteResponse> => {
|
|
468
|
+
const candidate = this.deps.validateWriteCandidate(request, namespace);
|
|
469
|
+
if (request.dryRun === true) {
|
|
470
|
+
return {
|
|
471
|
+
schemaVersion: ENGRAM_ACCESS_WRITE_SCHEMA_VERSION,
|
|
472
|
+
operation: "suggestion_submit",
|
|
473
|
+
namespace,
|
|
474
|
+
dryRun: true,
|
|
475
|
+
accepted: true,
|
|
476
|
+
queued: true,
|
|
477
|
+
status: "validated",
|
|
478
|
+
idempotencyKey: request.idempotencyKey?.trim() || undefined,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
const result = await queueExplicitCaptureForReview(
|
|
482
|
+
this.deps.orchestrator,
|
|
483
|
+
candidate,
|
|
484
|
+
"suggestion_submit",
|
|
485
|
+
new Error(request.sourceReason?.trim() || "submitted via engram suggestion_submit"),
|
|
486
|
+
);
|
|
487
|
+
// Seed the session binding only after a real, project-scoped submit commits
|
|
488
|
+
// (mirrors memory_store / recall; skips dryRun, replay, quota-reject, and
|
|
489
|
+
// explicit-namespace writes — Codex review).
|
|
490
|
+
await this.deps.attachCodingContextAfterScopedWrite(request);
|
|
491
|
+
const response: EngramAccessWriteResponse = {
|
|
492
|
+
schemaVersion: ENGRAM_ACCESS_WRITE_SCHEMA_VERSION,
|
|
493
|
+
operation: "suggestion_submit",
|
|
494
|
+
namespace,
|
|
495
|
+
dryRun: false,
|
|
496
|
+
accepted: true,
|
|
497
|
+
queued: true,
|
|
498
|
+
status: "queued_for_review",
|
|
499
|
+
memoryId: result.id,
|
|
500
|
+
duplicateOf: result.duplicateOf,
|
|
501
|
+
idempotencyKey: request.idempotencyKey?.trim() || undefined,
|
|
502
|
+
};
|
|
503
|
+
log.info(
|
|
504
|
+
`access-write op=suggestion_submit namespace=${namespace} dryRun=false status=${response.status} memoryId=${response.memoryId ?? "-"} idempotency=${response.idempotencyKey ? "yes" : "no"}`,
|
|
505
|
+
);
|
|
506
|
+
return response;
|
|
507
|
+
};
|
|
508
|
+
return this.deps.handleIdempotentWrite({
|
|
509
|
+
operation: "suggestion_submit",
|
|
510
|
+
idempotencyKey: request.idempotencyKey,
|
|
511
|
+
requestFingerprint: {
|
|
512
|
+
schemaVersion,
|
|
513
|
+
content: request.content,
|
|
514
|
+
category: request.category,
|
|
515
|
+
confidence: request.confidence,
|
|
516
|
+
namespace,
|
|
517
|
+
tags: request.tags,
|
|
518
|
+
entityRef: request.entityRef,
|
|
519
|
+
ttl: request.ttl,
|
|
520
|
+
sourceReason: request.sourceReason,
|
|
521
|
+
},
|
|
522
|
+
skip: request.dryRun === true,
|
|
523
|
+
beforeExecute: hooks?.enforceWriteQuota,
|
|
524
|
+
execute,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async runObserve(request: EngramAccessObserveRequest): Promise<EngramAccessObserveResponse> {
|
|
529
|
+
if (!request.sessionKey || typeof request.sessionKey !== "string" || request.sessionKey.trim().length === 0) {
|
|
530
|
+
throw new EngramAccessInputError("sessionKey is required and must be a non-empty string");
|
|
531
|
+
}
|
|
532
|
+
if (!Array.isArray(request.messages) || request.messages.length === 0) {
|
|
533
|
+
throw new EngramAccessInputError("messages is required and must be a non-empty array");
|
|
534
|
+
}
|
|
535
|
+
for (const msg of request.messages) {
|
|
536
|
+
if (!msg || typeof msg !== "object" || typeof msg.role !== "string" || typeof msg.content !== "string") {
|
|
537
|
+
throw new EngramAccessInputError("each message must have a string 'role' and 'content'");
|
|
538
|
+
}
|
|
539
|
+
if (msg.role !== "user" && msg.role !== "assistant") {
|
|
540
|
+
throw new EngramAccessInputError(`invalid message role: ${msg.role} (expected 'user' or 'assistant')`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// 1. Resolve the FULL effective scope plan BEFORE any session mutation
|
|
545
|
+
// (Codex P2 / Cursor "orphan context after overlay auth"). The plan is
|
|
546
|
+
// read-only and re-runs the SAME authorization as
|
|
547
|
+
// memory_store/suggestion_submit (rule 39): the explicit-namespace check
|
|
548
|
+
// AND the coding-overlay self-base `canWriteNamespace` check both run
|
|
549
|
+
// here. Because `maybeAttachCodingContext` has NOT run yet, the plan's
|
|
550
|
+
// overlay resolves from the per-call `cwd`/`projectTag` fallback
|
|
551
|
+
// (`resolveCodingContextFromOptions`) — identical to the context that
|
|
552
|
+
// would be attached — so the scope is the same either way. Running the
|
|
553
|
+
// plan first means an `observe` that ultimately throws on a non-writable
|
|
554
|
+
// self base leaves NO coding context bound to the session, matching how
|
|
555
|
+
// `memory_store` resolves its full scoped write namespace before any
|
|
556
|
+
// session mutation.
|
|
557
|
+
const scope = await this.deps.resolveMemoryScopePlan(request);
|
|
558
|
+
const writeNamespace = scope.writeNamespace;
|
|
559
|
+
|
|
560
|
+
// Backward-compatible BASE writable namespace (pre-#1495 response semantics)
|
|
561
|
+
// for the legacy `namespace` response field. DERIVED from the already-resolved
|
|
562
|
+
// scope plan — NOT a second writable-namespace resolution call
|
|
563
|
+
// call (#1505 thread jvO). The fresh call re-authorized `undefined ⇒
|
|
564
|
+
// config.defaultNamespace` a SECOND time; under a restrictive default-namespace
|
|
565
|
+
// write policy that re-auth could REJECT an otherwise valid project-scoped
|
|
566
|
+
// observe whose effective self/project write target the scope plan already
|
|
567
|
+
// authorized (the same target memory_store/suggestion_submit accept). Worse,
|
|
568
|
+
// that post-plan rejection fired AFTER `resolveMemoryScopePlan` may have seeded
|
|
569
|
+
// the coding context, leaving an orphaned session binding behind. The plan is
|
|
570
|
+
// the single authorization point (rule 22 / 39); the legacy field must reuse it
|
|
571
|
+
// and never re-authorize. Pre-#1495 semantics were exactly
|
|
572
|
+
// the writable-namespace resolver (overlay-agnostic): the explicit
|
|
573
|
+
// namespace when supplied, else `config.defaultNamespace` for user-project
|
|
574
|
+
// coding overlays. Hosted scope-profile layers such as `teamProject` report
|
|
575
|
+
// their effective profile write namespace because there is no legacy
|
|
576
|
+
// overlay-compatible base namespace for those writes.
|
|
577
|
+
const namespace = this.deps.legacyResponseNamespaceForScope(scope);
|
|
578
|
+
const shouldWriteObjectiveState =
|
|
579
|
+
resolveObjectiveStateCapabilities(this.deps.orchestrator.config).objectiveStateMemory === true &&
|
|
580
|
+
resolveObjectiveStateCapabilities(this.deps.orchestrator.config).objectiveStateSnapshotWrites === true;
|
|
581
|
+
|
|
582
|
+
// 2. Auto-resolve coding context from cwd/projectTag so a LATER bare recall
|
|
583
|
+
// on the same session is project-scoped (rule 42: same namespace layer as
|
|
584
|
+
// recall). Done AFTER the scope plan authorized the write, so a rejected
|
|
585
|
+
// request never leaves orphaned context on the session.
|
|
586
|
+
await this.deps.maybeAttachCodingContext(request.sessionKey, {
|
|
587
|
+
cwd: request.cwd,
|
|
588
|
+
projectTag: request.projectTag,
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
// Prefix sessionKey with the EFFECTIVE write namespace for LCM archival so
|
|
592
|
+
// observed turns are scoped to the same namespace project-scoped recall
|
|
593
|
+
// reads. The SAME `lcmSessionKeyForNamespace` helper is used by the
|
|
594
|
+
// orchestrator recall readers and by compaction flush/record, so the LCM
|
|
595
|
+
// write key and every read/flush key agree (#1495, rule 42). Only prefixes
|
|
596
|
+
// when the namespace diverges from the default store; a single-store
|
|
597
|
+
// deployment keeps the raw sessionKey unchanged.
|
|
598
|
+
const lcmSessionKey =
|
|
599
|
+
lcmSessionKeyForNamespace(
|
|
600
|
+
writeNamespace,
|
|
601
|
+
request.sessionKey,
|
|
602
|
+
this.deps.orchestrator.config.defaultNamespace,
|
|
603
|
+
) ?? request.sessionKey;
|
|
604
|
+
|
|
605
|
+
// 4. Objective-state snapshots → the scope plan's objective-state namespace.
|
|
606
|
+
// For explicit-namespace and coding-overlay writes this equals
|
|
607
|
+
// writeNamespace; for an IMPLICIT write it is the principal SELF base
|
|
608
|
+
// (#928 contract, already auth-checked inside the scope plan), not the
|
|
609
|
+
// general default-store write namespace.
|
|
610
|
+
if (shouldWriteObjectiveState) {
|
|
611
|
+
try {
|
|
612
|
+
const objectiveStateLocation =
|
|
613
|
+
await this.deps.objectiveStateStoreLocationForNamespace(
|
|
614
|
+
scope.objectiveStateNamespace,
|
|
615
|
+
);
|
|
616
|
+
await recordObjectiveStateSnapshotsFromObservedMessages({
|
|
617
|
+
memoryDir: objectiveStateLocation.memoryDir,
|
|
618
|
+
objectiveStateStoreDir: objectiveStateLocation.objectiveStateStoreDir,
|
|
619
|
+
objectiveStateMemoryEnabled: resolveObjectiveStateCapabilities(this.deps.orchestrator.config).objectiveStateMemory,
|
|
620
|
+
objectiveStateSnapshotWritesEnabled:
|
|
621
|
+
resolveObjectiveStateCapabilities(this.deps.orchestrator.config).objectiveStateSnapshotWrites,
|
|
622
|
+
sessionKey: request.sessionKey,
|
|
623
|
+
recordedAt: new Date().toISOString(),
|
|
624
|
+
messages: request.messages,
|
|
625
|
+
});
|
|
626
|
+
} catch (err) {
|
|
627
|
+
log.error(`access-observe objective-state snapshot write failed: ${err}`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// 5. LCM archival → effective write namespace.
|
|
632
|
+
// lcmArchived in the response means "LCM archival was queued" (not
|
|
633
|
+
// "completed"), matching extractionQueued semantics. Both run async.
|
|
634
|
+
let lcmArchived = false;
|
|
635
|
+
if (this.deps.orchestrator.lcmEngine && this.deps.orchestrator.lcmEngine.enabled) {
|
|
636
|
+
// Fire-and-forget: LCM archival writes to SQLite and builds summary
|
|
637
|
+
// DAGs, which can take tens of seconds for large sessions. Don't
|
|
638
|
+
// block the HTTP response — the caller only needs acknowledgment.
|
|
639
|
+
try {
|
|
640
|
+
this.deps.orchestrator.lcmEngine.enqueueObserveMessages(lcmSessionKey, request.messages);
|
|
641
|
+
lcmArchived = true;
|
|
642
|
+
} catch (err) {
|
|
643
|
+
log.error(`access-observe LCM enqueue failed: ${err}`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// 6. Extraction/replay → effective write namespace for STORAGE, ORIGINAL
|
|
648
|
+
// sessionKey for IDENTITY (provenance + threading).
|
|
649
|
+
let extractionQueued = false;
|
|
650
|
+
if (request.skipExtraction !== true) {
|
|
651
|
+
const turns = request.messages.map((m) => ({
|
|
652
|
+
source: "openclaw" as const,
|
|
653
|
+
// Identity-vs-routing separation (#1505 thread 1, cursor): extraction
|
|
654
|
+
// derives the provenance principal via `resolvePrincipal(turn.sessionKey)`
|
|
655
|
+
// and threads `turn.sessionKey` into conversation threading. Feeding the
|
|
656
|
+
// namespace-PREFIXED `lcmSessionKey` here mis-derived the principal to
|
|
657
|
+
// `default` (a `<ns>:<key>` string matches no prefix/map rule and fails
|
|
658
|
+
// the `agent:` heuristic). Pass the ORIGINAL sessionKey so identity is
|
|
659
|
+
// correct; storage routing is pinned separately via
|
|
660
|
+
// writeNamespaceOverride below, and the authenticated principal is pinned
|
|
661
|
+
// via principalOverride.
|
|
662
|
+
sessionKey: request.sessionKey,
|
|
663
|
+
role: m.role,
|
|
664
|
+
content: m.content,
|
|
665
|
+
parts: m.parts,
|
|
666
|
+
rawContent: m.rawContent,
|
|
667
|
+
sourceFormat: m.sourceFormat,
|
|
668
|
+
timestamp: new Date().toISOString(),
|
|
669
|
+
}));
|
|
670
|
+
// Pin extraction STORAGE to the effective namespace rather than letting the
|
|
671
|
+
// orchestrator re-derive one from the session key + coding overlay — that
|
|
672
|
+
// re-derivation would have to reparse identity and could miss the overlay
|
|
673
|
+
// (the #1495 drift). Passing writeNamespaceOverride makes the extraction
|
|
674
|
+
// target deterministic and identical to LCM/objective-state (rule 39).
|
|
675
|
+
//
|
|
676
|
+
// Pin WHENEVER namespaces are enabled, not only when writeNamespace differs
|
|
677
|
+
// from the default store (#1505 round 3, codex "Pin default-store extraction
|
|
678
|
+
// writes too"). For an unqualified/no-overlay observe by a principal that
|
|
679
|
+
// HAS a self namespace, writeNamespace is `config.defaultNamespace` but an
|
|
680
|
+
// unpinned `runExtraction` would fall back to
|
|
681
|
+
// `defaultNamespaceForPrincipal(principal)` = the SELF namespace — diverging
|
|
682
|
+
// from where LCM/objective-state/response wrote (`default`). Pinning the
|
|
683
|
+
// resolved writeNamespace forces all side effects onto the one scope-plan
|
|
684
|
+
// namespace. When namespaces are DISABLED the router collapses every
|
|
685
|
+
// namespace to one store, so leaving the override undefined preserves the
|
|
686
|
+
// existing single-store routing byte-for-byte.
|
|
687
|
+
const writeNamespaceOverride =
|
|
688
|
+
resolveNamespaceCapabilities(this.deps.orchestrator.config).namespaces === true
|
|
689
|
+
? writeNamespace
|
|
690
|
+
: undefined;
|
|
691
|
+
// Pin provenance PRINCIPAL to the scope plan's resolved principal (#1505
|
|
692
|
+
// thread 1). The scope plan already applied auth precedence
|
|
693
|
+
// (authenticatedPrincipal/principalOverride > resolvePrincipal(original
|
|
694
|
+
// sessionKey)), so this is the same identity the surface authorized — never
|
|
695
|
+
// a `default` fallback parsed from a prefixed key. Omitted when no principal
|
|
696
|
+
// resolved (namespaces-disabled / unauthenticated single-store), preserving
|
|
697
|
+
// existing behavior.
|
|
698
|
+
const principalOverride =
|
|
699
|
+
typeof scope.principal === "string" && scope.principal.length > 0
|
|
700
|
+
? scope.principal
|
|
701
|
+
: undefined;
|
|
702
|
+
// Fire-and-forget: queue extraction in the background so the HTTP
|
|
703
|
+
// response returns immediately. LCM archival (above) is also
|
|
704
|
+
// enqueue-only; extraction involves LLM calls that can take
|
|
705
|
+
// minutes under load and should not block the caller.
|
|
706
|
+
//
|
|
707
|
+
// Backpressure: the orchestrator's own extraction queue already
|
|
708
|
+
// limits concurrency (one extraction at a time per session via
|
|
709
|
+
// queueBufferedExtraction). Fire-and-forget here just decouples
|
|
710
|
+
// the HTTP response from the queue drain.
|
|
711
|
+
try {
|
|
712
|
+
const extractionPromise = this.deps.orchestrator.ingestReplayBatch(turns, {
|
|
713
|
+
archiveLcm: false,
|
|
714
|
+
writeNamespaceOverride,
|
|
715
|
+
principalOverride,
|
|
716
|
+
});
|
|
717
|
+
extractionPromise.catch((err) => {
|
|
718
|
+
log.error(`access-observe background extraction failed: ${err}`);
|
|
719
|
+
});
|
|
720
|
+
extractionQueued = true;
|
|
721
|
+
} catch (err) {
|
|
722
|
+
// Synchronous enqueue failure (e.g. orchestrator disposed)
|
|
723
|
+
log.error(`access-observe extraction enqueue failed: ${err}`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
log.info(
|
|
728
|
+
`access-observe namespace=${namespace} effectiveNamespace=${writeNamespace} sessionKey=${request.sessionKey} messages=${request.messages.length} lcm=${lcmArchived} extraction=${extractionQueued}`,
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
return {
|
|
732
|
+
accepted: request.messages.length,
|
|
733
|
+
sessionKey: request.sessionKey,
|
|
734
|
+
namespace,
|
|
735
|
+
effectiveNamespace: writeNamespace,
|
|
736
|
+
scopeDebug: {
|
|
737
|
+
principal: scope.principal,
|
|
738
|
+
explicitNamespace: scope.explicitNamespace,
|
|
739
|
+
baseNamespace: scope.baseNamespace,
|
|
740
|
+
writeNamespace: scope.writeNamespace,
|
|
741
|
+
codingOverlayApplied: scope.codingOverlayApplied,
|
|
742
|
+
readNamespaces: scope.readNamespaces,
|
|
743
|
+
scopeProfile: scope.scopeProfile,
|
|
744
|
+
writeLayer: scope.writeLayer,
|
|
745
|
+
layers: scope.layers,
|
|
746
|
+
promotionTargets: scope.promotionTargets,
|
|
747
|
+
},
|
|
748
|
+
lcmArchived,
|
|
749
|
+
extractionQueued,
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
async workTask(request: {
|
|
754
|
+
action: "create" | "get" | "list" | "update" | "transition" | "delete";
|
|
755
|
+
id?: string;
|
|
756
|
+
title?: string;
|
|
757
|
+
description?: string;
|
|
758
|
+
status?: string;
|
|
759
|
+
priority?: string;
|
|
760
|
+
owner?: string;
|
|
761
|
+
assignee?: string;
|
|
762
|
+
projectId?: string;
|
|
763
|
+
tags?: string[];
|
|
764
|
+
dueAt?: string;
|
|
765
|
+
}): Promise<unknown> {
|
|
766
|
+
const STATUSES = new Set(["todo", "in_progress", "blocked", "done", "cancelled"]);
|
|
767
|
+
const PRIORITIES = new Set(["low", "medium", "high"]);
|
|
768
|
+
const asStatus = (v?: string) => (v && STATUSES.has(v) ? v as "todo" | "in_progress" | "blocked" | "done" | "cancelled" : undefined);
|
|
769
|
+
const asPriority = (v?: string) => (v && PRIORITIES.has(v) ? v as "low" | "medium" | "high" : undefined);
|
|
770
|
+
|
|
771
|
+
const storage = new WorkStorage(this.deps.orchestrator.config.memoryDir);
|
|
772
|
+
await storage.ensureDirectories();
|
|
773
|
+
const action = request.action;
|
|
774
|
+
|
|
775
|
+
if (action === "create") {
|
|
776
|
+
if (!request.title?.trim()) throw new EngramAccessInputError("title is required for create");
|
|
777
|
+
const task = await storage.createTask({
|
|
778
|
+
title: request.title,
|
|
779
|
+
description: request.description,
|
|
780
|
+
status: asStatus(request.status),
|
|
781
|
+
priority: asPriority(request.priority),
|
|
782
|
+
owner: request.owner?.trim() || undefined,
|
|
783
|
+
assignee: request.assignee?.trim() || undefined,
|
|
784
|
+
projectId: request.projectId?.trim() || undefined,
|
|
785
|
+
tags: request.tags,
|
|
786
|
+
dueAt: request.dueAt?.trim() || undefined,
|
|
787
|
+
});
|
|
788
|
+
return { action, task };
|
|
789
|
+
}
|
|
790
|
+
if (action === "get") {
|
|
791
|
+
if (!request.id?.trim()) throw new EngramAccessInputError("id is required for get");
|
|
792
|
+
return { action, task: await storage.getTask(request.id) };
|
|
793
|
+
}
|
|
794
|
+
if (action === "list") {
|
|
795
|
+
const tasks = await storage.listTasks({
|
|
796
|
+
status: asStatus(request.status),
|
|
797
|
+
owner: request.owner?.trim() || undefined,
|
|
798
|
+
assignee: request.assignee?.trim() || undefined,
|
|
799
|
+
projectId: request.projectId?.trim() || undefined,
|
|
800
|
+
});
|
|
801
|
+
return { action, count: tasks.length, tasks };
|
|
802
|
+
}
|
|
803
|
+
if (action === "update") {
|
|
804
|
+
if (!request.id?.trim()) throw new EngramAccessInputError("id is required for update");
|
|
805
|
+
const patch: Record<string, unknown> = {};
|
|
806
|
+
if (request.title !== undefined) patch.title = request.title;
|
|
807
|
+
if (request.description !== undefined) patch.description = request.description;
|
|
808
|
+
const st = asStatus(request.status); if (st) patch.status = st;
|
|
809
|
+
const pr = asPriority(request.priority); if (pr) patch.priority = pr;
|
|
810
|
+
if (request.owner !== undefined) patch.owner = request.owner || null;
|
|
811
|
+
if (request.assignee !== undefined) patch.assignee = request.assignee || null;
|
|
812
|
+
if (request.projectId !== undefined) patch.projectId = request.projectId || null;
|
|
813
|
+
if (request.tags) patch.tags = request.tags;
|
|
814
|
+
if (request.dueAt !== undefined) patch.dueAt = request.dueAt || null;
|
|
815
|
+
return { action, task: await storage.updateTask(request.id, patch as any) };
|
|
816
|
+
}
|
|
817
|
+
if (action === "transition") {
|
|
818
|
+
if (!request.id?.trim()) throw new EngramAccessInputError("id is required for transition");
|
|
819
|
+
const st = asStatus(request.status);
|
|
820
|
+
if (!st) throw new EngramAccessInputError("valid status is required for transition");
|
|
821
|
+
return { action, task: await storage.transitionTask(request.id, st) };
|
|
822
|
+
}
|
|
823
|
+
if (action === "delete") {
|
|
824
|
+
if (!request.id?.trim()) throw new EngramAccessInputError("id is required for delete");
|
|
825
|
+
return { action, deleted: await storage.deleteTask(request.id) };
|
|
826
|
+
}
|
|
827
|
+
throw new EngramAccessInputError(`Unsupported work_task action: ${action}`);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
async workProject(request: {
|
|
831
|
+
action: "create" | "get" | "list" | "update" | "delete" | "link_task";
|
|
832
|
+
id?: string;
|
|
833
|
+
name?: string;
|
|
834
|
+
description?: string;
|
|
835
|
+
status?: string;
|
|
836
|
+
owner?: string;
|
|
837
|
+
tags?: string[];
|
|
838
|
+
taskId?: string;
|
|
839
|
+
projectId?: string;
|
|
840
|
+
}): Promise<unknown> {
|
|
841
|
+
const STATUSES = new Set(["active", "on_hold", "completed", "archived"]);
|
|
842
|
+
const asStatus = (v?: string) => (v && STATUSES.has(v) ? v as "active" | "on_hold" | "completed" | "archived" : undefined);
|
|
843
|
+
|
|
844
|
+
const storage = new WorkStorage(this.deps.orchestrator.config.memoryDir);
|
|
845
|
+
await storage.ensureDirectories();
|
|
846
|
+
const action = request.action;
|
|
847
|
+
|
|
848
|
+
if (action === "create") {
|
|
849
|
+
if (!request.name?.trim()) throw new EngramAccessInputError("name is required for create");
|
|
850
|
+
const project = await storage.createProject({
|
|
851
|
+
name: request.name,
|
|
852
|
+
description: request.description,
|
|
853
|
+
status: asStatus(request.status),
|
|
854
|
+
owner: request.owner?.trim() || undefined,
|
|
855
|
+
tags: request.tags,
|
|
856
|
+
});
|
|
857
|
+
return { action, project };
|
|
858
|
+
}
|
|
859
|
+
if (action === "get") {
|
|
860
|
+
if (!request.id?.trim()) throw new EngramAccessInputError("id is required for get");
|
|
861
|
+
return { action, project: await storage.getProject(request.id) };
|
|
862
|
+
}
|
|
863
|
+
if (action === "list") {
|
|
864
|
+
const projects = await storage.listProjects();
|
|
865
|
+
return { action, count: projects.length, projects };
|
|
866
|
+
}
|
|
867
|
+
if (action === "update") {
|
|
868
|
+
if (!request.id?.trim()) throw new EngramAccessInputError("id is required for update");
|
|
869
|
+
const patch: Record<string, unknown> = {};
|
|
870
|
+
if (request.name !== undefined) patch.name = request.name;
|
|
871
|
+
if (request.description !== undefined) patch.description = request.description;
|
|
872
|
+
const st = asStatus(request.status); if (st) patch.status = st;
|
|
873
|
+
if (request.owner !== undefined) patch.owner = request.owner || null;
|
|
874
|
+
if (request.tags) patch.tags = request.tags;
|
|
875
|
+
return { action, project: await storage.updateProject(request.id, patch as any) };
|
|
876
|
+
}
|
|
877
|
+
if (action === "delete") {
|
|
878
|
+
if (!request.id?.trim()) throw new EngramAccessInputError("id is required for delete");
|
|
879
|
+
return { action, deleted: await storage.deleteProject(request.id) };
|
|
880
|
+
}
|
|
881
|
+
if (action === "link_task") {
|
|
882
|
+
if (!request.taskId?.trim() || !request.projectId?.trim()) {
|
|
883
|
+
throw new EngramAccessInputError("taskId and projectId are required for link_task");
|
|
884
|
+
}
|
|
885
|
+
return { action, linked: await storage.linkTaskToProject(request.taskId, request.projectId) };
|
|
886
|
+
}
|
|
887
|
+
throw new EngramAccessInputError(`Unsupported work_project action: ${action}`);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
async workBoard(request: {
|
|
891
|
+
action: "export_markdown" | "export_snapshot" | "import_snapshot";
|
|
892
|
+
projectId?: string;
|
|
893
|
+
snapshotJson?: string;
|
|
894
|
+
linkToMemory?: boolean;
|
|
895
|
+
}): Promise<unknown> {
|
|
896
|
+
const memoryDir = this.deps.orchestrator.config.memoryDir;
|
|
897
|
+
await new WorkStorage(memoryDir).ensureDirectories();
|
|
898
|
+
const action = request.action;
|
|
899
|
+
const projectId = request.projectId?.trim() || undefined;
|
|
900
|
+
|
|
901
|
+
if (action === "export_markdown") {
|
|
902
|
+
const markdown = await exportWorkBoardMarkdown({ memoryDir, projectId });
|
|
903
|
+
return { action, markdown: wrapWorkLayerContext(markdown, { linkToMemory: request.linkToMemory === true }) };
|
|
904
|
+
}
|
|
905
|
+
if (action === "export_snapshot") {
|
|
906
|
+
const snapshot = await exportWorkBoardSnapshot({ memoryDir, projectId });
|
|
907
|
+
return { action, snapshot };
|
|
908
|
+
}
|
|
909
|
+
if (action === "import_snapshot") {
|
|
910
|
+
if (!request.snapshotJson?.trim()) throw new EngramAccessInputError("snapshotJson is required for import_snapshot");
|
|
911
|
+
const snapshot = JSON.parse(request.snapshotJson);
|
|
912
|
+
const result = await importWorkBoardSnapshot({ memoryDir, snapshot, projectId });
|
|
913
|
+
return { action, result };
|
|
914
|
+
}
|
|
915
|
+
throw new EngramAccessInputError(`Unsupported work_board action: ${action}`);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
async memoryActionApply(request: {
|
|
919
|
+
action: string;
|
|
920
|
+
outcome?: string;
|
|
921
|
+
reason?: string;
|
|
922
|
+
memoryId?: string;
|
|
923
|
+
namespace?: string;
|
|
924
|
+
principal?: string;
|
|
925
|
+
sessionKey?: string;
|
|
926
|
+
content?: string;
|
|
927
|
+
category?: string;
|
|
928
|
+
linkTargetId?: string;
|
|
929
|
+
linkType?: string;
|
|
930
|
+
linkStrength?: number;
|
|
931
|
+
artifactType?: string;
|
|
932
|
+
execute?: boolean;
|
|
933
|
+
sourcePrompt?: string;
|
|
934
|
+
dryRun?: boolean;
|
|
935
|
+
}): Promise<unknown> {
|
|
936
|
+
const actionTypes = new Set<MemoryActionType>([
|
|
937
|
+
"store_episode",
|
|
938
|
+
"store_note",
|
|
939
|
+
"update_note",
|
|
940
|
+
"create_artifact",
|
|
941
|
+
"summarize_node",
|
|
942
|
+
"discard",
|
|
943
|
+
"link_graph",
|
|
944
|
+
]);
|
|
945
|
+
if (!actionTypes.has(request.action as MemoryActionType)) {
|
|
946
|
+
throw new EngramAccessInputError(
|
|
947
|
+
`memory_action_apply: invalid action ${JSON.stringify(request.action)}`,
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
if (resolveCompressionCapabilities(this.deps.orchestrator.config).contextCompressionActions !== true) {
|
|
952
|
+
throw new EngramAccessInputError(
|
|
953
|
+
"memory_action_apply is disabled; enable contextCompressionActionsEnabled to use this tool",
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const outcome = request.outcome ?? "skipped";
|
|
958
|
+
if (outcome !== "applied" && outcome !== "skipped" && outcome !== "failed") {
|
|
959
|
+
throw new EngramAccessInputError(
|
|
960
|
+
`memory_action_apply: outcome must be "applied", "skipped", or "failed"; got ${JSON.stringify(outcome)}`,
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const resolvedNs = this.deps.writableNamespaceFor(
|
|
965
|
+
request.namespace,
|
|
966
|
+
request.sessionKey,
|
|
967
|
+
request.principal,
|
|
968
|
+
);
|
|
969
|
+
const inputSummaryParts = [
|
|
970
|
+
request.content,
|
|
971
|
+
request.category ? `category=${request.category}` : undefined,
|
|
972
|
+
request.linkTargetId ? `linkTargetId=${request.linkTargetId}` : undefined,
|
|
973
|
+
request.linkType ? `linkType=${request.linkType}` : undefined,
|
|
974
|
+
typeof request.linkStrength === "number"
|
|
975
|
+
? `linkStrength=${request.linkStrength}`
|
|
976
|
+
: undefined,
|
|
977
|
+
request.artifactType ? `artifactType=${request.artifactType}` : undefined,
|
|
978
|
+
typeof request.execute === "boolean" ? `execute=${request.execute}` : undefined,
|
|
979
|
+
].filter((part): part is string => typeof part === "string" && part.length > 0);
|
|
980
|
+
|
|
981
|
+
const event = {
|
|
982
|
+
action: request.action as MemoryActionType,
|
|
983
|
+
outcome: outcome as MemoryActionOutcome,
|
|
984
|
+
namespace: resolvedNs,
|
|
985
|
+
actor: "access.memory_action_apply",
|
|
986
|
+
subsystem: "access.memory_action_apply",
|
|
987
|
+
reason: request.reason,
|
|
988
|
+
memoryId: request.memoryId,
|
|
989
|
+
sourceSessionKey: request.sessionKey,
|
|
990
|
+
inputSummary: inputSummaryParts.length > 0
|
|
991
|
+
? inputSummaryParts.join(" | ").slice(0, 500)
|
|
992
|
+
: undefined,
|
|
993
|
+
dryRun: request.dryRun === true,
|
|
994
|
+
promptHash:
|
|
995
|
+
typeof request.sourcePrompt === "string" && request.sourcePrompt.length > 0
|
|
996
|
+
? createHash("sha256").update(request.sourcePrompt).digest("hex")
|
|
997
|
+
: undefined,
|
|
998
|
+
};
|
|
999
|
+
const preview = this.deps.orchestrator.previewMemoryActionEvent(event);
|
|
1000
|
+
if (request.dryRun === true) {
|
|
1001
|
+
return { recorded: false, dryRun: true, event: preview };
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const recorded = await this.deps.orchestrator.appendMemoryActionEvent(event);
|
|
1005
|
+
return { recorded, event: preview };
|
|
1006
|
+
}
|
|
1007
|
+
}
|