@memberjunction/server 6.1.4 → 6.1.6

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 (227) hide show
  1. package/README.md +30 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/agentSessions/SessionManager.d.ts +5 -2
  4. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  5. package/dist/agentSessions/SessionManager.js +17 -8
  6. package/dist/agentSessions/SessionManager.js.map +1 -1
  7. package/dist/agentSessions/SessionRunIDVerifier.d.ts +63 -0
  8. package/dist/agentSessions/SessionRunIDVerifier.d.ts.map +1 -0
  9. package/dist/agentSessions/SessionRunIDVerifier.js +129 -0
  10. package/dist/agentSessions/SessionRunIDVerifier.js.map +1 -0
  11. package/dist/agentSessions/index.d.ts +1 -0
  12. package/dist/agentSessions/index.d.ts.map +1 -1
  13. package/dist/agentSessions/index.js +1 -0
  14. package/dist/agentSessions/index.js.map +1 -1
  15. package/dist/auth/actionAuthorization.d.ts +40 -0
  16. package/dist/auth/actionAuthorization.d.ts.map +1 -0
  17. package/dist/auth/actionAuthorization.js +94 -0
  18. package/dist/auth/actionAuthorization.js.map +1 -0
  19. package/dist/auth/exampleNewUserSubClass.js +2 -1
  20. package/dist/auth/exampleNewUserSubClass.js.map +1 -1
  21. package/dist/auth/magicLink/MagicLinkService.d.ts +17 -15
  22. package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
  23. package/dist/auth/magicLink/MagicLinkService.js +55 -38
  24. package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
  25. package/dist/auth/magicLink/magicLinkCore.d.ts +14 -33
  26. package/dist/auth/magicLink/magicLinkCore.d.ts.map +1 -1
  27. package/dist/auth/magicLink/magicLinkCore.js +20 -52
  28. package/dist/auth/magicLink/magicLinkCore.js.map +1 -1
  29. package/dist/auth/newUsers.d.ts.map +1 -1
  30. package/dist/auth/newUsers.js +15 -11
  31. package/dist/auth/newUsers.js.map +1 -1
  32. package/dist/context.d.ts +6 -0
  33. package/dist/context.d.ts.map +1 -1
  34. package/dist/context.js +24 -34
  35. package/dist/context.js.map +1 -1
  36. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  37. package/dist/generated/graphql-schemas/__mj.js +3417 -3428
  38. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  39. package/dist/generic/CacheInvalidationResolver.d.ts +44 -8
  40. package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
  41. package/dist/generic/CacheInvalidationResolver.js +59 -19
  42. package/dist/generic/CacheInvalidationResolver.js.map +1 -1
  43. package/dist/generic/ResolverBase.d.ts +95 -11
  44. package/dist/generic/ResolverBase.d.ts.map +1 -1
  45. package/dist/generic/ResolverBase.js +186 -76
  46. package/dist/generic/ResolverBase.js.map +1 -1
  47. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  48. package/dist/generic/RunViewResolver.js +12 -0
  49. package/dist/generic/RunViewResolver.js.map +1 -1
  50. package/dist/index.d.ts +1 -0
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +44 -45
  53. package/dist/index.js.map +1 -1
  54. package/dist/isolatedProvider.d.ts +20 -0
  55. package/dist/isolatedProvider.d.ts.map +1 -0
  56. package/dist/isolatedProvider.js +22 -0
  57. package/dist/isolatedProvider.js.map +1 -0
  58. package/dist/multiTenancy/index.d.ts.map +1 -1
  59. package/dist/multiTenancy/index.js +22 -2
  60. package/dist/multiTenancy/index.js.map +1 -1
  61. package/dist/postgresqlCompat.d.ts +8 -0
  62. package/dist/postgresqlCompat.d.ts.map +1 -0
  63. package/dist/postgresqlCompat.js +19 -0
  64. package/dist/postgresqlCompat.js.map +1 -0
  65. package/dist/postgresqlPoolSettings.d.ts +108 -0
  66. package/dist/postgresqlPoolSettings.d.ts.map +1 -0
  67. package/dist/postgresqlPoolSettings.js +134 -0
  68. package/dist/postgresqlPoolSettings.js.map +1 -0
  69. package/dist/realtimeWidget/widgetGuestElevation.d.ts +4 -2
  70. package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
  71. package/dist/realtimeWidget/widgetGuestElevation.js +4 -2
  72. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
  73. package/dist/resolvers/ActionResolver.d.ts +31 -5
  74. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  75. package/dist/resolvers/ActionResolver.js +75 -14
  76. package/dist/resolvers/ActionResolver.js.map +1 -1
  77. package/dist/resolvers/AdhocQueryResolver.d.ts +35 -17
  78. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
  79. package/dist/resolvers/AdhocQueryResolver.js +92 -166
  80. package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
  81. package/dist/resolvers/DatasetResolver.d.ts +24 -3
  82. package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
  83. package/dist/resolvers/DatasetResolver.js +57 -8
  84. package/dist/resolvers/DatasetResolver.js.map +1 -1
  85. package/dist/resolvers/EntityRecordNameResolver.d.ts +8 -23
  86. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  87. package/dist/resolvers/EntityRecordNameResolver.js +11 -48
  88. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  89. package/dist/resolvers/FetchEntityVectorsResolver.js +2 -2
  90. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  91. package/dist/resolvers/GetDataContextDataResolver.d.ts +33 -7
  92. package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
  93. package/dist/resolvers/GetDataContextDataResolver.js +117 -38
  94. package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
  95. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  96. package/dist/resolvers/GetDataResolver.js +13 -1
  97. package/dist/resolvers/GetDataResolver.js.map +1 -1
  98. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  99. package/dist/resolvers/QuerySystemUserResolver.js +2 -1
  100. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  101. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +83 -22
  102. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  103. package/dist/resolvers/RealtimeClientSessionResolver.js +174 -72
  104. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  105. package/dist/resolvers/RemoteBrowserActionResolver.d.ts +2 -0
  106. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
  107. package/dist/resolvers/RemoteBrowserActionResolver.js +5 -1
  108. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
  109. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  110. package/dist/resolvers/RunAIAgentResolver.js +2 -1
  111. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  112. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
  113. package/dist/resolvers/RunTemplateResolver.js +12 -11
  114. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  115. package/dist/resolvers/SearchKnowledgeResolver.d.ts +1 -0
  116. package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
  117. package/dist/resolvers/SearchKnowledgeResolver.js +5 -0
  118. package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
  119. package/dist/resolvers/SearchKnowledgeSystemUserResolver.d.ts.map +1 -1
  120. package/dist/resolvers/SearchKnowledgeSystemUserResolver.js +1 -0
  121. package/dist/resolvers/SearchKnowledgeSystemUserResolver.js.map +1 -1
  122. package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
  123. package/dist/resolvers/SyncDataResolver.js +6 -2
  124. package/dist/resolvers/SyncDataResolver.js.map +1 -1
  125. package/dist/resolvers/VectorizeEntityResolver.d.ts +1 -1
  126. package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -1
  127. package/dist/resolvers/VectorizeEntityResolver.js +14 -10
  128. package/dist/resolvers/VectorizeEntityResolver.js.map +1 -1
  129. package/dist/resolvers/adhocTimeout.d.ts +7 -0
  130. package/dist/resolvers/adhocTimeout.d.ts.map +1 -0
  131. package/dist/resolvers/adhocTimeout.js +12 -0
  132. package/dist/resolvers/adhocTimeout.js.map +1 -0
  133. package/dist/rest/ViewOperationsHandler.d.ts +3 -1
  134. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  135. package/dist/rest/ViewOperationsHandler.js +29 -17
  136. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  137. package/dist/services/TaskGraphActionRunner.d.ts +21 -8
  138. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
  139. package/dist/services/TaskGraphActionRunner.js +43 -16
  140. package/dist/services/TaskGraphActionRunner.js.map +1 -1
  141. package/package.json +92 -92
  142. package/src/__tests__/ActionResolver.authorization.test.ts +353 -0
  143. package/src/__tests__/AdhocQueryResolver.bugs.test.ts +14 -35
  144. package/src/__tests__/AdhocQueryResolver.pagination.test.ts +6 -2
  145. package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +3 -3
  146. package/src/__tests__/AdhocQueryResolver.sqlAuthorizer.test.ts +104 -0
  147. package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
  148. package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +27 -72
  149. package/src/__tests__/EntityRecordNameResolver.rowSecurity.test.ts +152 -0
  150. package/src/__tests__/GetDataContextDataResolver.sqlItems.test.ts +416 -0
  151. package/src/__tests__/ListOperationsResolver.adhocScreen.test.ts +137 -0
  152. package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +383 -0
  153. package/src/__tests__/RealtimeClientSessionResolver.test.ts +310 -31
  154. package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +129 -0
  155. package/src/__tests__/ResolverBase.adhocReadScope.test.ts +227 -0
  156. package/src/__tests__/ResolverBase.aggregateScreen.test.ts +174 -0
  157. package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
  158. package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +127 -0
  159. package/src/__tests__/ResolverBase.filterEscaping.test.ts +24 -2
  160. package/src/__tests__/ResolverBase.isaPromotionCreate.test.ts +412 -0
  161. package/src/__tests__/RunTemplateResolver.test.ts +132 -0
  162. package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +165 -0
  163. package/src/__tests__/SessionManager.test.ts +94 -17
  164. package/src/__tests__/SessionRunIDVerifier.test.ts +251 -0
  165. package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +131 -0
  166. package/src/__tests__/TaskGraphActionRunner.storableOutputs.test.ts +76 -0
  167. package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
  168. package/src/__tests__/VectorizeEntityResolver.test.ts +106 -0
  169. package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +157 -0
  170. package/src/__tests__/actionAuthorization.test.ts +130 -0
  171. package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
  172. package/src/__tests__/cacheInvalidationRecordData.test.ts +64 -0
  173. package/src/__tests__/isolatedProviderTransactions.test.ts +169 -0
  174. package/src/__tests__/magicLink.test.ts +15 -76
  175. package/src/__tests__/magicLinkService.consume.test.ts +230 -0
  176. package/src/__tests__/magicLinkService.provisionLog.test.ts +5 -0
  177. package/src/__tests__/multiTenancy.postgresRewrite.test.ts +181 -0
  178. package/src/__tests__/multiTenancy.security.test.ts +51 -0
  179. package/src/__tests__/multiTenancy.test.ts +1 -1
  180. package/src/__tests__/newUsers.test.ts +62 -5
  181. package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
  182. package/src/__tests__/translateBracketsToPG.test.ts +20 -0
  183. package/src/agentSessions/SessionManager.ts +23 -25
  184. package/src/agentSessions/SessionRunIDVerifier.ts +171 -0
  185. package/src/agentSessions/index.ts +1 -0
  186. package/src/auth/actionAuthorization.ts +119 -0
  187. package/src/auth/exampleNewUserSubClass.ts +2 -2
  188. package/src/auth/magicLink/MagicLinkService.ts +61 -39
  189. package/src/auth/magicLink/magicLinkCore.ts +19 -56
  190. package/src/auth/newUsers.ts +15 -11
  191. package/src/context.ts +25 -34
  192. package/src/generated/graphql-schemas/__mj.ts +3417 -3428
  193. package/src/generic/CacheInvalidationResolver.ts +74 -19
  194. package/src/generic/ResolverBase.ts +207 -80
  195. package/src/generic/RunViewResolver.ts +17 -0
  196. package/src/index.ts +49 -46
  197. package/src/isolatedProvider.ts +22 -0
  198. package/src/multiTenancy/index.ts +24 -2
  199. package/src/postgresqlCompat.ts +21 -0
  200. package/src/postgresqlPoolSettings.ts +213 -0
  201. package/src/realtimeWidget/widgetGuestElevation.ts +4 -2
  202. package/src/resolvers/ActionResolver.ts +84 -21
  203. package/src/resolvers/AdhocQueryResolver.ts +105 -187
  204. package/src/resolvers/DatasetResolver.ts +70 -10
  205. package/src/resolvers/EntityRecordNameResolver.ts +11 -52
  206. package/src/resolvers/FetchEntityVectorsResolver.ts +2 -2
  207. package/src/resolvers/GetDataContextDataResolver.ts +139 -41
  208. package/src/resolvers/GetDataResolver.ts +14 -1
  209. package/src/resolvers/QuerySystemUserResolver.ts +2 -1
  210. package/src/resolvers/RealtimeClientSessionResolver.ts +196 -77
  211. package/src/resolvers/RemoteBrowserActionResolver.ts +11 -1
  212. package/src/resolvers/RunAIAgentResolver.ts +4 -1
  213. package/src/resolvers/RunTemplateResolver.ts +13 -12
  214. package/src/resolvers/SearchKnowledgeResolver.ts +4 -0
  215. package/src/resolvers/SearchKnowledgeSystemUserResolver.ts +1 -0
  216. package/src/resolvers/SyncDataResolver.ts +6 -2
  217. package/src/resolvers/VectorizeEntityResolver.ts +16 -11
  218. package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
  219. package/src/resolvers/adhocTimeout.ts +12 -0
  220. package/src/rest/ViewOperationsHandler.ts +37 -18
  221. package/src/services/TaskGraphActionRunner.ts +45 -16
  222. package/dist/resolvers/adhoc-query-helpers.d.ts +0 -27
  223. package/dist/resolvers/adhoc-query-helpers.d.ts.map +0 -1
  224. package/dist/resolvers/adhoc-query-helpers.js +0 -42
  225. package/dist/resolvers/adhoc-query-helpers.js.map +0 -1
  226. package/src/__tests__/adhoc-query-helpers.test.ts +0 -50
  227. package/src/resolvers/adhoc-query-helpers.ts +0 -50
