@remnic/core 9.3.755 → 9.3.756

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.
Files changed (181) hide show
  1. package/dist/access-admin-ops-surface.d.ts +1 -1
  2. package/dist/access-admin-ops-surface.js +34 -34
  3. package/dist/access-boundary.d.ts +1 -1
  4. package/dist/access-boundary.js +35 -35
  5. package/dist/access-cli.js +83 -83
  6. package/dist/access-http.d.ts +1 -1
  7. package/dist/access-http.js +44 -44
  8. package/dist/access-lcm-surface.d.ts +1 -1
  9. package/dist/access-lcm-surface.js +34 -34
  10. package/dist/access-mcp.d.ts +1 -1
  11. package/dist/access-mcp.js +41 -41
  12. package/dist/access-observe-write-surface.d.ts +225 -0
  13. package/dist/access-observe-write-surface.js +128 -0
  14. package/dist/access-operations-batch.js +38 -38
  15. package/dist/access-operations.d.ts +1 -1
  16. package/dist/access-operations.js +40 -40
  17. package/dist/access-recall-surface.d.ts +1 -1
  18. package/dist/access-recall-surface.js +34 -34
  19. package/dist/access-schema.d.ts +2 -2
  20. package/dist/access-schema.js +3 -3
  21. package/dist/{access-service-C3jkLi2U.d.ts → access-service-CJwcDoQp.d.ts} +4 -5
  22. package/dist/access-service.d.ts +1 -1
  23. package/dist/access-service.js +34 -34
  24. package/dist/access-surface-catalog.d.ts +1 -1
  25. package/dist/active-recall.js +1 -1
  26. package/dist/adapters/index.js +4 -4
  27. package/dist/adapters/registry.js +2 -2
  28. package/dist/{auto-sync-G4YKBPOU.js → auto-sync-RZO5ZPHR.js} +3 -3
  29. package/dist/briefing.js +7 -7
  30. package/dist/{capsule-crypto-CZJSLEFG.js → capsule-crypto-FFWJ4X4A.js} +2 -2
  31. package/dist/causal-behavior.js +2 -2
  32. package/dist/causal-chain.js +2 -2
  33. package/dist/causal-consolidation.js +15 -15
  34. package/dist/causal-retrieval.js +2 -2
  35. package/dist/causal-trajectory.js +1 -1
  36. package/dist/{chunk-M5QKGHCR.js → chunk-3I2IASNK.js} +2 -2
  37. package/dist/{chunk-OCFITZRV.js → chunk-4AC57RCM.js} +4 -4
  38. package/dist/{chunk-3DOD6ICJ.js → chunk-4I45UZ4V.js} +5 -5
  39. package/dist/{chunk-TAPSNIMA.js → chunk-5CEJH5ZN.js} +4 -4
  40. package/dist/{chunk-I6IAJCJS.js → chunk-5VWRYFQE.js} +7 -7
  41. package/dist/{chunk-4LQUYBZM.js → chunk-5ZKT7WTR.js} +7 -7
  42. package/dist/{chunk-VL5QHTJI.js → chunk-6DA5BBLS.js} +117 -117
  43. package/dist/{chunk-LFKEFIJA.js → chunk-6DXXIXED.js} +4 -4
  44. package/dist/{chunk-XBM5DG4O.js → chunk-7APVFQ34.js} +680 -595
  45. package/dist/chunk-7APVFQ34.js.map +1 -0
  46. package/dist/{chunk-NCNOYM6F.js → chunk-7CFGXWYX.js} +2 -2
  47. package/dist/{chunk-TJATYB3F.js → chunk-A6XCHI53.js} +5 -5
  48. package/dist/{chunk-UQJ6YTKR.js → chunk-BTH6MQJT.js} +7 -7
  49. package/dist/{chunk-G7LCG56O.js → chunk-CXW6Y7SI.js} +4 -4
  50. package/dist/{chunk-GCEV6SXP.js → chunk-DWDDQLSZ.js} +2298 -2218
  51. package/dist/chunk-DWDDQLSZ.js.map +1 -0
  52. package/dist/{chunk-DS4QM6I7.js → chunk-EEKDRAWX.js} +4 -4
  53. package/dist/{chunk-I3UWEZTK.js → chunk-EF3VFQC4.js} +2 -2
  54. package/dist/{chunk-437S3G37.js → chunk-EJJQDP3H.js} +1 -1
  55. package/dist/{chunk-FQLM6ZUP.js → chunk-EVVLGYHR.js} +3 -3
  56. package/dist/{chunk-GGYUR4M2.js → chunk-GBXP62CL.js} +10 -10
  57. package/dist/{chunk-N4AGSRVA.js → chunk-GG6G2RYR.js} +9 -9
  58. package/dist/{chunk-JITAVROY.js → chunk-H2T4OJNI.js} +4 -4
  59. package/dist/{chunk-2MR3MFQB.js → chunk-JTYM4HPA.js} +2 -2
  60. package/dist/{chunk-LD53WPMU.js → chunk-LQ6JI4VH.js} +4 -4
  61. package/dist/{chunk-GPFXPC2V.js → chunk-NG2NHB4C.js} +7 -7
  62. package/dist/{chunk-CIDGUFAL.js → chunk-NLXKSM46.js} +2 -2
  63. package/dist/{chunk-T6WHI3JW.js → chunk-OLAVQS6S.js} +6 -6
  64. package/dist/{chunk-ER3CCWK2.js → chunk-ORAK3LNZ.js} +2 -2
  65. package/dist/{chunk-PJ7DIEFC.js → chunk-PBZQVVLG.js} +5 -5
  66. package/dist/{chunk-PJQMISPD.js → chunk-PPAEJXVF.js} +5 -5
  67. package/dist/{chunk-GMWKWA6N.js → chunk-PRX77NPC.js} +5 -5
  68. package/dist/{chunk-24463PIV.js → chunk-PS572XYO.js} +2 -2
  69. package/dist/{chunk-KKOCFUI5.js → chunk-REZ4TUD2.js} +10 -10
  70. package/dist/{chunk-2QSZNTDO.js → chunk-RKNJBZ55.js} +4 -4
  71. package/dist/{chunk-V5UV7BOA.js → chunk-SQNED75R.js} +1 -1
  72. package/dist/{chunk-FLVL4L3L.js → chunk-TCKP4EHY.js} +2 -2
  73. package/dist/{chunk-SP42M3XP.js → chunk-TXV3I2O6.js} +7 -7
  74. package/dist/{chunk-S6UCPBPL.js → chunk-UNV6ZGTS.js} +2 -2
  75. package/dist/{chunk-2BFLDH5F.js → chunk-WEPWY4HB.js} +2 -2
  76. package/dist/{chunk-YQGHW5ML.js → chunk-WRFKZEO6.js} +4 -4
  77. package/dist/{chunk-XCXN3D2U.js → chunk-Y4TAFMUE.js} +6 -6
  78. package/dist/{chunk-H6S3DJVE.js → chunk-YMGQXBPS.js} +2 -2
  79. package/dist/{chunk-T422SYM6.js → chunk-ZUDM75KG.js} +5 -5
  80. package/dist/{chunk-XD6JVK5P.js → chunk-ZUHD6XZZ.js} +43 -43
  81. package/dist/{chunk-FOJY75BL.js → chunk-ZW5SZ3TH.js} +12 -12
  82. package/dist/{cli-BJQ2ahrh.d.ts → cli-cZyANcmY.d.ts} +1 -1
  83. package/dist/cli.d.ts +2 -2
  84. package/dist/cli.js +59 -59
  85. package/dist/compounding/engine.js +8 -8
  86. package/dist/config.js +1 -1
  87. package/dist/connectors/codex-materialize-runner.js +8 -8
  88. package/dist/connectors/index.js +9 -9
  89. package/dist/contradiction/index.js +2 -2
  90. package/dist/{contradiction-scan-2SODRNUF.js → contradiction-scan-3FJYWS3G.js} +3 -3
  91. package/dist/contradiction-scan-3FJYWS3G.js.map +1 -0
  92. package/dist/conversation-index/backend.js +2 -2
  93. package/dist/entity-retrieval.js +8 -8
  94. package/dist/extraction.js +2 -2
  95. package/dist/index.d.ts +2 -2
  96. package/dist/index.js +126 -126
  97. package/dist/lcm/engine.js +2 -2
  98. package/dist/lcm/index.js +2 -2
  99. package/dist/maintenance/memory-governance.js +8 -8
  100. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -7
  101. package/dist/maintenance/rebuild-memory-projection.js +9 -9
  102. package/dist/mcp-memory-inspector-app.d.ts +1 -1
  103. package/dist/namespaces/migrate.js +18 -18
  104. package/dist/namespaces/search.js +9 -9
  105. package/dist/namespaces/storage.js +9 -9
  106. package/dist/operator-toolkit.js +24 -24
  107. package/dist/orchestration/maintenance.js +12 -12
  108. package/dist/orchestrator.js +82 -82
  109. package/dist/resume-bundles.js +2 -2
  110. package/dist/schemas.d.ts +10 -10
  111. package/dist/search/factory.js +8 -8
  112. package/dist/search/index.js +8 -8
  113. package/dist/search/lancedb-backend.js +2 -2
  114. package/dist/search/meilisearch-backend.js +2 -2
  115. package/dist/search/orama-backend.js +2 -2
  116. package/dist/semantic-consolidation.js +11 -11
  117. package/dist/semantic-rule-promotion.js +7 -7
  118. package/dist/semantic-rule-verifier.js +8 -8
  119. package/dist/shared-context/manager.d.ts +2 -2
  120. package/dist/storage.d.ts +60 -78
  121. package/dist/storage.js +14 -6
  122. package/dist/summarizer.js +2 -2
  123. package/dist/transfer/backup.js +2 -2
  124. package/dist/transfer/capsule-export.js +2 -2
  125. package/dist/transfer/capsule-import.js +2 -2
  126. package/dist/transfer/types.d.ts +12 -12
  127. package/dist/verified-recall.js +8 -8
  128. package/package.json +2 -2
  129. package/src/access-observe-write-surface.ts +1007 -0
  130. package/src/access-service.ts +48 -794
  131. package/src/orchestration/self-deps.ts +9 -1
  132. package/src/storage/entity-store.ts +4 -2
  133. package/src/storage/memory-read-store.ts +756 -0
  134. package/src/storage.ts +67 -627
  135. package/dist/chunk-GCEV6SXP.js.map +0 -1
  136. package/dist/chunk-XBM5DG4O.js.map +0 -1
  137. /package/dist/{capsule-crypto-CZJSLEFG.js.map → access-observe-write-surface.js.map} +0 -0
  138. /package/dist/{auto-sync-G4YKBPOU.js.map → auto-sync-RZO5ZPHR.js.map} +0 -0
  139. /package/dist/{contradiction-scan-2SODRNUF.js.map → capsule-crypto-FFWJ4X4A.js.map} +0 -0
  140. /package/dist/{chunk-M5QKGHCR.js.map → chunk-3I2IASNK.js.map} +0 -0
  141. /package/dist/{chunk-OCFITZRV.js.map → chunk-4AC57RCM.js.map} +0 -0
  142. /package/dist/{chunk-3DOD6ICJ.js.map → chunk-4I45UZ4V.js.map} +0 -0
  143. /package/dist/{chunk-TAPSNIMA.js.map → chunk-5CEJH5ZN.js.map} +0 -0
  144. /package/dist/{chunk-I6IAJCJS.js.map → chunk-5VWRYFQE.js.map} +0 -0
  145. /package/dist/{chunk-4LQUYBZM.js.map → chunk-5ZKT7WTR.js.map} +0 -0
  146. /package/dist/{chunk-VL5QHTJI.js.map → chunk-6DA5BBLS.js.map} +0 -0
  147. /package/dist/{chunk-LFKEFIJA.js.map → chunk-6DXXIXED.js.map} +0 -0
  148. /package/dist/{chunk-NCNOYM6F.js.map → chunk-7CFGXWYX.js.map} +0 -0
  149. /package/dist/{chunk-TJATYB3F.js.map → chunk-A6XCHI53.js.map} +0 -0
  150. /package/dist/{chunk-UQJ6YTKR.js.map → chunk-BTH6MQJT.js.map} +0 -0
  151. /package/dist/{chunk-G7LCG56O.js.map → chunk-CXW6Y7SI.js.map} +0 -0
  152. /package/dist/{chunk-DS4QM6I7.js.map → chunk-EEKDRAWX.js.map} +0 -0
  153. /package/dist/{chunk-I3UWEZTK.js.map → chunk-EF3VFQC4.js.map} +0 -0
  154. /package/dist/{chunk-437S3G37.js.map → chunk-EJJQDP3H.js.map} +0 -0
  155. /package/dist/{chunk-FQLM6ZUP.js.map → chunk-EVVLGYHR.js.map} +0 -0
  156. /package/dist/{chunk-GGYUR4M2.js.map → chunk-GBXP62CL.js.map} +0 -0
  157. /package/dist/{chunk-N4AGSRVA.js.map → chunk-GG6G2RYR.js.map} +0 -0
  158. /package/dist/{chunk-JITAVROY.js.map → chunk-H2T4OJNI.js.map} +0 -0
  159. /package/dist/{chunk-2MR3MFQB.js.map → chunk-JTYM4HPA.js.map} +0 -0
  160. /package/dist/{chunk-LD53WPMU.js.map → chunk-LQ6JI4VH.js.map} +0 -0
  161. /package/dist/{chunk-GPFXPC2V.js.map → chunk-NG2NHB4C.js.map} +0 -0
  162. /package/dist/{chunk-CIDGUFAL.js.map → chunk-NLXKSM46.js.map} +0 -0
  163. /package/dist/{chunk-T6WHI3JW.js.map → chunk-OLAVQS6S.js.map} +0 -0
  164. /package/dist/{chunk-ER3CCWK2.js.map → chunk-ORAK3LNZ.js.map} +0 -0
  165. /package/dist/{chunk-PJ7DIEFC.js.map → chunk-PBZQVVLG.js.map} +0 -0
  166. /package/dist/{chunk-PJQMISPD.js.map → chunk-PPAEJXVF.js.map} +0 -0
  167. /package/dist/{chunk-GMWKWA6N.js.map → chunk-PRX77NPC.js.map} +0 -0
  168. /package/dist/{chunk-24463PIV.js.map → chunk-PS572XYO.js.map} +0 -0
  169. /package/dist/{chunk-KKOCFUI5.js.map → chunk-REZ4TUD2.js.map} +0 -0
  170. /package/dist/{chunk-2QSZNTDO.js.map → chunk-RKNJBZ55.js.map} +0 -0
  171. /package/dist/{chunk-V5UV7BOA.js.map → chunk-SQNED75R.js.map} +0 -0
  172. /package/dist/{chunk-FLVL4L3L.js.map → chunk-TCKP4EHY.js.map} +0 -0
  173. /package/dist/{chunk-SP42M3XP.js.map → chunk-TXV3I2O6.js.map} +0 -0
  174. /package/dist/{chunk-S6UCPBPL.js.map → chunk-UNV6ZGTS.js.map} +0 -0
  175. /package/dist/{chunk-2BFLDH5F.js.map → chunk-WEPWY4HB.js.map} +0 -0
  176. /package/dist/{chunk-YQGHW5ML.js.map → chunk-WRFKZEO6.js.map} +0 -0
  177. /package/dist/{chunk-XCXN3D2U.js.map → chunk-Y4TAFMUE.js.map} +0 -0
  178. /package/dist/{chunk-H6S3DJVE.js.map → chunk-YMGQXBPS.js.map} +0 -0
  179. /package/dist/{chunk-T422SYM6.js.map → chunk-ZUDM75KG.js.map} +0 -0
  180. /package/dist/{chunk-XD6JVK5P.js.map → chunk-ZUHD6XZZ.js.map} +0 -0
  181. /package/dist/{chunk-FOJY75BL.js.map → chunk-ZW5SZ3TH.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
+ }