@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
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-GDB4J2H3.js";
9
9
  import {
10
10
  importCapsule
11
- } from "./chunk-M5QKGHCR.js";
11
+ } from "./chunk-3I2IASNK.js";
12
12
  import {
13
13
  getTrustZoneStoreStatus,
14
14
  isTrustZoneName,
@@ -29,6 +29,9 @@ import {
29
29
  normalizeTags,
30
30
  parseTagMatch
31
31
  } from "./chunk-BT7NVCML.js";
32
+ import {
33
+ decideDisclosureEscalation
34
+ } from "./chunk-H7XKCNR6.js";
32
35
  import {
33
36
  toRecallExplainJson
34
37
  } from "./chunk-2KK5WNQB.js";
@@ -36,14 +39,8 @@ import {
36
39
  estimateRecallTokens
37
40
  } from "./chunk-6MMONPHM.js";
38
41
  import {
39
- decideDisclosureEscalation
40
- } from "./chunk-H7XKCNR6.js";
41
- import {
42
- recordObjectiveStateSnapshotsFromObservedMessages
43
- } from "./chunk-ICP53SKG.js";
44
- import {
45
- objectiveStateStoreOverrideForNamespace
46
- } from "./chunk-6HMYUWXR.js";
42
+ namespaceCollectionName
43
+ } from "./chunk-PS572XYO.js";
47
44
  import {
48
45
  combineNamespaces,
49
46
  defaultGitInvoker,
@@ -57,9 +54,6 @@ import {
57
54
  resolveWritableNamespaceValue,
58
55
  sanitizeFragment
59
56
  } from "./chunk-W3M34XT4.js";
60
- import {
61
- namespaceCollectionName
62
- } from "./chunk-24463PIV.js";
63
57
  import {
64
58
  namespaceIdentityFromToken
65
59
  } from "./chunk-ZFXCQPNO.js";
@@ -70,13 +64,24 @@ import {
70
64
  listMemoryGovernanceRuns,
71
65
  readMemoryGovernanceRunArtifact,
72
66
  runMemoryGovernance
73
- } from "./chunk-GMWKWA6N.js";
67
+ } from "./chunk-PRX77NPC.js";
68
+ import {
69
+ recordObjectiveStateSnapshotsFromObservedMessages
70
+ } from "./chunk-ICP53SKG.js";
71
+ import {
72
+ objectiveStateStoreOverrideForNamespace
73
+ } from "./chunk-6HMYUWXR.js";
74
74
  import {
75
75
  recordMemoryOutcome
76
76
  } from "./chunk-EIR5VLIH.js";
77
77
  import {
78
78
  buildGraphSnapshot
79
79
  } from "./chunk-TQT3USCG.js";
80
+ import {
81
+ persistExplicitCapture,
82
+ queueExplicitCaptureForReview,
83
+ validateExplicitCaptureInput
84
+ } from "./chunk-K5LLP4CY.js";
80
85
  import {
81
86
  wrapWorkLayerContext
82
87
  } from "./chunk-EI6V5UXY.js";
@@ -87,54 +92,20 @@ import {
87
92
  buildProcedurePersistBody,
88
93
  normalizeProcedureSteps
89
94
  } from "./chunk-QDW3E4RD.js";
90
- import {
91
- persistExplicitCapture,
92
- queueExplicitCaptureForReview,
93
- validateExplicitCaptureInput
94
- } from "./chunk-K5LLP4CY.js";
95
95
  import {
96
96
  CrossNamespaceBudget
97
97
  } from "./chunk-ODPLEWB6.js";
98
- import {
99
- defaultCapsulesDir
100
- } from "./chunk-ZY2MNJR6.js";
101
- import {
102
- filterTrajectoriesByLookbackDays,
103
- readCausalTrajectoryRecords
104
- } from "./chunk-V5UV7BOA.js";
105
- import {
106
- canReadNamespace,
107
- canWriteNamespace,
108
- defaultNamespaceForPrincipal,
109
- recallNamespacesForPrincipal,
110
- resolvePrincipal
111
- } from "./chunk-PE6XR4GG.js";
112
- import {
113
- resolveAccessSetupCapabilities,
114
- resolveCompressionCapabilities,
115
- resolveConsolidationCapabilities,
116
- resolveGraphConstructionCapabilities,
117
- resolveIdentityContinuityCapabilities,
118
- resolveIndexingCapabilities,
119
- resolveMemoryLifecycleCapabilities,
120
- resolveNamespaceCapabilities,
121
- resolveObjectiveStateCapabilities,
122
- resolveQmdCapabilities,
123
- resolveRecallAuxiliaryCapabilities,
124
- resolveRecallEnhancementCapabilities,
125
- resolveSecurityCapabilities
126
- } from "./chunk-VSF7AAGE.js";
127
98
  import {
128
99
  FileCalendarSource,
129
100
  buildBriefing,
130
101
  enqueuePassiveCorrectionNotification,
131
102
  parseBriefingFocus,
132
103
  parseBriefingWindow
133
- } from "./chunk-H6S3DJVE.js";
104
+ } from "./chunk-YMGQXBPS.js";
134
105
  import {
135
106
  parseEntityFile,
136
107
  selfDeps
137
- } from "./chunk-XBM5DG4O.js";
108
+ } from "./chunk-7APVFQ34.js";
138
109
  import {
139
110
  supersessionKeysForFact
140
111
  } from "./chunk-TKZ3AL5V.js";
@@ -148,6 +119,10 @@ import {
148
119
  DEFAULT_RECALL_DISCLOSURE,
149
120
  isRecallDisclosure
150
121
  } from "./chunk-JS2HKKOK.js";
122
+ import {
123
+ inferMemoryStatus,
124
+ toMemoryPathRel
125
+ } from "./chunk-RULE4VG5.js";
151
126
  import {
152
127
  normalizeProjectionPreview,
153
128
  normalizeProjectionTags
@@ -159,15 +134,40 @@ import {
159
134
  displayErrorDetail
160
135
  } from "./chunk-6KYMPV2O.js";
161
136
  import {
162
- inferMemoryStatus,
163
- toMemoryPathRel
164
- } from "./chunk-RULE4VG5.js";
137
+ computeContentHash
138
+ } from "./chunk-W5H6ZRPT.js";
165
139
  import {
166
140
  createVersion
167
141
  } from "./chunk-VF4XKTX3.js";
168
142
  import {
169
- computeContentHash
170
- } from "./chunk-W5H6ZRPT.js";
143
+ defaultCapsulesDir
144
+ } from "./chunk-ZY2MNJR6.js";
145
+ import {
146
+ filterTrajectoriesByLookbackDays,
147
+ readCausalTrajectoryRecords
148
+ } from "./chunk-SQNED75R.js";
149
+ import {
150
+ canReadNamespace,
151
+ canWriteNamespace,
152
+ defaultNamespaceForPrincipal,
153
+ recallNamespacesForPrincipal,
154
+ resolvePrincipal
155
+ } from "./chunk-PE6XR4GG.js";
156
+ import {
157
+ resolveAccessSetupCapabilities,
158
+ resolveCompressionCapabilities,
159
+ resolveConsolidationCapabilities,
160
+ resolveGraphConstructionCapabilities,
161
+ resolveIdentityContinuityCapabilities,
162
+ resolveIndexingCapabilities,
163
+ resolveMemoryLifecycleCapabilities,
164
+ resolveNamespaceCapabilities,
165
+ resolveObjectiveStateCapabilities,
166
+ resolveQmdCapabilities,
167
+ resolveRecallAuxiliaryCapabilities,
168
+ resolveRecallEnhancementCapabilities,
169
+ resolveSecurityCapabilities
170
+ } from "./chunk-VSF7AAGE.js";
171
171
  import {
172
172
  sanitizeMemoryContent
173
173
  } from "./chunk-5GKPFL3S.js";
@@ -182,7 +182,7 @@ import {
182
182
  } from "./chunk-NU3CSQ4H.js";
183
183
  import {
184
184
  exportCapsule
185
- } from "./chunk-FLVL4L3L.js";
185
+ } from "./chunk-TCKP4EHY.js";
186
186
  import {
187
187
  evaluateActionConfidence
188
188
  } from "./chunk-Z2YFLXHU.js";
@@ -208,6 +208,9 @@ import {
208
208
  hashAccessIdempotencyPayload
209
209
  } from "./chunk-VFUEZZBS.js";
210
210
 
211
+ // src/access-observe-write-surface.ts
212
+ import { createHash as createHash6 } from "crypto";
213
+
211
214
  // src/access-service.ts
212
215
  import { stat as stat2 } from "fs/promises";
213
216
  import * as nodeFs2 from "fs/promises";