@@ -23,9 +23,9 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
23
23
  * and **executes** every tool call the browser relays back. The three mutations here cover the MVP:
24
24
  *
25
25
  * 1. {@link RealtimeClientSessionResolver.StartRealtimeClientSession} — authorize, create the
26
- * session (storing the target agent id server-side, authoritatively), mint the ephemeral config.
27
- * 2. {@link RealtimeClientSessionResolver.ExecuteRealtimeSessionTool} — execute a relayed tool call,
28
- * reading the target agent id from the session (NOT the client) so the browser can't swap targets.
26
+ * session (storing the target agent id on it), mint the ephemeral config.
27
+ * 2. {@link RealtimeClientSessionResolver.ExecuteRealtimeSessionTool} — execute a relayed tool call
28
+ * for the target agent stored on the session, re-authorized for the caller on every call.
29
29
  * 3. {@link RealtimeClientSessionResolver.RelayRealtimeTranscript} — persist a transcript turn as a
30
30
  * `Conversation Detail`.
31
31
  *
@@ -33,6 +33,13 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
33
33
  * - **Start**: the caller must have `CanRun` on the *target* agent (the Realtime Co-Agent is an internal
34
34
  * orchestration agent, so the meaningful gate is the agent doing the real work).
35
35
  * - **Execute / Relay**: inbound ownership — the session's `UserID` must equal `contextUser.ID`.
36
+ * - **Session `Config`**: the owner can edit it through the generic entity API, so nothing in it is
37
+ * trusted. Execute re-checks `CanRun` on its target, direct actions never come from it, and its
38
+ * run ids are used only when {@link SessionRunIDVerifier} finds them to be records of the session.
39
+ * - **Scope-limited callers** (anonymous and resource-scoped magic-link sessions, including public
40
+ * web-widget guests): no direct actions. Their scope exists only as row-level filters, which an
41
+ * action can read around, and a scoped anonymous caller's relay runs as the system user. They
42
+ * reach the target agent only through `invoke-target-agent`.
36
43
  *
37
44
  * @module @memberjunction/server
38
45
  */
@@ -41,10 +48,12 @@ import { AuthorizationEvaluator, LogError, LogStatus, RunView } from '@memberjun
41
48
  import { UUIDsEqual, IsValidUUID } from '@memberjunction/global';
42
49
  import { AIEngine } from '@memberjunction/aiengine';
43
50
  import { AIAgentPermissionHelper, AIEngineBase } from '@memberjunction/ai-engine-base';
44
- import { RealtimeClientSessionService, RealtimeChannelServerHost, EvaluateRuntimeOverrideAuthorization, FindIgnoredRealtimeConfigKeys, ParseRealtimeTypeConfiguration, ResolveEffectiveRealtimeConfig, REALTIME_ADVANCED_SESSION_CONTROLS_AUTHORIZATION, resolveRecordingStorageAccountID, storeRealtimeRecording, writeRealtimeRecordingSegment, deleteRealtimeRecordingSegments, } from '@memberjunction/ai-agents';
51
+ import { RealtimeClientSessionService, RealtimeChannelServerHost, EvaluateRuntimeOverrideAuthorization, FindIgnoredRealtimeConfigKeys, ParseRealtimeTypeConfiguration, ResolveEffectiveRealtimeConfig, INVOKE_TARGET_AGENT_TOOL_NAME, REALTIME_ADVANCED_SESSION_CONTROLS_AUTHORIZATION, resolveRecordingStorageAccountID, storeRealtimeRecording, writeRealtimeRecordingSegment, deleteRealtimeRecordingSegments, } from '@memberjunction/ai-agents';
45
52
  import { ResolverBase } from '../generic/ResolverBase.js';
46
53
  import { GetReadWriteProvider } from '../util.js';
47
54
  import { SessionManager } from '../agentSessions/index.js';
55
+ import { SessionRunIDVerifier } from '../agentSessions/SessionRunIDVerifier.js';
56
+ import { IsScopeLimitedPrincipal } from '../auth/scopeLimitedPrincipal.js';
48
57
  import { resolveWidgetGuestRunContext, ResolveScopedAnonymousRunUser } from '../realtimeWidget/widgetGuestElevation.js';
49
58
  /**
50
59
  * Progress steps worth narrating to the realtime model — mirrors the normal agent-run path's filter
@@ -265,6 +274,31 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
265
274
  // state on — otherwise that cleanup silently no-ops on a freshly-constructed instance.
266
275
  this.clientSessionService = new RealtimeClientSessionService();
267
276
  this.sessionManager = new SessionManager(this.clientSessionService);
277
+ this.runIDVerifier = new SessionRunIDVerifier();
278
+ }
279
+ /**
280
+ * The reason a `Save()`/`Delete()` returned false, for a log line. `CompleteMessage` renders
281
+ * `Message`, `Error` and every `Errors[]` entry (validation errors included, since `Save()`
282
+ * runs `Validate()` and records its errors there), so there is no separate `Validate()` call.
283
+ * When a subclass refused the write without recording anything, say so explicitly instead of
284
+ * "unknown error", which is what hid MJ#4791.
285
+ *
286
+ * Only a NULL `LatestResult` proves the refusal happened before the provider (nothing was
287
+ * registered at all). A SUCCESS `LatestResult` is an earlier save on the same instance — the
288
+ * refusal registered nothing — so its text must never be reported as this failure's reason.
289
+ */
290
+ describeSaveFailure(entity) {
291
+ const latest = entity.LatestResult;
292
+ if (!latest) {
293
+ return `no failure detail recorded (LatestResult null; ResultHistory length ${entity.ResultHistory.length}) ` +
294
+ '— the write was refused before reaching the provider';
295
+ }
296
+ if (latest.Success) {
297
+ return 'no failure detail recorded (latest result entry is a prior success; ' +
298
+ `ResultHistory length ${entity.ResultHistory.length})`;
299
+ }
300
+ const detail = latest.CompleteMessage?.trim();
301
+ return detail || `no failure detail recorded (LatestResult empty; ResultHistory length ${entity.ResultHistory.length})`;
268
302
  }