@@ -4749,1266 +4752,1574 @@ async function promoteMemory(options) {
4749
4752
  // src/access-service.ts
4750
4753
  import * as nodePath3 from "path";
4751
4754
 
4752
- // src/access-admin-ops-surface.ts
4753
- import * as nodeFs from "fs/promises";
4754
- import * as nodePath from "path";
4755
- var AccessAdminOpsSurface = class {
4755
+ // src/access-lcm-surface.ts
4756
+ var AccessLcmSurface = class {
4756
4757
  constructor(deps) {
4757
4758
  this.deps = deps;
4758
4759
  }
4759
4760
  deps;
4760
- async reviewQueue(runId, namespace, principal) {
4761
- const resolvedNamespace = this.deps.resolveReadableNamespace(namespace, principal);
4762
- const storage = await this.deps.orchestrator.getStorage(resolvedNamespace);
4763
- const projected = await storage.getProjectedGovernanceRecord();
4764
- if (projected && (!runId || projected.runId === runId.trim())) {
4765
- const projectedAppliedActions = projected.appliedActionRows.map((row) => ({
4766
- action: row.action,
4767
- memoryId: row.memoryId,
4768
- reasonCode: row.reasonCode,
4769
- beforeStatus: row.beforeStatus,
4770
- afterStatus: row.afterStatus,
4771
- originalPath: row.originalPath,
4772
- currentPath: row.currentPath
4773
- }));
4774
- const projectedProposedActions = await buildProjectedGovernanceProposedActions(storage, projected);
4775
- const projectedArtifact = await (async () => {
4776
- try {
4777
- return await readMemoryGovernanceRunArtifact(storage.dir, projected.runId);
4778
- } catch {
4779
- return null;
4780
- }
4781
- })();
4782
- const metrics = projected.metrics;
4783
- const fallbackTransitionReport = {
4784
- proposed: groupActionsByStatus(projectedProposedActions),
4785
- applied: groupActionsByStatus(projectedAppliedActions)
4761
+ async lcmSearch(request) {
4762
+ if (!request.query || typeof request.query !== "string" || request.query.trim().length === 0) {
4763
+ throw new EngramAccessInputError("query is required and must be a non-empty string");
4764
+ }
4765
+ const principal = this.deps.resolveRequestPrincipal(request.sessionKey, request.authenticatedPrincipal);
4766
+ const hasExplicitNamespace = typeof request.namespace === "string" && request.namespace.trim().length > 0;
4767
+ const profileLcmReadNamespaces = hasExplicitNamespace ? null : this.deps.resolveScopeProfileLcmReadNamespaces(
4768
+ request.sessionKey,
4769
+ request.authenticatedPrincipal
4770
+ );
4771
+ const namespace = hasExplicitNamespace ? this.deps.resolveReadableNamespace(request.namespace, principal) : profileLcmReadNamespaces !== null ? profileLcmReadNamespaces[0] : this.deps.resolveImplicitLcmReadFallbackNamespace(principal);
4772
+ if (!this.deps.orchestrator.lcmEngine || !this.deps.orchestrator.lcmEngine.enabled) {
4773
+ return {
4774
+ query: request.query,
4775
+ namespace: namespace ?? this.deps.orchestrator.config.defaultNamespace,
4776
+ results: [],
4777
+ count: 0,
4778
+ lcmEnabled: false
4786
4779
  };
4787
- const transitionReport = projectedArtifact?.transitionReport ? {
4788
- proposed: hasGroupedGovernanceActions(projectedArtifact.transitionReport.proposed) || projectedProposedActions.length === 0 ? projectedArtifact.transitionReport.proposed : fallbackTransitionReport.proposed,
4789
- applied: hasGroupedGovernanceActions(projectedArtifact.transitionReport.applied) || projectedAppliedActions.length === 0 ? projectedArtifact.transitionReport.applied : fallbackTransitionReport.applied
4790
- } : fallbackTransitionReport;
4791
- const qualityScore = projectedArtifact?.qualityScore ?? metrics?.qualityScore ?? buildQualityScore(metrics?.reviewReasons ?? {
4792
- exact_duplicate: 0,
4793
- semantic_duplicate_candidate: 0,
4794
- disputed_memory: 0,
4795
- speculative_low_confidence: 0,
4796
- archive_candidate: 0,
4797
- explicit_capture_review: 0,
4798
- malformed_import: 0
4799
- });
4800
- const effectiveMetrics = metrics ? { ...metrics, qualityScore: metrics.qualityScore ?? qualityScore } : metrics;
4780
+ }
4781
+ if (profileLcmReadNamespaces !== null && profileLcmReadNamespaces.length === 0) {
4801
4782
  return {
4802
- found: true,
4803
- namespace: resolvedNamespace,
4804
- runId: projected.runId,
4805
- summary: projected.summary,
4806
- metrics: effectiveMetrics,
4807
- qualityScore,
4808
- reviewQueue: projected.reviewQueueRows.map((row) => ({
4809
- entryId: row.entryId,
4810
- memoryId: row.memoryId,
4811
- path: row.path,
4812
- reasonCode: row.reasonCode,
4813
- severity: row.severity,
4814
- suggestedAction: row.suggestedAction,
4815
- suggestedStatus: row.suggestedStatus,
4816
- relatedMemoryIds: row.relatedMemoryIds
4817
- })),
4818
- appliedActions: projectedAppliedActions,
4819
- transitionReport,
4820
- report: projected.report
4783
+ query: request.query,
4784
+ namespace: this.deps.orchestrator.config.defaultNamespace,
4785
+ results: [],
4786
+ count: 0,
4787
+ lcmEnabled: true
4821
4788
  };
4822
4789
  }
4823
- const resolvedRunId = runId?.trim() || (await listMemoryGovernanceRuns(storage.dir))[0];
4824
- if (!resolvedRunId) return { found: false, namespace: resolvedNamespace };
4825
- const artifact = await readMemoryGovernanceRunArtifact(storage.dir, resolvedRunId);
4826
- return {
4827
- found: true,
4828
- namespace: resolvedNamespace,
4829
- runId: resolvedRunId,
4830
- summary: artifact.summary,
4831
- metrics: artifact.metrics,
4832
- qualityScore: artifact.qualityScore,
4833
- reviewQueue: artifact.reviewQueue,
4834
- appliedActions: artifact.appliedActions,
4835
- transitionReport: artifact.transitionReport,
4836
- report: artifact.report
4837
- };
4838
- }
4839
- async governanceRun(request, principal) {
4840
- const deepSleep = this.deps.orchestrator.config.dreamsPhases.deepSleep;
4841
- if (deepSleep.enabled === false && deepSleep.enabledExplicitlySet === true) {
4842
- throw new Error("memory governance is disabled by dreams.phases.deepSleep.enabled=false");
4790
+ if (namespace === void 0) {
4791
+ return {
4792
+ query: request.query,
4793
+ namespace: this.deps.orchestrator.config.defaultNamespace,
4794
+ results: [],
4795
+ count: 0,
4796
+ lcmEnabled: true
4797
+ };
4843
4798
  }
4844
- const resolvedNamespace = this.deps.writableNamespaceFor(
4799
+ const limit = Math.max(1, Math.min(request.limit ?? 10, 100));
4800
+ const lcmReadNamespace = profileLcmReadNamespaces !== null ? profileLcmReadNamespaces[0] ?? this.deps.orchestrator.config.defaultNamespace : this.deps.resolveLcmReadNamespace(
4845
4801
  request.namespace,
4846
- void 0,
4847
- request.authenticatedPrincipal ?? principal
4802
+ namespace,
4803
+ request.sessionKey,
4804
+ request.authenticatedPrincipal
4848
4805
  );
4849
- const storage = await this.deps.orchestrator.getStorage(resolvedNamespace);
4850
- const mode = request.mode === "apply" ? "apply" : "shadow";
4851
- const boundedBatchSize = typeof request.batchSize === "number" && Number.isFinite(request.batchSize) ? Math.max(1, Math.floor(request.batchSize)) : void 0;
4852
- const result = await runMemoryGovernance({
4853
- memoryDir: storage.dir,
4854
- mode,
4855
- recentDays: typeof request.recentDays === "number" && Number.isFinite(request.recentDays) ? Math.max(1, Math.floor(request.recentDays)) : void 0,
4856
- maxMemories: typeof request.maxMemories === "number" && Number.isFinite(request.maxMemories) ? Math.max(1, Math.floor(request.maxMemories)) : void 0,
4857
- batchSize: boundedBatchSize
4858
- });
4859
- if (mode === "apply") {
4860
- try {
4861
- await this.deps.orchestrator.processEntitySynthesisQueue(
4862
- resolvedNamespace,
4863
- Math.min(boundedBatchSize ?? 5, 5)
4864
- );
4865
- } catch (error) {
4866
- log.debug(`governanceRun: entity synthesis refresh failed after governance apply: ${error}`);
4867
- }
4868
- }
4869
- return {
4870
- namespace: resolvedNamespace,
4871
- runId: result.runId,
4872
- traceId: result.traceId,
4873
- mode: result.mode,
4874
- reviewQueueCount: result.reviewQueue.length,
4875
- proposedActionCount: result.proposedActions.length,
4876
- appliedActionCount: result.appliedActions.length,
4877
- summaryPath: result.summaryPath,
4878
- reportPath: result.reportPath
4879
- };
4880
- }
4881
- async conversationIndexUpdate(request = {}) {
4882
- if (!resolveIndexingCapabilities(this.deps.orchestrator.config).conversationIndex) {
4883
- return {
4884
- enabled: false,
4885
- sessions: 0,
4886
- chunks: 0,
4887
- skipped: 0,
4888
- skippedSessionKeys: [],
4889
- embeddedRuns: 0,
4890
- reason: "disabled"
4891
- };
4892
- }
4893
- const hours = typeof request.hours === "number" && Number.isFinite(request.hours) ? Math.max(1, Math.floor(request.hours)) : 24;
4894
- let sessionKey;
4895
- if (request.sessionKey !== void 0) {
4896
- if (typeof request.sessionKey !== "string" || request.sessionKey.trim().length === 0) {
4897
- throw new EngramAccessInputError("sessionKey must be a non-empty string when provided");
4898
- }
4899
- sessionKey = request.sessionKey.trim();
4900
- }
4901
- if (sessionKey) {
4902
- const result = await this.deps.orchestrator.updateConversationIndex(
4903
- sessionKey,
4904
- hours,
4905
- { embed: request.embed }
4906
- );
4806
+ const lcmSessionKeyIds = request.sessionKey ? profileLcmReadNamespaces !== null ? this.deps.lcmSessionIdsForNamespaces(
4807
+ profileLcmReadNamespaces,
4808
+ request.sessionKey
4809
+ ) : this.deps.resolveLcmReadSessionIds(
4810
+ request.namespace,
4811
+ namespace,
4812
+ request.sessionKey,
4813
+ request.authenticatedPrincipal
4814
+ ) : [void 0];
4815
+ const lcmSessionPrefixes = request.sessionPrefix ? profileLcmReadNamespaces !== null && !request.sessionKey ? this.deps.lcmSessionIdsForNamespaces(
4816
+ profileLcmReadNamespaces,
4817
+ request.sessionPrefix
4818
+ ) : [
4819
+ lcmSessionKeyForNamespace(
4820
+ lcmReadNamespace,
4821
+ request.sessionPrefix,
4822
+ this.deps.orchestrator.config.defaultNamespace
4823
+ ) ?? request.sessionPrefix
4824
+ ] : [void 0];
4825
+ const hasScopedSession = typeof request.sessionKey === "string" && request.sessionKey.length > 0 || lcmSessionPrefixes.some((prefix) => typeof prefix === "string" && prefix.length > 0);
4826
+ if (!hasScopedSession && resolveNamespaceCapabilities(this.deps.orchestrator.config).namespaces === true) {
4907
4827
  return {
4908
- enabled: true,
4909
- sessionKey,
4910
- sessions: 1,
4911
- chunks: result.chunks,
4912
- skipped: result.skipped ? 1 : 0,
4913
- skippedSessionKeys: result.skipped ? [sessionKey] : [],
4914
- embeddedRuns: result.embedded ? 1 : 0,
4915
- reason: result.reason,
4916
- retryAfterMs: result.retryAfterMs
4828
+ query: request.query,
4829
+ namespace,
4830
+ results: [],
4831
+ count: 0,
4832
+ lcmEnabled: true
4917
4833
  };
4918
4834
  }
4919
- const sessionKeys = await this.deps.orchestrator.transcript.listSessionKeys();
4920
- let chunks = 0;
4921
- let skipped = 0;
4922
- const skippedSessionKeys = [];
4923
- let embeddedRuns = 0;
4924
- for (const sessionKey2 of sessionKeys) {
4925
- const result = await this.deps.orchestrator.updateConversationIndex(
4926
- sessionKey2,
4927
- hours,
4928
- { embed: request.embed }
4929
- );
4930
- chunks += result.chunks;
4931
- if (result.skipped) {
4932
- skipped += 1;
4933
- skippedSessionKeys.push(sessionKey2);
4934
- }
4935
- if (result.embedded) {
4936
- embeddedRuns += 1;
4835
+ const seenRows = /* @__PURE__ */ new Set();
4836
+ const results = [];
4837
+ const lcmSearches = [];
4838
+ for (const lcmSessionKey of lcmSessionKeyIds) {
4839
+ for (const lcmSessionPrefix of lcmSessionPrefixes) {
4840
+ lcmSearches.push({
4841
+ key: lcmSessionKey,
4842
+ prefix: lcmSessionPrefix,
4843
+ promise: this.deps.orchestrator.lcmEngine.searchContextFull(
4844
+ request.query,
4845
+ limit,
4846
+ lcmSessionKey,
4847
+ lcmSessionPrefix
4848
+ )
4849
+ });
4937
4850
  }
4938
4851
  }
4939
- return {
4940
- enabled: true,
4941
- sessions: sessionKeys.length,
4942
- chunks,
4943
- skipped,
4944
- skippedSessionKeys,
4945
- embeddedRuns
4946
- };
4947
- }
4948
- async profilingReport(request = {}) {
4949
- const profiler = this.deps.orchestrator.profiler;
4950
- if (!profiler.isEnabled) {
4951
- return {
4952
- enabled: false,
4953
- reason: "disabled",
4954
- message: "Profiling is disabled. Set profilingEnabled: true in your plugin config to enable."
4955
- };
4956
- }
4957
- const format = request.format ?? "ascii";
4958
- if (format !== "ascii" && format !== "json") {
4959
- throw new EngramAccessInputError("format must be one of: ascii, json");
4960
- }
4961
- const limit = request.limit ?? 5;
4962
- if (!Number.isInteger(limit) || limit < 1 || limit > 20) {
4963
- throw new EngramAccessInputError("limit must be an integer between 1 and 20");
4964
- }
4965
- const traces = profiler.getRecentTraces(limit);
4966
- const stats = profiler.getStats();
4967
- const bottleneck = profiler.identifyBottleneck();
4968
- if (format === "json") {
4969
- return {
4970
- enabled: true,
4971
- format,
4972
- traces,
4973
- stats,
4974
- bottleneck
4975
- };
4976
- }
4977
- const lines = [];
4978
- lines.push("Engram Profiling Report");
4979
- lines.push("=".repeat(60));
4980
- lines.push("");
4981
- const allBuckets = [
4982
- ["byKind", stats.byKind],
4983
- ["bySpan", stats.bySpan]
4984
- ];
4985
- const hasStats = allBuckets.some(([, entries]) => Object.keys(entries).length > 0);
4986
- if (hasStats) {
4987
- lines.push("Aggregate Stats (all retained traces):");
4988
- for (const [bucket, entries] of allBuckets) {
4989
- for (const [key, summary] of Object.entries(entries)) {
4990
- lines.push(
4991
- ` ${bucket}/${key}: avg=${summary.avgMs}ms p50=${summary.p50Ms}ms p95=${summary.p95Ms}ms max=${summary.maxMs}ms (n=${summary.count})`
4992
- );
4993
- }
4852
+ const settledSearches = await Promise.allSettled(
4853
+ lcmSearches.map((search) => search.promise)
4854
+ );
4855
+ for (let i = 0; i < settledSearches.length; i += 1) {
4856
+ if (results.length >= limit) break;
4857
+ const settled = settledSearches[i];
4858
+ if (!settled || settled.status === "rejected") {
4859
+ const failed = lcmSearches[i];
4860
+ log.warn(
4861
+ `lcmSearch: failed for key=${failed?.key ?? "<none>"} prefix=${failed?.prefix ?? "<none>"}: ${settled?.status === "rejected" ? settled.reason : "missing result"}`
4862
+ );
4863
+ continue;
4994
4864
  }
4995
- lines.push("");
4996
- }
4997
- if (bottleneck) {
4998
- lines.push(`Bottleneck: ${bottleneck}`);
4999
- lines.push("");
5000
- }
5001
- if (traces.length === 0) {
5002
- lines.push("No traces recorded yet. Trigger a recall or extraction to see timing data.");
5003
- } else {
5004
- for (const trace of traces) {
5005
- lines.push(formatProfileTraceAscii(trace));
5006
- lines.push("");
4865
+ for (const r of settled.value) {
4866
+ const dedupeKey = `${r.session_id}\0${r.turn_index}`;
4867
+ if (seenRows.has(dedupeKey)) continue;
4868
+ seenRows.add(dedupeKey);
4869
+ results.push({
4870
+ sessionId: r.session_id,
4871
+ content: r.content,
4872
+ turnIndex: r.turn_index
4873
+ });
4874
+ if (results.length >= limit) break;
5007
4875
  }
5008
4876
  }
5009
4877
  return {
5010
- enabled: true,
5011
- format,
5012
- report: lines.join("\n")
4878
+ query: request.query,
4879
+ namespace,
4880
+ results,
4881
+ count: results.length,
4882
+ lcmEnabled: true
5013
4883
  };
5014
4884
  }
5015
4885
  /**
5016
- * Read-only graph snapshot for the admin pane (issue #691 PR 2/5).
4886
+ * Resolve the effective LCM NAMESPACE a same-session operation must prefix
4887
+ * with (the namespace half of {@link resolveLcmReadSessionKey}). Split out so
4888
+ * `lcmSearch` can apply ONE namespace to BOTH its `sessionKey` and its
4889
+ * `sessionPrefix` — the prefix is a search fragment, not a real session, so its
4890
+ * own coding context can't be looked up; it must inherit the namespace resolved
4891
+ * from the real session (`sessionKeyForOverlay`).
5017
4892
  *
5018
- * Reads adjacency from the JSONL edge store written by `GraphIndex` and
5019
- * resolves node metadata via the namespaced storage manager. Namespace
5020
- * resolution mirrors the read-side path used by `recall` /
5021
- * `procedureStats`, so multi-principal deployments can't leak edges from
5022
- * a peer namespace (CLAUDE.md rule 42).
4893
+ * `purpose` selects the AUTHORIZATION gate applied before honouring the
4894
+ * coding overlay (#1505 round 3 + round 4, codex P2):
4895
+ *
4896
+ * - `"read"` (`lcmSearch` / raw-excerpt recall): the overlay rows are only
4897
+ * visible when the principal SELF base is in the READABLE RECALL SET — the
4898
+ * same gate the orchestrator's `lcmReadNamespaceForSession` and the recall
4899
+ * namespace set use (`recallNamespacesForPrincipal`, gated by both
4900
+ * `defaultRecallNamespaces.includes("self")` AND `canReadNamespace`). A
4901
+ * caller that passed the default read check must NOT receive
4902
+ * `<principal>-project-*` rows the policy never granted (cross-tenant read
4903
+ * leak). When the self base is not readable, keep the just-authorized
4904
+ * namespace (collapses to the raw key on the default store).
4905
+ *
4906
+ * - `"write"` (`lcmCompactionFlush` / `lcmCompactionRecord`): these are
4907
+ * write/maintenance operations on the SAME queue `observe` just wrote, so
4908
+ * the gate must mirror observe's WRITE authorization (`canWriteNamespace`
4909
+ * on the self base), NOT readability. A principal that can WRITE but not
4910
+ * READ its self namespace (or whose `defaultRecallNamespaces` omits `self`)
4911
+ * archived under the overlay key via `observe`; gating compaction by
4912
+ * readability would fall back to the default/raw key and leave that queue
4913
+ * never flushed/recorded (round-4 codex P2). Write-authorized ⇒ overlay
4914
+ * key, matching the observe write key (rule 42 read/write parity; rule 39
4915
+ * identical gates across paths).
5023
4916
  */
5024
- async graphSnapshot(request, authenticatedPrincipal) {
5025
- const namespace = this.deps.resolveReadableNamespace(
5026
- request.namespace,
4917
+ resolveLcmReadNamespace(explicitNamespace, resolvedNamespace, sessionKeyForOverlay, authenticatedPrincipal, purpose = "read") {
4918
+ const hasExplicitNamespace = typeof explicitNamespace === "string" && explicitNamespace.trim().length > 0;
4919
+ if (hasExplicitNamespace) return resolvedNamespace;
4920
+ const principal = this.deps.resolveRequestPrincipal(
4921
+ sessionKeyForOverlay,
5027
4922
  authenticatedPrincipal
5028
4923
  );
5029
- const storage = await this.deps.orchestrator.getStorage(namespace);
5030
- const cfg = this.deps.orchestrator.config;
5031
- const graphCaps = resolveGraphConstructionCapabilities(cfg);
5032
- let namespaceRootReal;
5033
- try {
5034
- namespaceRootReal = await nodeFs.realpath(storage.dir);
5035
- } catch {
5036
- namespaceRootReal = nodePath.resolve(storage.dir);
5037
- }
5038
- const namespaceRootWithSep = namespaceRootReal.endsWith(nodePath.sep) ? namespaceRootReal : namespaceRootReal + nodePath.sep;
5039
- const loadNode = async (relPath) => {
5040
- if (nodePath.isAbsolute(relPath)) {
5041
- log.warn(
5042
- `graphSnapshot: rejected absolute edge endpoint (len=${relPath.length}) outside namespace root`
5043
- );
5044
- return null;
5045
- }
5046
- const candidate = nodePath.resolve(namespaceRootReal, relPath);
5047
- if (candidate !== namespaceRootReal && !candidate.startsWith(namespaceRootWithSep)) {
5048
- log.warn(
5049
- `graphSnapshot: rejected traversing edge endpoint (len=${relPath.length}) outside namespace root`
5050
- );
5051
- return null;
5052
- }
5053
- let canonical;
5054
- try {
5055
- canonical = await nodeFs.realpath(candidate);
5056
- } catch {
5057
- canonical = candidate;
5058
- }
5059
- if (canonical !== namespaceRootReal && !canonical.startsWith(namespaceRootWithSep)) {
5060
- log.warn(
5061
- `graphSnapshot: rejected symlinked edge endpoint (len=${relPath.length}) that resolved outside namespace root`
5062
- );
5063
- return null;
5064
- }
5065
- const memory = await storage.readMemoryByPath(canonical);
5066
- if (!memory) return null;
5067
- const fm = memory.frontmatter;
5068
- return {
5069
- category: fm.category ?? "unknown",
5070
- label: fm.id ?? nodePath.basename(canonical, nodePath.extname(canonical)),
5071
- updated: fm.updated
5072
- };
5073
- };
5074
- return buildGraphSnapshot({
5075
- memoryDir: namespaceRootReal,
5076
- graphConfig: {
5077
- entityGraph: graphCaps.entityGraph,
5078
- timeGraph: graphCaps.timeGraph,
5079
- causalGraph: graphCaps.causalGraph
5080
- },
5081
- request: {
5082
- limit: request.limit,
5083
- since: request.since,
5084
- focusNodeId: request.focusNodeId,
5085
- categories: request.categories
5086
- },
5087
- loadNode
5088
- });
4924
+ const base = defaultNamespaceForPrincipal(principal, this.deps.orchestrator.config);
4925
+ const overlaid = this.deps.orchestrator.applyCodingNamespaceOverlay(
4926
+ sessionKeyForOverlay,
4927
+ base
4928
+ );
4929
+ if (overlaid === base) return this.deps.orchestrator.config.defaultNamespace;
4930
+ const authorized = purpose === "write" ? canWriteNamespace(principal, base, this.deps.orchestrator.config) : recallNamespacesForPrincipal(
4931
+ principal,
4932
+ this.deps.orchestrator.config
4933
+ ).includes(base);
4934
+ if (authorized) return overlaid;
4935
+ if (purpose === "read") return this.deps.orchestrator.config.defaultNamespace;
4936
+ return resolvedNamespace;
5089
4937
  }
5090
4938
  /**
5091
- * List capsule archives in the namespace-scoped capsule store.
4939
+ * Resolve the namespace the raw-disclosure excerpt lookup
4940
+ * ({@link fetchRawExcerpts}) must prefix its LCM `session_id` with (#1505
4941
+ * thread 2f7). Raw disclosure reads the SAME LCM archive `lcmSearch` and the
4942
+ * in-prompt LCM sections read, so it MUST pass through the identical
4943
+ * read-authorization gate — NOT `snapshot.namespace`, which records the
4944
+ * effective WRITE/overlay namespace (`<principal>-project-*`) even when the
4945
+ * principal can WRITE but not READ its self base (or `defaultRecallNamespaces`
4946
+ * omits `self`). Routing through `resolveLcmReadNamespace(..., "read")` makes
4947
+ * raw disclosure fall back to the default store exactly like normal recall +
4948
+ * `lcmSearch`, so it never attaches overlay transcript rows the read gate
4949
+ * excludes (cross-tenant read leak). Collapses to the default store / raw
4950
+ * sessionKey for single-store / no-overlay / explicit-default flows, so
4951
+ * single-user recall is byte-for-byte unchanged.
5092
4952
  *
5093
- * MCP uses this access-layer method instead of reading arbitrary paths so
5094
- * capsule discovery remains bound to the same namespace ACLs as export and
5095
- * import.
5096
- */
5097
- async capsuleList(options) {
5098
- const resolvedNamespace = this.deps.resolveReadableNamespace(options?.namespace, options?.principal);
5099
- const storage = await this.deps.orchestrator.getStorage(resolvedNamespace);
5100
- const capsulesDir = defaultCapsulesDir(storage.dir);
5101
- let dirEntries;
5102
- try {
5103
- const capsulesDirStat = await nodeFs.lstat(capsulesDir);
5104
- if (capsulesDirStat.isSymbolicLink()) {
5105
- throw new EngramAccessInputError("capsule list failed: capsule store directory must not be a symlink");
5106
- }
5107
- if (!capsulesDirStat.isDirectory()) {
5108
- throw new EngramAccessInputError("capsule list failed: capsule store path must be a directory");
5109
- }
5110
- dirEntries = await nodeFs.readdir(capsulesDir, { withFileTypes: true });
5111
- } catch (err) {
5112
- const code = typeof err === "object" && err !== null && "code" in err ? err.code : void 0;
5113
- if (code === "ENOENT") {
5114
- return { namespace: resolvedNamespace, capsulesDir, capsules: [] };
5115
- }
5116
- throw err;
5117
- }
5118
- const archiveNames = dirEntries.filter(
5119
- (entry) => entry.isFile() && (entry.name.endsWith(".capsule.json.gz") || entry.name.endsWith(".capsule.json.gz.enc"))
5120
- ).map((entry) => entry.name).sort();
5121
- const capsules = [];
5122
- for (const archiveName of archiveNames) {
5123
- const archivePath = nodePath.join(capsulesDir, archiveName);
5124
- const id = archiveName.replace(/\.capsule\.json\.gz\.enc$/, "").replace(/\.capsule\.json\.gz$/, "");
5125
- const manifestPath = nodePath.join(capsulesDir, `${id}.manifest.json`);
5126
- let createdAt = null;
5127
- let pluginVersion = null;
5128
- let fileCount = null;
5129
- let description = null;
5130
- let manifestPathOrNull = manifestPath;
5131
- try {
5132
- const manifestStat = await nodeFs.lstat(manifestPath);
5133
- if (manifestStat.isSymbolicLink() || !manifestStat.isFile()) {
5134
- capsules.push({
5135
- id,
5136
- archivePath,
5137
- manifestPath: manifestPathOrNull,
5138
- createdAt,
5139
- pluginVersion,
5140
- fileCount,
5141
- description
5142
- });
5143
- continue;
5144
- }
5145
- const raw = await nodeFs.readFile(manifestPath, "utf-8");
5146
- const sidecar = JSON.parse(raw);
5147
- createdAt = typeof sidecar.createdAt === "string" ? sidecar.createdAt : null;
5148
- pluginVersion = typeof sidecar.pluginVersion === "string" ? sidecar.pluginVersion : null;
5149
- fileCount = Array.isArray(sidecar.files) ? sidecar.files.length : null;
5150
- const capsule = sidecar.capsule;
5151
- description = capsule && typeof capsule.description === "string" ? capsule.description : null;
5152
- } catch (err) {
5153
- const code = typeof err === "object" && err !== null && "code" in err ? err.code : void 0;
5154
- if (code === "ENOENT") {
5155
- manifestPathOrNull = null;
5156
- }
5157
- }
5158
- capsules.push({
5159
- id,
5160
- archivePath,
5161
- manifestPath: manifestPathOrNull,
5162
- createdAt,
5163
- pluginVersion,
5164
- fileCount,
5165
- description
5166
- });
5167
- }
5168
- return { namespace: resolvedNamespace, capsulesDir, capsules };
5169
- }
5170
- /**
5171
- * Manually invoke a single Dreams phase pass (PR 4/4).
4953
+ * Returns `undefined` when NO readable LCM namespace exists for an IMPLICIT
4954
+ * (no explicit `namespace`) raw recall i.e. a restrictive `default` READ
4955
+ * policy denies the principal `default` AND no overlay/self namespace is
4956
+ * readable. In that case the caller emits NO excerpts rather than throwing
4957
+ * `namespace is not readable: default` (#1505 thread NBHWz): normal recall
4958
+ * still succeeds via `recallNamespacesForPrincipal`, so `disclosure: "raw"`
4959
+ * must degrade gracefully (empty excerpts), never pre-authorize `default`.
5172
4960
  *
5173
- * Deep-sleep delegates to memory governance (shadow → dry-run, apply → live).
5174
- * Light-sleep and REM scan the observation ledger and memory corpus
5175
- * respectively, returning the same telemetry shape as a scheduled run.
4961
+ * IMPLICIT-namespace fallback selection derives from the ALREADY
4962
+ * read-authorized recall namespace set (`recallNamespacesForPrincipal` +
4963
+ * `canReadNamespace`) the principal's self base when it is in the readable
4964
+ * recall set, else `config.defaultNamespace` ONLY when the principal may read
4965
+ * it. It NEVER pre-authorizes `default`. An EXPLICIT `namespace` is still
4966
+ * authorized strictly via `resolveReadableNamespace` (explicit reads must pass
4967
+ * the ACL — no behavior change).
5176
4968
  */
5177
- async dreamsRun(options) {
5178
- const { runDreamsPhase } = await import("./dreams-ledger-6UODQOX6.js");
5179
- const validPhases = ["lightSleep", "rem", "deepSleep"];
5180
- if (!validPhases.includes(options.phase)) {
5181
- throw new EngramAccessInputError(
5182
- `Invalid phase: ${String(options.phase)}. Must be one of: ${validPhases.join(", ")}`
4969
+ resolveRawExcerptReadNamespace(explicitNamespace, sessionKey, authenticatedPrincipal) {
4970
+ const principal = this.deps.resolveRequestPrincipal(
4971
+ sessionKey,
4972
+ authenticatedPrincipal
4973
+ );
4974
+ const hasExplicitNamespace = typeof explicitNamespace === "string" && explicitNamespace.trim().length > 0;
4975
+ if (hasExplicitNamespace) {
4976
+ const resolvedNamespace = this.deps.resolveReadableNamespace(
4977
+ explicitNamespace,
4978
+ principal
5183
4979
  );
5184
- }
5185
- const deepSleep = this.deps.orchestrator.config.dreamsPhases.deepSleep;
5186
- if (options.phase === "deepSleep" && deepSleep.enabled === false && deepSleep.enabledExplicitlySet === true) {
5187
- throw new EngramAccessInputError(
5188
- "memory governance is disabled by dreams.phases.deepSleep.enabled=false"
4980
+ return this.deps.resolveLcmReadNamespace(
4981
+ explicitNamespace,
4982
+ resolvedNamespace,
4983
+ sessionKey,
4984
+ authenticatedPrincipal,
4985
+ "read"
5189
4986
  );
5190
4987
  }
5191
- const dryRun = options.dryRun === true;
5192
- const resolvedNamespace = this.deps.writableNamespaceFor(
5193
- options.namespace,
5194
- void 0,
5195
- options.authenticatedPrincipal
4988
+ const profileReadNamespaces = this.deps.resolveScopeProfileLcmReadNamespaces(
4989
+ sessionKey,
4990
+ authenticatedPrincipal
5196
4991
  );
5197
- const storage = await this.deps.orchestrator.getStorage(resolvedNamespace);
5198
- const memoryDir = storage.dir;
5199
- const phaseRunner = dryRun || options.phase === "deepSleep" ? void 0 : async (_opts) => {
5200
- if (_opts.phase === "lightSleep") {
5201
- const result3 = await this.deps.orchestrator.runLifecyclePolicyNow(storage);
5202
- return {
5203
- itemsProcessed: result3.memoriesAssessed,
5204
- notes: `scored ${result3.memoriesAssessed} memories`
5205
- };
5206
- }
5207
- const result2 = await this.deps.orchestrator.runSemanticConsolidationNow({
5208
- dryRun: false,
5209
- storage
5210
- });
5211
- const itemsProcessed = result2.clusters.reduce(
5212
- (sum, cluster) => sum + cluster.memories.length,
5213
- 0
5214
- );
5215
- return {
5216
- itemsProcessed,
5217
- notes: `REM consolidation found ${result2.clustersFound} clusters`
5218
- };
5219
- };
5220
- const governanceRunner = options.phase === "deepSleep" ? async (_opts) => {
5221
- return this.deps.orchestrator.runDeepSleepGovernanceNow({
5222
- storage,
5223
- dryRun: _opts.dryRun
5224
- });
5225
- } : void 0;
5226
- const result = await runDreamsPhase(
5227
- { memoryDir, phase: options.phase, dryRun },
5228
- governanceRunner,
5229
- phaseRunner
4992
+ if (profileReadNamespaces !== null) return profileReadNamespaces[0];
4993
+ const fallbackNamespace = this.deps.resolveImplicitLcmReadFallbackNamespace(principal);
4994
+ if (fallbackNamespace === void 0) return void 0;
4995
+ return this.deps.resolveLcmReadNamespace(
4996
+ explicitNamespace,
4997
+ fallbackNamespace,
4998
+ sessionKey,
4999
+ authenticatedPrincipal,
5000
+ "read"
5230
5001
  );
5231
- return {
5232
- phase: result.phase,
5233
- dryRun: result.dryRun,
5234
- durationMs: result.durationMs,
5235
- itemsProcessed: result.itemsProcessed,
5236
- notes: result.notes
5237
- };
5238
5002
  }
5239
- /**
5240
- * Manually promote a memory into one or more authorized targets. Requires
5241
- * a non-empty reason (audit-logged). Reuses the scope-profile promotion
5242
- * resolution and `canWriteNamespace` gate — there is no dashboard-only
5243
- * write path.
5244
- */
5245
- async adminPromoteMemory(request) {
5246
- const config = this.deps.orchestrator.config;
5247
- const namespacesEnabled = resolveNamespaceCapabilities(config).namespaces === true;
5248
- let sourceNamespace;
5249
- if (request.namespace) {
5250
- sourceNamespace = this.deps.resolveReadableNamespace(
5251
- request.namespace,
5252
- request.principal
5253
- );
5254
- } else {
5255
- const codingContext2 = request.sessionKey ? this.deps.orchestrator.getCodingContextForSession(request.sessionKey) ?? null : null;
5256
- const plan = resolveScopePlan({
5257
- config,
5258
- sessionKey: request.sessionKey,
5259
- principalOverride: request.principal,
5260
- codingContext: codingContext2,
5261
- namespacesEnabled
5262
- });
5263
- sourceNamespace = this.deps.resolveReadableNamespace(
5264
- plan.baseNamespace,
5265
- request.principal
5266
- );
5003
+ resolveLcmReadSessionIds(explicitNamespace, resolvedNamespace, sessionKey, authenticatedPrincipal) {
5004
+ const primary = this.deps.resolveLcmReadSessionKey(
5005
+ explicitNamespace,
5006
+ resolvedNamespace,
5007
+ sessionKey,
5008
+ authenticatedPrincipal,
5009
+ "read"
5010
+ );
5011
+ const hasExplicitNamespace = typeof explicitNamespace === "string" && explicitNamespace.trim().length > 0;
5012
+ if (hasExplicitNamespace) return [primary];
5013
+ const profileReadNamespaces = this.deps.resolveScopeProfileLcmReadNamespaces(
5014
+ sessionKey,
5015
+ authenticatedPrincipal
5016
+ );
5017
+ if (profileReadNamespaces !== null) {
5018
+ return this.deps.lcmSessionIdsForNamespaces(profileReadNamespaces, sessionKey);
5267
5019
  }
5268
- const codingContext = request.sessionKey ? this.deps.orchestrator.getCodingContextForSession(request.sessionKey) ?? null : null;
5269
- const codingOverlay = codingContext ? resolveCodingNamespaceOverlay(
5270
- codingContext,
5271
- config.codingMode,
5272
- config.defaultNamespace
5273
- ) : null;
5274
- const scopeProfilePlan = resolveScopeProfilePlan({
5275
- config,
5276
- principal: request.principal,
5277
- codingContext,
5278
- codingOverlay
5279
- });
5280
- const storage = {
5281
- readMemory: async (namespace, memoryId) => {
5282
- const resolved = await this.deps.orchestrator.getStorage(namespace);
5283
- const memory = await resolved.getMemoryById(memoryId);
5284
- if (!memory) return null;
5285
- return {
5286
- category: memory.frontmatter.category,
5287
- content: memory.content,
5288
- frontmatter: memory.frontmatter
5289
- };
5290
- },
5291
- writePromotedMemory: async (namespace, memory) => {
5292
- const resolved = await this.deps.orchestrator.getStorage(namespace);
5293
- const { id, tombstoneBlocked } = await resolved.writeMemory(
5294
- memory.category,
5295
- memory.content,
5296
- {
5297
- confidence: memory.confidence,
5298
- tags: memory.tags,
5299
- entityRef: memory.entityRef,
5300
- source: `admin-promotion:${memory.sourceNamespace}:${memory.reason.slice(0, 120)}`,
5301
- lineage: memory.lineage,
5302
- sourceMemoryId: memory.sourceMemoryId,
5303
- actor: memory.actor,
5304
- validAt: memory.validAt
5305
- }
5306
- );
5307
- if (tombstoneBlocked) {
5308
- throw new Error(
5309
- "target namespace tombstone-blocked the promoted content (pending_review)"
5310
- );
5311
- }
5312
- return id;
5313
- }
5314
- };
5315
- try {
5316
- return await promoteMemory({
5317
- config,
5318
- sourceMemoryId: request.sourceMemoryId,
5319
- sourceNamespace,
5320
- principal: request.principal,
5321
- targets: request.targets,
5322
- reason: request.reason,
5323
- actor: request.principal ?? "admin-console",
5324
- storage,
5325
- scopeProfilePlan
5326
- });
5327
- } catch (err) {
5328
- if (err instanceof AdminPromotionError) {
5329
- throw new EngramAccessInputError(err.message);
5020
+ const principal = this.deps.resolveRequestPrincipal(
5021
+ sessionKey,
5022
+ authenticatedPrincipal
5023
+ );
5024
+ const base = defaultNamespaceForPrincipal(
5025
+ principal,
5026
+ this.deps.orchestrator.config
5027
+ );
5028
+ const overlaid = this.deps.orchestrator.applyCodingNamespaceOverlay(
5029
+ sessionKey,
5030
+ base
5031
+ );
5032
+ if (overlaid === base) return [primary];
5033
+ const selfReadableInRecall = recallNamespacesForPrincipal(
5034
+ principal,
5035
+ this.deps.orchestrator.config
5036
+ ).includes(base);
5037
+ if (!selfReadableInRecall) return [primary];
5038
+ const overlay = resolveCodingNamespaceOverlay(
5039
+ this.deps.orchestrator.getCodingContextForSession(sessionKey),
5040
+ this.deps.orchestrator.config.codingMode,
5041
+ this.deps.orchestrator.config.defaultNamespace
5042
+ );
5043
+ const fallbackNamespaces = (overlay?.readFallbacks ?? []).map(
5044
+ (fallback) => combineNamespaces(base, fallback)
5045
+ );
5046
+ const out = [primary];
5047
+ const seen = /* @__PURE__ */ new Set([primary]);
5048
+ for (const ns of fallbackNamespaces) {
5049
+ const key = lcmSessionKeyForNamespace(
5050
+ ns,
5051
+ sessionKey,
5052
+ this.deps.orchestrator.config.defaultNamespace
5053
+ ) ?? sessionKey;
5054
+ if (!seen.has(key)) {
5055
+ seen.add(key);
5056
+ out.push(key);
5330
5057
  }
5331
- throw err;
5332
5058
  }
5059
+ return out;
5333
5060
  }
5334
5061
  };
5335
5062
 
5336
- // src/access-recall-surface.ts
5337
- import { createHash as createHash4 } from "crypto";
5338
- import * as nodePath2 from "path";
5339
-
5340
- // src/orchestration/qmd-result-resolver.ts
5341
- import path8 from "path";
5342
- function qmdCollectionPathParts(resultPath) {
5343
- if (!resultPath || path8.isAbsolute(resultPath)) return null;
5344
- const normalized = resultPath.replace(/\\/g, "/").replace(/^\/+/, "");
5345
- const slashIndex = normalized.indexOf("/");
5346
- if (slashIndex <= 0 || slashIndex >= normalized.length - 1) return null;
5347
- const collection = normalized.slice(0, slashIndex);
5348
- if (/^\d{4}-\d{2}-\d{2}$/.test(collection)) return null;
5349
- return {
5350
- collection,
5351
- relativePath: normalized.slice(slashIndex + 1)
5352
- };
5353
- }
5354
- function qmdResultPathCandidates(storageDir, resultPath) {
5355
- const candidates = /* @__PURE__ */ new Set();
5356
- const storageRoot = path8.resolve(storageDir);
5357
- const addCandidate = (candidate) => {
5358
- const resolved = path8.resolve(candidate);
5359
- if (isPathInsideStorageRoot(storageRoot, resolved)) {
5360
- candidates.add(resolved);
5361
- }
5362
- };
5363
- const addRelativeCandidates = (relativePath) => {
5364
- const normalized = relativePath.replace(/\\/g, "/").replace(/^\/+/, "");
5365
- if (!normalized) return;
5366
- addCandidate(path8.join(storageRoot, normalized));
5367
- if (/^\d{4}-\d{2}-\d{2}\//.test(normalized)) {
5368
- addCandidate(path8.join(storageRoot, "facts", normalized));
5369
- }
5370
- };
5371
- if (path8.isAbsolute(resultPath)) {
5372
- addCandidate(resultPath);
5373
- } else {
5374
- addRelativeCandidates(resultPath);
5375
- }
5376
- return [...candidates];
5377
- }
5378
- function storageDirOrNull(storage) {
5379
- const dir = storage.dir;
5380
- return typeof dir === "string" && dir.length > 0 ? dir : null;
5381
- }
5382
- var QmdResultResolver = class {
5383
- getConfig;
5384
- storageFor;
5385
- storageDirNamespace;
5386
- qmdCollectionNamespaceFromPrefix;
5387
- namespaceFromPath;
5388
- constructor(options) {
5389
- this.getConfig = options.getConfig;
5390
- this.storageFor = options.storageFor;
5391
- this.storageDirNamespace = options.storageDirNamespace;
5392
- this.qmdCollectionNamespaceFromPrefix = options.qmdCollectionNamespaceFromPrefix;
5393
- this.namespaceFromPath = options.namespaceFromPath;
5063
+ // src/access-admin-ops-surface.ts
5064
+ import * as nodeFs from "fs/promises";
5065
+ import * as nodePath from "path";
5066
+ var AccessAdminOpsSurface = class {
5067
+ constructor(deps) {
5068
+ this.deps = deps;
5394
5069
  }
5395
- async readQmdResultMemory(resultPath, fallbackStorage, recallNamespaces = []) {
5396
- const parts = qmdCollectionPathParts(resultPath);
5397
- const fallbackStorageDir = storageDirOrNull(fallbackStorage);
5398
- const config = this.getConfig();
5399
- const coldCollection = config.qmdColdCollection ?? "openclaw-engram-cold";
5400
- if (parts && parts.collection === coldCollection) {
5401
- const storages = [];
5402
- const seenStorageDirs = /* @__PURE__ */ new Set();
5403
- const addStorage = (storage) => {
5404
- const storageDir = storageDirOrNull(storage);
5405
- const storageKey = storageDir ? path8.resolve(storageDir) : `storage-without-dir-${storages.length}`;
5406
- if (seenStorageDirs.has(storageKey)) return;
5407
- seenStorageDirs.add(storageKey);
5408
- storages.push(storage);
5409
- };
5410
- const fallbackNamespace = fallbackStorageDir !== null ? this.storageDirNamespace(fallbackStorageDir) : config.defaultNamespace;
5411
- if (recallNamespaces.length === 0 || !resolveNamespaceCapabilities(config).namespaces || recallNamespaces.includes(fallbackNamespace)) {
5412
- addStorage(fallbackStorage);
5413
- }
5414
- if (recallNamespaces.length > 0) {
5415
- for (const namespace of recallNamespaces) {
5416
- try {
5417
- addStorage(await this.storageFor(namespace));
5418
- } catch (err) {
5419
- log.debug("qmd cold result namespace storage lookup skipped", {
5420
- path: resultPath,
5421
- namespace,
5422
- error: err.message
5423
- });
5424
- }
5425
- }
5426
- }
5427
- for (const storage of storages) {
5428
- const storageDir = storageDirOrNull(storage);
5429
- if (!storageDir) {
5430
- const memory = await storage.readMemoryByPath(resultPath);
5431
- if (memory) return memory;
5432
- continue;
5433
- }
5070
+ deps;
5071
+ async reviewQueue(runId, namespace, principal) {
5072
+ const resolvedNamespace = this.deps.resolveReadableNamespace(namespace, principal);
5073
+ const storage = await this.deps.orchestrator.getStorage(resolvedNamespace);
5074
+ const projected = await storage.getProjectedGovernanceRecord();
5075
+ if (projected && (!runId || projected.runId === runId.trim())) {
5076
+ const projectedAppliedActions = projected.appliedActionRows.map((row) => ({
5077
+ action: row.action,
5078
+ memoryId: row.memoryId,
5079
+ reasonCode: row.reasonCode,
5080
+ beforeStatus: row.beforeStatus,
5081
+ afterStatus: row.afterStatus,
5082
+ originalPath: row.originalPath,
5083
+ currentPath: row.currentPath
5084
+ }));
5085
+ const projectedProposedActions = await buildProjectedGovernanceProposedActions(storage, projected);
5086
+ const projectedArtifact = await (async () => {
5434
5087
  try {
5435
- const coldRoot = path8.join(storageDir, "cold");
5436
- for (const candidate of qmdResultPathCandidates(
5437
- coldRoot,
5438
- parts.relativePath
5439
- )) {
5440
- const memory = await storage.readMemoryByPath(candidate);
5441
- if (memory) return memory;
5442
- }
5443
- } catch (err) {
5444
- if (err instanceof SecureStoreLockedError) throw err;
5445
- log.debug("qmd cold result path lookup failed open", {
5446
- path: resultPath,
5447
- collection: coldCollection,
5448
- error: err.message
5449
- });
5088
+ return await readMemoryGovernanceRunArtifact(storage.dir, projected.runId);
5089
+ } catch {
5090
+ return null;
5450
5091
  }
5451
- }
5452
- return null;
5092
+ })();
5093
+ const metrics = projected.metrics;
5094
+ const fallbackTransitionReport = {
5095
+ proposed: groupActionsByStatus(projectedProposedActions),
5096
+ applied: groupActionsByStatus(projectedAppliedActions)
5097
+ };
5098
+ const transitionReport = projectedArtifact?.transitionReport ? {
5099
+ proposed: hasGroupedGovernanceActions(projectedArtifact.transitionReport.proposed) || projectedProposedActions.length === 0 ? projectedArtifact.transitionReport.proposed : fallbackTransitionReport.proposed,
5100
+ applied: hasGroupedGovernanceActions(projectedArtifact.transitionReport.applied) || projectedAppliedActions.length === 0 ? projectedArtifact.transitionReport.applied : fallbackTransitionReport.applied
5101
+ } : fallbackTransitionReport;
5102
+ const qualityScore = projectedArtifact?.qualityScore ?? metrics?.qualityScore ?? buildQualityScore(metrics?.reviewReasons ?? {
5103
+ exact_duplicate: 0,
5104
+ semantic_duplicate_candidate: 0,
5105
+ disputed_memory: 0,
5106
+ speculative_low_confidence: 0,
5107
+ archive_candidate: 0,
5108
+ explicit_capture_review: 0,
5109
+ malformed_import: 0
5110
+ });
5111
+ const effectiveMetrics = metrics ? { ...metrics, qualityScore: metrics.qualityScore ?? qualityScore } : metrics;
5112
+ return {
5113
+ found: true,
5114
+ namespace: resolvedNamespace,
5115
+ runId: projected.runId,
5116
+ summary: projected.summary,
5117
+ metrics: effectiveMetrics,
5118
+ qualityScore,
5119
+ reviewQueue: projected.reviewQueueRows.map((row) => ({
5120
+ entryId: row.entryId,
5121
+ memoryId: row.memoryId,
5122
+ path: row.path,
5123
+ reasonCode: row.reasonCode,
5124
+ severity: row.severity,
5125
+ suggestedAction: row.suggestedAction,
5126
+ suggestedStatus: row.suggestedStatus,
5127
+ relatedMemoryIds: row.relatedMemoryIds
5128
+ })),
5129
+ appliedActions: projectedAppliedActions,
5130
+ transitionReport,
5131
+ report: projected.report
5132
+ };
5453
5133
  }
5454
- const collectionNamespace = parts ? this.qmdCollectionNamespaceFromPrefix(parts.collection) : null;
5455
- if (parts && collectionNamespace) {
5456
- try {
5457
- const collectionStorage = await this.storageFor(collectionNamespace);
5458
- for (const candidate of qmdResultPathCandidates(
5459
- collectionStorage.dir,
5460
- parts.relativePath
5461
- )) {
5462
- const memory = await collectionStorage.readMemoryByPath(candidate);
5463
- if (memory) return memory;
5464
- }
5465
- return null;
5466
- } catch (err) {
5467
- if (err instanceof SecureStoreLockedError) throw err;
5468
- log.debug("qmd result namespace path lookup failed open", {
5469
- path: resultPath,
5470
- namespace: collectionNamespace,
5471
- error: err.message
5472
- });
5473
- return null;
5474
- }
5134
+ const resolvedRunId = runId?.trim() || (await listMemoryGovernanceRuns(storage.dir))[0];
5135
+ if (!resolvedRunId) return { found: false, namespace: resolvedNamespace };
5136
+ const artifact = await readMemoryGovernanceRunArtifact(storage.dir, resolvedRunId);
5137
+ return {
5138
+ found: true,
5139
+ namespace: resolvedNamespace,
5140
+ runId: resolvedRunId,
5141
+ summary: artifact.summary,
5142
+ metrics: artifact.metrics,
5143
+ qualityScore: artifact.qualityScore,
5144
+ reviewQueue: artifact.reviewQueue,
5145
+ appliedActions: artifact.appliedActions,
5146
+ transitionReport: artifact.transitionReport,
5147
+ report: artifact.report
5148
+ };
5149
+ }
5150
+ async governanceRun(request, principal) {
5151
+ const deepSleep = this.deps.orchestrator.config.dreamsPhases.deepSleep;
5152
+ if (deepSleep.enabled === false && deepSleep.enabledExplicitlySet === true) {
5153
+ throw new Error("memory governance is disabled by dreams.phases.deepSleep.enabled=false");
5475
5154
  }
5476
- if (path8.isAbsolute(resultPath)) {
5477
- if (!fallbackStorageDir) {
5478
- return await fallbackStorage.readMemoryByPath(resultPath);
5479
- }
5480
- const ownerStorage = await this.storageForAbsoluteQmdResultPath(
5481
- resultPath,
5482
- fallbackStorage,
5483
- recallNamespaces
5484
- );
5485
- if (!ownerStorage) return null;
5486
- for (const candidate of qmdResultPathCandidates(
5487
- ownerStorage.dir,
5488
- resultPath
5489
- )) {
5490
- const memory = await ownerStorage.storage.readMemoryByPath(candidate);
5491
- if (memory) return memory;
5155
+ const resolvedNamespace = this.deps.writableNamespaceFor(
5156
+ request.namespace,
5157
+ void 0,
5158
+ request.authenticatedPrincipal ?? principal
5159
+ );
5160
+ const storage = await this.deps.orchestrator.getStorage(resolvedNamespace);
5161
+ const mode = request.mode === "apply" ? "apply" : "shadow";
5162
+ const boundedBatchSize = typeof request.batchSize === "number" && Number.isFinite(request.batchSize) ? Math.max(1, Math.floor(request.batchSize)) : void 0;
5163
+ const result = await runMemoryGovernance({
5164
+ memoryDir: storage.dir,
5165
+ mode,
5166
+ recentDays: typeof request.recentDays === "number" && Number.isFinite(request.recentDays) ? Math.max(1, Math.floor(request.recentDays)) : void 0,
5167
+ maxMemories: typeof request.maxMemories === "number" && Number.isFinite(request.maxMemories) ? Math.max(1, Math.floor(request.maxMemories)) : void 0,
5168
+ batchSize: boundedBatchSize
5169
+ });
5170
+ if (mode === "apply") {
5171
+ try {
5172
+ await this.deps.orchestrator.processEntitySynthesisQueue(
5173
+ resolvedNamespace,
5174
+ Math.min(boundedBatchSize ?? 5, 5)
5175
+ );
5176
+ } catch (error) {
5177
+ log.debug(`governanceRun: entity synthesis refresh failed after governance apply: ${error}`);
5492
5178
  }
5493
- return null;
5494
5179
  }
5495
- if (!fallbackStorageDir) {
5496
- return await fallbackStorage.readMemoryByPath(resultPath);
5180
+ return {
5181
+ namespace: resolvedNamespace,
5182
+ runId: result.runId,
5183
+ traceId: result.traceId,
5184
+ mode: result.mode,
5185
+ reviewQueueCount: result.reviewQueue.length,
5186
+ proposedActionCount: result.proposedActions.length,
5187
+ appliedActionCount: result.appliedActions.length,
5188
+ summaryPath: result.summaryPath,
5189
+ reportPath: result.reportPath
5190
+ };
5191
+ }
5192
+ async conversationIndexUpdate(request = {}) {
5193
+ if (!resolveIndexingCapabilities(this.deps.orchestrator.config).conversationIndex) {
5194
+ return {
5195
+ enabled: false,
5196
+ sessions: 0,
5197
+ chunks: 0,
5198
+ skipped: 0,
5199
+ skippedSessionKeys: [],
5200
+ embeddedRuns: 0,
5201
+ reason: "disabled"
5202
+ };
5497
5203
  }
5498
- for (const candidate of qmdResultPathCandidates(
5499
- fallbackStorageDir,
5500
- resultPath
5501
- )) {
5502
- const memory = await fallbackStorage.readMemoryByPath(candidate);
5503
- if (memory) return memory;
5204
+ const hours = typeof request.hours === "number" && Number.isFinite(request.hours) ? Math.max(1, Math.floor(request.hours)) : 24;
5205
+ let sessionKey;
5206
+ if (request.sessionKey !== void 0) {
5207
+ if (typeof request.sessionKey !== "string" || request.sessionKey.trim().length === 0) {
5208
+ throw new EngramAccessInputError("sessionKey must be a non-empty string when provided");
5209
+ }
5210
+ sessionKey = request.sessionKey.trim();
5504
5211
  }
5505
- return null;
5506
- }
5507
- async resolveColdQmdResultForRecall(result, fallbackStorage, recallNamespaces = []) {
5508
- const memory = await this.readQmdResultMemory(
5509
- result.path,
5510
- fallbackStorage,
5511
- recallNamespaces
5512
- );
5513
- if (!memory) return null;
5514
- let ownerNamespace = null;
5515
- if (path8.isAbsolute(memory.path)) {
5516
- const ownerStorage = await this.storageForAbsoluteQmdResultPath(
5517
- memory.path,
5518
- fallbackStorage,
5519
- recallNamespaces
5212
+ if (sessionKey) {
5213
+ const result = await this.deps.orchestrator.updateConversationIndex(
5214
+ sessionKey,
5215
+ hours,
5216
+ { embed: request.embed }
5520
5217
  );
5521
- ownerNamespace = ownerStorage?.namespace ?? null;
5522
- if (!ownerNamespace && resolveNamespaceCapabilities(this.getConfig()).namespaces) return null;
5218
+ return {
5219
+ enabled: true,
5220
+ sessionKey,
5221
+ sessions: 1,
5222
+ chunks: result.chunks,
5223
+ skipped: result.skipped ? 1 : 0,
5224
+ skippedSessionKeys: result.skipped ? [sessionKey] : [],
5225
+ embeddedRuns: result.embedded ? 1 : 0,
5226
+ reason: result.reason,
5227
+ retryAfterMs: result.retryAfterMs
5228
+ };
5523
5229
  }
5524
- ownerNamespace ??= this.namespaceFromPath(memory.path);
5525
- if (recallNamespaces.length > 0 && !recallNamespaces.includes(ownerNamespace)) {
5526
- return null;
5230
+ const sessionKeys = await this.deps.orchestrator.transcript.listSessionKeys();
5231
+ let chunks = 0;
5232
+ let skipped = 0;
5233
+ const skippedSessionKeys = [];
5234
+ let embeddedRuns = 0;
5235
+ for (const sessionKey2 of sessionKeys) {
5236
+ const result = await this.deps.orchestrator.updateConversationIndex(
5237
+ sessionKey2,
5238
+ hours,
5239
+ { embed: request.embed }
5240
+ );
5241
+ chunks += result.chunks;
5242
+ if (result.skipped) {
5243
+ skipped += 1;
5244
+ skippedSessionKeys.push(sessionKey2);
5245
+ }
5246
+ if (result.embedded) {
5247
+ embeddedRuns += 1;
5248
+ }
5527
5249
  }
5528
5250
  return {
5529
- namespace: ownerNamespace,
5530
- result: {
5531
- ...result,
5532
- docid: result.docid || memory.frontmatter.id,
5533
- path: memory.path,
5534
- snippet: result.snippet || memory.content.slice(0, 400)
5535
- }
5251
+ enabled: true,
5252
+ sessions: sessionKeys.length,
5253
+ chunks,
5254
+ skipped,
5255
+ skippedSessionKeys,
5256
+ embeddedRuns
5536
5257
  };
5537
5258
  }
5538
- async storageForAbsoluteQmdResultPath(resultPath, fallbackStorage, recallNamespaces = []) {
5539
- const resolvedPath = path8.resolve(resultPath);
5540
- const config = this.getConfig();
5541
- const memoryRoot = path8.resolve(config.memoryDir);
5542
- const namespacesRoot = path8.join(memoryRoot, "namespaces");
5543
- const fallbackStorageDir = storageDirOrNull(fallbackStorage);
5544
- const matches = [];
5545
- const seenDirs = /* @__PURE__ */ new Set();
5546
- const maybeAddStorage = (storage, namespace) => {
5547
- const storageDir = storageDirOrNull(storage);
5548
- if (!storageDir) return;
5549
- const candidateRoot = path8.resolve(storageDir);
5550
- if (seenDirs.has(candidateRoot)) return;
5551
- if (!isPathInsideStorageRoot(candidateRoot, resolvedPath)) return;
5552
- if (candidateRoot === memoryRoot && isPathInsideStorageRoot(namespacesRoot, resolvedPath)) {
5553
- return;
5554
- }
5555
- seenDirs.add(candidateRoot);
5556
- matches.push({ storage, dir: candidateRoot, namespace });
5557
- };
5558
- const fallbackNamespace = fallbackStorageDir !== null ? this.storageDirNamespace(fallbackStorageDir) : config.defaultNamespace;
5559
- maybeAddStorage(fallbackStorage, fallbackNamespace);
5560
- const candidateNamespaces = /* @__PURE__ */ new Set();
5561
- candidateNamespaces.add(config.defaultNamespace);
5562
- candidateNamespaces.add(config.sharedNamespace);
5563
- for (const ns of recallNamespaces) {
5564
- candidateNamespaces.add(ns);
5259
+ async profilingReport(request = {}) {
5260
+ const profiler = this.deps.orchestrator.profiler;
5261
+ if (!profiler.isEnabled) {
5262
+ return {
5263
+ enabled: false,
5264
+ reason: "disabled",
5265
+ message: "Profiling is disabled. Set profilingEnabled: true in your plugin config to enable."
5266
+ };
5565
5267
  }
5566
- if (isPathInsideStorageRoot(namespacesRoot, resolvedPath)) {
5567
- const relativeToNamespaces = path8.relative(namespacesRoot, resolvedPath);
5568
- const [namespaceSegment] = relativeToNamespaces.split(/[\\/]/);
5569
- if (namespaceSegment) {
5570
- candidateNamespaces.add(
5571
- namespaceIdentityFromToken(namespaceSegment) ?? namespaceSegment
5572
- );
5573
- }
5268
+ const format = request.format ?? "ascii";
5269
+ if (format !== "ascii" && format !== "json") {
5270
+ throw new EngramAccessInputError("format must be one of: ascii, json");
5574
5271
  }
5575
- for (const policy of config.namespacePolicies ?? []) {
5576
- candidateNamespaces.add(policy.name);
5272
+ const limit = request.limit ?? 5;
5273
+ if (!Number.isInteger(limit) || limit < 1 || limit > 20) {
5274
+ throw new EngramAccessInputError("limit must be an integer between 1 and 20");
5577
5275
  }
5578
- for (const ns of candidateNamespaces) {
5579
- if (!ns) continue;
5580
- try {
5581
- maybeAddStorage(await this.storageFor(ns), ns);
5582
- } catch {
5583
- continue;
5276
+ const traces = profiler.getRecentTraces(limit);
5277
+ const stats = profiler.getStats();
5278
+ const bottleneck = profiler.identifyBottleneck();
5279
+ if (format === "json") {
5280
+ return {
5281
+ enabled: true,
5282
+ format,
5283
+ traces,
5284
+ stats,
5285
+ bottleneck
5286
+ };
5287
+ }
5288
+ const lines = [];
5289
+ lines.push("Engram Profiling Report");
5290
+ lines.push("=".repeat(60));
5291
+ lines.push("");
5292
+ const allBuckets = [
5293
+ ["byKind", stats.byKind],
5294
+ ["bySpan", stats.bySpan]
5295
+ ];
5296
+ const hasStats = allBuckets.some(([, entries]) => Object.keys(entries).length > 0);
5297
+ if (hasStats) {
5298
+ lines.push("Aggregate Stats (all retained traces):");
5299
+ for (const [bucket, entries] of allBuckets) {
5300
+ for (const [key, summary] of Object.entries(entries)) {
5301
+ lines.push(
5302
+ ` ${bucket}/${key}: avg=${summary.avgMs}ms p50=${summary.p50Ms}ms p95=${summary.p95Ms}ms max=${summary.maxMs}ms (n=${summary.count})`
5303
+ );
5304
+ }
5584
5305
  }
5306
+ lines.push("");
5585
5307
  }
5586
- matches.sort((a, b) => b.dir.length - a.dir.length);
5587
- return matches[0] ?? null;
5588
- }
5589
- };
5590
-
5591
- // src/access-recall-surface.ts
5592
- var AccessRecallSurface = class {
5593
- constructor(deps) {
5594
- this.deps = deps;
5595
- }
5596
- deps;
5597
- async buildRecallResponseFromXraySnapshot(options) {
5598
- const memoryIds = options.snapshot.results.map((result) => result.memoryId);
5599
- const resultPaths = options.snapshot.results.map((result) => result.path);
5600
- const namespace = options.snapshot.namespace ? this.deps.resolveNamespace(options.snapshot.namespace) : this.deps.orchestrator.config.defaultNamespace;
5601
- const sourcesUsed = Array.from(
5602
- new Set(options.snapshot.results.map((result) => result.servedBy))
5603
- );
5604
- const snapshotForSerialization = {
5605
- sessionKey: options.sessionKey ?? "",
5606
- recordedAt: new Date(options.snapshot.capturedAt).toISOString(),
5607
- queryHash: createHash4("sha256").update(options.query).digest("hex"),
5608
- queryLen: options.query.length,
5609
- memoryIds,
5610
- namespace,
5611
- recallNamespaces: [namespace],
5612
- traceId: options.snapshot.traceId,
5613
- plannerMode: options.normalizedMode,
5614
- requestedMode: options.requestedMode && options.requestedMode !== "auto" ? options.requestedMode : void 0,
5615
- sourcesUsed,
5616
- budgetsApplied: {
5617
- appliedTopK: memoryIds.length,
5618
- recallBudgetChars: options.snapshot.budget.chars,
5619
- maxMemoryTokens: this.deps.orchestrator.config.maxMemoryTokens,
5620
- finalContextChars: options.snapshot.budget.used
5621
- },
5622
- latencyMs: Date.now() - options.startedAt,
5623
- resultPaths
5624
- };
5625
- const results = await this.deps.serializeRecallResults(
5626
- snapshotForSerialization,
5627
- options.disclosure,
5628
- {
5629
- query: options.query,
5630
- ...options.sessionKey ? { sessionKey: options.sessionKey } : {},
5631
- ...options.rawExcerptNamespace ? { rawExcerptNamespace: options.rawExcerptNamespace } : {},
5632
- ...options.rawExcerptSessionIds !== void 0 ? { rawExcerptSessionIds: options.rawExcerptSessionIds } : {},
5633
- ...options.rawExcerptsSuppressed ? { rawExcerptsSuppressed: options.rawExcerptsSuppressed } : {}
5308
+ if (bottleneck) {
5309
+ lines.push(`Bottleneck: ${bottleneck}`);
5310
+ lines.push("");
5311
+ }
5312
+ if (traces.length === 0) {
5313
+ lines.push("No traces recorded yet. Trigger a recall or extraction to see timing data.");
5314
+ } else {
5315
+ for (const trace of traces) {
5316
+ lines.push(formatProfileTraceAscii(trace));
5317
+ lines.push("");
5634
5318
  }
5635
- );
5636
- const context = results.map((result) => {
5637
- const content = typeof result.content === "string" && result.content.length > 0 ? result.content : "";
5638
- return content || result.preview;
5639
- }).filter((text) => text.length > 0).join("\n\n");
5319
+ }
5640
5320
  return {
5641
- query: options.query,
5642
- ...options.sessionKey ? { sessionKey: options.sessionKey } : {},
5643
- namespace,
5644
- context,
5645
- count: memoryIds.length,
5646
- memoryIds,
5647
- results,
5648
- recordedAt: snapshotForSerialization.recordedAt,
5649
- traceId: options.snapshot.traceId,
5650
- plannerMode: options.normalizedMode,
5651
- fallbackUsed: sourcesUsed.some((source) => source !== "hybrid"),
5652
- sourcesUsed,
5653
- disclosure: options.disclosure,
5654
- budgetsApplied: snapshotForSerialization.budgetsApplied,
5655
- latencyMs: snapshotForSerialization.latencyMs
5321
+ enabled: true,
5322
+ format,
5323
+ report: lines.join("\n")
5656
5324
  };
5657
5325
  }
5658
- async serializeRecallResults(snapshot, disclosure, rawContext = null) {
5659
- if (!snapshot) return [];
5660
- const namespace = snapshot.namespace ? this.deps.resolveNamespace(snapshot.namespace) : this.deps.orchestrator.config.defaultNamespace;
5661
- const storage = await this.deps.orchestrator.getStorage(namespace);
5662
- const storageDir = storage.dir;
5663
- const recallNamespaces = Array.from(
5664
- new Set(
5665
- [
5666
- namespace,
5667
- ...Array.isArray(snapshot.recallNamespaces) ? snapshot.recallNamespaces.map((ns) => this.deps.resolveNamespace(ns)) : [],
5668
- this.deps.orchestrator.config.defaultNamespace,
5669
- this.deps.orchestrator.config.sharedNamespace,
5670
- ...(this.deps.orchestrator.config.namespacePolicies ?? []).map((p) => p.name)
5671
- ].filter((ns) => typeof ns === "string" && ns.length > 0)
5672
- )
5326
+ /**
5327
+ * Read-only graph snapshot for the admin pane (issue #691 PR 2/5).
5328
+ *
5329
+ * Reads adjacency from the JSONL edge store written by `GraphIndex` and
5330
+ * resolves node metadata via the namespaced storage manager. Namespace
5331
+ * resolution mirrors the read-side path used by `recall` /
5332
+ * `procedureStats`, so multi-principal deployments can't leak edges from
5333
+ * a peer namespace (CLAUDE.md rule 42).
5334
+ */
5335
+ async graphSnapshot(request, authenticatedPrincipal) {
5336
+ const namespace = this.deps.resolveReadableNamespace(
5337
+ request.namespace,
5338
+ authenticatedPrincipal
5673
5339
  );
5674
- const results = [];
5675
- const seen = /* @__PURE__ */ new Set();
5676
- const collectionNamespaceFromPrefix = (collectionPrefix) => {
5677
- const baseCollection = this.deps.orchestrator.config.qmdCollection;
5678
- if (collectionPrefix === baseCollection) return this.deps.orchestrator.config.defaultNamespace;
5679
- const namespaceSuffix = collectionPrefix.startsWith(`${baseCollection}--`) ? collectionPrefix.slice(baseCollection.length + 2) : "";
5680
- if (!namespaceSuffix) return null;
5681
- const decoded = namespaceIdentityFromToken(namespaceSuffix);
5682
- if (decoded !== null) return decoded || this.deps.orchestrator.config.defaultNamespace;
5683
- if (namespaceSuffix.startsWith("ns--")) {
5684
- const legacyNamespace = namespaceSuffix.slice("ns--".length).trim();
5685
- return legacyNamespace || null;
5340
+ const storage = await this.deps.orchestrator.getStorage(namespace);
5341
+ const cfg = this.deps.orchestrator.config;
5342
+ const graphCaps = resolveGraphConstructionCapabilities(cfg);
5343
+ let namespaceRootReal;
5344
+ try {
5345
+ namespaceRootReal = await nodeFs.realpath(storage.dir);
5346
+ } catch {
5347
+ namespaceRootReal = nodePath.resolve(storage.dir);
5348
+ }
5349
+ const namespaceRootWithSep = namespaceRootReal.endsWith(nodePath.sep) ? namespaceRootReal : namespaceRootReal + nodePath.sep;
5350
+ const loadNode = async (relPath) => {
5351
+ if (nodePath.isAbsolute(relPath)) {
5352
+ log.warn(
5353
+ `graphSnapshot: rejected absolute edge endpoint (len=${relPath.length}) outside namespace root`
5354
+ );
5355
+ return null;
5686
5356
  }
5687
- return null;
5688
- };
5689
- const readResultPath = async (memoryPath) => {
5690
- const parts = qmdCollectionPathParts(memoryPath);
5691
- const coldCollection = this.deps.orchestrator.config.qmdColdCollection ?? "openclaw-engram-cold";
5692
- if (parts && parts.collection === coldCollection) {
5693
- const storages = [];
5694
- const seenStorageDirs = /* @__PURE__ */ new Set();
5695
- const addStorage = (candidateStorage) => {
5696
- const candidateDir = nodePath2.resolve(candidateStorage.dir);
5697
- if (seenStorageDirs.has(candidateDir)) return;
5698
- seenStorageDirs.add(candidateDir);
5699
- storages.push({ storage: candidateStorage, dir: candidateDir });
5700
- };
5701
- addStorage(storage);
5702
- for (const recallNamespace of recallNamespaces) {
5703
- try {
5704
- addStorage(await this.deps.orchestrator.getStorage(recallNamespace));
5705
- } catch {
5706
- continue;
5707
- }
5708
- }
5709
- for (const candidateStorage of storages) {
5710
- try {
5711
- const coldRoot = nodePath2.join(candidateStorage.dir, "cold");
5712
- for (const candidatePath of qmdResultPathCandidates(
5713
- coldRoot,
5714
- parts.relativePath
5715
- )) {
5716
- const memory = await candidateStorage.storage.readMemoryByPath(candidatePath);
5717
- if (memory) return { memory, baseDir: candidateStorage.dir };
5718
- }
5719
- } catch (err) {
5720
- if (err instanceof SecureStoreLockedError) throw err;
5721
- continue;
5722
- }
5723
- }
5357
+ const candidate = nodePath.resolve(namespaceRootReal, relPath);
5358
+ if (candidate !== namespaceRootReal && !candidate.startsWith(namespaceRootWithSep)) {
5359
+ log.warn(
5360
+ `graphSnapshot: rejected traversing edge endpoint (len=${relPath.length}) outside namespace root`
5361
+ );
5724
5362
  return null;
5725
5363
  }
5726
- const collectionNamespace = parts ? collectionNamespaceFromPrefix(parts.collection) : null;
5727
- if (parts && collectionNamespace) {
5728
- try {
5729
- const collectionStorage = await this.deps.orchestrator.getStorage(collectionNamespace);
5730
- for (const candidate of qmdResultPathCandidates(
5731
- collectionStorage.dir,
5732
- parts.relativePath
5733
- )) {
5734
- const memory = await collectionStorage.readMemoryByPath(candidate);
5735
- if (memory) return { memory, baseDir: collectionStorage.dir };
5736
- }
5737
- return null;
5738
- } catch (err) {
5739
- if (err instanceof SecureStoreLockedError) throw err;
5740
- return null;
5741
- }
5364
+ let canonical;
5365
+ try {
5366
+ canonical = await nodeFs.realpath(candidate);
5367
+ } catch {
5368
+ canonical = candidate;
5742
5369
  }
5743
- if (nodePath2.isAbsolute(memoryPath)) {
5744
- const ownerStorage = await this.deps.storageForAbsoluteRecallPath(
5745
- memoryPath,
5746
- namespace,
5747
- recallNamespaces
5370
+ if (canonical !== namespaceRootReal && !canonical.startsWith(namespaceRootWithSep)) {
5371
+ log.warn(
5372
+ `graphSnapshot: rejected symlinked edge endpoint (len=${relPath.length}) that resolved outside namespace root`
5748
5373
  );
5749
- if (!ownerStorage) return null;
5750
- for (const candidate of qmdResultPathCandidates(
5751
- ownerStorage.dir,
5752
- memoryPath
5753
- )) {
5754
- const memory = await ownerStorage.storage.readMemoryByPath(candidate);
5755
- if (memory) return { memory, baseDir: ownerStorage.dir };
5756
- }
5757
5374
  return null;
5758
5375
  }
5759
- for (const candidate of qmdResultPathCandidates(storageDir, memoryPath)) {
5760
- const memory = await storage.readMemoryByPath(candidate);
5761
- if (memory) return { memory, baseDir: storageDir };
5762
- }
5763
- return null;
5376
+ const memory = await storage.readMemoryByPath(canonical);
5377
+ if (!memory) return null;
5378
+ const fm = memory.frontmatter;
5379
+ return {
5380
+ category: fm.category ?? "unknown",
5381
+ label: fm.id ?? nodePath.basename(canonical, nodePath.extname(canonical)),
5382
+ updated: fm.updated
5383
+ };
5764
5384
  };
5765
- const rawExcerptsResult = rawContext?.rawExcerptsSuppressed === true ? (
5766
- // Implicit raw recall with NO readable LCM namespace (#1505 thread
5767
- // NBHWz): emit empty excerpts rather than falling back to the
5768
- // write/overlay `namespace` the read gate excludes.
5769
- []
5770
- ) : await this.deps.fetchRawExcerpts(
5771
- disclosure,
5772
- rawContext ? {
5773
- query: rawContext.query,
5774
- ...rawContext.sessionKey ? { sessionKey: rawContext.sessionKey } : {},
5775
- namespace: rawContext.rawExcerptNamespace ?? namespace,
5776
- ...rawContext.rawExcerptSessionIds !== void 0 ? { lcmSessionIds: rawContext.rawExcerptSessionIds } : {}
5777
- } : null
5778
- );
5779
- const rawExcerpts = rawExcerptsResult ?? void 0;
5780
- for (const memoryPath of snapshot.resultPaths ?? []) {
5781
- if (!memoryPath || seen.has(memoryPath)) continue;
5782
- const resolved = await readResultPath(memoryPath);
5783
- if (!resolved) continue;
5784
- const { memory, baseDir } = resolved;
5785
- seen.add(memoryPath);
5786
- results.push(
5787
- this.deps.serializeMemorySummary(
5788
- memory,
5789
- baseDir,
5790
- disclosure,
5791
- // Attach the (possibly empty) raw excerpts to the first raw
5792
- // result; subsequent results do not duplicate the array.
5793
- results.length === 0 ? rawExcerpts : void 0
5794
- )
5795
- );
5796
- }
5797
- if (results.length > 0) return results;
5798
- for (const memoryId of snapshot.memoryIds) {
5799
- const memory = await storage.getMemoryById(memoryId);
5800
- if (!memory || seen.has(memory.path)) continue;
5801
- seen.add(memory.path);
5802
- results.push(
5803
- this.deps.serializeMemorySummary(
5804
- memory,
5805
- storageDir,
5806
- disclosure,
5807
- results.length === 0 ? rawExcerpts : void 0
5808
- )
5809
- );
5810
- }
5811
- return results;
5385
+ return buildGraphSnapshot({
5386
+ memoryDir: namespaceRootReal,
5387
+ graphConfig: {
5388
+ entityGraph: graphCaps.entityGraph,
5389
+ timeGraph: graphCaps.timeGraph,
5390
+ causalGraph: graphCaps.causalGraph
5391
+ },
5392
+ request: {
5393
+ limit: request.limit,
5394
+ since: request.since,
5395
+ focusNodeId: request.focusNodeId,
5396
+ categories: request.categories
5397
+ },
5398
+ loadNode
5399
+ });
5812
5400
  }
5813
5401
  /**
5814
- * Fetch raw transcript excerpts from the LCM archive for `disclosure ===
5815
- * "raw"` recalls (issue #677 PR 2/4). Returns `null` for non-raw recall
5816
- * depths, an empty array when LCM is disabled / not initialized / has no
5817
- * matches, and an array of LCM-side excerpts otherwise. Errors are
5818
- * swallowed and treated as "no excerpts" so a failing LCM never breaks
5819
- * the recall response.
5402
+ * List capsule archives in the namespace-scoped capsule store.
5820
5403
  *
5821
- * Namespace handling: LCM archival prefixes non-default-namespace
5822
- * sessions with `${namespace}:${sessionKey}` (see `observe()` around
5823
- * line 2498), so the lookup must mirror that prefix or raw recalls in
5824
- * non-default namespaces miss their own excerpts.
5404
+ * MCP uses this access-layer method instead of reading arbitrary paths so
5405
+ * capsule discovery remains bound to the same namespace ACLs as export and
5406
+ * import.
5825
5407
  */
5826
- async fetchRawExcerpts(disclosure, context) {
5827
- if (disclosure !== "raw") return null;
5828
- if (!context || !context.query) return [];
5829
- if (!context.sessionKey) return [];
5830
- const lcm = this.deps.orchestrator.lcmEngine;
5831
- if (!lcm || !lcm.enabled) return [];
5408
+ async capsuleList(options) {
5409
+ const resolvedNamespace = this.deps.resolveReadableNamespace(options?.namespace, options?.principal);
5410
+ const storage = await this.deps.orchestrator.getStorage(resolvedNamespace);
5411
+ const capsulesDir = defaultCapsulesDir(storage.dir);
5412
+ let dirEntries;
5832
5413
  try {
5833
- const legacyKey = context.namespace && context.namespace !== this.deps.orchestrator.config.defaultNamespace ? `${context.namespace}:${context.sessionKey}` : context.sessionKey;
5834
- const lcmSessionIds = context.lcmSessionIds !== void 0 ? context.lcmSessionIds : [legacyKey];
5835
- const limit = 5;
5836
- const seenRows = /* @__PURE__ */ new Set();
5837
- const excerpts = [];
5838
- const settledRows = await Promise.allSettled(
5839
- lcmSessionIds.map(
5840
- async (lcmSessionKey) => lcm.searchContextFull(context.query, limit, lcmSessionKey)
5841
- )
5842
- );
5843
- for (const result of settledRows) {
5844
- if (excerpts.length >= limit) break;
5845
- if (result.status !== "fulfilled") continue;
5846
- for (const r of result.value) {
5847
- const dedupeKey = `${r.session_id} ${r.turn_index}`;
5848
- if (seenRows.has(dedupeKey)) continue;
5849
- seenRows.add(dedupeKey);
5850
- excerpts.push({
5851
- turnIndex: r.turn_index,
5852
- role: r.role,
5853
- content: r.content,
5854
- sessionId: r.session_id
5855
- });
5856
- if (excerpts.length >= limit) break;
5857
- }
5414
+ const capsulesDirStat = await nodeFs.lstat(capsulesDir);
5415
+ if (capsulesDirStat.isSymbolicLink()) {
5416
+ throw new EngramAccessInputError("capsule list failed: capsule store directory must not be a symlink");
5858
5417
  }
5859
- return excerpts;
5860
- } catch {
5861
- return [];
5862
- }
5863
- }
5864
- async executeRecall(request) {
5865
- const query = request.query;
5866
- const callerProvidedDisclosure = request.disclosure !== void 0 && request.disclosure !== null;
5867
- const requestedDisclosure = (() => {
5868
- if (!callerProvidedDisclosure) {
5869
- return DEFAULT_RECALL_DISCLOSURE;
5418
+ if (!capsulesDirStat.isDirectory()) {
5419
+ throw new EngramAccessInputError("capsule list failed: capsule store path must be a directory");
5870
5420
  }
5871
- if (!isRecallDisclosure(request.disclosure)) {
5872
- throw new EngramAccessInputError(
5873
- `disclosure must be one of: chunk, section, raw (got: ${String(request.disclosure)})`
5874
- );
5421
+ dirEntries = await nodeFs.readdir(capsulesDir, { withFileTypes: true });
5422
+ } catch (err) {
5423
+ const code = typeof err === "object" && err !== null && "code" in err ? err.code : void 0;
5424
+ if (code === "ENOENT") {
5425
+ return { namespace: resolvedNamespace, capsulesDir, capsules: [] };
5875
5426
  }
5876
- return request.disclosure;
5877
- })();
5878
- if (request.codingContext !== void 0 && request.sessionKey) {
5879
- this.deps.setCodingContext({
5880
- sessionKey: request.sessionKey,
5881
- codingContext: request.codingContext
5882
- });
5427
+ throw err;
5883
5428
  }
5884
- if (request.codingContext === void 0 && request.sessionKey) {
5885
- await this.deps.maybeAttachCodingContext(request.sessionKey, {
5886
- cwd: request.cwd,
5887
- projectTag: request.projectTag
5429
+ const archiveNames = dirEntries.filter(
5430
+ (entry) => entry.isFile() && (entry.name.endsWith(".capsule.json.gz") || entry.name.endsWith(".capsule.json.gz.enc"))
5431
+ ).map((entry) => entry.name).sort();
5432
+ const capsules = [];
5433
+ for (const archiveName of archiveNames) {
5434
+ const archivePath = nodePath.join(capsulesDir, archiveName);
5435
+ const id = archiveName.replace(/\.capsule\.json\.gz\.enc$/, "").replace(/\.capsule\.json\.gz$/, "");
5436
+ const manifestPath = nodePath.join(capsulesDir, `${id}.manifest.json`);
5437
+ let createdAt = null;
5438
+ let pluginVersion = null;
5439
+ let fileCount = null;
5440
+ let description = null;
5441
+ let manifestPathOrNull = manifestPath;
5442
+ try {
5443
+ const manifestStat = await nodeFs.lstat(manifestPath);
5444
+ if (manifestStat.isSymbolicLink() || !manifestStat.isFile()) {
5445
+ capsules.push({
5446
+ id,
5447
+ archivePath,
5448
+ manifestPath: manifestPathOrNull,
5449
+ createdAt,
5450
+ pluginVersion,
5451
+ fileCount,
5452
+ description
5453
+ });
5454
+ continue;
5455
+ }
5456
+ const raw = await nodeFs.readFile(manifestPath, "utf-8");
5457
+ const sidecar = JSON.parse(raw);
5458
+ createdAt = typeof sidecar.createdAt === "string" ? sidecar.createdAt : null;
5459
+ pluginVersion = typeof sidecar.pluginVersion === "string" ? sidecar.pluginVersion : null;
5460
+ fileCount = Array.isArray(sidecar.files) ? sidecar.files.length : null;
5461
+ const capsule = sidecar.capsule;
5462
+ description = capsule && typeof capsule.description === "string" ? capsule.description : null;
5463
+ } catch (err) {
5464
+ const code = typeof err === "object" && err !== null && "code" in err ? err.code : void 0;
5465
+ if (code === "ENOENT") {
5466
+ manifestPathOrNull = null;
5467
+ }
5468
+ }
5469
+ capsules.push({
5470
+ id,
5471
+ archivePath,
5472
+ manifestPath: manifestPathOrNull,
5473
+ createdAt,
5474
+ pluginVersion,
5475
+ fileCount,
5476
+ description
5888
5477
  });
5889
5478
  }
5890
- const authenticatedPrincipal = request.authenticatedPrincipal?.trim();
5891
- const namespaceOverride = this.deps.resolveRecallNamespace(
5892
- request.namespace,
5893
- request.sessionKey,
5894
- authenticatedPrincipal
5895
- );
5896
- const namespace = namespaceOverride ?? this.deps.orchestrator.config.defaultNamespace;
5897
- const mode = this.deps.normalizeRecallMode(request.mode);
5898
- const maybePrincipal = this.deps.resolveRequestPrincipal(request.sessionKey, authenticatedPrincipal);
5899
- if (resolveNamespaceCapabilities(this.deps.orchestrator.config).namespaces && !maybePrincipal) {
5479
+ return { namespace: resolvedNamespace, capsulesDir, capsules };
5480
+ }
5481
+ /**
5482
+ * Manually invoke a single Dreams phase pass (PR 4/4).
5483
+ *
5484
+ * Deep-sleep delegates to memory governance (shadow → dry-run, apply → live).
5485
+ * Light-sleep and REM scan the observation ledger and memory corpus
5486
+ * respectively, returning the same telemetry shape as a scheduled run.
5487
+ */
5488
+ async dreamsRun(options) {
5489
+ const { runDreamsPhase } = await import("./dreams-ledger-6UODQOX6.js");
5490
+ const validPhases = ["lightSleep", "rem", "deepSleep"];
5491
+ if (!validPhases.includes(options.phase)) {
5900
5492
  throw new EngramAccessInputError(
5901
- "authentication required: namespaces are enabled and no principal was supplied"
5493
+ `Invalid phase: ${String(options.phase)}. Must be one of: ${validPhases.join(", ")}`
5902
5494
  );
5903
5495
  }
5904
- const principal = maybePrincipal ?? "default";
5905
- const principalNamespace = defaultNamespaceForPrincipal(principal, this.deps.orchestrator.config);
5906
- const profileCodingContext = request.sessionKey && typeof this.deps.orchestrator.getCodingContextForSession === "function" ? this.deps.orchestrator.getCodingContextForSession(request.sessionKey) : null;
5907
- const profileCodingOverlay = !namespaceOverride && profileCodingContext && resolveNamespaceCapabilities(this.deps.orchestrator.config).namespaces && this.deps.orchestrator.config.codingMode?.projectScope ? resolveCodingNamespaceOverlay(
5908
- profileCodingContext,
5909
- this.deps.orchestrator.config.codingMode,
5910
- this.deps.orchestrator.config.defaultNamespace
5911
- ) : null;
5912
- const profilePlan = namespaceOverride ? null : resolveScopeProfilePlan({
5913
- config: this.deps.orchestrator.config,
5914
- principal,
5915
- codingContext: profileCodingContext,
5916
- codingOverlay: profileCodingOverlay
5917
- });
5918
- const modeSkipsBudget = mode === "no_recall";
5919
- const legacyRecallNamespaces = Array.isArray(this.deps.orchestrator.config.defaultRecallNamespaces) ? recallNamespacesForPrincipal(principal, this.deps.orchestrator.config) : [];
5920
- const effectiveNamespaces = namespaceOverride ? [namespaceOverride] : profilePlan ? expandScopeProfileReadNamespaces({
5921
- profilePlan,
5922
- principalSelfNamespace: profilePlan.baseNamespace,
5923
- config: this.deps.orchestrator.config,
5924
- principal,
5925
- codingOverlay: profileCodingOverlay,
5926
- legacyRecallNamespaces
5927
- }) : legacyRecallNamespaces;
5928
- const budgetPrincipalNamespace = profilePlan?.baseNamespace ?? principalNamespace;
5929
- let budgetDecision;
5930
- let recordBudgetAfterSuccess = false;
5931
- if (modeSkipsBudget) {
5932
- budgetDecision = {
5933
- allowed: true,
5934
- reason: "allowed-same-namespace",
5935
- count: 0,
5936
- limit: {
5937
- softLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetSoftLimit ?? 10,
5938
- hardLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetHardLimit ?? 30,
5939
- windowMs: this.deps.orchestrator.config.recallCrossNamespaceBudgetWindowMs ?? 6e4
5940
- }
5496
+ const deepSleep = this.deps.orchestrator.config.dreamsPhases.deepSleep;
5497
+ if (options.phase === "deepSleep" && deepSleep.enabled === false && deepSleep.enabledExplicitlySet === true) {
5498
+ throw new EngramAccessInputError(
5499
+ "memory governance is disabled by dreams.phases.deepSleep.enabled=false"
5500
+ );
5501
+ }
5502
+ const dryRun = options.dryRun === true;
5503
+ const resolvedNamespace = this.deps.writableNamespaceFor(
5504
+ options.namespace,
5505
+ void 0,
5506
+ options.authenticatedPrincipal
5507
+ );
5508
+ const storage = await this.deps.orchestrator.getStorage(resolvedNamespace);
5509
+ const memoryDir = storage.dir;
5510
+ const phaseRunner = dryRun || options.phase === "deepSleep" ? void 0 : async (_opts) => {
5511
+ if (_opts.phase === "lightSleep") {
5512
+ const result3 = await this.deps.orchestrator.runLifecyclePolicyNow(storage);
5513
+ return {
5514
+ itemsProcessed: result3.memoriesAssessed,
5515
+ notes: `scored ${result3.memoriesAssessed} memories`
5516
+ };
5517
+ }
5518
+ const result2 = await this.deps.orchestrator.runSemanticConsolidationNow({
5519
+ dryRun: false,
5520
+ storage
5521
+ });
5522
+ const itemsProcessed = result2.clusters.reduce(
5523
+ (sum, cluster) => sum + cluster.memories.length,
5524
+ 0
5525
+ );
5526
+ return {
5527
+ itemsProcessed,
5528
+ notes: `REM consolidation found ${result2.clustersFound} clusters`
5941
5529
  };
5530
+ };
5531
+ const governanceRunner = options.phase === "deepSleep" ? async (_opts) => {
5532
+ return this.deps.orchestrator.runDeepSleepGovernanceNow({
5533
+ storage,
5534
+ dryRun: _opts.dryRun
5535
+ });
5536
+ } : void 0;
5537
+ const result = await runDreamsPhase(
5538
+ { memoryDir, phase: options.phase, dryRun },
5539
+ governanceRunner,
5540
+ phaseRunner
5541
+ );
5542
+ return {
5543
+ phase: result.phase,
5544
+ dryRun: result.dryRun,
5545
+ durationMs: result.durationMs,
5546
+ itemsProcessed: result.itemsProcessed,
5547
+ notes: result.notes
5548
+ };
5549
+ }
5550
+ /**
5551
+ * Manually promote a memory into one or more authorized targets. Requires
5552
+ * a non-empty reason (audit-logged). Reuses the scope-profile promotion
5553
+ * resolution and `canWriteNamespace` gate — there is no dashboard-only
5554
+ * write path.
5555
+ */
5556
+ async adminPromoteMemory(request) {
5557
+ const config = this.deps.orchestrator.config;
5558
+ const namespacesEnabled = resolveNamespaceCapabilities(config).namespaces === true;
5559
+ let sourceNamespace;
5560
+ if (request.namespace) {
5561
+ sourceNamespace = this.deps.resolveReadableNamespace(
5562
+ request.namespace,
5563
+ request.principal
5564
+ );
5942
5565
  } else {
5943
- let anyCrossNamespace = false;
5944
- let denied = null;
5945
- let crossNamespaceDecision = null;
5946
- for (const ns of effectiveNamespaces) {
5947
- const peek = this.deps.budget.peek({
5948
- principal,
5949
- principalNamespace: budgetPrincipalNamespace,
5950
- queryNamespace: ns
5951
- });
5952
- if (peek.reason !== "allowed-same-namespace") {
5953
- anyCrossNamespace = true;
5954
- crossNamespaceDecision ??= peek;
5955
- }
5956
- if (!peek.allowed) {
5957
- denied = peek;
5958
- break;
5959
- }
5960
- }
5961
- if (denied) {
5962
- budgetDecision = denied;
5963
- } else if (anyCrossNamespace) {
5964
- budgetDecision = crossNamespaceDecision ?? {
5965
- allowed: true,
5966
- reason: "allowed-under-soft",
5967
- count: 0,
5968
- limit: {
5969
- softLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetSoftLimit ?? 10,
5970
- hardLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetHardLimit ?? 30,
5971
- windowMs: this.deps.orchestrator.config.recallCrossNamespaceBudgetWindowMs ?? 6e4
5972
- }
5566
+ const codingContext2 = request.sessionKey ? this.deps.orchestrator.getCodingContextForSession(request.sessionKey) ?? null : null;
5567
+ const plan = resolveScopePlan({
5568
+ config,
5569
+ sessionKey: request.sessionKey,
5570
+ principalOverride: request.principal,
5571
+ codingContext: codingContext2,
5572
+ namespacesEnabled
5573
+ });
5574
+ sourceNamespace = this.deps.resolveReadableNamespace(
5575
+ plan.baseNamespace,
5576
+ request.principal
5577
+ );
5578
+ }
5579
+ const codingContext = request.sessionKey ? this.deps.orchestrator.getCodingContextForSession(request.sessionKey) ?? null : null;
5580
+ const codingOverlay = codingContext ? resolveCodingNamespaceOverlay(
5581
+ codingContext,
5582
+ config.codingMode,
5583
+ config.defaultNamespace
5584
+ ) : null;
5585
+ const scopeProfilePlan = resolveScopeProfilePlan({
5586
+ config,
5587
+ principal: request.principal,
5588
+ codingContext,
5589
+ codingOverlay
5590
+ });
5591
+ const storage = {
5592
+ readMemory: async (namespace, memoryId) => {
5593
+ const resolved = await this.deps.orchestrator.getStorage(namespace);
5594
+ const memory = await resolved.getMemoryById(memoryId);
5595
+ if (!memory) return null;
5596
+ return {
5597
+ category: memory.frontmatter.category,
5598
+ content: memory.content,
5599
+ frontmatter: memory.frontmatter
5973
5600
  };
5974
- recordBudgetAfterSuccess = true;
5975
- } else {
5976
- budgetDecision = {
5977
- allowed: true,
5978
- reason: "allowed-same-namespace",
5979
- count: 0,
5980
- limit: {
5981
- softLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetSoftLimit ?? 10,
5982
- hardLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetHardLimit ?? 30,
5983
- windowMs: this.deps.orchestrator.config.recallCrossNamespaceBudgetWindowMs ?? 6e4
5601
+ },
5602
+ writePromotedMemory: async (namespace, memory) => {
5603
+ const resolved = await this.deps.orchestrator.getStorage(namespace);
5604
+ const { id, tombstoneBlocked } = await resolved.writeMemory(
5605
+ memory.category,
5606
+ memory.content,
5607
+ {
5608
+ confidence: memory.confidence,
5609
+ tags: memory.tags,
5610
+ entityRef: memory.entityRef,
5611
+ source: `admin-promotion:${memory.sourceNamespace}:${memory.reason.slice(0, 120)}`,
5612
+ lineage: memory.lineage,
5613
+ sourceMemoryId: memory.sourceMemoryId,
5614
+ actor: memory.actor,
5615
+ validAt: memory.validAt
5984
5616
  }
5985
- };
5986
- }
5987
- if (!budgetDecision.allowed) {
5988
- throw new EngramAccessInputError(
5989
- `recall denied: cross-namespace budget exceeded (${budgetDecision.count}/${budgetDecision.limit.hardLimit} in ${budgetDecision.limit.windowMs}ms window)`
5990
- );
5991
- }
5992
- this.deps.budget.gc();
5993
- }
5994
- const topK = Number.isFinite(request.topK) ? Math.max(0, Math.floor(request.topK ?? 0)) : void 0;
5995
- let asOf;
5996
- if (request.asOf !== void 0 && request.asOf !== null) {
5997
- if (typeof request.asOf !== "string" || request.asOf.trim().length === 0) {
5998
- throw new EngramAccessInputError(
5999
- "asOf must be a non-empty ISO 8601 timestamp string"
6000
5617
  );
5618
+ if (tombstoneBlocked) {
5619
+ throw new Error(
5620
+ "target namespace tombstone-blocked the promoted content (pending_review)"
5621
+ );
5622
+ }
5623
+ return id;
6001
5624
  }
6002
- const parsed = Date.parse(request.asOf);
6003
- if (!Number.isFinite(parsed)) {
6004
- throw new EngramAccessInputError(
6005
- `asOf must be a parseable ISO 8601 timestamp (got: "${request.asOf}")`
6006
- );
5625
+ };
5626
+ try {
5627
+ return await promoteMemory({
5628
+ config,
5629
+ sourceMemoryId: request.sourceMemoryId,
5630
+ sourceNamespace,
5631
+ principal: request.principal,
5632
+ targets: request.targets,
5633
+ reason: request.reason,
5634
+ actor: request.principal ?? "admin-console",
5635
+ storage,
5636
+ scopeProfilePlan
5637
+ });
5638
+ } catch (err) {
5639
+ if (err instanceof AdminPromotionError) {
5640
+ throw new EngramAccessInputError(err.message);
6007
5641
  }
6008
- asOf = request.asOf;
5642
+ throw err;
6009
5643
  }
6010
- const recallOptions = {
6011
- namespace: namespaceOverride,
5644
+ }
5645
+ };
5646
+
5647
+ // src/access-recall-surface.ts
5648
+ import { createHash as createHash4 } from "crypto";
5649
+ import * as nodePath2 from "path";
5650
+
5651
+ // src/orchestration/qmd-result-resolver.ts
5652
+ import path8 from "path";
5653
+ function qmdCollectionPathParts(resultPath) {
5654
+ if (!resultPath || path8.isAbsolute(resultPath)) return null;
5655
+ const normalized = resultPath.replace(/\\/g, "/").replace(/^\/+/, "");
5656
+ const slashIndex = normalized.indexOf("/");
5657
+ if (slashIndex <= 0 || slashIndex >= normalized.length - 1) return null;
5658
+ const collection = normalized.slice(0, slashIndex);
5659
+ if (/^\d{4}-\d{2}-\d{2}$/.test(collection)) return null;
5660
+ return {
5661
+ collection,
5662
+ relativePath: normalized.slice(slashIndex + 1)
5663
+ };
5664
+ }
5665
+ function qmdResultPathCandidates(storageDir, resultPath) {
5666
+ const candidates = /* @__PURE__ */ new Set();
5667
+ const storageRoot = path8.resolve(storageDir);
5668
+ const addCandidate = (candidate) => {
5669
+ const resolved = path8.resolve(candidate);
5670
+ if (isPathInsideStorageRoot(storageRoot, resolved)) {
5671
+ candidates.add(resolved);
5672
+ }
5673
+ };
5674
+ const addRelativeCandidates = (relativePath) => {
5675
+ const normalized = relativePath.replace(/\\/g, "/").replace(/^\/+/, "");
5676
+ if (!normalized) return;
5677
+ addCandidate(path8.join(storageRoot, normalized));
5678
+ if (/^\d{4}-\d{2}-\d{2}\//.test(normalized)) {
5679
+ addCandidate(path8.join(storageRoot, "facts", normalized));
5680
+ }
5681
+ };
5682
+ if (path8.isAbsolute(resultPath)) {
5683
+ addCandidate(resultPath);
5684
+ } else {
5685
+ addRelativeCandidates(resultPath);
5686
+ }
5687
+ return [...candidates];
5688
+ }
5689
+ function storageDirOrNull(storage) {
5690
+ const dir = storage.dir;
5691
+ return typeof dir === "string" && dir.length > 0 ? dir : null;
5692
+ }
5693
+ var QmdResultResolver = class {
5694
+ getConfig;
5695
+ storageFor;
5696
+ storageDirNamespace;
5697
+ qmdCollectionNamespaceFromPrefix;
5698
+ namespaceFromPath;
5699
+ constructor(options) {
5700
+ this.getConfig = options.getConfig;
5701
+ this.storageFor = options.storageFor;
5702
+ this.storageDirNamespace = options.storageDirNamespace;
5703
+ this.qmdCollectionNamespaceFromPrefix = options.qmdCollectionNamespaceFromPrefix;
5704
+ this.namespaceFromPath = options.namespaceFromPath;
5705
+ }
5706
+ async readQmdResultMemory(resultPath, fallbackStorage, recallNamespaces = []) {
5707
+ const parts = qmdCollectionPathParts(resultPath);
5708
+ const fallbackStorageDir = storageDirOrNull(fallbackStorage);
5709
+ const config = this.getConfig();
5710
+ const coldCollection = config.qmdColdCollection ?? "openclaw-engram-cold";
5711
+ if (parts && parts.collection === coldCollection) {
5712
+ const storages = [];
5713
+ const seenStorageDirs = /* @__PURE__ */ new Set();
5714
+ const addStorage = (storage) => {
5715
+ const storageDir = storageDirOrNull(storage);
5716
+ const storageKey = storageDir ? path8.resolve(storageDir) : `storage-without-dir-${storages.length}`;
5717
+ if (seenStorageDirs.has(storageKey)) return;
5718
+ seenStorageDirs.add(storageKey);
5719
+ storages.push(storage);
5720
+ };
5721
+ const fallbackNamespace = fallbackStorageDir !== null ? this.storageDirNamespace(fallbackStorageDir) : config.defaultNamespace;
5722
+ if (recallNamespaces.length === 0 || !resolveNamespaceCapabilities(config).namespaces || recallNamespaces.includes(fallbackNamespace)) {
5723
+ addStorage(fallbackStorage);
5724
+ }
5725
+ if (recallNamespaces.length > 0) {
5726
+ for (const namespace of recallNamespaces) {
5727
+ try {
5728
+ addStorage(await this.storageFor(namespace));
5729
+ } catch (err) {
5730
+ log.debug("qmd cold result namespace storage lookup skipped", {
5731
+ path: resultPath,
5732
+ namespace,
5733
+ error: err.message
5734
+ });
5735
+ }
5736
+ }
5737
+ }
5738
+ for (const storage of storages) {
5739
+ const storageDir = storageDirOrNull(storage);
5740
+ if (!storageDir) {
5741
+ const memory = await storage.readMemoryByPath(resultPath);
5742
+ if (memory) return memory;
5743
+ continue;
5744
+ }
5745
+ try {
5746
+ const coldRoot = path8.join(storageDir, "cold");
5747
+ for (const candidate of qmdResultPathCandidates(
5748
+ coldRoot,
5749
+ parts.relativePath
5750
+ )) {
5751
+ const memory = await storage.readMemoryByPath(candidate);
5752
+ if (memory) return memory;
5753
+ }
5754
+ } catch (err) {
5755
+ if (err instanceof SecureStoreLockedError) throw err;
5756
+ log.debug("qmd cold result path lookup failed open", {
5757
+ path: resultPath,
5758
+ collection: coldCollection,
5759
+ error: err.message
5760
+ });
5761
+ }
5762
+ }
5763
+ return null;
5764
+ }
5765
+ const collectionNamespace = parts ? this.qmdCollectionNamespaceFromPrefix(parts.collection) : null;
5766
+ if (parts && collectionNamespace) {
5767
+ try {
5768
+ const collectionStorage = await this.storageFor(collectionNamespace);
5769
+ for (const candidate of qmdResultPathCandidates(
5770
+ collectionStorage.dir,
5771
+ parts.relativePath
5772
+ )) {
5773
+ const memory = await collectionStorage.readMemoryByPath(candidate);
5774
+ if (memory) return memory;
5775
+ }
5776
+ return null;
5777
+ } catch (err) {
5778
+ if (err instanceof SecureStoreLockedError) throw err;
5779
+ log.debug("qmd result namespace path lookup failed open", {
5780
+ path: resultPath,
5781
+ namespace: collectionNamespace,
5782
+ error: err.message
5783
+ });
5784
+ return null;
5785
+ }
5786
+ }
5787
+ if (path8.isAbsolute(resultPath)) {
5788
+ if (!fallbackStorageDir) {
5789
+ return await fallbackStorage.readMemoryByPath(resultPath);
5790
+ }
5791
+ const ownerStorage = await this.storageForAbsoluteQmdResultPath(
5792
+ resultPath,
5793
+ fallbackStorage,
5794
+ recallNamespaces
5795
+ );
5796
+ if (!ownerStorage) return null;
5797
+ for (const candidate of qmdResultPathCandidates(
5798
+ ownerStorage.dir,
5799
+ resultPath
5800
+ )) {
5801
+ const memory = await ownerStorage.storage.readMemoryByPath(candidate);
5802
+ if (memory) return memory;
5803
+ }
5804
+ return null;
5805
+ }
5806
+ if (!fallbackStorageDir) {
5807
+ return await fallbackStorage.readMemoryByPath(resultPath);
5808
+ }
5809
+ for (const candidate of qmdResultPathCandidates(
5810
+ fallbackStorageDir,
5811
+ resultPath
5812
+ )) {
5813
+ const memory = await fallbackStorage.readMemoryByPath(candidate);
5814
+ if (memory) return memory;
5815
+ }
5816
+ return null;
5817
+ }
5818
+ async resolveColdQmdResultForRecall(result, fallbackStorage, recallNamespaces = []) {
5819
+ const memory = await this.readQmdResultMemory(
5820
+ result.path,
5821
+ fallbackStorage,
5822
+ recallNamespaces
5823
+ );
5824
+ if (!memory) return null;
5825
+ let ownerNamespace = null;
5826
+ if (path8.isAbsolute(memory.path)) {
5827
+ const ownerStorage = await this.storageForAbsoluteQmdResultPath(
5828
+ memory.path,
5829
+ fallbackStorage,
5830
+ recallNamespaces
5831
+ );
5832
+ ownerNamespace = ownerStorage?.namespace ?? null;
5833
+ if (!ownerNamespace && resolveNamespaceCapabilities(this.getConfig()).namespaces) return null;
5834
+ }
5835
+ ownerNamespace ??= this.namespaceFromPath(memory.path);
5836
+ if (recallNamespaces.length > 0 && !recallNamespaces.includes(ownerNamespace)) {
5837
+ return null;
5838
+ }
5839
+ return {
5840
+ namespace: ownerNamespace,
5841
+ result: {
5842
+ ...result,
5843
+ docid: result.docid || memory.frontmatter.id,
5844
+ path: memory.path,
5845
+ snippet: result.snippet || memory.content.slice(0, 400)
5846
+ }
5847
+ };
5848
+ }
5849
+ async storageForAbsoluteQmdResultPath(resultPath, fallbackStorage, recallNamespaces = []) {
5850
+ const resolvedPath = path8.resolve(resultPath);
5851
+ const config = this.getConfig();
5852
+ const memoryRoot = path8.resolve(config.memoryDir);
5853
+ const namespacesRoot = path8.join(memoryRoot, "namespaces");
5854
+ const fallbackStorageDir = storageDirOrNull(fallbackStorage);
5855
+ const matches = [];
5856
+ const seenDirs = /* @__PURE__ */ new Set();
5857
+ const maybeAddStorage = (storage, namespace) => {
5858
+ const storageDir = storageDirOrNull(storage);
5859
+ if (!storageDir) return;
5860
+ const candidateRoot = path8.resolve(storageDir);
5861
+ if (seenDirs.has(candidateRoot)) return;
5862
+ if (!isPathInsideStorageRoot(candidateRoot, resolvedPath)) return;
5863
+ if (candidateRoot === memoryRoot && isPathInsideStorageRoot(namespacesRoot, resolvedPath)) {
5864
+ return;
5865
+ }
5866
+ seenDirs.add(candidateRoot);
5867
+ matches.push({ storage, dir: candidateRoot, namespace });
5868
+ };
5869
+ const fallbackNamespace = fallbackStorageDir !== null ? this.storageDirNamespace(fallbackStorageDir) : config.defaultNamespace;
5870
+ maybeAddStorage(fallbackStorage, fallbackNamespace);
5871
+ const candidateNamespaces = /* @__PURE__ */ new Set();
5872
+ candidateNamespaces.add(config.defaultNamespace);
5873
+ candidateNamespaces.add(config.sharedNamespace);
5874
+ for (const ns of recallNamespaces) {
5875
+ candidateNamespaces.add(ns);
5876
+ }
5877
+ if (isPathInsideStorageRoot(namespacesRoot, resolvedPath)) {
5878
+ const relativeToNamespaces = path8.relative(namespacesRoot, resolvedPath);
5879
+ const [namespaceSegment] = relativeToNamespaces.split(/[\\/]/);
5880
+ if (namespaceSegment) {
5881
+ candidateNamespaces.add(
5882
+ namespaceIdentityFromToken(namespaceSegment) ?? namespaceSegment
5883
+ );
5884
+ }
5885
+ }
5886
+ for (const policy of config.namespacePolicies ?? []) {
5887
+ candidateNamespaces.add(policy.name);
5888
+ }
5889
+ for (const ns of candidateNamespaces) {
5890
+ if (!ns) continue;
5891
+ try {
5892
+ maybeAddStorage(await this.storageFor(ns), ns);
5893
+ } catch {
5894
+ continue;
5895
+ }
5896
+ }
5897
+ matches.sort((a, b) => b.dir.length - a.dir.length);
5898
+ return matches[0] ?? null;
5899
+ }
5900
+ };
5901
+
5902
+ // src/access-recall-surface.ts
5903
+ var AccessRecallSurface = class {
5904
+ constructor(deps) {
5905
+ this.deps = deps;
5906
+ }
5907
+ deps;
5908
+ async buildRecallResponseFromXraySnapshot(options) {
5909
+ const memoryIds = options.snapshot.results.map((result) => result.memoryId);
5910
+ const resultPaths = options.snapshot.results.map((result) => result.path);
5911
+ const namespace = options.snapshot.namespace ? this.deps.resolveNamespace(options.snapshot.namespace) : this.deps.orchestrator.config.defaultNamespace;
5912
+ const sourcesUsed = Array.from(
5913
+ new Set(options.snapshot.results.map((result) => result.servedBy))
5914
+ );
5915
+ const snapshotForSerialization = {
5916
+ sessionKey: options.sessionKey ?? "",
5917
+ recordedAt: new Date(options.snapshot.capturedAt).toISOString(),
5918
+ queryHash: createHash4("sha256").update(options.query).digest("hex"),
5919
+ queryLen: options.query.length,
5920
+ memoryIds,
5921
+ namespace,
5922
+ recallNamespaces: [namespace],
5923
+ traceId: options.snapshot.traceId,
5924
+ plannerMode: options.normalizedMode,
5925
+ requestedMode: options.requestedMode && options.requestedMode !== "auto" ? options.requestedMode : void 0,
5926
+ sourcesUsed,
5927
+ budgetsApplied: {
5928
+ appliedTopK: memoryIds.length,
5929
+ recallBudgetChars: options.snapshot.budget.chars,
5930
+ maxMemoryTokens: this.deps.orchestrator.config.maxMemoryTokens,
5931
+ finalContextChars: options.snapshot.budget.used
5932
+ },
5933
+ latencyMs: Date.now() - options.startedAt,
5934
+ resultPaths
5935
+ };
5936
+ const results = await this.deps.serializeRecallResults(
5937
+ snapshotForSerialization,
5938
+ options.disclosure,
5939
+ {
5940
+ query: options.query,
5941
+ ...options.sessionKey ? { sessionKey: options.sessionKey } : {},
5942
+ ...options.rawExcerptNamespace ? { rawExcerptNamespace: options.rawExcerptNamespace } : {},
5943
+ ...options.rawExcerptSessionIds !== void 0 ? { rawExcerptSessionIds: options.rawExcerptSessionIds } : {},
5944
+ ...options.rawExcerptsSuppressed ? { rawExcerptsSuppressed: options.rawExcerptsSuppressed } : {}
5945
+ }
5946
+ );
5947
+ const context = results.map((result) => {
5948
+ const content = typeof result.content === "string" && result.content.length > 0 ? result.content : "";
5949
+ return content || result.preview;
5950
+ }).filter((text) => text.length > 0).join("\n\n");
5951
+ return {
5952
+ query: options.query,
5953
+ ...options.sessionKey ? { sessionKey: options.sessionKey } : {},
5954
+ namespace,
5955
+ context,
5956
+ count: memoryIds.length,
5957
+ memoryIds,
5958
+ results,
5959
+ recordedAt: snapshotForSerialization.recordedAt,
5960
+ traceId: options.snapshot.traceId,
5961
+ plannerMode: options.normalizedMode,
5962
+ fallbackUsed: sourcesUsed.some((source) => source !== "hybrid"),
5963
+ sourcesUsed,
5964
+ disclosure: options.disclosure,
5965
+ budgetsApplied: snapshotForSerialization.budgetsApplied,
5966
+ latencyMs: snapshotForSerialization.latencyMs
5967
+ };
5968
+ }
5969
+ async serializeRecallResults(snapshot, disclosure, rawContext = null) {
5970
+ if (!snapshot) return [];
5971
+ const namespace = snapshot.namespace ? this.deps.resolveNamespace(snapshot.namespace) : this.deps.orchestrator.config.defaultNamespace;
5972
+ const storage = await this.deps.orchestrator.getStorage(namespace);
5973
+ const storageDir = storage.dir;
5974
+ const recallNamespaces = Array.from(
5975
+ new Set(
5976
+ [
5977
+ namespace,
5978
+ ...Array.isArray(snapshot.recallNamespaces) ? snapshot.recallNamespaces.map((ns) => this.deps.resolveNamespace(ns)) : [],
5979
+ this.deps.orchestrator.config.defaultNamespace,
5980
+ this.deps.orchestrator.config.sharedNamespace,
5981
+ ...(this.deps.orchestrator.config.namespacePolicies ?? []).map((p) => p.name)
5982
+ ].filter((ns) => typeof ns === "string" && ns.length > 0)
5983
+ )
5984
+ );
5985
+ const results = [];
5986
+ const seen = /* @__PURE__ */ new Set();
5987
+ const collectionNamespaceFromPrefix = (collectionPrefix) => {
5988
+ const baseCollection = this.deps.orchestrator.config.qmdCollection;
5989
+ if (collectionPrefix === baseCollection) return this.deps.orchestrator.config.defaultNamespace;
5990
+ const namespaceSuffix = collectionPrefix.startsWith(`${baseCollection}--`) ? collectionPrefix.slice(baseCollection.length + 2) : "";
5991
+ if (!namespaceSuffix) return null;
5992
+ const decoded = namespaceIdentityFromToken(namespaceSuffix);
5993
+ if (decoded !== null) return decoded || this.deps.orchestrator.config.defaultNamespace;
5994
+ if (namespaceSuffix.startsWith("ns--")) {
5995
+ const legacyNamespace = namespaceSuffix.slice("ns--".length).trim();
5996
+ return legacyNamespace || null;
5997
+ }
5998
+ return null;
5999
+ };
6000
+ const readResultPath = async (memoryPath) => {
6001
+ const parts = qmdCollectionPathParts(memoryPath);
6002
+ const coldCollection = this.deps.orchestrator.config.qmdColdCollection ?? "openclaw-engram-cold";
6003
+ if (parts && parts.collection === coldCollection) {
6004
+ const storages = [];
6005
+ const seenStorageDirs = /* @__PURE__ */ new Set();
6006
+ const addStorage = (candidateStorage) => {
6007
+ const candidateDir = nodePath2.resolve(candidateStorage.dir);
6008
+ if (seenStorageDirs.has(candidateDir)) return;
6009
+ seenStorageDirs.add(candidateDir);
6010
+ storages.push({ storage: candidateStorage, dir: candidateDir });
6011
+ };
6012
+ addStorage(storage);
6013
+ for (const recallNamespace of recallNamespaces) {
6014
+ try {
6015
+ addStorage(await this.deps.orchestrator.getStorage(recallNamespace));
6016
+ } catch {
6017
+ continue;
6018
+ }
6019
+ }
6020
+ for (const candidateStorage of storages) {
6021
+ try {
6022
+ const coldRoot = nodePath2.join(candidateStorage.dir, "cold");
6023
+ for (const candidatePath of qmdResultPathCandidates(
6024
+ coldRoot,
6025
+ parts.relativePath
6026
+ )) {
6027
+ const memory = await candidateStorage.storage.readMemoryByPath(candidatePath);
6028
+ if (memory) return { memory, baseDir: candidateStorage.dir };
6029
+ }
6030
+ } catch (err) {
6031
+ if (err instanceof SecureStoreLockedError) throw err;
6032
+ continue;
6033
+ }
6034
+ }
6035
+ return null;
6036
+ }
6037
+ const collectionNamespace = parts ? collectionNamespaceFromPrefix(parts.collection) : null;
6038
+ if (parts && collectionNamespace) {
6039
+ try {
6040
+ const collectionStorage = await this.deps.orchestrator.getStorage(collectionNamespace);
6041
+ for (const candidate of qmdResultPathCandidates(
6042
+ collectionStorage.dir,
6043
+ parts.relativePath
6044
+ )) {
6045
+ const memory = await collectionStorage.readMemoryByPath(candidate);
6046
+ if (memory) return { memory, baseDir: collectionStorage.dir };
6047
+ }
6048
+ return null;
6049
+ } catch (err) {
6050
+ if (err instanceof SecureStoreLockedError) throw err;
6051
+ return null;
6052
+ }
6053
+ }
6054
+ if (nodePath2.isAbsolute(memoryPath)) {
6055
+ const ownerStorage = await this.deps.storageForAbsoluteRecallPath(
6056
+ memoryPath,
6057
+ namespace,
6058
+ recallNamespaces
6059
+ );
6060
+ if (!ownerStorage) return null;
6061
+ for (const candidate of qmdResultPathCandidates(
6062
+ ownerStorage.dir,
6063
+ memoryPath
6064
+ )) {
6065
+ const memory = await ownerStorage.storage.readMemoryByPath(candidate);
6066
+ if (memory) return { memory, baseDir: ownerStorage.dir };
6067
+ }
6068
+ return null;
6069
+ }
6070
+ for (const candidate of qmdResultPathCandidates(storageDir, memoryPath)) {
6071
+ const memory = await storage.readMemoryByPath(candidate);
6072
+ if (memory) return { memory, baseDir: storageDir };
6073
+ }
6074
+ return null;
6075
+ };
6076
+ const rawExcerptsResult = rawContext?.rawExcerptsSuppressed === true ? (
6077
+ // Implicit raw recall with NO readable LCM namespace (#1505 thread
6078
+ // NBHWz): emit empty excerpts rather than falling back to the
6079
+ // write/overlay `namespace` the read gate excludes.
6080
+ []
6081
+ ) : await this.deps.fetchRawExcerpts(
6082
+ disclosure,
6083
+ rawContext ? {
6084
+ query: rawContext.query,
6085
+ ...rawContext.sessionKey ? { sessionKey: rawContext.sessionKey } : {},
6086
+ namespace: rawContext.rawExcerptNamespace ?? namespace,
6087
+ ...rawContext.rawExcerptSessionIds !== void 0 ? { lcmSessionIds: rawContext.rawExcerptSessionIds } : {}
6088
+ } : null
6089
+ );
6090
+ const rawExcerpts = rawExcerptsResult ?? void 0;
6091
+ for (const memoryPath of snapshot.resultPaths ?? []) {
6092
+ if (!memoryPath || seen.has(memoryPath)) continue;
6093
+ const resolved = await readResultPath(memoryPath);
6094
+ if (!resolved) continue;
6095
+ const { memory, baseDir } = resolved;
6096
+ seen.add(memoryPath);
6097
+ results.push(
6098
+ this.deps.serializeMemorySummary(
6099
+ memory,
6100
+ baseDir,
6101
+ disclosure,
6102
+ // Attach the (possibly empty) raw excerpts to the first raw
6103
+ // result; subsequent results do not duplicate the array.
6104
+ results.length === 0 ? rawExcerpts : void 0
6105
+ )
6106
+ );
6107
+ }
6108
+ if (results.length > 0) return results;
6109
+ for (const memoryId of snapshot.memoryIds) {
6110
+ const memory = await storage.getMemoryById(memoryId);
6111
+ if (!memory || seen.has(memory.path)) continue;
6112
+ seen.add(memory.path);
6113
+ results.push(
6114
+ this.deps.serializeMemorySummary(
6115
+ memory,
6116
+ storageDir,
6117
+ disclosure,
6118
+ results.length === 0 ? rawExcerpts : void 0
6119
+ )
6120
+ );
6121
+ }
6122
+ return results;
6123
+ }
6124
+ /**
6125
+ * Fetch raw transcript excerpts from the LCM archive for `disclosure ===
6126
+ * "raw"` recalls (issue #677 PR 2/4). Returns `null` for non-raw recall
6127
+ * depths, an empty array when LCM is disabled / not initialized / has no
6128
+ * matches, and an array of LCM-side excerpts otherwise. Errors are
6129
+ * swallowed and treated as "no excerpts" so a failing LCM never breaks
6130
+ * the recall response.
6131
+ *
6132
+ * Namespace handling: LCM archival prefixes non-default-namespace
6133
+ * sessions with `${namespace}:${sessionKey}` (see `observe()` around
6134
+ * line 2498), so the lookup must mirror that prefix or raw recalls in
6135
+ * non-default namespaces miss their own excerpts.
6136
+ */
6137
+ async fetchRawExcerpts(disclosure, context) {
6138
+ if (disclosure !== "raw") return null;
6139
+ if (!context || !context.query) return [];
6140
+ if (!context.sessionKey) return [];
6141
+ const lcm = this.deps.orchestrator.lcmEngine;
6142
+ if (!lcm || !lcm.enabled) return [];
6143
+ try {
6144
+ const legacyKey = context.namespace && context.namespace !== this.deps.orchestrator.config.defaultNamespace ? `${context.namespace}:${context.sessionKey}` : context.sessionKey;
6145
+ const lcmSessionIds = context.lcmSessionIds !== void 0 ? context.lcmSessionIds : [legacyKey];
6146
+ const limit = 5;
6147
+ const seenRows = /* @__PURE__ */ new Set();
6148
+ const excerpts = [];
6149
+ const settledRows = await Promise.allSettled(
6150
+ lcmSessionIds.map(
6151
+ async (lcmSessionKey) => lcm.searchContextFull(context.query, limit, lcmSessionKey)
6152
+ )
6153
+ );
6154
+ for (const result of settledRows) {
6155
+ if (excerpts.length >= limit) break;
6156
+ if (result.status !== "fulfilled") continue;
6157
+ for (const r of result.value) {
6158
+ const dedupeKey = `${r.session_id} ${r.turn_index}`;
6159
+ if (seenRows.has(dedupeKey)) continue;
6160
+ seenRows.add(dedupeKey);
6161
+ excerpts.push({
6162
+ turnIndex: r.turn_index,
6163
+ role: r.role,
6164
+ content: r.content,
6165
+ sessionId: r.session_id
6166
+ });
6167
+ if (excerpts.length >= limit) break;
6168
+ }
6169
+ }
6170
+ return excerpts;
6171
+ } catch {
6172
+ return [];
6173
+ }
6174
+ }
6175
+ async executeRecall(request) {
6176
+ const query = request.query;
6177
+ const callerProvidedDisclosure = request.disclosure !== void 0 && request.disclosure !== null;
6178
+ const requestedDisclosure = (() => {
6179
+ if (!callerProvidedDisclosure) {
6180
+ return DEFAULT_RECALL_DISCLOSURE;
6181
+ }
6182
+ if (!isRecallDisclosure(request.disclosure)) {
6183
+ throw new EngramAccessInputError(
6184
+ `disclosure must be one of: chunk, section, raw (got: ${String(request.disclosure)})`
6185
+ );
6186
+ }
6187
+ return request.disclosure;
6188
+ })();
6189
+ if (request.codingContext !== void 0 && request.sessionKey) {
6190
+ this.deps.setCodingContext({
6191
+ sessionKey: request.sessionKey,
6192
+ codingContext: request.codingContext
6193
+ });
6194
+ }
6195
+ if (request.codingContext === void 0 && request.sessionKey) {
6196
+ await this.deps.maybeAttachCodingContext(request.sessionKey, {
6197
+ cwd: request.cwd,
6198
+ projectTag: request.projectTag
6199
+ });
6200
+ }
6201
+ const authenticatedPrincipal = request.authenticatedPrincipal?.trim();
6202
+ const namespaceOverride = this.deps.resolveRecallNamespace(
6203
+ request.namespace,
6204
+ request.sessionKey,
6205
+ authenticatedPrincipal
6206
+ );
6207
+ const namespace = namespaceOverride ?? this.deps.orchestrator.config.defaultNamespace;
6208
+ const mode = this.deps.normalizeRecallMode(request.mode);
6209
+ const maybePrincipal = this.deps.resolveRequestPrincipal(request.sessionKey, authenticatedPrincipal);
6210
+ if (resolveNamespaceCapabilities(this.deps.orchestrator.config).namespaces && !maybePrincipal) {
6211
+ throw new EngramAccessInputError(
6212
+ "authentication required: namespaces are enabled and no principal was supplied"
6213
+ );
6214
+ }
6215
+ const principal = maybePrincipal ?? "default";
6216
+ const principalNamespace = defaultNamespaceForPrincipal(principal, this.deps.orchestrator.config);
6217
+ const profileCodingContext = request.sessionKey && typeof this.deps.orchestrator.getCodingContextForSession === "function" ? this.deps.orchestrator.getCodingContextForSession(request.sessionKey) : null;
6218
+ const profileCodingOverlay = !namespaceOverride && profileCodingContext && resolveNamespaceCapabilities(this.deps.orchestrator.config).namespaces && this.deps.orchestrator.config.codingMode?.projectScope ? resolveCodingNamespaceOverlay(
6219
+ profileCodingContext,
6220
+ this.deps.orchestrator.config.codingMode,
6221
+ this.deps.orchestrator.config.defaultNamespace
6222
+ ) : null;
6223
+ const profilePlan = namespaceOverride ? null : resolveScopeProfilePlan({
6224
+ config: this.deps.orchestrator.config,
6225
+ principal,
6226
+ codingContext: profileCodingContext,
6227
+ codingOverlay: profileCodingOverlay
6228
+ });
6229
+ const modeSkipsBudget = mode === "no_recall";
6230
+ const legacyRecallNamespaces = Array.isArray(this.deps.orchestrator.config.defaultRecallNamespaces) ? recallNamespacesForPrincipal(principal, this.deps.orchestrator.config) : [];
6231
+ const effectiveNamespaces = namespaceOverride ? [namespaceOverride] : profilePlan ? expandScopeProfileReadNamespaces({
6232
+ profilePlan,
6233
+ principalSelfNamespace: profilePlan.baseNamespace,
6234
+ config: this.deps.orchestrator.config,
6235
+ principal,
6236
+ codingOverlay: profileCodingOverlay,
6237
+ legacyRecallNamespaces
6238
+ }) : legacyRecallNamespaces;
6239
+ const budgetPrincipalNamespace = profilePlan?.baseNamespace ?? principalNamespace;
6240
+ let budgetDecision;
6241
+ let recordBudgetAfterSuccess = false;
6242
+ if (modeSkipsBudget) {
6243
+ budgetDecision = {
6244
+ allowed: true,
6245
+ reason: "allowed-same-namespace",
6246
+ count: 0,
6247
+ limit: {
6248
+ softLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetSoftLimit ?? 10,
6249
+ hardLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetHardLimit ?? 30,
6250
+ windowMs: this.deps.orchestrator.config.recallCrossNamespaceBudgetWindowMs ?? 6e4
6251
+ }
6252
+ };
6253
+ } else {
6254
+ let anyCrossNamespace = false;
6255
+ let denied = null;
6256
+ let crossNamespaceDecision = null;
6257
+ for (const ns of effectiveNamespaces) {
6258
+ const peek = this.deps.budget.peek({
6259
+ principal,
6260
+ principalNamespace: budgetPrincipalNamespace,
6261
+ queryNamespace: ns
6262
+ });
6263
+ if (peek.reason !== "allowed-same-namespace") {
6264
+ anyCrossNamespace = true;
6265
+ crossNamespaceDecision ??= peek;
6266
+ }
6267
+ if (!peek.allowed) {
6268
+ denied = peek;
6269
+ break;
6270
+ }
6271
+ }
6272
+ if (denied) {
6273
+ budgetDecision = denied;
6274
+ } else if (anyCrossNamespace) {
6275
+ budgetDecision = crossNamespaceDecision ?? {
6276
+ allowed: true,
6277
+ reason: "allowed-under-soft",
6278
+ count: 0,
6279
+ limit: {
6280
+ softLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetSoftLimit ?? 10,
6281
+ hardLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetHardLimit ?? 30,
6282
+ windowMs: this.deps.orchestrator.config.recallCrossNamespaceBudgetWindowMs ?? 6e4
6283
+ }
6284
+ };
6285
+ recordBudgetAfterSuccess = true;
6286
+ } else {
6287
+ budgetDecision = {
6288
+ allowed: true,
6289
+ reason: "allowed-same-namespace",
6290
+ count: 0,
6291
+ limit: {
6292
+ softLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetSoftLimit ?? 10,
6293
+ hardLimit: this.deps.orchestrator.config.recallCrossNamespaceBudgetHardLimit ?? 30,
6294
+ windowMs: this.deps.orchestrator.config.recallCrossNamespaceBudgetWindowMs ?? 6e4
6295
+ }
6296
+ };
6297
+ }
6298
+ if (!budgetDecision.allowed) {
6299
+ throw new EngramAccessInputError(
6300
+ `recall denied: cross-namespace budget exceeded (${budgetDecision.count}/${budgetDecision.limit.hardLimit} in ${budgetDecision.limit.windowMs}ms window)`
6301
+ );
6302
+ }
6303
+ this.deps.budget.gc();
6304
+ }
6305
+ const topK = Number.isFinite(request.topK) ? Math.max(0, Math.floor(request.topK ?? 0)) : void 0;
6306
+ let asOf;
6307
+ if (request.asOf !== void 0 && request.asOf !== null) {
6308
+ if (typeof request.asOf !== "string" || request.asOf.trim().length === 0) {
6309
+ throw new EngramAccessInputError(
6310
+ "asOf must be a non-empty ISO 8601 timestamp string"
6311
+ );
6312
+ }
6313
+ const parsed = Date.parse(request.asOf);
6314
+ if (!Number.isFinite(parsed)) {
6315
+ throw new EngramAccessInputError(
6316
+ `asOf must be a parseable ISO 8601 timestamp (got: "${request.asOf}")`
6317
+ );
6318
+ }
6319
+ asOf = request.asOf;
6320
+ }
6321
+ const recallOptions = {
6322
+ namespace: namespaceOverride,
6012
6323
  topK,
6013
6324
  mode,
6014
6325
  ...authenticatedPrincipal ? { principalOverride: authenticatedPrincipal } : {},
@@ -6554,6 +6865,16 @@ var EngramAccessService = class {
6554
6865
  budgetLocks = /* @__PURE__ */ new Map();
6555
6866
  budget;
6556
6867
  auditAdapter;
6868
+ /** AccessObserveWriteSurface (access-service decomposition). Lazy; selfDeps live wiring. */
6869
+ _accessObserveWriteSurface;
6870
+ get accessObserveWriteSurface() {
6871
+ if (!this._accessObserveWriteSurface) {
6872
+ this._accessObserveWriteSurface = new AccessObserveWriteSurface(
6873
+ selfDeps(this)
6874
+ );
6875
+ }
6876
+ return this._accessObserveWriteSurface;
6877
+ }
6557
6878
  /** AccessLcmSurface (access-service decomposition). Lazy; selfDeps live wiring. */
6558
6879
  _accessLcmSurface;
6559
6880
  get accessLcmSurface() {
@@ -7291,114 +7612,20 @@ var EngramAccessService = class {
7291
7612
  });
7292
7613
  }
7293
7614
  async qmdHealth(searchBackend, qmdEnabled, namespace, collection) {
7294
- if (searchBackend !== "qmd" || !qmdEnabled) {
7295
- return {
7296
- enabled: qmdEnabled,
7297
- active: false,
7298
- degraded: false,
7299
- mode: searchBackend !== "qmd" ? "not-selected" : "disabled",
7300
- collection,
7301
- collectionState: "skipped",
7302
- installedVersion: null,
7303
- supportedVersion: null,
7304
- supported: null,
7305
- upgradeAvailable: null,
7306
- doctorAvailable: null,
7307
- debugStatus: searchBackend !== "qmd" ? `backend=${searchBackend}` : "backend=disabled"
7308
- };
7309
- }
7310
- if (resolveNamespaceCapabilities(this.orchestrator.config).namespaces === true) {
7311
- const namespaceHealth = await this.namespaceQmdHealth(searchBackend, qmdEnabled, namespace, collection);
7312
- if (namespaceHealth) return namespaceHealth;
7313
- }
7314
- const qmd = this.orchestrator.qmd;
7315
- if (!qmd) {
7316
- return {
7317
- enabled: true,
7318
- active: false,
7319
- degraded: true,
7320
- mode: "fallback",
7321
- collection,
7322
- collectionState: "unknown",
7323
- installedVersion: null,
7324
- supportedVersion: null,
7325
- supported: null,
7326
- upgradeAvailable: null,
7327
- doctorAvailable: null,
7328
- debugStatus: "backend=unavailable"
7329
- };
7330
- }
7331
- const diagnosticAvailable = await this.qmdProbeAvailable(searchBackend, qmdEnabled);
7332
- const operationalAvailable = diagnosticAvailable || qmd.isAvailable();
7333
- const collectionState = diagnosticAvailable ? await this.qmdCollectionState(searchBackend, qmdEnabled, collection) : "unknown";
7334
- const active = operationalAvailable && collectionState !== "missing";
7335
- const degraded = searchBackend === "qmd" && qmdEnabled && (!active || !diagnosticAvailable || collectionState === "unknown");
7336
- const debugStatus = qmd.debugStatus();
7337
- const versionStatus = "getVersionStatus" in qmd && typeof qmd.getVersionStatus === "function" ? qmd.getVersionStatus() : null;
7338
- const daemonMode = "isDaemonMode" in qmd && typeof qmd.isDaemonMode === "function" ? qmd.isDaemonMode() === true : false;
7339
- const mode = searchBackend !== "qmd" ? "not-selected" : !qmdEnabled ? "disabled" : !active ? "fallback" : daemonMode ? "daemon" : "cli";
7340
- return {
7341
- enabled: qmdEnabled,
7342
- active,
7343
- degraded,
7344
- mode,
7345
- collection,
7346
- collectionState,
7347
- installedVersion: versionStatus?.installedVersion ?? null,
7348
- supportedVersion: versionStatus?.supportedVersion ?? null,
7349
- supported: versionStatus?.supported ?? null,
7350
- upgradeAvailable: versionStatus?.upgradeAvailable ?? null,
7351
- doctorAvailable: versionStatus?.capabilities?.doctor ?? null,
7352
- debugStatus
7353
- };
7615
+ return this.accessObserveWriteSurface.qmdHealth(
7616
+ searchBackend,
7617
+ qmdEnabled,
7618
+ namespace,
7619
+ collection
7620
+ );
7354
7621
  }
7355
7622
  async namespaceQmdHealth(searchBackend, qmdEnabled, namespace, fallbackCollection) {
7356
- if (searchBackend !== "qmd" || !qmdEnabled) return null;
7357
- const searchHealthForNamespace = this.orchestrator.searchHealthForNamespace;
7358
- if (typeof searchHealthForNamespace !== "function") return null;
7359
- const controller = new AbortController();
7360
- const timer = setTimeout(() => controller.abort(), 2e3);
7361
- timer.unref?.();
7362
- try {
7363
- const health = await searchHealthForNamespace.call(this.orchestrator, namespace, {
7364
- signal: controller.signal
7365
- });
7366
- const active = health.available && health.collectionState !== "missing";
7367
- const degraded = !active || health.collectionState === "unknown";
7368
- const mode = !active ? "fallback" : health.daemonMode === true ? "daemon" : "cli";
7369
- return {
7370
- enabled: true,
7371
- active,
7372
- degraded,
7373
- mode,
7374
- collection: health.collection || fallbackCollection,
7375
- collectionState: health.collectionState,
7376
- installedVersion: health.installedVersion,
7377
- supportedVersion: health.supportedVersion,
7378
- supported: health.supported,
7379
- upgradeAvailable: health.upgradeAvailable,
7380
- doctorAvailable: health.doctorAvailable,
7381
- debugStatus: health.debugStatus
7382
- };
7383
- } catch (error) {
7384
- const detail = displayErrorDetail(error) || "unknown";
7385
- return {
7386
- enabled: true,
7387
- active: false,
7388
- degraded: true,
7389
- mode: "fallback",
7390
- collection: fallbackCollection,
7391
- collectionState: "unknown",
7392
- installedVersion: null,
7393
- supportedVersion: null,
7394
- supported: null,
7395
- upgradeAvailable: null,
7396
- doctorAvailable: null,
7397
- debugStatus: `backend=namespace-unavailable error=${detail}`
7398
- };
7399
- } finally {
7400
- clearTimeout(timer);
7401
- }
7623
+ return this.accessObserveWriteSurface.namespaceQmdHealth(
7624
+ searchBackend,
7625
+ qmdEnabled,
7626
+ namespace,
7627
+ fallbackCollection
7628
+ );
7402
7629
  }
7403
7630
  async qmdCollectionState(searchBackend, qmdEnabled, collection) {
7404
7631
  if (searchBackend !== "qmd" || !qmdEnabled) return "skipped";
@@ -7453,70 +7680,14 @@ var EngramAccessService = class {
7453
7680
  if (memories.length === 0) {
7454
7681
  return this.orchestrator.generateDaySummaryAuto(namespace, {
7455
7682
  timeZone: request.timeZone
7456
- });
7457
- }
7458
- return this.orchestrator.generateDaySummary(memories);
7459
- }
7460
- /**
7461
- * Build a daily context briefing. Gracefully degrades when the OpenAI key
7462
- * or Responses API is unavailable — never throws for LLM-related problems.
7463
- */
7464
- async briefing(request) {
7465
- const config = this.orchestrator.config;
7466
- if (!config.briefing.enabled) {
7467
- throw new EngramAccessInputError("briefing is disabled");
7468
- }
7469
- const namespace = this.resolveReadableNamespace(request.namespace, request.principal);
7470
- const storage = await this.orchestrator.getStorage(namespace);
7471
- const token = typeof request.since === "string" && request.since.trim().length > 0 ? request.since.trim() : config.briefing.defaultWindow;
7472
- const window = parseBriefingWindow(token);
7473
- if (!window) {
7474
- throw new EngramAccessInputError(`invalid briefing window: ${token}`);
7475
- }
7476
- const rawFocus = typeof request.focus === "string" ? request.focus.trim() : "";
7477
- let focus = null;
7478
- if (rawFocus.length > 0) {
7479
- focus = parseBriefingFocus(rawFocus);
7480
- if (!focus) {
7481
- throw new EngramAccessInputError(
7482
- `invalid briefing focus filter: ${request.focus}`
7483
- );
7484
- }
7485
- }
7486
- const SUPPORTED_FORMATS = ["markdown", "json"];
7487
- if (typeof request.format === "string" && !SUPPORTED_FORMATS.includes(request.format)) {
7488
- throw new EngramAccessInputError(
7489
- `unsupported briefing format: "${request.format}". Accepted: ${SUPPORTED_FORMATS.join(", ")}.`
7490
- );
7683
+ });
7491
7684
  }
7492
- const format = request.format === "json" ? "json" : request.format === "markdown" ? "markdown" : config.briefing.defaultFormat;
7493
- const maxFollowups = typeof request.maxFollowups === "number" && Number.isFinite(request.maxFollowups) ? Math.max(0, Math.min(10, Math.floor(request.maxFollowups))) : config.briefing.maxFollowups;
7494
- const calendarSource = config.briefing.calendarSource ? new FileCalendarSource(config.briefing.calendarSource) : void 0;
7495
- const result = await buildBriefing({
7496
- storage,
7497
- namespace,
7498
- window,
7499
- focus,
7500
- calendarSource,
7501
- maxFollowups,
7502
- allowLlm: config.briefing.llmFollowups,
7503
- openaiApiKey: config.openaiApiKey,
7504
- openaiBaseUrl: config.openaiBaseUrl,
7505
- model: config.model,
7506
- // Without a direct OpenAI key, route follow-ups through the configured
7507
- // LLM chain (gateway model source or local LLM) — same fallback every
7508
- // other LLM feature uses. A configured key keeps its precedence so
7509
- // existing deployments are unchanged.
7510
- followupGenerator: config.openaiApiKey ? void 0 : this.orchestrator.briefingChainFollowupGenerator
7511
- });
7512
- return {
7513
- format,
7514
- window: result.window,
7515
- namespace,
7516
- markdown: result.markdown,
7517
- json: result.json,
7518
- followupsUnavailableReason: result.followupsUnavailableReason
7519
- };
7685
+ return this.orchestrator.generateDaySummary(memories);
7686
+ }
7687
+ async briefing(request) {
7688
+ return this.accessObserveWriteSurface.briefing(
7689
+ request
7690
+ );
7520
7691
  }
7521
7692
  /**
7522
7693
  * Attach a coding context to a session (issue #569). Used by the Claude
@@ -7742,63 +7913,10 @@ var EngramAccessService = class {
7742
7913
  // per-tenant) do not block each other.
7743
7914
  xrayQueue = Promise.resolve();
7744
7915
  async memoryStore(request, hooks) {
7745
- const namespace = await this.resolveCodingScopedWriteNamespace(request);
7746
- const schemaVersion = request.schemaVersion ?? ENGRAM_ACCESS_WRITE_SCHEMA_VERSION;
7747
- if (schemaVersion !== ENGRAM_ACCESS_WRITE_SCHEMA_VERSION) {
7748
- throw new EngramAccessInputError(`unsupported schemaVersion: ${schemaVersion}`);
7749
- }
7750
- const execute = async () => {
7751
- const candidate = this.validateWriteCandidate(request, namespace);
7752
- if (request.dryRun === true) {
7753
- return {
7754
- schemaVersion: ENGRAM_ACCESS_WRITE_SCHEMA_VERSION,
7755
- operation: "memory_store",
7756
- namespace,
7757
- dryRun: true,
7758
- accepted: true,
7759
- queued: false,
7760
- status: "validated",
7761
- idempotencyKey: request.idempotencyKey?.trim() || void 0
7762
- };
7763
- }
7764
- const result = await persistExplicitCapture(this.orchestrator, candidate, "memory_store");
7765
- await this.attachCodingContextAfterScopedWrite(request);
7766
- const blocked = result.tombstoneBlocked === true;
7767
- const response = {
7768
- schemaVersion: ENGRAM_ACCESS_WRITE_SCHEMA_VERSION,
7769
- operation: "memory_store",
7770
- namespace,
7771
- dryRun: false,
7772
- accepted: true,
7773
- queued: blocked,
7774
- status: blocked ? "queued_for_review" : result.duplicateOf ? "duplicate" : "stored",
7775
- memoryId: result.id,
7776
- duplicateOf: result.duplicateOf,
7777
- idempotencyKey: request.idempotencyKey?.trim() || void 0
7778
- };
7779
- log.info(
7780
- `access-write op=memory_store namespace=${namespace} dryRun=false status=${response.status} memoryId=${response.memoryId ?? "-"} idempotency=${response.idempotencyKey ? "yes" : "no"}`
7781
- );
7782
- return response;
7783
- };
7784
- return this.handleIdempotentWrite({
7785
- operation: "memory_store",
7786
- idempotencyKey: request.idempotencyKey,
7787
- requestFingerprint: {
7788
- schemaVersion,
7789
- content: request.content,
7790
- category: request.category,
7791
- confidence: request.confidence,
7792
- namespace,
7793
- tags: request.tags,
7794
- entityRef: request.entityRef,
7795
- ttl: request.ttl,
7796
- sourceReason: request.sourceReason
7797
- },
7798
- skip: request.dryRun === true,
7799
- beforeExecute: hooks?.enforceWriteQuota,
7800
- execute
7801
- });
7916
+ return this.accessObserveWriteSurface.memoryStore(
7917
+ request,
7918
+ hooks
7919
+ );
7802
7920
  }
7803
7921
  async peekMemoryStoreIdempotency(request) {
7804
7922
  const namespace = await this.resolveCodingScopedWriteNamespace(request);
@@ -7824,67 +7942,10 @@ var EngramAccessService = class {
7824
7942
  });
7825
7943
  }
7826
7944
  async suggestionSubmit(request, hooks) {
7827
- const namespace = await this.resolveCodingScopedWriteNamespace(request);
7828
- const schemaVersion = request.schemaVersion ?? ENGRAM_ACCESS_WRITE_SCHEMA_VERSION;
7829
- if (schemaVersion !== ENGRAM_ACCESS_WRITE_SCHEMA_VERSION) {
7830
- throw new EngramAccessInputError(`unsupported schemaVersion: ${schemaVersion}`);
7831
- }
7832
- const execute = async () => {
7833
- const candidate = this.validateWriteCandidate(request, namespace);
7834
- if (request.dryRun === true) {
7835
- return {
7836
- schemaVersion: ENGRAM_ACCESS_WRITE_SCHEMA_VERSION,
7837
- operation: "suggestion_submit",
7838
- namespace,
7839
- dryRun: true,
7840
- accepted: true,
7841
- queued: true,
7842
- status: "validated",
7843
- idempotencyKey: request.idempotencyKey?.trim() || void 0
7844
- };
7845
- }
7846
- const result = await queueExplicitCaptureForReview(
7847
- this.orchestrator,
7848
- candidate,
7849
- "suggestion_submit",
7850
- new Error(request.sourceReason?.trim() || "submitted via engram suggestion_submit")
7851
- );
7852
- await this.attachCodingContextAfterScopedWrite(request);
7853
- const response = {
7854
- schemaVersion: ENGRAM_ACCESS_WRITE_SCHEMA_VERSION,
7855
- operation: "suggestion_submit",
7856
- namespace,
7857
- dryRun: false,
7858
- accepted: true,
7859
- queued: true,
7860
- status: "queued_for_review",
7861
- memoryId: result.id,
7862
- duplicateOf: result.duplicateOf,
7863
- idempotencyKey: request.idempotencyKey?.trim() || void 0
7864
- };
7865
- log.info(
7866
- `access-write op=suggestion_submit namespace=${namespace} dryRun=false status=${response.status} memoryId=${response.memoryId ?? "-"} idempotency=${response.idempotencyKey ? "yes" : "no"}`
7867
- );
7868
- return response;
7869
- };
7870
- return this.handleIdempotentWrite({
7871
- operation: "suggestion_submit",
7872
- idempotencyKey: request.idempotencyKey,
7873
- requestFingerprint: {
7874
- schemaVersion,
7875
- content: request.content,
7876
- category: request.category,
7877
- confidence: request.confidence,
7878
- namespace,
7879
- tags: request.tags,
7880
- entityRef: request.entityRef,
7881
- ttl: request.ttl,
7882
- sourceReason: request.sourceReason
7883
- },
7884
- skip: request.dryRun === true,
7885
- beforeExecute: hooks?.enforceWriteQuota,
7886
- execute
7887
- });
7945
+ return this.accessObserveWriteSurface.suggestionSubmit(
7946
+ request,
7947
+ hooks
7948
+ );
7888
7949
  }
7889
7950
  async peekSuggestionSubmitIdempotency(request) {
7890
7951
  const namespace = await this.resolveCodingScopedWriteNamespace(request);
@@ -8698,145 +8759,34 @@ var EngramAccessService = class {
8698
8759
  }
8699
8760
  serializeMemorySummary(memory, baseDir, disclosure, rawExcerpts) {
8700
8761
  return shapeMemorySummary(memory, baseDir, disclosure, rawExcerpts);
8701
- }
8702
- async observe(request, hooks) {
8703
- const idempotencyKey = request.idempotencyKey?.trim();
8704
- let effectiveCodingContext;
8705
- if (idempotencyKey && !(typeof request.namespace === "string" && request.namespace.trim().length > 0)) {
8706
- effectiveCodingContext = (typeof this.orchestrator.getCodingContextForSession === "function" ? this.orchestrator.getCodingContextForSession(request.sessionKey) : null) ?? await this.resolveCodingContextFromOptions(request);
8707
- }
8708
- return this.handleIdempotentWrite({
8709
- operation: "observe",
8710
- idempotencyKey: request.idempotencyKey,
8711
- requestFingerprint: {
8712
- sessionKey: request.sessionKey,
8713
- messages: request.messages,
8714
- namespace: request.namespace,
8715
- skipExtraction: request.skipExtraction,
8716
- authenticatedPrincipal: request.authenticatedPrincipal,
8717
- cwd: request.cwd,
8718
- projectTag: request.projectTag,
8719
- effectiveCodingContext: effectiveCodingContext ?? null
8720
- },
8721
- beforeExecute: hooks?.enforceWriteQuota,
8722
- execute: () => this.runObserve(request)
8723
- });
8724
- }
8725
- async runObserve(request) {
8726
- if (!request.sessionKey || typeof request.sessionKey !== "string" || request.sessionKey.trim().length === 0) {
8727
- throw new EngramAccessInputError("sessionKey is required and must be a non-empty string");
8728
- }
8729
- if (!Array.isArray(request.messages) || request.messages.length === 0) {
8730
- throw new EngramAccessInputError("messages is required and must be a non-empty array");
8731
- }
8732
- for (const msg of request.messages) {
8733
- if (!msg || typeof msg !== "object" || typeof msg.role !== "string" || typeof msg.content !== "string") {
8734
- throw new EngramAccessInputError("each message must have a string 'role' and 'content'");
8735
- }
8736
- if (msg.role !== "user" && msg.role !== "assistant") {
8737
- throw new EngramAccessInputError(`invalid message role: ${msg.role} (expected 'user' or 'assistant')`);
8738
- }
8739
- }
8740
- const scope = await this.resolveMemoryScopePlan(request);
8741
- const writeNamespace = scope.writeNamespace;
8742
- const namespace = this.legacyResponseNamespaceForScope(scope);
8743
- const shouldWriteObjectiveState = resolveObjectiveStateCapabilities(this.orchestrator.config).objectiveStateMemory === true && resolveObjectiveStateCapabilities(this.orchestrator.config).objectiveStateSnapshotWrites === true;
8744
- await this.maybeAttachCodingContext(request.sessionKey, {
8745
- cwd: request.cwd,
8746
- projectTag: request.projectTag
8747
- });
8748
- const lcmSessionKey = lcmSessionKeyForNamespace(
8749
- writeNamespace,
8750
- request.sessionKey,
8751
- this.orchestrator.config.defaultNamespace
8752
- ) ?? request.sessionKey;
8753
- if (shouldWriteObjectiveState) {
8754
- try {
8755
- const objectiveStateLocation = await this.objectiveStateStoreLocationForNamespace(
8756
- scope.objectiveStateNamespace
8757
- );
8758
- await recordObjectiveStateSnapshotsFromObservedMessages({
8759
- memoryDir: objectiveStateLocation.memoryDir,
8760
- objectiveStateStoreDir: objectiveStateLocation.objectiveStateStoreDir,
8761
- objectiveStateMemoryEnabled: resolveObjectiveStateCapabilities(this.orchestrator.config).objectiveStateMemory,
8762
- objectiveStateSnapshotWritesEnabled: resolveObjectiveStateCapabilities(this.orchestrator.config).objectiveStateSnapshotWrites,
8763
- sessionKey: request.sessionKey,
8764
- recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
8765
- messages: request.messages
8766
- });
8767
- } catch (err) {
8768
- log.error(`access-observe objective-state snapshot write failed: ${err}`);
8769
- }
8770
- }
8771
- let lcmArchived = false;
8772
- if (this.orchestrator.lcmEngine && this.orchestrator.lcmEngine.enabled) {
8773
- try {
8774
- this.orchestrator.lcmEngine.enqueueObserveMessages(lcmSessionKey, request.messages);
8775
- lcmArchived = true;
8776
- } catch (err) {
8777
- log.error(`access-observe LCM enqueue failed: ${err}`);
8778
- }
8779
- }
8780
- let extractionQueued = false;
8781
- if (request.skipExtraction !== true) {
8782
- const turns = request.messages.map((m) => ({
8783
- source: "openclaw",
8784
- // Identity-vs-routing separation (#1505 thread 1, cursor): extraction
8785
- // derives the provenance principal via `resolvePrincipal(turn.sessionKey)`
8786
- // and threads `turn.sessionKey` into conversation threading. Feeding the
8787
- // namespace-PREFIXED `lcmSessionKey` here mis-derived the principal to
8788
- // `default` (a `<ns>:<key>` string matches no prefix/map rule and fails
8789
- // the `agent:` heuristic). Pass the ORIGINAL sessionKey so identity is
8790
- // correct; storage routing is pinned separately via
8791
- // writeNamespaceOverride below, and the authenticated principal is pinned
8792
- // via principalOverride.
8793
- sessionKey: request.sessionKey,
8794
- role: m.role,
8795
- content: m.content,
8796
- parts: m.parts,
8797
- rawContent: m.rawContent,
8798
- sourceFormat: m.sourceFormat,
8799
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
8800
- }));
8801
- const writeNamespaceOverride = resolveNamespaceCapabilities(this.orchestrator.config).namespaces === true ? writeNamespace : void 0;
8802
- const principalOverride = typeof scope.principal === "string" && scope.principal.length > 0 ? scope.principal : void 0;
8803
- try {
8804
- const extractionPromise = this.orchestrator.ingestReplayBatch(turns, {
8805
- archiveLcm: false,
8806
- writeNamespaceOverride,
8807
- principalOverride
8808
- });
8809
- extractionPromise.catch((err) => {
8810
- log.error(`access-observe background extraction failed: ${err}`);
8811
- });
8812
- extractionQueued = true;
8813
- } catch (err) {
8814
- log.error(`access-observe extraction enqueue failed: ${err}`);
8815
- }
8762
+ }
8763
+ async observe(request, hooks) {
8764
+ const idempotencyKey = request.idempotencyKey?.trim();
8765
+ let effectiveCodingContext;
8766
+ if (idempotencyKey && !(typeof request.namespace === "string" && request.namespace.trim().length > 0)) {
8767
+ effectiveCodingContext = (typeof this.orchestrator.getCodingContextForSession === "function" ? this.orchestrator.getCodingContextForSession(request.sessionKey) : null) ?? await this.resolveCodingContextFromOptions(request);
8816
8768
  }
8817
- log.info(
8818
- `access-observe namespace=${namespace} effectiveNamespace=${writeNamespace} sessionKey=${request.sessionKey} messages=${request.messages.length} lcm=${lcmArchived} extraction=${extractionQueued}`
8819
- );
8820
- return {
8821
- accepted: request.messages.length,
8822
- sessionKey: request.sessionKey,
8823
- namespace,
8824
- effectiveNamespace: writeNamespace,
8825
- scopeDebug: {
8826
- principal: scope.principal,
8827
- explicitNamespace: scope.explicitNamespace,
8828
- baseNamespace: scope.baseNamespace,
8829
- writeNamespace: scope.writeNamespace,
8830
- codingOverlayApplied: scope.codingOverlayApplied,
8831
- readNamespaces: scope.readNamespaces,
8832
- scopeProfile: scope.scopeProfile,
8833
- writeLayer: scope.writeLayer,
8834
- layers: scope.layers,
8835
- promotionTargets: scope.promotionTargets
8769
+ return this.handleIdempotentWrite({
8770
+ operation: "observe",
8771
+ idempotencyKey: request.idempotencyKey,
8772
+ requestFingerprint: {
8773
+ sessionKey: request.sessionKey,
8774
+ messages: request.messages,
8775
+ namespace: request.namespace,
8776
+ skipExtraction: request.skipExtraction,
8777
+ authenticatedPrincipal: request.authenticatedPrincipal,
8778
+ cwd: request.cwd,
8779
+ projectTag: request.projectTag,
8780
+ effectiveCodingContext: effectiveCodingContext ?? null
8836
8781
  },
8837
- lcmArchived,
8838
- extractionQueued
8839
- };
8782
+ beforeExecute: hooks?.enforceWriteQuota,
8783
+ execute: () => this.runObserve(request)
8784
+ });
8785
+ }
8786
+ async runObserve(request) {
8787
+ return this.accessObserveWriteSurface.runObserve(
8788
+ request
8789
+ );
8840
8790
  }
8841
8791
  async lcmSearch(request) {
8842
8792
  return this.accessLcmSurface.lcmSearch(
@@ -9233,137 +9183,19 @@ var EngramAccessService = class {
9233
9183
  }
9234
9184
  // ── Work Layer ──────────────────────────────────────────────────────────
9235
9185
  async workTask(request) {
9236
- const STATUSES = /* @__PURE__ */ new Set(["todo", "in_progress", "blocked", "done", "cancelled"]);
9237
- const PRIORITIES = /* @__PURE__ */ new Set(["low", "medium", "high"]);
9238
- const asStatus = (v) => v && STATUSES.has(v) ? v : void 0;
9239
- const asPriority = (v) => v && PRIORITIES.has(v) ? v : void 0;
9240
- const storage = new WorkStorage(this.orchestrator.config.memoryDir);
9241
- await storage.ensureDirectories();
9242
- const action = request.action;
9243
- if (action === "create") {
9244
- if (!request.title?.trim()) throw new EngramAccessInputError("title is required for create");
9245
- const task = await storage.createTask({
9246
- title: request.title,
9247
- description: request.description,
9248
- status: asStatus(request.status),
9249
- priority: asPriority(request.priority),
9250
- owner: request.owner?.trim() || void 0,
9251
- assignee: request.assignee?.trim() || void 0,
9252
- projectId: request.projectId?.trim() || void 0,
9253
- tags: request.tags,
9254
- dueAt: request.dueAt?.trim() || void 0
9255
- });
9256
- return { action, task };
9257
- }
9258
- if (action === "get") {
9259
- if (!request.id?.trim()) throw new EngramAccessInputError("id is required for get");
9260
- return { action, task: await storage.getTask(request.id) };
9261
- }
9262
- if (action === "list") {
9263
- const tasks = await storage.listTasks({
9264
- status: asStatus(request.status),
9265
- owner: request.owner?.trim() || void 0,
9266
- assignee: request.assignee?.trim() || void 0,
9267
- projectId: request.projectId?.trim() || void 0
9268
- });
9269
- return { action, count: tasks.length, tasks };
9270
- }
9271
- if (action === "update") {
9272
- if (!request.id?.trim()) throw new EngramAccessInputError("id is required for update");
9273
- const patch = {};
9274
- if (request.title !== void 0) patch.title = request.title;
9275
- if (request.description !== void 0) patch.description = request.description;
9276
- const st = asStatus(request.status);
9277
- if (st) patch.status = st;
9278
- const pr = asPriority(request.priority);
9279
- if (pr) patch.priority = pr;
9280
- if (request.owner !== void 0) patch.owner = request.owner || null;
9281
- if (request.assignee !== void 0) patch.assignee = request.assignee || null;
9282
- if (request.projectId !== void 0) patch.projectId = request.projectId || null;
9283
- if (request.tags) patch.tags = request.tags;
9284
- if (request.dueAt !== void 0) patch.dueAt = request.dueAt || null;
9285
- return { action, task: await storage.updateTask(request.id, patch) };
9286
- }
9287
- if (action === "transition") {
9288
- if (!request.id?.trim()) throw new EngramAccessInputError("id is required for transition");
9289
- const st = asStatus(request.status);
9290
- if (!st) throw new EngramAccessInputError("valid status is required for transition");
9291
- return { action, task: await storage.transitionTask(request.id, st) };
9292
- }
9293
- if (action === "delete") {
9294
- if (!request.id?.trim()) throw new EngramAccessInputError("id is required for delete");
9295
- return { action, deleted: await storage.deleteTask(request.id) };
9296
- }
9297
- throw new EngramAccessInputError(`Unsupported work_task action: ${action}`);
9186
+ return this.accessObserveWriteSurface.workTask(
9187
+ request
9188
+ );
9298
9189
  }
9299
9190
  async workProject(request) {
9300
- const STATUSES = /* @__PURE__ */ new Set(["active", "on_hold", "completed", "archived"]);
9301
- const asStatus = (v) => v && STATUSES.has(v) ? v : void 0;
9302
- const storage = new WorkStorage(this.orchestrator.config.memoryDir);
9303
- await storage.ensureDirectories();
9304
- const action = request.action;
9305
- if (action === "create") {
9306
- if (!request.name?.trim()) throw new EngramAccessInputError("name is required for create");
9307
- const project = await storage.createProject({
9308
- name: request.name,
9309
- description: request.description,
9310
- status: asStatus(request.status),
9311
- owner: request.owner?.trim() || void 0,
9312
- tags: request.tags
9313
- });
9314
- return { action, project };
9315
- }
9316
- if (action === "get") {
9317
- if (!request.id?.trim()) throw new EngramAccessInputError("id is required for get");
9318
- return { action, project: await storage.getProject(request.id) };
9319
- }
9320
- if (action === "list") {
9321
- const projects = await storage.listProjects();
9322
- return { action, count: projects.length, projects };
9323
- }
9324
- if (action === "update") {
9325
- if (!request.id?.trim()) throw new EngramAccessInputError("id is required for update");
9326
- const patch = {};
9327
- if (request.name !== void 0) patch.name = request.name;
9328
- if (request.description !== void 0) patch.description = request.description;
9329
- const st = asStatus(request.status);
9330
- if (st) patch.status = st;
9331
- if (request.owner !== void 0) patch.owner = request.owner || null;
9332
- if (request.tags) patch.tags = request.tags;
9333
- return { action, project: await storage.updateProject(request.id, patch) };
9334
- }
9335
- if (action === "delete") {
9336
- if (!request.id?.trim()) throw new EngramAccessInputError("id is required for delete");
9337
- return { action, deleted: await storage.deleteProject(request.id) };
9338
- }
9339
- if (action === "link_task") {
9340
- if (!request.taskId?.trim() || !request.projectId?.trim()) {
9341
- throw new EngramAccessInputError("taskId and projectId are required for link_task");
9342
- }
9343
- return { action, linked: await storage.linkTaskToProject(request.taskId, request.projectId) };
9344
- }
9345
- throw new EngramAccessInputError(`Unsupported work_project action: ${action}`);
9191
+ return this.accessObserveWriteSurface.workProject(
9192
+ request
9193
+ );
9346
9194
  }
9347
9195
  async workBoard(request) {
9348
- const memoryDir = this.orchestrator.config.memoryDir;
9349
- await new WorkStorage(memoryDir).ensureDirectories();
9350
- const action = request.action;
9351
- const projectId = request.projectId?.trim() || void 0;
9352
- if (action === "export_markdown") {
9353
- const markdown = await exportWorkBoardMarkdown({ memoryDir, projectId });
9354
- return { action, markdown: wrapWorkLayerContext(markdown, { linkToMemory: request.linkToMemory === true }) };
9355
- }
9356
- if (action === "export_snapshot") {
9357
- const snapshot = await exportWorkBoardSnapshot({ memoryDir, projectId });
9358
- return { action, snapshot };
9359
- }
9360
- if (action === "import_snapshot") {
9361
- if (!request.snapshotJson?.trim()) throw new EngramAccessInputError("snapshotJson is required for import_snapshot");
9362
- const snapshot = JSON.parse(request.snapshotJson);
9363
- const result = await importWorkBoardSnapshot({ memoryDir, snapshot, projectId });
9364
- return { action, result };
9365
- }
9366
- throw new EngramAccessInputError(`Unsupported work_board action: ${action}`);
9196
+ return this.accessObserveWriteSurface.workBoard(
9197
+ request
9198
+ );
9367
9199
  }
9368
9200
  // ── Shared Context / Compounding ────────────────────────────────────────
9369
9201
  async sharedContextWriteOutput(request) {
@@ -9647,90 +9479,35 @@ ${next}`);
9647
9479
  */
9648
9480
  async memoryOutcome(request) {
9649
9481
  if (request.memoryId.includes("/") || request.memoryId.includes("\\")) {
9650
- throw new EngramAccessInputError(
9651
- "memoryId must not contain path separators"
9652
- );
9653
- }
9654
- const resolvedNs = this.writableNamespaceFor(
9655
- request.namespace,
9656
- request.sessionKey,
9657
- request.principal
9658
- );
9659
- const storage = await this.orchestrator.getStorage(resolvedNs);
9660
- return recordMemoryOutcome(storage, {
9661
- memoryPath: `${request.memoryId}.md`,
9662
- outcome: request.outcome,
9663
- timestamp: request.timestamp
9664
- });
9665
- }
9666
- async memoryPromote(request) {
9667
- const resolvedNs = this.writableNamespaceFor(request.namespace, request.sessionKey, request.principal);
9668
- const storage = await this.orchestrator.getStorage(resolvedNs);
9669
- await storage.updateMemoryFrontmatter(request.memoryId, {
9670
- lifecycleState: "active",
9671
- updated: (/* @__PURE__ */ new Date()).toISOString()
9672
- });
9673
- return { promoted: true, memoryId: request.memoryId };
9674
- }
9675
- async memoryActionApply(request) {
9676
- const actionTypes = /* @__PURE__ */ new Set([
9677
- "store_episode",
9678
- "store_note",
9679
- "update_note",
9680
- "create_artifact",
9681
- "summarize_node",
9682
- "discard",
9683
- "link_graph"
9684
- ]);
9685
- if (!actionTypes.has(request.action)) {
9686
- throw new EngramAccessInputError(
9687
- `memory_action_apply: invalid action ${JSON.stringify(request.action)}`
9688
- );
9689
- }
9690
- if (resolveCompressionCapabilities(this.orchestrator.config).contextCompressionActions !== true) {
9691
- throw new EngramAccessInputError(
9692
- "memory_action_apply is disabled; enable contextCompressionActionsEnabled to use this tool"
9693
- );
9694
- }
9695
- const outcome = request.outcome ?? "skipped";
9696
- if (outcome !== "applied" && outcome !== "skipped" && outcome !== "failed") {
9697
- throw new EngramAccessInputError(
9698
- `memory_action_apply: outcome must be "applied", "skipped", or "failed"; got ${JSON.stringify(outcome)}`
9699
- );
9700
- }
9701
- const resolvedNs = this.writableNamespaceFor(
9702
- request.namespace,
9703
- request.sessionKey,
9704
- request.principal
9705
- );
9706
- const inputSummaryParts = [
9707
- request.content,
9708
- request.category ? `category=${request.category}` : void 0,
9709
- request.linkTargetId ? `linkTargetId=${request.linkTargetId}` : void 0,
9710
- request.linkType ? `linkType=${request.linkType}` : void 0,
9711
- typeof request.linkStrength === "number" ? `linkStrength=${request.linkStrength}` : void 0,
9712
- request.artifactType ? `artifactType=${request.artifactType}` : void 0,
9713
- typeof request.execute === "boolean" ? `execute=${request.execute}` : void 0
9714
- ].filter((part) => typeof part === "string" && part.length > 0);
9715
- const event = {
9716
- action: request.action,
9717
- outcome,
9718
- namespace: resolvedNs,
9719
- actor: "access.memory_action_apply",
9720
- subsystem: "access.memory_action_apply",
9721
- reason: request.reason,
9722
- memoryId: request.memoryId,
9723
- sourceSessionKey: request.sessionKey,
9724
- inputSummary: inputSummaryParts.length > 0 ? inputSummaryParts.join(" | ").slice(0, 500) : void 0,
9725
- dryRun: request.dryRun === true,
9726
- promptHash: typeof request.sourcePrompt === "string" && request.sourcePrompt.length > 0 ? createHash5("sha256").update(request.sourcePrompt).digest("hex") : void 0
9727
- };
9728
- const preview = this.orchestrator.previewMemoryActionEvent(event);
9729
- if (request.dryRun === true) {
9730
- return { recorded: false, dryRun: true, event: preview };
9482
+ throw new EngramAccessInputError(
9483
+ "memoryId must not contain path separators"
9484
+ );
9731
9485
  }
9732
- const recorded = await this.orchestrator.appendMemoryActionEvent(event);
9733
- return { recorded, event: preview };
9486
+ const resolvedNs = this.writableNamespaceFor(
9487
+ request.namespace,
9488
+ request.sessionKey,
9489
+ request.principal
9490
+ );
9491
+ const storage = await this.orchestrator.getStorage(resolvedNs);
9492
+ return recordMemoryOutcome(storage, {
9493
+ memoryPath: `${request.memoryId}.md`,
9494
+ outcome: request.outcome,
9495
+ timestamp: request.timestamp
9496
+ });
9497
+ }
9498
+ async memoryPromote(request) {
9499
+ const resolvedNs = this.writableNamespaceFor(request.namespace, request.sessionKey, request.principal);
9500
+ const storage = await this.orchestrator.getStorage(resolvedNs);
9501
+ await storage.updateMemoryFrontmatter(request.memoryId, {
9502
+ lifecycleState: "active",
9503
+ updated: (/* @__PURE__ */ new Date()).toISOString()
9504
+ });
9505
+ return { promoted: true, memoryId: request.memoryId };
9506
+ }
9507
+ async memoryActionApply(request) {
9508
+ return this.accessObserveWriteSurface.memoryActionApply(
9509
+ request
9510
+ );
9734
9511
  }
9735
9512
  async contextCheckpoint(request) {
9736
9513
  const resolvedNs = this.writableNamespaceFor(request.namespace, request.sessionKey, request.principal);
@@ -10347,397 +10124,699 @@ ${next}`);
10347
10124
  limit: request.limit
10348
10125
  });
10349
10126
  }
10350
- async wearablesTranscriptMemories(request) {
10351
- return this.orchestrator.getWearablesService().transcriptMemories({
10352
- source: request.source,
10353
- date: request.date,
10354
- limit: request.limit
10127
+ async wearablesTranscriptMemories(request) {
10128
+ return this.orchestrator.getWearablesService().transcriptMemories({
10129
+ source: request.source,
10130
+ date: request.date,
10131
+ limit: request.limit
10132
+ });
10133
+ }
10134
+ // ── Admin console surfaces (issue #1502) ────────────────────────────────
10135
+ //
10136
+ // Thin delegators. Every method threads `this.orchestrator` references
10137
+ // into the pure `admin/admin-surfaces.ts` functions so the dashboard
10138
+ // never re-resolves scope, re-derives promotion targets, or re-lists
10139
+ // namespaces.
10140
+ /**
10141
+ * Resolve an effective scope inspection for the console. Inputs are
10142
+ * redacted before return so an operator-supplied principal preview /
10143
+ * namespace override never echoes a pasted credential back.
10144
+ */
10145
+ async adminInspectScope(options) {
10146
+ const config = this.orchestrator.config;
10147
+ const codingContext = options.sessionKey ? this.orchestrator.getCodingContextForSession(options.sessionKey) ?? null : null;
10148
+ const inspection = inspectScope({
10149
+ config,
10150
+ sessionKey: options.sessionKey,
10151
+ namespace: options.namespace,
10152
+ principalOverride: options.principalOverride,
10153
+ codingContext,
10154
+ operation: options.operation
10155
+ });
10156
+ return redactSensitive(inspection);
10157
+ }
10158
+ /**
10159
+ * List configured + discovered namespaces from the catalog with the
10160
+ * admin filters the console exposes.
10161
+ */
10162
+ async adminListNamespaces(filter) {
10163
+ const result = await listAdminNamespaces({
10164
+ catalog: this.orchestrator.namespaceCatalog,
10165
+ filter
10166
+ });
10167
+ return redactSensitive(result);
10168
+ }
10169
+ /**
10170
+ * Per-namespace maintenance + QMD health. QMD diagnostics are probed via
10171
+ * the orchestrator's search health accessor (one probe per namespace).
10172
+ */
10173
+ async adminMaintenanceHealth() {
10174
+ const report = await gatherMaintenanceHealth({
10175
+ catalog: this.orchestrator.namespaceCatalog,
10176
+ qmdHealthProvider: async (namespace) => {
10177
+ try {
10178
+ const health = await this.orchestrator.searchHealthForNamespace(namespace);
10179
+ return {
10180
+ namespace,
10181
+ collection: health.collection,
10182
+ available: health.available,
10183
+ collectionState: health.collectionState,
10184
+ debugStatus: health.debugStatus,
10185
+ installedVersion: health.installedVersion,
10186
+ supportedVersion: health.supportedVersion,
10187
+ supported: health.supported,
10188
+ upgradeAvailable: health.upgradeAvailable,
10189
+ daemonMode: health.daemonMode
10190
+ };
10191
+ } catch (err) {
10192
+ throw err;
10193
+ }
10194
+ }
10195
+ });
10196
+ return redactSensitive(report);
10197
+ }
10198
+ /**
10199
+ * Dry-run transcript/session audit. Detects mixed `other/default` data
10200
+ * and legacy stranded directories. Never applies a migration.
10201
+ */
10202
+ async adminTranscriptAudit() {
10203
+ const result = await auditTranscripts(this.orchestrator.config.memoryDir);
10204
+ return redactSensitive(result);
10205
+ }
10206
+ async adminPromoteMemory(request) {
10207
+ return this.accessAdminOpsSurface.adminPromoteMemory(
10208
+ request
10209
+ );
10210
+ }
10211
+ };
10212
+
10213
+ // src/access-observe-write-surface.ts
10214
+ var AccessObserveWriteSurface = class {
10215
+ constructor(deps) {
10216
+ this.deps = deps;
10217
+ }
10218
+ deps;
10219
+ async qmdHealth(searchBackend, qmdEnabled, namespace, collection) {
10220
+ if (searchBackend !== "qmd" || !qmdEnabled) {
10221
+ return {
10222
+ enabled: qmdEnabled,
10223
+ active: false,
10224
+ degraded: false,
10225
+ mode: searchBackend !== "qmd" ? "not-selected" : "disabled",
10226
+ collection,
10227
+ collectionState: "skipped",
10228
+ installedVersion: null,
10229
+ supportedVersion: null,
10230
+ supported: null,
10231
+ upgradeAvailable: null,
10232
+ doctorAvailable: null,
10233
+ debugStatus: searchBackend !== "qmd" ? `backend=${searchBackend}` : "backend=disabled"
10234
+ };
10235
+ }
10236
+ if (resolveNamespaceCapabilities(this.deps.orchestrator.config).namespaces === true) {
10237
+ const namespaceHealth = await this.deps.namespaceQmdHealth(searchBackend, qmdEnabled, namespace, collection);
10238
+ if (namespaceHealth) return namespaceHealth;
10239
+ }
10240
+ const qmd = this.deps.orchestrator.qmd;
10241
+ if (!qmd) {
10242
+ return {
10243
+ enabled: true,
10244
+ active: false,
10245
+ degraded: true,
10246
+ mode: "fallback",
10247
+ collection,
10248
+ collectionState: "unknown",
10249
+ installedVersion: null,
10250
+ supportedVersion: null,
10251
+ supported: null,
10252
+ upgradeAvailable: null,
10253
+ doctorAvailable: null,
10254
+ debugStatus: "backend=unavailable"
10255
+ };
10256
+ }
10257
+ const diagnosticAvailable = await this.deps.qmdProbeAvailable(searchBackend, qmdEnabled);
10258
+ const operationalAvailable = diagnosticAvailable || qmd.isAvailable();
10259
+ const collectionState = diagnosticAvailable ? await this.deps.qmdCollectionState(searchBackend, qmdEnabled, collection) : "unknown";
10260
+ const active = operationalAvailable && collectionState !== "missing";
10261
+ const degraded = searchBackend === "qmd" && qmdEnabled && (!active || !diagnosticAvailable || collectionState === "unknown");
10262
+ const debugStatus = qmd.debugStatus();
10263
+ const versionStatus = "getVersionStatus" in qmd && typeof qmd.getVersionStatus === "function" ? qmd.getVersionStatus() : null;
10264
+ const daemonMode = "isDaemonMode" in qmd && typeof qmd.isDaemonMode === "function" ? qmd.isDaemonMode() === true : false;
10265
+ const mode = searchBackend !== "qmd" ? "not-selected" : !qmdEnabled ? "disabled" : !active ? "fallback" : daemonMode ? "daemon" : "cli";
10266
+ return {
10267
+ enabled: qmdEnabled,
10268
+ active,
10269
+ degraded,
10270
+ mode,
10271
+ collection,
10272
+ collectionState,
10273
+ installedVersion: versionStatus?.installedVersion ?? null,
10274
+ supportedVersion: versionStatus?.supportedVersion ?? null,
10275
+ supported: versionStatus?.supported ?? null,
10276
+ upgradeAvailable: versionStatus?.upgradeAvailable ?? null,
10277
+ doctorAvailable: versionStatus?.capabilities?.doctor ?? null,
10278
+ debugStatus
10279
+ };
10280
+ }
10281
+ async namespaceQmdHealth(searchBackend, qmdEnabled, namespace, fallbackCollection) {
10282
+ if (searchBackend !== "qmd" || !qmdEnabled) return null;
10283
+ const searchHealthForNamespace = this.deps.orchestrator.searchHealthForNamespace;
10284
+ if (typeof searchHealthForNamespace !== "function") return null;
10285
+ const controller = new AbortController();
10286
+ const timer = setTimeout(() => controller.abort(), 2e3);
10287
+ timer.unref?.();
10288
+ try {
10289
+ const health = await searchHealthForNamespace.call(this.deps.orchestrator, namespace, {
10290
+ signal: controller.signal
10291
+ });
10292
+ const active = health.available && health.collectionState !== "missing";
10293
+ const degraded = !active || health.collectionState === "unknown";
10294
+ const mode = !active ? "fallback" : health.daemonMode === true ? "daemon" : "cli";
10295
+ return {
10296
+ enabled: true,
10297
+ active,
10298
+ degraded,
10299
+ mode,
10300
+ collection: health.collection || fallbackCollection,
10301
+ collectionState: health.collectionState,
10302
+ installedVersion: health.installedVersion,
10303
+ supportedVersion: health.supportedVersion,
10304
+ supported: health.supported,
10305
+ upgradeAvailable: health.upgradeAvailable,
10306
+ doctorAvailable: health.doctorAvailable,
10307
+ debugStatus: health.debugStatus
10308
+ };
10309
+ } catch (error) {
10310
+ const detail = displayErrorDetail(error) || "unknown";
10311
+ return {
10312
+ enabled: true,
10313
+ active: false,
10314
+ degraded: true,
10315
+ mode: "fallback",
10316
+ collection: fallbackCollection,
10317
+ collectionState: "unknown",
10318
+ installedVersion: null,
10319
+ supportedVersion: null,
10320
+ supported: null,
10321
+ upgradeAvailable: null,
10322
+ doctorAvailable: null,
10323
+ debugStatus: `backend=namespace-unavailable error=${detail}`
10324
+ };
10325
+ } finally {
10326
+ clearTimeout(timer);
10327
+ }
10328
+ }
10329
+ /**
10330
+ * Build a daily context briefing. Gracefully degrades when the OpenAI key
10331
+ * or Responses API is unavailable — never throws for LLM-related problems.
10332
+ */
10333
+ async briefing(request) {
10334
+ const config = this.deps.orchestrator.config;
10335
+ if (!config.briefing.enabled) {
10336
+ throw new EngramAccessInputError("briefing is disabled");
10337
+ }
10338
+ const namespace = this.deps.resolveReadableNamespace(request.namespace, request.principal);
10339
+ const storage = await this.deps.orchestrator.getStorage(namespace);
10340
+ const token = typeof request.since === "string" && request.since.trim().length > 0 ? request.since.trim() : config.briefing.defaultWindow;
10341
+ const window = parseBriefingWindow(token);
10342
+ if (!window) {
10343
+ throw new EngramAccessInputError(`invalid briefing window: ${token}`);
10344
+ }
10345
+ const rawFocus = typeof request.focus === "string" ? request.focus.trim() : "";
10346
+ let focus = null;
10347
+ if (rawFocus.length > 0) {
10348
+ focus = parseBriefingFocus(rawFocus);
10349
+ if (!focus) {
10350
+ throw new EngramAccessInputError(
10351
+ `invalid briefing focus filter: ${request.focus}`
10352
+ );
10353
+ }
10354
+ }
10355
+ const SUPPORTED_FORMATS = ["markdown", "json"];
10356
+ if (typeof request.format === "string" && !SUPPORTED_FORMATS.includes(request.format)) {
10357
+ throw new EngramAccessInputError(
10358
+ `unsupported briefing format: "${request.format}". Accepted: ${SUPPORTED_FORMATS.join(", ")}.`
10359
+ );
10360
+ }
10361
+ const format = request.format === "json" ? "json" : request.format === "markdown" ? "markdown" : config.briefing.defaultFormat;
10362
+ const maxFollowups = typeof request.maxFollowups === "number" && Number.isFinite(request.maxFollowups) ? Math.max(0, Math.min(10, Math.floor(request.maxFollowups))) : config.briefing.maxFollowups;
10363
+ const calendarSource = config.briefing.calendarSource ? new FileCalendarSource(config.briefing.calendarSource) : void 0;
10364
+ const result = await buildBriefing({
10365
+ storage,
10366
+ namespace,
10367
+ window,
10368
+ focus,
10369
+ calendarSource,
10370
+ maxFollowups,
10371
+ allowLlm: config.briefing.llmFollowups,
10372
+ openaiApiKey: config.openaiApiKey,
10373
+ openaiBaseUrl: config.openaiBaseUrl,
10374
+ model: config.model,
10375
+ // Without a direct OpenAI key, route follow-ups through the configured
10376
+ // LLM chain (gateway model source or local LLM) — same fallback every
10377
+ // other LLM feature uses. A configured key keeps its precedence so
10378
+ // existing deployments are unchanged.
10379
+ followupGenerator: config.openaiApiKey ? void 0 : this.deps.orchestrator.briefingChainFollowupGenerator
10380
+ });
10381
+ return {
10382
+ format,
10383
+ window: result.window,
10384
+ namespace,
10385
+ markdown: result.markdown,
10386
+ json: result.json,
10387
+ followupsUnavailableReason: result.followupsUnavailableReason
10388
+ };
10389
+ }
10390
+ async memoryStore(request, hooks) {
10391
+ const namespace = await this.deps.resolveCodingScopedWriteNamespace(request);
10392
+ const schemaVersion = request.schemaVersion ?? ENGRAM_ACCESS_WRITE_SCHEMA_VERSION;
10393
+ if (schemaVersion !== ENGRAM_ACCESS_WRITE_SCHEMA_VERSION) {
10394
+ throw new EngramAccessInputError(`unsupported schemaVersion: ${schemaVersion}`);
10395
+ }
10396
+ const execute = async () => {
10397
+ const candidate = this.deps.validateWriteCandidate(request, namespace);
10398
+ if (request.dryRun === true) {
10399
+ return {
10400
+ schemaVersion: ENGRAM_ACCESS_WRITE_SCHEMA_VERSION,
10401
+ operation: "memory_store",
10402
+ namespace,
10403
+ dryRun: true,
10404
+ accepted: true,
10405
+ queued: false,
10406
+ status: "validated",
10407
+ idempotencyKey: request.idempotencyKey?.trim() || void 0
10408
+ };
10409
+ }
10410
+ const result = await persistExplicitCapture(this.deps.orchestrator, candidate, "memory_store");
10411
+ await this.deps.attachCodingContextAfterScopedWrite(request);
10412
+ const blocked = result.tombstoneBlocked === true;
10413
+ const response = {
10414
+ schemaVersion: ENGRAM_ACCESS_WRITE_SCHEMA_VERSION,
10415
+ operation: "memory_store",
10416
+ namespace,
10417
+ dryRun: false,
10418
+ accepted: true,
10419
+ queued: blocked,
10420
+ status: blocked ? "queued_for_review" : result.duplicateOf ? "duplicate" : "stored",
10421
+ memoryId: result.id,
10422
+ duplicateOf: result.duplicateOf,
10423
+ idempotencyKey: request.idempotencyKey?.trim() || void 0
10424
+ };
10425
+ log.info(
10426
+ `access-write op=memory_store namespace=${namespace} dryRun=false status=${response.status} memoryId=${response.memoryId ?? "-"} idempotency=${response.idempotencyKey ? "yes" : "no"}`
10427
+ );
10428
+ return response;
10429
+ };
10430
+ return this.deps.handleIdempotentWrite({
10431
+ operation: "memory_store",
10432
+ idempotencyKey: request.idempotencyKey,
10433
+ requestFingerprint: {
10434
+ schemaVersion,
10435
+ content: request.content,
10436
+ category: request.category,
10437
+ confidence: request.confidence,
10438
+ namespace,
10439
+ tags: request.tags,
10440
+ entityRef: request.entityRef,
10441
+ ttl: request.ttl,
10442
+ sourceReason: request.sourceReason
10443
+ },
10444
+ skip: request.dryRun === true,
10445
+ beforeExecute: hooks?.enforceWriteQuota,
10446
+ execute
10355
10447
  });
10356
10448
  }
10357
- // ── Admin console surfaces (issue #1502) ────────────────────────────────
10358
- //
10359
- // Thin delegators. Every method threads `this.orchestrator` references
10360
- // into the pure `admin/admin-surfaces.ts` functions so the dashboard
10361
- // never re-resolves scope, re-derives promotion targets, or re-lists
10362
- // namespaces.
10363
- /**
10364
- * Resolve an effective scope inspection for the console. Inputs are
10365
- * redacted before return so an operator-supplied principal preview /
10366
- * namespace override never echoes a pasted credential back.
10367
- */
10368
- async adminInspectScope(options) {
10369
- const config = this.orchestrator.config;
10370
- const codingContext = options.sessionKey ? this.orchestrator.getCodingContextForSession(options.sessionKey) ?? null : null;
10371
- const inspection = inspectScope({
10372
- config,
10373
- sessionKey: options.sessionKey,
10374
- namespace: options.namespace,
10375
- principalOverride: options.principalOverride,
10376
- codingContext,
10377
- operation: options.operation
10449
+ async suggestionSubmit(request, hooks) {
10450
+ const namespace = await this.deps.resolveCodingScopedWriteNamespace(request);
10451
+ const schemaVersion = request.schemaVersion ?? ENGRAM_ACCESS_WRITE_SCHEMA_VERSION;
10452
+ if (schemaVersion !== ENGRAM_ACCESS_WRITE_SCHEMA_VERSION) {
10453
+ throw new EngramAccessInputError(`unsupported schemaVersion: ${schemaVersion}`);
10454
+ }
10455
+ const execute = async () => {
10456
+ const candidate = this.deps.validateWriteCandidate(request, namespace);
10457
+ if (request.dryRun === true) {
10458
+ return {
10459
+ schemaVersion: ENGRAM_ACCESS_WRITE_SCHEMA_VERSION,
10460
+ operation: "suggestion_submit",
10461
+ namespace,
10462
+ dryRun: true,
10463
+ accepted: true,
10464
+ queued: true,
10465
+ status: "validated",
10466
+ idempotencyKey: request.idempotencyKey?.trim() || void 0
10467
+ };
10468
+ }
10469
+ const result = await queueExplicitCaptureForReview(
10470
+ this.deps.orchestrator,
10471
+ candidate,
10472
+ "suggestion_submit",
10473
+ new Error(request.sourceReason?.trim() || "submitted via engram suggestion_submit")
10474
+ );
10475
+ await this.deps.attachCodingContextAfterScopedWrite(request);
10476
+ const response = {
10477
+ schemaVersion: ENGRAM_ACCESS_WRITE_SCHEMA_VERSION,
10478
+ operation: "suggestion_submit",
10479
+ namespace,
10480
+ dryRun: false,
10481
+ accepted: true,
10482
+ queued: true,
10483
+ status: "queued_for_review",
10484
+ memoryId: result.id,
10485
+ duplicateOf: result.duplicateOf,
10486
+ idempotencyKey: request.idempotencyKey?.trim() || void 0
10487
+ };
10488
+ log.info(
10489
+ `access-write op=suggestion_submit namespace=${namespace} dryRun=false status=${response.status} memoryId=${response.memoryId ?? "-"} idempotency=${response.idempotencyKey ? "yes" : "no"}`
10490
+ );
10491
+ return response;
10492
+ };
10493
+ return this.deps.handleIdempotentWrite({
10494
+ operation: "suggestion_submit",
10495
+ idempotencyKey: request.idempotencyKey,
10496
+ requestFingerprint: {
10497
+ schemaVersion,
10498
+ content: request.content,
10499
+ category: request.category,
10500
+ confidence: request.confidence,
10501
+ namespace,
10502
+ tags: request.tags,
10503
+ entityRef: request.entityRef,
10504
+ ttl: request.ttl,
10505
+ sourceReason: request.sourceReason
10506
+ },
10507
+ skip: request.dryRun === true,
10508
+ beforeExecute: hooks?.enforceWriteQuota,
10509
+ execute
10378
10510
  });
10379
- return redactSensitive(inspection);
10380
10511
  }
10381
- /**
10382
- * List configured + discovered namespaces from the catalog with the
10383
- * admin filters the console exposes.
10384
- */
10385
- async adminListNamespaces(filter) {
10386
- const result = await listAdminNamespaces({
10387
- catalog: this.orchestrator.namespaceCatalog,
10388
- filter
10512
+ async runObserve(request) {
10513
+ if (!request.sessionKey || typeof request.sessionKey !== "string" || request.sessionKey.trim().length === 0) {
10514
+ throw new EngramAccessInputError("sessionKey is required and must be a non-empty string");
10515
+ }
10516
+ if (!Array.isArray(request.messages) || request.messages.length === 0) {
10517
+ throw new EngramAccessInputError("messages is required and must be a non-empty array");
10518
+ }
10519
+ for (const msg of request.messages) {
10520
+ if (!msg || typeof msg !== "object" || typeof msg.role !== "string" || typeof msg.content !== "string") {
10521
+ throw new EngramAccessInputError("each message must have a string 'role' and 'content'");
10522
+ }
10523
+ if (msg.role !== "user" && msg.role !== "assistant") {
10524
+ throw new EngramAccessInputError(`invalid message role: ${msg.role} (expected 'user' or 'assistant')`);
10525
+ }
10526
+ }
10527
+ const scope = await this.deps.resolveMemoryScopePlan(request);
10528
+ const writeNamespace = scope.writeNamespace;
10529
+ const namespace = this.deps.legacyResponseNamespaceForScope(scope);
10530
+ const shouldWriteObjectiveState = resolveObjectiveStateCapabilities(this.deps.orchestrator.config).objectiveStateMemory === true && resolveObjectiveStateCapabilities(this.deps.orchestrator.config).objectiveStateSnapshotWrites === true;
10531
+ await this.deps.maybeAttachCodingContext(request.sessionKey, {
10532
+ cwd: request.cwd,
10533
+ projectTag: request.projectTag
10389
10534
  });
10390
- return redactSensitive(result);
10391
- }
10392
- /**
10393
- * Per-namespace maintenance + QMD health. QMD diagnostics are probed via
10394
- * the orchestrator's search health accessor (one probe per namespace).
10395
- */
10396
- async adminMaintenanceHealth() {
10397
- const report = await gatherMaintenanceHealth({
10398
- catalog: this.orchestrator.namespaceCatalog,
10399
- qmdHealthProvider: async (namespace) => {
10400
- try {
10401
- const health = await this.orchestrator.searchHealthForNamespace(namespace);
10402
- return {
10403
- namespace,
10404
- collection: health.collection,
10405
- available: health.available,
10406
- collectionState: health.collectionState,
10407
- debugStatus: health.debugStatus,
10408
- installedVersion: health.installedVersion,
10409
- supportedVersion: health.supportedVersion,
10410
- supported: health.supported,
10411
- upgradeAvailable: health.upgradeAvailable,
10412
- daemonMode: health.daemonMode
10413
- };
10414
- } catch (err) {
10415
- throw err;
10416
- }
10535
+ const lcmSessionKey = lcmSessionKeyForNamespace(
10536
+ writeNamespace,
10537
+ request.sessionKey,
10538
+ this.deps.orchestrator.config.defaultNamespace
10539
+ ) ?? request.sessionKey;
10540
+ if (shouldWriteObjectiveState) {
10541
+ try {
10542
+ const objectiveStateLocation = await this.deps.objectiveStateStoreLocationForNamespace(
10543
+ scope.objectiveStateNamespace
10544
+ );
10545
+ await recordObjectiveStateSnapshotsFromObservedMessages({
10546
+ memoryDir: objectiveStateLocation.memoryDir,
10547
+ objectiveStateStoreDir: objectiveStateLocation.objectiveStateStoreDir,
10548
+ objectiveStateMemoryEnabled: resolveObjectiveStateCapabilities(this.deps.orchestrator.config).objectiveStateMemory,
10549
+ objectiveStateSnapshotWritesEnabled: resolveObjectiveStateCapabilities(this.deps.orchestrator.config).objectiveStateSnapshotWrites,
10550
+ sessionKey: request.sessionKey,
10551
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
10552
+ messages: request.messages
10553
+ });
10554
+ } catch (err) {
10555
+ log.error(`access-observe objective-state snapshot write failed: ${err}`);
10556
+ }
10557
+ }
10558
+ let lcmArchived = false;
10559
+ if (this.deps.orchestrator.lcmEngine && this.deps.orchestrator.lcmEngine.enabled) {
10560
+ try {
10561
+ this.deps.orchestrator.lcmEngine.enqueueObserveMessages(lcmSessionKey, request.messages);
10562
+ lcmArchived = true;
10563
+ } catch (err) {
10564
+ log.error(`access-observe LCM enqueue failed: ${err}`);
10565
+ }
10566
+ }
10567
+ let extractionQueued = false;
10568
+ if (request.skipExtraction !== true) {
10569
+ const turns = request.messages.map((m) => ({
10570
+ source: "openclaw",
10571
+ // Identity-vs-routing separation (#1505 thread 1, cursor): extraction
10572
+ // derives the provenance principal via `resolvePrincipal(turn.sessionKey)`
10573
+ // and threads `turn.sessionKey` into conversation threading. Feeding the
10574
+ // namespace-PREFIXED `lcmSessionKey` here mis-derived the principal to
10575
+ // `default` (a `<ns>:<key>` string matches no prefix/map rule and fails
10576
+ // the `agent:` heuristic). Pass the ORIGINAL sessionKey so identity is
10577
+ // correct; storage routing is pinned separately via
10578
+ // writeNamespaceOverride below, and the authenticated principal is pinned
10579
+ // via principalOverride.
10580
+ sessionKey: request.sessionKey,
10581
+ role: m.role,
10582
+ content: m.content,
10583
+ parts: m.parts,
10584
+ rawContent: m.rawContent,
10585
+ sourceFormat: m.sourceFormat,
10586
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
10587
+ }));
10588
+ const writeNamespaceOverride = resolveNamespaceCapabilities(this.deps.orchestrator.config).namespaces === true ? writeNamespace : void 0;
10589
+ const principalOverride = typeof scope.principal === "string" && scope.principal.length > 0 ? scope.principal : void 0;
10590
+ try {
10591
+ const extractionPromise = this.deps.orchestrator.ingestReplayBatch(turns, {
10592
+ archiveLcm: false,
10593
+ writeNamespaceOverride,
10594
+ principalOverride
10595
+ });
10596
+ extractionPromise.catch((err) => {
10597
+ log.error(`access-observe background extraction failed: ${err}`);
10598
+ });
10599
+ extractionQueued = true;
10600
+ } catch (err) {
10601
+ log.error(`access-observe extraction enqueue failed: ${err}`);
10417
10602
  }
10418
- });
10419
- return redactSensitive(report);
10420
- }
10421
- /**
10422
- * Dry-run transcript/session audit. Detects mixed `other/default` data
10423
- * and legacy stranded directories. Never applies a migration.
10424
- */
10425
- async adminTranscriptAudit() {
10426
- const result = await auditTranscripts(this.orchestrator.config.memoryDir);
10427
- return redactSensitive(result);
10428
- }
10429
- async adminPromoteMemory(request) {
10430
- return this.accessAdminOpsSurface.adminPromoteMemory(
10431
- request
10603
+ }
10604
+ log.info(
10605
+ `access-observe namespace=${namespace} effectiveNamespace=${writeNamespace} sessionKey=${request.sessionKey} messages=${request.messages.length} lcm=${lcmArchived} extraction=${extractionQueued}`
10432
10606
  );
10607
+ return {
10608
+ accepted: request.messages.length,
10609
+ sessionKey: request.sessionKey,
10610
+ namespace,
10611
+ effectiveNamespace: writeNamespace,
10612
+ scopeDebug: {
10613
+ principal: scope.principal,
10614
+ explicitNamespace: scope.explicitNamespace,
10615
+ baseNamespace: scope.baseNamespace,
10616
+ writeNamespace: scope.writeNamespace,
10617
+ codingOverlayApplied: scope.codingOverlayApplied,
10618
+ readNamespaces: scope.readNamespaces,
10619
+ scopeProfile: scope.scopeProfile,
10620
+ writeLayer: scope.writeLayer,
10621
+ layers: scope.layers,
10622
+ promotionTargets: scope.promotionTargets
10623
+ },
10624
+ lcmArchived,
10625
+ extractionQueued
10626
+ };
10433
10627
  }
10434
- };
10435
-
10436
- // src/access-lcm-surface.ts
10437
- var AccessLcmSurface = class {
10438
- constructor(deps) {
10439
- this.deps = deps;
10440
- }
10441
- deps;
10442
- async lcmSearch(request) {
10443
- if (!request.query || typeof request.query !== "string" || request.query.trim().length === 0) {
10444
- throw new EngramAccessInputError("query is required and must be a non-empty string");
10628
+ async workTask(request) {
10629
+ const STATUSES = /* @__PURE__ */ new Set(["todo", "in_progress", "blocked", "done", "cancelled"]);
10630
+ const PRIORITIES = /* @__PURE__ */ new Set(["low", "medium", "high"]);
10631
+ const asStatus = (v) => v && STATUSES.has(v) ? v : void 0;
10632
+ const asPriority = (v) => v && PRIORITIES.has(v) ? v : void 0;
10633
+ const storage = new WorkStorage(this.deps.orchestrator.config.memoryDir);
10634
+ await storage.ensureDirectories();
10635
+ const action = request.action;
10636
+ if (action === "create") {
10637
+ if (!request.title?.trim()) throw new EngramAccessInputError("title is required for create");
10638
+ const task = await storage.createTask({
10639
+ title: request.title,
10640
+ description: request.description,
10641
+ status: asStatus(request.status),
10642
+ priority: asPriority(request.priority),
10643
+ owner: request.owner?.trim() || void 0,
10644
+ assignee: request.assignee?.trim() || void 0,
10645
+ projectId: request.projectId?.trim() || void 0,
10646
+ tags: request.tags,
10647
+ dueAt: request.dueAt?.trim() || void 0
10648
+ });
10649
+ return { action, task };
10445
10650
  }
10446
- const principal = this.deps.resolveRequestPrincipal(request.sessionKey, request.authenticatedPrincipal);
10447
- const hasExplicitNamespace = typeof request.namespace === "string" && request.namespace.trim().length > 0;
10448
- const profileLcmReadNamespaces = hasExplicitNamespace ? null : this.deps.resolveScopeProfileLcmReadNamespaces(
10449
- request.sessionKey,
10450
- request.authenticatedPrincipal
10451
- );
10452
- const namespace = hasExplicitNamespace ? this.deps.resolveReadableNamespace(request.namespace, principal) : profileLcmReadNamespaces !== null ? profileLcmReadNamespaces[0] : this.deps.resolveImplicitLcmReadFallbackNamespace(principal);
10453
- if (!this.deps.orchestrator.lcmEngine || !this.deps.orchestrator.lcmEngine.enabled) {
10454
- return {
10455
- query: request.query,
10456
- namespace: namespace ?? this.deps.orchestrator.config.defaultNamespace,
10457
- results: [],
10458
- count: 0,
10459
- lcmEnabled: false
10460
- };
10651
+ if (action === "get") {
10652
+ if (!request.id?.trim()) throw new EngramAccessInputError("id is required for get");
10653
+ return { action, task: await storage.getTask(request.id) };
10461
10654
  }
10462
- if (profileLcmReadNamespaces !== null && profileLcmReadNamespaces.length === 0) {
10463
- return {
10464
- query: request.query,
10465
- namespace: this.deps.orchestrator.config.defaultNamespace,
10466
- results: [],
10467
- count: 0,
10468
- lcmEnabled: true
10469
- };
10655
+ if (action === "list") {
10656
+ const tasks = await storage.listTasks({
10657
+ status: asStatus(request.status),
10658
+ owner: request.owner?.trim() || void 0,
10659
+ assignee: request.assignee?.trim() || void 0,
10660
+ projectId: request.projectId?.trim() || void 0
10661
+ });
10662
+ return { action, count: tasks.length, tasks };
10470
10663
  }
10471
- if (namespace === void 0) {
10472
- return {
10473
- query: request.query,
10474
- namespace: this.deps.orchestrator.config.defaultNamespace,
10475
- results: [],
10476
- count: 0,
10477
- lcmEnabled: true
10478
- };
10664
+ if (action === "update") {
10665
+ if (!request.id?.trim()) throw new EngramAccessInputError("id is required for update");
10666
+ const patch = {};
10667
+ if (request.title !== void 0) patch.title = request.title;
10668
+ if (request.description !== void 0) patch.description = request.description;
10669
+ const st = asStatus(request.status);
10670
+ if (st) patch.status = st;
10671
+ const pr = asPriority(request.priority);
10672
+ if (pr) patch.priority = pr;
10673
+ if (request.owner !== void 0) patch.owner = request.owner || null;
10674
+ if (request.assignee !== void 0) patch.assignee = request.assignee || null;
10675
+ if (request.projectId !== void 0) patch.projectId = request.projectId || null;
10676
+ if (request.tags) patch.tags = request.tags;
10677
+ if (request.dueAt !== void 0) patch.dueAt = request.dueAt || null;
10678
+ return { action, task: await storage.updateTask(request.id, patch) };
10479
10679
  }
10480
- const limit = Math.max(1, Math.min(request.limit ?? 10, 100));
10481
- const lcmReadNamespace = profileLcmReadNamespaces !== null ? profileLcmReadNamespaces[0] ?? this.deps.orchestrator.config.defaultNamespace : this.deps.resolveLcmReadNamespace(
10482
- request.namespace,
10483
- namespace,
10484
- request.sessionKey,
10485
- request.authenticatedPrincipal
10486
- );
10487
- const lcmSessionKeyIds = request.sessionKey ? profileLcmReadNamespaces !== null ? this.deps.lcmSessionIdsForNamespaces(
10488
- profileLcmReadNamespaces,
10489
- request.sessionKey
10490
- ) : this.deps.resolveLcmReadSessionIds(
10491
- request.namespace,
10492
- namespace,
10493
- request.sessionKey,
10494
- request.authenticatedPrincipal
10495
- ) : [void 0];
10496
- const lcmSessionPrefixes = request.sessionPrefix ? profileLcmReadNamespaces !== null && !request.sessionKey ? this.deps.lcmSessionIdsForNamespaces(
10497
- profileLcmReadNamespaces,
10498
- request.sessionPrefix
10499
- ) : [
10500
- lcmSessionKeyForNamespace(
10501
- lcmReadNamespace,
10502
- request.sessionPrefix,
10503
- this.deps.orchestrator.config.defaultNamespace
10504
- ) ?? request.sessionPrefix
10505
- ] : [void 0];
10506
- const hasScopedSession = typeof request.sessionKey === "string" && request.sessionKey.length > 0 || lcmSessionPrefixes.some((prefix) => typeof prefix === "string" && prefix.length > 0);
10507
- if (!hasScopedSession && resolveNamespaceCapabilities(this.deps.orchestrator.config).namespaces === true) {
10508
- return {
10509
- query: request.query,
10510
- namespace,
10511
- results: [],
10512
- count: 0,
10513
- lcmEnabled: true
10514
- };
10680
+ if (action === "transition") {
10681
+ if (!request.id?.trim()) throw new EngramAccessInputError("id is required for transition");
10682
+ const st = asStatus(request.status);
10683
+ if (!st) throw new EngramAccessInputError("valid status is required for transition");
10684
+ return { action, task: await storage.transitionTask(request.id, st) };
10515
10685
  }
10516
- const seenRows = /* @__PURE__ */ new Set();
10517
- const results = [];
10518
- const lcmSearches = [];
10519
- for (const lcmSessionKey of lcmSessionKeyIds) {
10520
- for (const lcmSessionPrefix of lcmSessionPrefixes) {
10521
- lcmSearches.push({
10522
- key: lcmSessionKey,
10523
- prefix: lcmSessionPrefix,
10524
- promise: this.deps.orchestrator.lcmEngine.searchContextFull(
10525
- request.query,
10526
- limit,
10527
- lcmSessionKey,
10528
- lcmSessionPrefix
10529
- )
10530
- });
10531
- }
10686
+ if (action === "delete") {
10687
+ if (!request.id?.trim()) throw new EngramAccessInputError("id is required for delete");
10688
+ return { action, deleted: await storage.deleteTask(request.id) };
10532
10689
  }
10533
- const settledSearches = await Promise.allSettled(
10534
- lcmSearches.map((search) => search.promise)
10535
- );
10536
- for (let i = 0; i < settledSearches.length; i += 1) {
10537
- if (results.length >= limit) break;
10538
- const settled = settledSearches[i];
10539
- if (!settled || settled.status === "rejected") {
10540
- const failed = lcmSearches[i];
10541
- log.warn(
10542
- `lcmSearch: failed for key=${failed?.key ?? "<none>"} prefix=${failed?.prefix ?? "<none>"}: ${settled?.status === "rejected" ? settled.reason : "missing result"}`
10543
- );
10544
- continue;
10545
- }
10546
- for (const r of settled.value) {
10547
- const dedupeKey = `${r.session_id}\0${r.turn_index}`;
10548
- if (seenRows.has(dedupeKey)) continue;
10549
- seenRows.add(dedupeKey);
10550
- results.push({
10551
- sessionId: r.session_id,
10552
- content: r.content,
10553
- turnIndex: r.turn_index
10554
- });
10555
- if (results.length >= limit) break;
10690
+ throw new EngramAccessInputError(`Unsupported work_task action: ${action}`);
10691
+ }
10692
+ async workProject(request) {
10693
+ const STATUSES = /* @__PURE__ */ new Set(["active", "on_hold", "completed", "archived"]);
10694
+ const asStatus = (v) => v && STATUSES.has(v) ? v : void 0;
10695
+ const storage = new WorkStorage(this.deps.orchestrator.config.memoryDir);
10696
+ await storage.ensureDirectories();
10697
+ const action = request.action;
10698
+ if (action === "create") {
10699
+ if (!request.name?.trim()) throw new EngramAccessInputError("name is required for create");
10700
+ const project = await storage.createProject({
10701
+ name: request.name,
10702
+ description: request.description,
10703
+ status: asStatus(request.status),
10704
+ owner: request.owner?.trim() || void 0,
10705
+ tags: request.tags
10706
+ });
10707
+ return { action, project };
10708
+ }
10709
+ if (action === "get") {
10710
+ if (!request.id?.trim()) throw new EngramAccessInputError("id is required for get");
10711
+ return { action, project: await storage.getProject(request.id) };
10712
+ }
10713
+ if (action === "list") {
10714
+ const projects = await storage.listProjects();
10715
+ return { action, count: projects.length, projects };
10716
+ }
10717
+ if (action === "update") {
10718
+ if (!request.id?.trim()) throw new EngramAccessInputError("id is required for update");
10719
+ const patch = {};
10720
+ if (request.name !== void 0) patch.name = request.name;
10721
+ if (request.description !== void 0) patch.description = request.description;
10722
+ const st = asStatus(request.status);
10723
+ if (st) patch.status = st;
10724
+ if (request.owner !== void 0) patch.owner = request.owner || null;
10725
+ if (request.tags) patch.tags = request.tags;
10726
+ return { action, project: await storage.updateProject(request.id, patch) };
10727
+ }
10728
+ if (action === "delete") {
10729
+ if (!request.id?.trim()) throw new EngramAccessInputError("id is required for delete");
10730
+ return { action, deleted: await storage.deleteProject(request.id) };
10731
+ }
10732
+ if (action === "link_task") {
10733
+ if (!request.taskId?.trim() || !request.projectId?.trim()) {
10734
+ throw new EngramAccessInputError("taskId and projectId are required for link_task");
10556
10735
  }
10736
+ return { action, linked: await storage.linkTaskToProject(request.taskId, request.projectId) };
10557
10737
  }
10558
- return {
10559
- query: request.query,
10560
- namespace,
10561
- results,
10562
- count: results.length,
10563
- lcmEnabled: true
10564
- };
10738
+ throw new EngramAccessInputError(`Unsupported work_project action: ${action}`);
10565
10739
  }
10566
- /**
10567
- * Resolve the effective LCM NAMESPACE a same-session operation must prefix
10568
- * with (the namespace half of {@link resolveLcmReadSessionKey}). Split out so
10569
- * `lcmSearch` can apply ONE namespace to BOTH its `sessionKey` and its
10570
- * `sessionPrefix` the prefix is a search fragment, not a real session, so its
10571
- * own coding context can't be looked up; it must inherit the namespace resolved
10572
- * from the real session (`sessionKeyForOverlay`).
10573
- *
10574
- * `purpose` selects the AUTHORIZATION gate applied before honouring the
10575
- * coding overlay (#1505 round 3 + round 4, codex P2):
10576
- *
10577
- * - `"read"` (`lcmSearch` / raw-excerpt recall): the overlay rows are only
10578
- * visible when the principal SELF base is in the READABLE RECALL SET — the
10579
- * same gate the orchestrator's `lcmReadNamespaceForSession` and the recall
10580
- * namespace set use (`recallNamespacesForPrincipal`, gated by both
10581
- * `defaultRecallNamespaces.includes("self")` AND `canReadNamespace`). A
10582
- * caller that passed the default read check must NOT receive
10583
- * `<principal>-project-*` rows the policy never granted (cross-tenant read
10584
- * leak). When the self base is not readable, keep the just-authorized
10585
- * namespace (collapses to the raw key on the default store).
10586
- *
10587
- * - `"write"` (`lcmCompactionFlush` / `lcmCompactionRecord`): these are
10588
- * write/maintenance operations on the SAME queue `observe` just wrote, so
10589
- * the gate must mirror observe's WRITE authorization (`canWriteNamespace`
10590
- * on the self base), NOT readability. A principal that can WRITE but not
10591
- * READ its self namespace (or whose `defaultRecallNamespaces` omits `self`)
10592
- * archived under the overlay key via `observe`; gating compaction by
10593
- * readability would fall back to the default/raw key and leave that queue
10594
- * never flushed/recorded (round-4 codex P2). Write-authorized ⇒ overlay
10595
- * key, matching the observe write key (rule 42 read/write parity; rule 39
10596
- * identical gates across paths).
10597
- */
10598
- resolveLcmReadNamespace(explicitNamespace, resolvedNamespace, sessionKeyForOverlay, authenticatedPrincipal, purpose = "read") {
10599
- const hasExplicitNamespace = typeof explicitNamespace === "string" && explicitNamespace.trim().length > 0;
10600
- if (hasExplicitNamespace) return resolvedNamespace;
10601
- const principal = this.deps.resolveRequestPrincipal(
10602
- sessionKeyForOverlay,
10603
- authenticatedPrincipal
10604
- );
10605
- const base = defaultNamespaceForPrincipal(principal, this.deps.orchestrator.config);
10606
- const overlaid = this.deps.orchestrator.applyCodingNamespaceOverlay(
10607
- sessionKeyForOverlay,
10608
- base
10609
- );
10610
- if (overlaid === base) return this.deps.orchestrator.config.defaultNamespace;
10611
- const authorized = purpose === "write" ? canWriteNamespace(principal, base, this.deps.orchestrator.config) : recallNamespacesForPrincipal(
10612
- principal,
10613
- this.deps.orchestrator.config
10614
- ).includes(base);
10615
- if (authorized) return overlaid;
10616
- if (purpose === "read") return this.deps.orchestrator.config.defaultNamespace;
10617
- return resolvedNamespace;
10740
+ async workBoard(request) {
10741
+ const memoryDir = this.deps.orchestrator.config.memoryDir;
10742
+ await new WorkStorage(memoryDir).ensureDirectories();
10743
+ const action = request.action;
10744
+ const projectId = request.projectId?.trim() || void 0;
10745
+ if (action === "export_markdown") {
10746
+ const markdown = await exportWorkBoardMarkdown({ memoryDir, projectId });
10747
+ return { action, markdown: wrapWorkLayerContext(markdown, { linkToMemory: request.linkToMemory === true }) };
10748
+ }
10749
+ if (action === "export_snapshot") {
10750
+ const snapshot = await exportWorkBoardSnapshot({ memoryDir, projectId });
10751
+ return { action, snapshot };
10752
+ }
10753
+ if (action === "import_snapshot") {
10754
+ if (!request.snapshotJson?.trim()) throw new EngramAccessInputError("snapshotJson is required for import_snapshot");
10755
+ const snapshot = JSON.parse(request.snapshotJson);
10756
+ const result = await importWorkBoardSnapshot({ memoryDir, snapshot, projectId });
10757
+ return { action, result };
10758
+ }
10759
+ throw new EngramAccessInputError(`Unsupported work_board action: ${action}`);
10618
10760
  }
10619
- /**
10620
- * Resolve the namespace the raw-disclosure excerpt lookup
10621
- * ({@link fetchRawExcerpts}) must prefix its LCM `session_id` with (#1505
10622
- * thread 2f7). Raw disclosure reads the SAME LCM archive `lcmSearch` and the
10623
- * in-prompt LCM sections read, so it MUST pass through the identical
10624
- * read-authorization gate — NOT `snapshot.namespace`, which records the
10625
- * effective WRITE/overlay namespace (`<principal>-project-*`) even when the
10626
- * principal can WRITE but not READ its self base (or `defaultRecallNamespaces`
10627
- * omits `self`). Routing through `resolveLcmReadNamespace(..., "read")` makes
10628
- * raw disclosure fall back to the default store exactly like normal recall +
10629
- * `lcmSearch`, so it never attaches overlay transcript rows the read gate
10630
- * excludes (cross-tenant read leak). Collapses to the default store / raw
10631
- * sessionKey for single-store / no-overlay / explicit-default flows, so
10632
- * single-user recall is byte-for-byte unchanged.
10633
- *
10634
- * Returns `undefined` when NO readable LCM namespace exists for an IMPLICIT
10635
- * (no explicit `namespace`) raw recall — i.e. a restrictive `default` READ
10636
- * policy denies the principal `default` AND no overlay/self namespace is
10637
- * readable. In that case the caller emits NO excerpts rather than throwing
10638
- * `namespace is not readable: default` (#1505 thread NBHWz): normal recall
10639
- * still succeeds via `recallNamespacesForPrincipal`, so `disclosure: "raw"`
10640
- * must degrade gracefully (empty excerpts), never pre-authorize `default`.
10641
- *
10642
- * IMPLICIT-namespace fallback selection derives from the ALREADY
10643
- * read-authorized recall namespace set (`recallNamespacesForPrincipal` +
10644
- * `canReadNamespace`) — the principal's self base when it is in the readable
10645
- * recall set, else `config.defaultNamespace` ONLY when the principal may read
10646
- * it. It NEVER pre-authorizes `default`. An EXPLICIT `namespace` is still
10647
- * authorized strictly via `resolveReadableNamespace` (explicit reads must pass
10648
- * the ACL — no behavior change).
10649
- */
10650
- resolveRawExcerptReadNamespace(explicitNamespace, sessionKey, authenticatedPrincipal) {
10651
- const principal = this.deps.resolveRequestPrincipal(
10652
- sessionKey,
10653
- authenticatedPrincipal
10654
- );
10655
- const hasExplicitNamespace = typeof explicitNamespace === "string" && explicitNamespace.trim().length > 0;
10656
- if (hasExplicitNamespace) {
10657
- const resolvedNamespace = this.deps.resolveReadableNamespace(
10658
- explicitNamespace,
10659
- principal
10761
+ async memoryActionApply(request) {
10762
+ const actionTypes = /* @__PURE__ */ new Set([
10763
+ "store_episode",
10764
+ "store_note",
10765
+ "update_note",
10766
+ "create_artifact",
10767
+ "summarize_node",
10768
+ "discard",
10769
+ "link_graph"
10770
+ ]);
10771
+ if (!actionTypes.has(request.action)) {
10772
+ throw new EngramAccessInputError(
10773
+ `memory_action_apply: invalid action ${JSON.stringify(request.action)}`
10660
10774
  );
10661
- return this.deps.resolveLcmReadNamespace(
10662
- explicitNamespace,
10663
- resolvedNamespace,
10664
- sessionKey,
10665
- authenticatedPrincipal,
10666
- "read"
10775
+ }
10776
+ if (resolveCompressionCapabilities(this.deps.orchestrator.config).contextCompressionActions !== true) {
10777
+ throw new EngramAccessInputError(
10778
+ "memory_action_apply is disabled; enable contextCompressionActionsEnabled to use this tool"
10667
10779
  );
10668
10780
  }
10669
- const profileReadNamespaces = this.deps.resolveScopeProfileLcmReadNamespaces(
10670
- sessionKey,
10671
- authenticatedPrincipal
10672
- );
10673
- if (profileReadNamespaces !== null) return profileReadNamespaces[0];
10674
- const fallbackNamespace = this.deps.resolveImplicitLcmReadFallbackNamespace(principal);
10675
- if (fallbackNamespace === void 0) return void 0;
10676
- return this.deps.resolveLcmReadNamespace(
10677
- explicitNamespace,
10678
- fallbackNamespace,
10679
- sessionKey,
10680
- authenticatedPrincipal,
10681
- "read"
10682
- );
10683
- }
10684
- resolveLcmReadSessionIds(explicitNamespace, resolvedNamespace, sessionKey, authenticatedPrincipal) {
10685
- const primary = this.deps.resolveLcmReadSessionKey(
10686
- explicitNamespace,
10687
- resolvedNamespace,
10688
- sessionKey,
10689
- authenticatedPrincipal,
10690
- "read"
10691
- );
10692
- const hasExplicitNamespace = typeof explicitNamespace === "string" && explicitNamespace.trim().length > 0;
10693
- if (hasExplicitNamespace) return [primary];
10694
- const profileReadNamespaces = this.deps.resolveScopeProfileLcmReadNamespaces(
10695
- sessionKey,
10696
- authenticatedPrincipal
10697
- );
10698
- if (profileReadNamespaces !== null) {
10699
- return this.deps.lcmSessionIdsForNamespaces(profileReadNamespaces, sessionKey);
10781
+ const outcome = request.outcome ?? "skipped";
10782
+ if (outcome !== "applied" && outcome !== "skipped" && outcome !== "failed") {
10783
+ throw new EngramAccessInputError(
10784
+ `memory_action_apply: outcome must be "applied", "skipped", or "failed"; got ${JSON.stringify(outcome)}`
10785
+ );
10700
10786
  }
10701
- const principal = this.deps.resolveRequestPrincipal(
10702
- sessionKey,
10703
- authenticatedPrincipal
10704
- );
10705
- const base = defaultNamespaceForPrincipal(
10706
- principal,
10707
- this.deps.orchestrator.config
10708
- );
10709
- const overlaid = this.deps.orchestrator.applyCodingNamespaceOverlay(
10710
- sessionKey,
10711
- base
10712
- );
10713
- if (overlaid === base) return [primary];
10714
- const selfReadableInRecall = recallNamespacesForPrincipal(
10715
- principal,
10716
- this.deps.orchestrator.config
10717
- ).includes(base);
10718
- if (!selfReadableInRecall) return [primary];
10719
- const overlay = resolveCodingNamespaceOverlay(
10720
- this.deps.orchestrator.getCodingContextForSession(sessionKey),
10721
- this.deps.orchestrator.config.codingMode,
10722
- this.deps.orchestrator.config.defaultNamespace
10723
- );
10724
- const fallbackNamespaces = (overlay?.readFallbacks ?? []).map(
10725
- (fallback) => combineNamespaces(base, fallback)
10787
+ const resolvedNs = this.deps.writableNamespaceFor(
10788
+ request.namespace,
10789
+ request.sessionKey,
10790
+ request.principal
10726
10791
  );
10727
- const out = [primary];
10728
- const seen = /* @__PURE__ */ new Set([primary]);
10729
- for (const ns of fallbackNamespaces) {
10730
- const key = lcmSessionKeyForNamespace(
10731
- ns,
10732
- sessionKey,
10733
- this.deps.orchestrator.config.defaultNamespace
10734
- ) ?? sessionKey;
10735
- if (!seen.has(key)) {
10736
- seen.add(key);
10737
- out.push(key);
10738
- }
10792
+ const inputSummaryParts = [
10793
+ request.content,
10794
+ request.category ? `category=${request.category}` : void 0,
10795
+ request.linkTargetId ? `linkTargetId=${request.linkTargetId}` : void 0,
10796
+ request.linkType ? `linkType=${request.linkType}` : void 0,
10797
+ typeof request.linkStrength === "number" ? `linkStrength=${request.linkStrength}` : void 0,
10798
+ request.artifactType ? `artifactType=${request.artifactType}` : void 0,
10799
+ typeof request.execute === "boolean" ? `execute=${request.execute}` : void 0
10800
+ ].filter((part) => typeof part === "string" && part.length > 0);
10801
+ const event = {
10802
+ action: request.action,
10803
+ outcome,
10804
+ namespace: resolvedNs,
10805
+ actor: "access.memory_action_apply",
10806
+ subsystem: "access.memory_action_apply",
10807
+ reason: request.reason,
10808
+ memoryId: request.memoryId,
10809
+ sourceSessionKey: request.sessionKey,
10810
+ inputSummary: inputSummaryParts.length > 0 ? inputSummaryParts.join(" | ").slice(0, 500) : void 0,
10811
+ dryRun: request.dryRun === true,
10812
+ promptHash: typeof request.sourcePrompt === "string" && request.sourcePrompt.length > 0 ? createHash6("sha256").update(request.sourcePrompt).digest("hex") : void 0
10813
+ };
10814
+ const preview = this.deps.orchestrator.previewMemoryActionEvent(event);
10815
+ if (request.dryRun === true) {
10816
+ return { recorded: false, dryRun: true, event: preview };
10739
10817
  }
10740
- return out;
10818
+ const recorded = await this.deps.orchestrator.appendMemoryActionEvent(event);
10819
+ return { recorded, event: preview };
10741
10820
  }
10742
10821
  };
10743
10822
 
@@ -10752,6 +10831,7 @@ export {
10752
10831
  clusterByKey,
10753
10832
  computeProcedureStats,
10754
10833
  formatProcedureStatsText,
10834
+ AccessObserveWriteSurface,
10755
10835
  AccessLcmSurface,
10756
10836
  qmdCollectionPathParts,
10757
10837
  QmdResultResolver,
@@ -10764,4 +10844,4 @@ export {
10764
10844
  EngramAccessService,
10765
10845
  AccessAdminOpsSurface
10766
10846
  };
10767
- //# sourceMappingURL=chunk-GCEV6SXP.js.map
10847
+ //# sourceMappingURL=chunk-DWDDQLSZ.js.map