269
303
  /**
270
304
  * Start a client-direct realtime voice session targeting `targetAgentId`.
@@ -276,7 +310,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
276
310
  * agent's `DefaultCoAgentID` → type-level `AIAgentCoAgent` default row → global Realtime Co-Agent) —
277
311
  * see {@link RealtimeClientSessionResolver.resolveCoAgentID} for the full contract.
278
312
  * 3. Create the durable `AIAgentSession` (run by the co-agent), storing `targetAgentID` in its
279
- * config server-side — this is the authoritative target for all later relays.
313
+ * config — the target later relays use, re-authorized for the caller on each relay.
280
314
  * 4. Mint the {@link import('@memberjunction/ai').ClientRealtimeSessionConfig} via the service.
281
315
  * On failure the just-created session is closed so no half-open session leaks.
282
316
  *
@@ -363,8 +397,10 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
363
397
  * Execute a single tool call the browser relayed from its provider socket and return the
364
398
  * serialized result for the browser to relay back to the model.
365
399
  *
366
- * Ownership-gated; the target agent id is read from the **session config** (authoritative), never
367
- * from the client. Heartbeats the session on success.
400
+ * Ownership-gated. The target agent id is read from the session config and `CanRun` on it is
401
+ * checked for the caller on every call, because the owner can edit the config. A direct-action
402
+ * call from a scope-limited caller is refused before any elevation. Heartbeats the session on
403
+ * success.
368
404
  *
369
405
  * @returns The serialized tool result JSON.
370
406
  */
@@ -372,12 +408,17 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
372
408
  const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
373
409
  const session = await this.loadOwnedActiveSession(agentSessionId, contextUser, provider);
374
410
  const config = this.readSessionConfig(session);
411
+ await this.assertCanRunTarget(config.targetAgentID, contextUser, provider);
412
+ if (toolName !== INVOKE_TARGET_AGENT_TOOL_NAME && IsScopeLimitedPrincipal(contextUser)) {
413
+ return this.refuseDirectAction(agentSessionId, toolName);
414
+ }
375
415
  // SCOPED-ANONYMOUS ELEVATION (issue #3371): once ownership is proven above, the delegated
376
416
  // run + its AI-run-entity writes execute as the system user for a scoped anonymous caller
377
417
  // (the caller's role deliberately holds no grants on the run entities). The lead
378
- // targetAgentID comes from the session config and was CanRun-gated at start; the colleague
379
- // union is gated just below, against the CALLER, so elevation never widens agent authority.
418
+ // targetAgentID is CanRun-gated against the CALLER just above, and the colleague union
419
+ // just below, so elevation never widens agent authority.
380
420
  const runUser = ResolveScopedAnonymousRunUser(contextUser);
421
+ const runIDs = await this.runIDVerifier.Verify(session.ID, { CoAgentRunID: config.coAgentRunID, PendingFeedbackRunID: config.pendingFeedbackRunID }, runUser, provider);
381
422
  if (runUser !== contextUser) {
382
423
  LogStatus(`ExecuteRealtimeSessionTool: dispatching relayed tool '${toolName}' for session ${agentSessionId} ` +
383
424
  'under the system user (scoped-anonymous caller).');
@@ -390,20 +431,21 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
390
431
  // Multi-target (Move 4): the session's persisted allowed-agent union — a model-named
391
432
  // colleague in the call is validated against this; absent ⇒ single-target behavior.
392
433
  AllowedAgents: await this.filterAllowedAgentsByCanRun(config.allowedAgents, contextUser),
393
- DirectActions: config.directActions,
434
+ // No DirectActions: the service takes them from the session's start-time projection
435
+ // or the target agent's metadata, never from the owner-editable config.
394
436
  // Attribution follows the VISITOR even when `runUser` is elevated: the delegated run
395
437
  // row and its context-memory scope must stay the person's, not the system user's.
396
438
  AttributionUserID: contextUser.ID,
397
439
  // Nest the delegated target-agent run under the co-agent observability run (when present).
398
- ParentRunID: config.coAgentRunID,
440
+ ParentRunID: runIDs.CoAgentRunID,
399
441
  Call: { CallID: callId, ToolName: toolName, Arguments: argsJson },
400
442
  OnProgress: this.buildDelegationProgressCallback(pubSub, userPayload, agentSessionId, callId),
401
443
  // Resume a previously-paused delegated run (if any) with the user's answer.
402
- ResumeRunID: config.pendingFeedbackRunID,
444
+ ResumeRunID: runIDs.PendingFeedbackRunID,
403
445
  }, runUser, provider);
404
446
  const durationMs = Date.now() - startTime;
405
447
  LogStatus(`ExecuteRealtimeSessionTool: completed relayed tool '${toolName}' (callId: ${callId}) in ${durationMs}ms`);
406
- if (toolName !== 'invoke-target-agent') {
448
+ if (toolName !== INVOKE_TARGET_AGENT_TOOL_NAME) {
407
449
  await this.persistDirectActionTurn(session, callId, toolName, argsJson, ResultJson, durationMs, contextUser, provider);
408
450
  }
409
451
  // Roll the paused-run id forward in the session config: clear the one we just consumed, and
@@ -412,7 +454,9 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
412
454
  // Junction-link any artifacts the delegated run produced into the session's conversation
413
455
  // history (best-effort) — so chat, session review, and resume carryover can all see them.
414
456
  // Runs as `runUser`: the junction entity is not among an anonymous caller's relay grants.
415
- await this.linkDelegatedArtifactsToConversation(session, Artifacts, runUser, provider);
457
+ // The hidden anchor detail it may need to create, though, is written by the CALLER (#4791)
458
+ // — same reason `persistDirectActionTurn` is: the System user is not the conversation owner.
459
+ await this.linkDelegatedArtifactsToConversation(session, Artifacts, runUser, contextUser, provider);
416
460
  await this.sessionManager.Heartbeat(agentSessionId, contextUser, provider);
417
461
  return ResultJson;
418
462
  }
@@ -439,6 +483,18 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
439
483
  }), userPayload);
440
484
  };
441
485
  }
486
+ /**
487
+ * The tool result for a direct-action call from a scope-limited caller (see the authorization
488
+ * model above). It has the broker's result shape, so the model hears that the tool is not
489
+ * available and can ask the target agent through `invoke-target-agent` instead.
490
+ */
491
+ refuseDirectAction(agentSessionId, toolName) {
492
+ LogStatus(`ExecuteRealtimeSessionTool: refused direct action '${toolName}' for a scope-limited caller on session ${agentSessionId}.`);
493
+ return JSON.stringify({
494
+ success: false,
495
+ output: `The tool '${toolName}' is not available in this session. Ask the agent to do it with ${INVOKE_TARGET_AGENT_TOOL_NAME}.`,
496
+ });
497
+ }
442
498
  /**
443
499
  * Rolls the session's `pendingFeedbackRunID` forward after a relayed tool call. The id we just
444
500
  * consumed (resumed or none) is dropped; a new paused run id is stored only when the run paused
@@ -463,7 +519,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
463
519
  };
464
520
  session.Config_ = JSON.stringify(next);
465
521
  if (!(await session.Save())) {
466
- LogError(`RealtimeClientSessionResolver.updatePendingFeedbackRunID save failed: ${session.LatestResult?.CompleteMessage ?? 'unknown error'}`);
522
+ LogError(`RealtimeClientSessionResolver.updatePendingFeedbackRunID save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`);
467
523
  }
468
524
  }
469
525
  /**
@@ -492,9 +548,10 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
492
548
  // conversation (run-viewer observability parity). Best-effort — never fails the transcript relay.
493
549
  // The prompt-run write runs as the scoped-anonymous elevated user (issue #3371) — the visible
494
550
  // Conversation Detail above deliberately stays on the caller.
495
- const promptRunID = this.readPromptRunID(session);
551
+ const runUser = ResolveScopedAnonymousRunUser(contextUser);
552
+ const promptRunID = await this.readPromptRunID(session, runUser, provider);
496
553
  if (promptRunID) {
497
- await this.clientSessionService.AppendPromptRunMessage(promptRunID, this.mapTranscriptRoleToChatRole(role), text, replacesPrevious ?? false, ResolveScopedAnonymousRunUser(contextUser), provider);
554
+ await this.clientSessionService.AppendPromptRunMessage(promptRunID, this.mapTranscriptRoleToChatRole(role), text, replacesPrevious ?? false, runUser, provider);
498
555
  }
499
556
  await this.sessionManager.Heartbeat(agentSessionId, contextUser, provider);
500
557
  return true;
@@ -647,11 +704,12 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
647
704
  // Persist the tool turn facts into a hidden ConversationDetail turn so session review
648
705
  // can reconstruct the tool card dynamically on load (Option 1). Best-effort.
649
706
  await this.persistDirectActionTurn(session, undefined, toolName, argsJson, resultJson, undefined, contextUser, provider);
650
- const promptRunID = this.readPromptRunID(session);
707
+ const runUser = ResolveScopedAnonymousRunUser(contextUser);
708
+ const promptRunID = await this.readPromptRunID(session, runUser, provider);
651
709
  if (!promptRunID) {
652
710
  return false;
653
711
  }
654
- return this.clientSessionService.AppendPromptRunMessage(promptRunID, 'assistant', this.formatToolTurn(toolName, argsJson, resultJson), false, ResolveScopedAnonymousRunUser(contextUser), provider);
712
+ return this.clientSessionService.AppendPromptRunMessage(promptRunID, 'assistant', this.formatToolTurn(toolName, argsJson, resultJson), false, runUser, provider);
655
713
  }
656
714
  /**
657
715
  * Formats a co-agent tool call into a compact, human-readable run line: `🔧 <tool> <args> → <result>`,
@@ -726,14 +784,15 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
726
784
  if (inputDelta === 0 && outputDelta === 0) {
727
785
  return true; // nothing to add — a no-op flush is a success, not a failure
728
786
  }
729
- const promptRunID = this.readPromptRunID(session);
787
+ // Runs as the scoped-anonymous elevated user (issue #3371) — the caller's role holds no prompt-run grants.
788
+ const runUser = ResolveScopedAnonymousRunUser(contextUser);
789
+ const promptRunID = await this.readPromptRunID(session, runUser, provider);
730
790
  if (!promptRunID) {
731
791
  return false; // no observability prompt run for this session — logged in the helper
732
792
  }
733
793
  // Delegate to the service so usage writes share the per-run serialization with transcript-message
734
794
  // appends — otherwise the frequent usage save clobbers freshly-appended Messages (and vice-versa).
735
- // Runs as the scoped-anonymous elevated user (issue #3371) — the caller's role holds no prompt-run grants.
736
- return this.clientSessionService.AccumulatePromptRunUsage(promptRunID, inputDelta, outputDelta, ResolveScopedAnonymousRunUser(contextUser), provider);
795
+ return this.clientSessionService.AccumulatePromptRunUsage(promptRunID, inputDelta, outputDelta, runUser, provider);
737
796
  }
738
797
  /** Clamps a relayed token delta: negative / non-finite values become 0. */
739
798
  clampTokenDelta(value) {
@@ -757,23 +816,29 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
757
816
  }
758
817
  /**
759
818
  * Reads the co-agent `AIPromptRun` id from the session config WITHOUT the relay path's
760
- * throw-on-missing-target semantics — usage relay is best-effort, so a missing/malformed
761
- * config or absent id just logs and returns `null`.
819
+ * throw-on-missing-target semantics, and keeps it only when it is the prompt run of the
820
+ * session's co-agent run (the owner can edit the config). Usage relay is best-effort, so a
821
+ * missing/malformed config, an absent id, or an id of another session just logs and returns `null`.
822
+ *
823
+ * @param runUser The user the prompt-run write runs as; the check runs as the same user.
762
824
  */
763
- readPromptRunID(session) {
764
- try {
765
- const config = this.readSessionConfig(session);
766
- if (config.promptRunID) {
767
- return config.promptRunID;
768
- }
825
+ async readPromptRunID(session, runUser, provider) {
826
+ const config = this.tryReadSessionConfig(session);
827
+ if (!config) {
828
+ LogError(`RelayRealtimeUsage: session ${session.ID} has no parseable config — usage delta dropped.`);
829
+ return null;
830
+ }
831
+ if (!config.promptRunID) {
769
832
  LogStatus(`RelayRealtimeUsage: session ${session.ID} has no co-agent promptRunID (observability run ` +
770
833
  'creation was skipped or failed) — usage delta dropped.');
771
834
  return null;
772
835
  }
773
- catch {
774
- LogError(`RelayRealtimeUsage: session ${session.ID} has no parseable config — usage delta dropped.`);
836
+ const verified = await this.runIDVerifier.Verify(session.ID, { CoAgentRunID: config.coAgentRunID, PromptRunID: config.promptRunID }, runUser, provider);
837
+ if (!verified.PromptRunID) {
838
+ LogError(`RelayRealtimeUsage: session ${session.ID} names a promptRunID that is not its co-agent prompt run — ignored.`);
775
839
  return null;
776
840
  }
841
+ return verified.PromptRunID;
777
842
  }
778
843
  /**
779
844
  * Persist an interactive channel's state of record (e.g. the live whiteboard's serialized
@@ -888,8 +953,10 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
888
953
  return { contextUser, provider: GetReadWriteProvider(providers) };
889
954
  }
890
955
  /**
891
- * Authorization gate for {@link RealtimeClientSessionResolver.StartRealtimeClientSession}: the
892
- * caller must be able to run the **target** agent. Denial throws (no session is created).
956
+ * Authorization gate for the **target** agent: the caller must be able to run it. Denial throws.
957
+ * {@link RealtimeClientSessionResolver.StartRealtimeClientSession} runs it before any session is
958
+ * created, and {@link RealtimeClientSessionResolver.ExecuteRealtimeSessionTool} runs it on every
959
+ * relayed call, because the owner can edit the target in the session config.
893
960
  */
894
961
  async assertCanRunTarget(targetAgentId, contextUser, _provider) {
895
962
  const canRun = await AIAgentPermissionHelper.HasPermission(targetAgentId, contextUser, 'run');
@@ -1315,6 +1382,9 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1315
1382
  // (allowed-agent union); the snapshot is injected into the system prompt at mint.
1316
1383
  ApplicationID: applicationId,
1317
1384
  AppContext: appContext,
1385
+ // Scope-limited callers get no direct actions (see the authorization model above), so
1386
+ // the model is not given tools that the relay refuses.
1387
+ ExcludeDirectActions: IsScopeLimitedPrincipal(contextUser),
1318
1388
  },
1319
1389
  // SCOPED-ANONYMOUS ELEVATION (issue #3371): the prepare creates the co-agent
1320
1390
  // observability AIAgentRun/AIPromptRun/run-step, which a scoped anonymous caller's role
@@ -1346,9 +1416,10 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1346
1416
  }
1347
1417
  /**
1348
1418
  * Writes the co-agent observability run ids into the session's `Config_` alongside the
1349
- * authoritative `targetAgentID`, then saves the session. These ids are read back on close to
1350
- * finalize the runs (and on relay to nest delegated runs). Best-effort: a save failure is logged,
1351
- * not thrown — the voice session still proceeds, it just won't carry the run ids.
1419
+ * `targetAgentID`, then saves the session. These ids are read back on close to
1420
+ * finalize the runs (and on relay to nest delegated runs). Relays never read the direct actions
1421
+ * from it. Best-effort: a save failure is logged, not thrown — the voice session still proceeds,
1422
+ * it just won't carry the run ids.
1352
1423
  */
1353
1424
  async persistObservabilityRunIDs(session, targetAgentID, coAgentRunID, promptRunID, coAgentRunStepID, applicationID, allowedAgents, directActions) {
1354
1425
  // Preserve any server-authoritative voice deadline stamped at session start (this rebuilds the
@@ -1367,7 +1438,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1367
1438
  session.Config_ = JSON.stringify(config);
1368
1439
  const saved = await session.Save();
1369
1440
  if (!saved) {
1370
- LogError(`RealtimeClientSessionResolver.persistObservabilityRunIDs save failed: ${session.LatestResult?.CompleteMessage ?? 'unknown error'}`);
1441
+ LogError(`RealtimeClientSessionResolver.persistObservabilityRunIDs save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`);
1371
1442
  }
1372
1443
  }
1373
1444
  /**
@@ -1451,7 +1522,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1451
1522
  const saved = await row.Save();
1452
1523
  if (!saved) {
1453
1524
  LogError(`SaveSessionChannelState: save failed for session ${agentSessionID} / channel ${channelID}: ` +
1454
- `${row.LatestResult?.CompleteMessage ?? 'unknown error'}`);
1525
+ `${this.describeSaveFailure(row)}`);
1455
1526
  }
1456
1527
  return saved;
1457
1528
  }
@@ -1675,7 +1746,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1675
1746
  artifact.UserID = contextUser.ID;
1676
1747
  artifact.Visibility = 'Always';
1677
1748
  if (!(await artifact.Save())) {
1678
- const message = `SaveSessionChannelArtifact: artifact save failed: ${artifact.LatestResult?.CompleteMessage ?? 'unknown error'}`;
1749
+ const message = `SaveSessionChannelArtifact: artifact save failed for session ${session.ID}: ${this.describeSaveFailure(artifact)}`;
1679
1750
  LogError(message);
1680
1751
  return { Success: false, ErrorMessage: message, ConversationDetailLinked: false };
1681
1752
  }
@@ -1686,7 +1757,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1686
1757
  version.Content = contentJson;
1687
1758
  version.UserID = contextUser.ID;
1688
1759
  if (!(await version.Save())) {
1689
- const message = `SaveSessionChannelArtifact: artifact version save failed: ${version.LatestResult?.CompleteMessage ?? 'unknown error'}`;
1760
+ const message = `SaveSessionChannelArtifact: artifact version save failed for session ${session.ID} (artifact ${artifact.ID}): ${this.describeSaveFailure(version)}`;
1690
1761
  LogError(message);
1691
1762
  return { Success: false, ErrorMessage: message, ArtifactID: artifact.ID, ConversationDetailLinked: false };
1692
1763
  }
@@ -1736,21 +1807,29 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1736
1807
  * queries (which scan junctions across ALL details) still surface the artifact. This is the
1737
1808
  * least-invasive correct anchor — no fake visible message, no orphaned artifact.
1738
1809
  *
1810
+ * TWO PRINCIPALS, deliberately: the lookup (`findLatestSessionDetailID`) and the junction row
1811
+ * itself (`saveDetailArtifactJunction`, `MJ: Conversation Detail Artifacts`) run as `runUser`
1812
+ * (elevated for a scoped-anonymous caller) — that junction entity is outside an anonymous
1813
+ * caller's relay grants. The hidden ANCHOR `Conversation Detail` this may need to create runs as
1814
+ * `callerUser` instead: like every other `MJ: Conversation Details` write, the System user is
1815
+ * refused because it is not the conversation's owner (see {@link persistDirectActionTurn},
1816
+ * issue #4791) — only the actual caller can create it.
1817
+ *
1739
1818
  * Strictly best-effort: every failure path logs and returns; a relayed tool call NEVER fails
1740
1819
  * because history linking did.
1741
1820
  */
1742
- async linkDelegatedArtifactsToConversation(session, artifacts, contextUser, provider) {
1821
+ async linkDelegatedArtifactsToConversation(session, artifacts, runUser, callerUser, provider) {
1743
1822
  if (!artifacts || artifacts.length === 0 || !session.ConversationID) {
1744
1823
  return;
1745
1824
  }
1746
1825
  try {
1747
- const detailID = (await this.findLatestSessionDetailID(session, contextUser, provider)) ??
1748
- (await this.createHiddenSessionAnchorDetail(session, contextUser, provider));
1826
+ const detailID = (await this.findLatestSessionDetailID(session, runUser, provider)) ??
1827
+ (await this.createHiddenSessionAnchorDetail(session, callerUser, provider));
1749
1828
  if (!detailID) {
1750
1829
  return; // anchor unavailable — logged in the helpers
1751
1830
  }
1752
1831
  for (const artifact of artifacts) {
1753
- await this.saveDetailArtifactJunction(detailID, artifact.ArtifactVersionID, contextUser, provider);
1832
+ await this.saveDetailArtifactJunction(detailID, artifact.ArtifactVersionID, runUser, provider);
1754
1833
  }
1755
1834
  }
1756
1835
  catch (error) {
@@ -1781,22 +1860,22 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1781
1860
  * {@link linkDelegatedArtifactsToConversation}). Returns the new detail's id, or `null`
1782
1861
  * (logged) when the save fails.
1783
1862
  */
1784
- async createHiddenSessionAnchorDetail(session, contextUser, provider) {
1785
- const detail = await provider.GetEntityObject(CONVERSATION_DETAIL_ENTITY, contextUser);
1863
+ async createHiddenSessionAnchorDetail(session, callerUser, provider) {
1864
+ const detail = await provider.GetEntityObject(CONVERSATION_DETAIL_ENTITY, callerUser);
1786
1865
  detail.NewRecord();
1787
1866
  detail.ConversationID = session.ConversationID;
1788
1867
  detail.Role = 'AI';
1789
1868
  detail.HiddenToUser = true;
1790
1869
  detail.Message = 'Artifacts produced during a realtime session (system anchor).';
1791
1870
  detail.AgentSessionID = session.ID;
1792
- // Attribute the anchor to the SESSION owner, not the (possibly elevated) writer — identical
1793
- // for every non-elevated caller, whose ownership of the session is already proven.
1871
+ // Attribute to the SESSION owner — the caller writing this row, whose ownership
1872
+ // `loadOwnedActiveSession` already proved.
1794
1873
  detail.UserID = session.UserID;
1795
1874
  if (await detail.Save()) {
1796
1875
  return detail.ID;
1797
1876
  }
1798
1877
  LogError(`ExecuteRealtimeSessionTool: hidden anchor detail save failed for session ${session.ID}: ` +
1799
- `${detail.LatestResult?.CompleteMessage ?? 'unknown error'}`);
1878
+ `${this.describeSaveFailure(detail)}`);
1800
1879
  return null;
1801
1880
  }
1802
1881
  /**
@@ -1812,7 +1891,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1812
1891
  const saved = await junction.Save();
1813
1892
  if (!saved) {
1814
1893
  LogError(`RealtimeClientSessionResolver: artifact junction save failed for detail ${conversationDetailID} / version ${artifactVersionID}: ` +
1815
- `${junction.LatestResult?.CompleteMessage ?? 'unknown error'}`);
1894
+ `${this.describeSaveFailure(junction)}`);
1816
1895
  }
1817
1896
  return saved;
1818
1897
  }
@@ -1841,7 +1920,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1841
1920
  row.LastActiveAt = new Date();
1842
1921
  if (!(await row.Save())) {
1843
1922
  LogError(`SaveSessionChannelArtifact: LastActiveAt stamp failed for session ${agentSessionID} / channel ${channelID}: ` +
1844
- `${row.LatestResult?.CompleteMessage ?? 'unknown error'}`);
1923
+ `${this.describeSaveFailure(row)}`);
1845
1924
  }
1846
1925
  }
1847
1926
  catch (error) {
@@ -1922,9 +2001,10 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1922
2001
  tool.ParametersSchema !== null && typeof tool.ParametersSchema === 'object' && !Array.isArray(tool.ParametersSchema));
1923
2002
  }
1924
2003
  /**
1925
- * Parses the session's persisted config, returning the authoritative `targetAgentID` plus any
1926
- * observability run ids (`coAgentRunID`/`promptRunID`). Throws when the config is missing/malformed
1927
- * or carries no target — a relay cannot proceed without a target.
2004
+ * Parses the session's persisted config, returning its `targetAgentID` plus any observability run
2005
+ * ids (`coAgentRunID`/`promptRunID`). Throws when the config is missing/malformed or carries no
2006
+ * target — a relay cannot proceed without a target. The owner can edit the config, so callers
2007
+ * re-check what they act on.
1928
2008
  */
1929
2009
  readSessionConfig(session) {
1930
2010
  const raw = session.Config_;
@@ -1974,12 +2054,6 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1974
2054
  const minutes = elevation?.widget.VoiceMaxSessionMinutes;
1975
2055
  return minutes && minutes > 0 ? minutes * 60 : undefined;
1976
2056
  }
1977
- /**
1978
- * Persists a single transcript turn as a `Conversation Detail` stamped with the session's
1979
- * conversation, the mapped role, the turn text, the session id, and the owning user.
1980
- *
1981
- * @returns The boolean save result (logs `CompleteMessage` on failure).
1982
- */
1983
2057
  /**
1984
2058
  * Stamps `RecordingStartedAt` (the recording `t0` alignment origin) + `RecordingMedia` on a
1985
2059
  * just-started session when the browser captured WITH consent. Best-effort: a parse/save failure
@@ -2001,9 +2075,16 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
2001
2075
  session.RecordingStartedAt = startedAt;
2002
2076
  session.RecordingMedia = 'Audio';
2003
2077
  if (!(await session.Save())) {
2004
- LogError(`RealtimeClientSessionResolver.stampRecordingStart save failed: ${session.LatestResult?.CompleteMessage ?? 'unknown error'}`);
2078
+ LogError(`RealtimeClientSessionResolver.stampRecordingStart save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`);
2005
2079
  }
2006
2080
  }
2081
+ /**
2082
+ * Persists a single transcript turn as a `Conversation Detail` stamped with the session's
2083
+ * conversation, the mapped role, the turn text, the session id, and the owning user.
2084
+ *
2085
+ * @returns The boolean save result. On failure it logs the session, the write user and the
2086
+ * reason from {@link describeSaveFailure}.
2087
+ */
2007
2088
  async persistTranscriptTurn(session, role, text, contextUser, provider, utteranceStartMs, utteranceEndMs) {
2008
2089
  const detail = await provider.GetEntityObject(CONVERSATION_DETAIL_ENTITY, contextUser);
2009
2090
  detail.NewRecord();
@@ -2020,7 +2101,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
2020
2101
  }
2021
2102
  const saved = await detail.Save();
2022
2103
  if (!saved) {
2023
- LogError(`RealtimeClientSessionResolver.persistTranscriptTurn save failed: ${detail.LatestResult?.CompleteMessage ?? 'unknown error'}`);
2104
+ LogError(`RealtimeClientSessionResolver.persistTranscriptTurn save failed for session ${session.ID} (write user ${contextUser.ID}): ${this.describeSaveFailure(detail)}`);
2024
2105
  }
2025
2106
  return saved;
2026
2107
  }
@@ -2051,8 +2132,13 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
2051
2132
  // ⚠️ ELEVATING INSTEAD WAS TRIED AND IS A TRAP. `ResolveScopedAnonymousRunUser` returns
2052
2133
  // `UserCache.GetSystemUser()`, which on this deployment resolves to the unconfigured
2053
2134
  // placeholder `not.set@nowhere.com` ("Configured provisioning user not found; falling back
2054
- // to an Owner"). Saving as a user that does not exist returns false with a NULL
2055
- // `LatestResult` — indistinguishable from a permission denial, and just as silent.
2135
+ // to an Owner"). That user EXISTS — it is not a missing/invalid principal — and even carries
2136
+ // full CRUD on `MJ: Conversation Details`. It is refused anyway:
2137
+ // `MJConversationDetailEntityExtended`'s owner gate accepts only the conversation's OWNER or
2138
+ // an Edit/Owner Resource Permission grantee, and the System user is neither. Measured on
2139
+ // MJ#4791 (before the core-entities fix that now records the denial): `Save()` → `false`,
2140
+ // `LatestResult` null — indistinguishable from a permission denial, and just as silent. The
2141
+ // denial itself still happens; that fix only made it stop being silent.
2056
2142
  const writeUser = contextUser;
2057
2143
  const rv = RunView.FromMetadataProvider(provider);
2058
2144
  const result = await rv.RunView({
@@ -2070,8 +2156,8 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
2070
2156
  //
2071
2157
  // `ResultType: 'entity_object'` hands back hydrated entities, but they do not carry the
2072
2158
  // context user the way `GetEntityObject(entity, user)` does — so `Save()` ran with no
2073
- // principal and failed with an EMPTY message ("unknown error"), which is what the elevation
2074
- // fix above looked like when it was still broken. The insert path beside this one has
2159
+ // principal and failed with an EMPTY message ("unknown error"), which is what an empty-message
2160
+ // failure here looked like before the re-load. The insert path beside this one has
2075
2161
  // always used `GetEntityObject`; this now matches it, which is also why they now succeed
2076
2162
  // and fail for the same reasons.
2077
2163
  const previous = await provider.GetEntityObject(CONVERSATION_DETAIL_ENTITY, writeUser);
@@ -2097,7 +2183,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
2097
2183
  LogError(`RealtimeClientSessionResolver.replacePreviousTranscriptTurn save failed for session `
2098
2184
  + `${session.ID} — the ${mappedRole} turn keeps its PREVIOUS, shorter text and the `
2099
2185
  + `transcript now understates what was said: `
2100
- + `${previous.LatestResult?.CompleteMessage || JSON.stringify(previous.LatestResult ?? null)}`);
2186
+ + `${this.describeSaveFailure(previous)}`);
2101
2187
  }
2102
2188
  return saved;
2103
2189
  }
@@ -2114,14 +2200,30 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
2114
2200
  * Stored as stable facts (Role: 'AI', HiddenToUser: true, ExternalID: callId, CompletionTime: durationMs,
2115
2201
  * Message: JSON-serialized tool facts) so that session review can reconstruct the action card dynamically
2116
2202
  * on load (Option 1) without polluting user-facing speech chat bubbles.
2203
+ *
2204
+ * Writes as the CALLER, never the elevated run user (issue #4791). Owning the session
2205
+ * (`loadOwnedActiveSession`'s gate has already run on every path here) does NOT imply owning
2206
+ * the conversation: the conversation id was client-supplied at session start. What authorizes
2207
+ * this write is the save itself, evaluated for the caller — the conversation owner gate in
2208
+ * `MJConversationDetailEntityExtended`, plus the caller's own entity permission and Create RLS
2209
+ * on `MJ: Conversation Details` (for the magic-link role that RLS scopes by the invite's
2210
+ * `ScopeResourceID`, and the role belongs to a downstream app, not MJ core). That is why
2211
+ * writing as the caller is safe even though the conversation id originated client-side.
2212
+ * Elevating to the System user would bypass RLS entirely, and the owner gate refuses it anyway
2213
+ * because the System user is not the conversation owner. Measured on MJ#4791 (before the
2214
+ * core-entities fix that now records the denial): `Save()` returned `false` with a NULL `LatestResult` and an empty
2215
+ * `ResultHistory` — indistinguishable from any other silent failure. Elevating this write is
2216
+ * what silently dropped both hidden tool-execution turns of a real magic-link session; the
2217
+ * denial itself still happens today, only its silence was fixed separately.
2218
+ * `ResolveScopedAnonymousRunUser` elevates AI-**run**-entity writes only (prompt runs, agent
2219
+ * runs, the relayed dispatch itself) — never this one.
2117
2220
  */
2118
2221
  async persistDirectActionTurn(session, callId, toolName, argsJson, resultJson, durationMs, contextUser, provider) {
2119
2222
  if (!session.ConversationID) {
2120
2223
  return false;
2121
2224
  }
2122
2225
  try {
2123
- const writeUser = ResolveScopedAnonymousRunUser(contextUser);
2124
- const detail = await provider.GetEntityObject(CONVERSATION_DETAIL_ENTITY, writeUser);
2226
+ const detail = await provider.GetEntityObject(CONVERSATION_DETAIL_ENTITY, contextUser);
2125
2227
  detail.NewRecord();
2126
2228
  detail.ConversationID = session.ConversationID;
2127
2229
  detail.Role = 'AI';
@@ -2159,13 +2261,13 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
2159
2261
  detail.Message = JSON.stringify(factPayload);
2160
2262
  const saved = await detail.Save();
2161
2263
  if (!saved) {
2162
- LogError(`RealtimeClientSessionResolver.persistDirectActionTurn save failed: ${detail.LatestResult?.CompleteMessage ?? 'unknown error'}`);
2264
+ LogError(`RealtimeClientSessionResolver.persistDirectActionTurn save failed for session ${session.ID} (tool '${toolName}', call ${callId ?? 'n/a'}, write user ${contextUser.ID}): ${this.describeSaveFailure(detail)}`);
2163
2265
  }
2164
2266
  return saved;
2165
2267
  }
2166
2268
  catch (error) {
2167
2269
  const message = error instanceof Error ? error.message : String(error);
2168
- LogError(`RealtimeClientSessionResolver.persistDirectActionTurn unexpected error: ${message}`);
2270
+ LogError(`RealtimeClientSessionResolver.persistDirectActionTurn unexpected error for session ${session.ID}: ${message}`);
2169
2271
  return false;
2170
2272
  }
2171
2273
  }