@memberjunction/server 6.2.0-edge.1 → 6.2.0-edge.3

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 (220) hide show
  1. package/README.md +30 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/config.d.ts +2189 -71
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +123 -3
  6. package/dist/config.js.map +1 -1
  7. package/dist/context.d.ts.map +1 -1
  8. package/dist/context.js +11 -23
  9. package/dist/context.js.map +1 -1
  10. package/dist/generated/graphql-schemas/__mj.d.ts +2143 -205
  11. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  12. package/dist/generated/graphql-schemas/__mj.js +24441 -14433
  13. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  14. package/dist/generic/FireAndForgetHeartbeat.d.ts +15 -1
  15. package/dist/generic/FireAndForgetHeartbeat.d.ts.map +1 -1
  16. package/dist/generic/FireAndForgetHeartbeat.js +16 -2
  17. package/dist/generic/FireAndForgetHeartbeat.js.map +1 -1
  18. package/dist/generic/OrphanedConversationDetailReconciler.d.ts +31 -0
  19. package/dist/generic/OrphanedConversationDetailReconciler.d.ts.map +1 -0
  20. package/dist/generic/OrphanedConversationDetailReconciler.js +215 -0
  21. package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -0
  22. package/dist/generic/PushStatusResolver.d.ts +38 -0
  23. package/dist/generic/PushStatusResolver.d.ts.map +1 -1
  24. package/dist/generic/PushStatusResolver.js +84 -1
  25. package/dist/generic/PushStatusResolver.js.map +1 -1
  26. package/dist/generic/ResolverBase.d.ts +56 -4
  27. package/dist/generic/ResolverBase.d.ts.map +1 -1
  28. package/dist/generic/ResolverBase.js +191 -74
  29. package/dist/generic/ResolverBase.js.map +1 -1
  30. package/dist/generic/RunViewResolver.d.ts +4 -0
  31. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  32. package/dist/generic/RunViewResolver.js +33 -1
  33. package/dist/generic/RunViewResolver.js.map +1 -1
  34. package/dist/index.d.ts +4 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +204 -50
  37. package/dist/index.js.map +1 -1
  38. package/dist/postgresqlCompat.d.ts +8 -0
  39. package/dist/postgresqlCompat.d.ts.map +1 -0
  40. package/dist/postgresqlCompat.js +19 -0
  41. package/dist/postgresqlCompat.js.map +1 -0
  42. package/dist/postgresqlPoolSettings.d.ts +108 -0
  43. package/dist/postgresqlPoolSettings.d.ts.map +1 -0
  44. package/dist/postgresqlPoolSettings.js +134 -0
  45. package/dist/postgresqlPoolSettings.js.map +1 -0
  46. package/dist/resolvers/AdhocQueryResolver.d.ts +23 -18
  47. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
  48. package/dist/resolvers/AdhocQueryResolver.js +77 -167
  49. package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
  50. package/dist/resolvers/AgentRunStatusPublisher.d.ts +55 -0
  51. package/dist/resolvers/AgentRunStatusPublisher.d.ts.map +1 -0
  52. package/dist/resolvers/AgentRunStatusPublisher.js +141 -0
  53. package/dist/resolvers/AgentRunStatusPublisher.js.map +1 -0
  54. package/dist/resolvers/ConversationRunEventsResolver.d.ts +90 -0
  55. package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -0
  56. package/dist/resolvers/ConversationRunEventsResolver.js +283 -0
  57. package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -0
  58. package/dist/resolvers/DuplicateEntryCheckResolver.d.ts +98 -0
  59. package/dist/resolvers/DuplicateEntryCheckResolver.d.ts.map +1 -0
  60. package/dist/resolvers/DuplicateEntryCheckResolver.js +227 -0
  61. package/dist/resolvers/DuplicateEntryCheckResolver.js.map +1 -0
  62. package/dist/resolvers/EntityRecordNameResolver.d.ts +7 -23
  63. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  64. package/dist/resolvers/EntityRecordNameResolver.js +9 -47
  65. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  66. package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
  67. package/dist/resolvers/FetchEntityVectorsResolver.js +2 -1
  68. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  69. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  70. package/dist/resolvers/GetDataResolver.js +9 -0
  71. package/dist/resolvers/GetDataResolver.js.map +1 -1
  72. package/dist/resolvers/HumanHandoffResolver.d.ts +107 -0
  73. package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -0
  74. package/dist/resolvers/HumanHandoffResolver.js +324 -0
  75. package/dist/resolvers/HumanHandoffResolver.js.map +1 -0
  76. package/dist/resolvers/MeetingResolver.d.ts +69 -0
  77. package/dist/resolvers/MeetingResolver.d.ts.map +1 -0
  78. package/dist/resolvers/MeetingResolver.js +381 -0
  79. package/dist/resolvers/MeetingResolver.js.map +1 -0
  80. package/dist/resolvers/MergeRecordsResolver.d.ts +7 -0
  81. package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
  82. package/dist/resolvers/MergeRecordsResolver.js +16 -2
  83. package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
  84. package/dist/resolvers/RealtimeBridgeResolver.d.ts +34 -1
  85. package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
  86. package/dist/resolvers/RealtimeBridgeResolver.js +202 -7
  87. package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
  88. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +45 -89
  89. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  90. package/dist/resolvers/RealtimeClientSessionResolver.js +94 -240
  91. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  92. package/dist/resolvers/RunAIAgentResolver.d.ts +0 -16
  93. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  94. package/dist/resolvers/RunAIAgentResolver.js +23 -153
  95. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  96. package/dist/resolvers/RunDecisionResolver.d.ts +149 -0
  97. package/dist/resolvers/RunDecisionResolver.d.ts.map +1 -0
  98. package/dist/resolvers/RunDecisionResolver.js +396 -0
  99. package/dist/resolvers/RunDecisionResolver.js.map +1 -0
  100. package/dist/resolvers/VectorizeEntityResolver.d.ts +1 -1
  101. package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -1
  102. package/dist/resolvers/VectorizeEntityResolver.js +14 -10
  103. package/dist/resolvers/VectorizeEntityResolver.js.map +1 -1
  104. package/dist/resolvers/adhocTimeout.d.ts +7 -0
  105. package/dist/resolvers/adhocTimeout.d.ts.map +1 -0
  106. package/dist/resolvers/adhocTimeout.js +12 -0
  107. package/dist/resolvers/adhocTimeout.js.map +1 -0
  108. package/dist/resolvers/meetings/index.d.ts +5 -0
  109. package/dist/resolvers/meetings/index.d.ts.map +1 -0
  110. package/dist/resolvers/meetings/index.js +5 -0
  111. package/dist/resolvers/meetings/index.js.map +1 -0
  112. package/dist/resolvers/meetings/meetingHelpers.d.ts +17 -0
  113. package/dist/resolvers/meetings/meetingHelpers.d.ts.map +1 -0
  114. package/dist/resolvers/meetings/meetingHelpers.js +212 -0
  115. package/dist/resolvers/meetings/meetingHelpers.js.map +1 -0
  116. package/dist/resolvers/meetings/meetingMutations.d.ts +12 -0
  117. package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -0
  118. package/dist/resolvers/meetings/meetingMutations.js +418 -0
  119. package/dist/resolvers/meetings/meetingMutations.js.map +1 -0
  120. package/dist/resolvers/meetings/meetingQueries.d.ts +7 -0
  121. package/dist/resolvers/meetings/meetingQueries.d.ts.map +1 -0
  122. package/dist/resolvers/meetings/meetingQueries.js +113 -0
  123. package/dist/resolvers/meetings/meetingQueries.js.map +1 -0
  124. package/dist/resolvers/meetings/meetingTypes.d.ts +89 -0
  125. package/dist/resolvers/meetings/meetingTypes.d.ts.map +1 -0
  126. package/dist/resolvers/meetings/meetingTypes.js +345 -0
  127. package/dist/resolvers/meetings/meetingTypes.js.map +1 -0
  128. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  129. package/dist/rest/ViewOperationsHandler.js +8 -5
  130. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  131. package/dist/services/TaskGraphActionRunner.d.ts +8 -0
  132. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
  133. package/dist/services/TaskGraphActionRunner.js +27 -1
  134. package/dist/services/TaskGraphActionRunner.js.map +1 -1
  135. package/dist/services/WorkQueueHostService.d.ts +36 -0
  136. package/dist/services/WorkQueueHostService.d.ts.map +1 -0
  137. package/dist/services/WorkQueueHostService.js +97 -0
  138. package/dist/services/WorkQueueHostService.js.map +1 -0
  139. package/dist/services/workQueueConfig.d.ts +103 -0
  140. package/dist/services/workQueueConfig.d.ts.map +1 -0
  141. package/dist/services/workQueueConfig.js +48 -0
  142. package/dist/services/workQueueConfig.js.map +1 -0
  143. package/dist/types.d.ts +5 -0
  144. package/dist/types.d.ts.map +1 -1
  145. package/dist/types.js.map +1 -1
  146. package/package.json +95 -92
  147. package/src/__tests__/AdhocQueryResolver.bugs.test.ts +14 -35
  148. package/src/__tests__/AdhocQueryResolver.pagination.test.ts +6 -2
  149. package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +3 -3
  150. package/src/__tests__/AgentRunStatusPublisher.test.ts +175 -0
  151. package/src/__tests__/DuplicateEntryCheckResolver.test.ts +300 -0
  152. package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +27 -72
  153. package/src/__tests__/HumanHandoffResolver.test.ts +233 -0
  154. package/src/__tests__/MeetingResolver.test.ts +374 -0
  155. package/src/__tests__/RealtimeBridgeResolver.test.ts +240 -2
  156. package/src/__tests__/RealtimeClientSessionResolver.test.ts +216 -2
  157. package/src/__tests__/RemoteBrowserActionRecovery.test.ts +7 -1
  158. package/src/__tests__/RemoteBrowserAudioStream.test.ts +7 -1
  159. package/src/__tests__/RemoteBrowserSnapshot.test.ts +7 -1
  160. package/src/__tests__/ResolverBase.adhocReadScope.test.ts +96 -0
  161. package/src/__tests__/ResolverBase.clientOldValues.test.ts +52 -0
  162. package/src/__tests__/ResolverBase.filterEscaping.test.ts +21 -2
  163. package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +291 -0
  164. package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +182 -12
  165. package/src/__tests__/RunAIAgentResolver.streaming.test.ts +18 -27
  166. package/src/__tests__/RunDecisionResolver.test.ts +699 -0
  167. package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +43 -0
  168. package/src/__tests__/VectorizeEntityResolver.test.ts +106 -0
  169. package/src/__tests__/WorkQueueHostService.test.ts +133 -0
  170. package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
  171. package/src/__tests__/conversationRunEvents.test.ts +399 -0
  172. package/src/__tests__/orphanedConversationDetailReconciler.test.ts +283 -0
  173. package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
  174. package/src/__tests__/pushStatusFanOut.test.ts +206 -0
  175. package/src/__tests__/translateBracketsToPG.test.ts +20 -0
  176. package/src/auth/__tests__/backward-compatibility.test.ts +1 -0
  177. package/src/config.ts +130 -3
  178. package/src/context.ts +11 -24
  179. package/src/generated/graphql-schemas/__mj.ts +24183 -17182
  180. package/src/generic/FireAndForgetHeartbeat.ts +16 -2
  181. package/src/generic/OrphanedConversationDetailReconciler.ts +267 -0
  182. package/src/generic/PushStatusResolver.ts +97 -1
  183. package/src/generic/ResolverBase.ts +220 -76
  184. package/src/generic/RunViewResolver.ts +33 -2
  185. package/src/index.ts +228 -51
  186. package/src/postgresqlCompat.ts +21 -0
  187. package/src/postgresqlPoolSettings.ts +213 -0
  188. package/src/resolvers/AdhocQueryResolver.ts +88 -188
  189. package/src/resolvers/AgentRunStatusPublisher.ts +175 -0
  190. package/src/resolvers/ConversationRunEventsResolver.ts +308 -0
  191. package/src/resolvers/DuplicateEntryCheckResolver.ts +232 -0
  192. package/src/resolvers/EntityRecordNameResolver.ts +9 -51
  193. package/src/resolvers/FetchEntityVectorsResolver.ts +2 -1
  194. package/src/resolvers/GetDataResolver.ts +10 -0
  195. package/src/resolvers/HumanHandoffResolver.ts +304 -0
  196. package/src/resolvers/MeetingResolver.ts +438 -0
  197. package/src/resolvers/MergeRecordsResolver.ts +14 -2
  198. package/src/resolvers/RealtimeBridgeResolver.ts +227 -13
  199. package/src/resolvers/RealtimeClientSessionResolver.ts +99 -287
  200. package/src/resolvers/RunAIAgentResolver.ts +28 -174
  201. package/src/resolvers/RunDecisionResolver.ts +435 -0
  202. package/src/resolvers/VectorizeEntityResolver.ts +16 -11
  203. package/src/resolvers/__tests__/MergeRecordsResolver.test.ts +93 -0
  204. package/src/resolvers/adhocTimeout.ts +12 -0
  205. package/src/resolvers/meetings/index.ts +4 -0
  206. package/src/resolvers/meetings/meetingHelpers.ts +307 -0
  207. package/src/resolvers/meetings/meetingMutations.ts +535 -0
  208. package/src/resolvers/meetings/meetingQueries.ts +172 -0
  209. package/src/resolvers/meetings/meetingTypes.ts +238 -0
  210. package/src/rest/ViewOperationsHandler.ts +8 -5
  211. package/src/services/TaskGraphActionRunner.ts +29 -1
  212. package/src/services/WorkQueueHostService.ts +129 -0
  213. package/src/services/workQueueConfig.ts +52 -0
  214. package/src/types.ts +5 -0
  215. package/dist/resolvers/adhoc-query-helpers.d.ts +0 -33
  216. package/dist/resolvers/adhoc-query-helpers.d.ts.map +0 -1
  217. package/dist/resolvers/adhoc-query-helpers.js +0 -50
  218. package/dist/resolvers/adhoc-query-helpers.js.map +0 -1
  219. package/src/__tests__/adhoc-query-helpers.test.ts +0 -50
  220. package/src/resolvers/adhoc-query-helpers.ts +0 -67
@@ -41,7 +41,7 @@ import { AuthorizationEvaluator, LogError, LogStatus, RunView } from '@memberjun
41
41
  import { UUIDsEqual, IsValidUUID } from '@memberjunction/global';
42
42
  import { AIEngine } from '@memberjunction/aiengine';
43
43
  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';
44
+ import { RealtimeClientSessionService, RealtimeChannelServerHost, EvaluateRuntimeOverrideAuthorization, FindIgnoredRealtimeConfigKeys, ParseRealtimeTypeConfiguration, ResolveEffectiveRealtimeConfig, ResolveRealtimeCoAgentID, FilterAllowedAgentsByCanRun, REALTIME_ADVANCED_SESSION_CONTROLS_AUTHORIZATION, resolveRecordingStorageAccountID, storeRealtimeRecording, writeRealtimeRecordingSegment, deleteRealtimeRecordingSegments, } from '@memberjunction/ai-agents';
45
45
  import { ResolverBase } from '../generic/ResolverBase.js';
46
46
  import { GetReadWriteProvider } from '../util.js';
47
47
  import { SessionManager } from '../agentSessions/index.js';
@@ -52,29 +52,6 @@ import { ResolveWidgetGuestRunContext, ResolveScopedAnonymousRunUser } from '../
52
52
  * noise is dropped so the model only narrates meaningful work.
53
53
  */
54
54
  const SIGNIFICANT_PROGRESS_STEPS = ['prompt_execution', 'action_execution', 'subagent_execution', 'decision_processing'];
55
- /**
56
- * The seeded name of the internal orchestration agent that fronts a target agent in realtime
57
- * sessions (voice + interactive channels). This is the GLOBAL DEFAULT co-agent — the final step of
58
- * the co-agent resolution chain (see {@link RealtimeClientSessionResolver.resolveCoAgentID}).
59
- * Deployments can override it per agent (`AIAgent.DefaultCoAgentID`), per agent type (an
60
- * `AIAgentCoAgent` row with `TargetAgentTypeID` + `IsDefault`), or per call (the `coAgentId`
61
- * mutation argument) without
62
- * touching this seed.
63
- */
64
- const REALTIME_CO_AGENT_NAME = 'Realtime Co-Agent';
65
- /**
66
- * DEPRECATED legacy seed name of {@link REALTIME_CO_AGENT_NAME}, from before the agent's rename
67
- * from "Voice Co-Agent" to "Realtime Co-Agent". Deployments that have not re-synced the agent seed
68
- * still carry this name, so {@link RealtimeClientSessionResolver.resolveGlobalCoAgentID} falls
69
- * back to it (with a deprecation log) when no agent named {@link REALTIME_CO_AGENT_NAME} exists.
70
- */
71
- const LEGACY_REALTIME_CO_AGENT_NAME = 'Voice Co-Agent';
72
- /**
73
- * The seeded name of the Realtime agent TYPE. Every co-agent candidate (explicit or metadata
74
- * default) must be an Active agent of this type — a co-agent drives a streaming full-duplex
75
- * realtime session, which only `RealtimeAgentType`-driven agents support.
76
- */
77
- const REALTIME_AGENT_TYPE_NAME = 'Realtime';
78
55
  /** Entity name — centralised so the `MJ:`-prefix convention is applied in exactly one place. */
79
56
  const SESSION_ENTITY = 'MJ: AI Agent Sessions';
80
57
  const CO_AGENT_ENTITY = 'MJ: AI Agent Co Agents';
@@ -270,6 +247,30 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
270
247
  this.clientSessionService = new RealtimeClientSessionService();
271
248
  this.sessionManager = new SessionManager(this.clientSessionService);
272
249
  }
250
+ /**
251
+ * The reason a `Save()`/`Delete()` returned false, for a log line. `CompleteMessage` renders
252
+ * `Message`, `Error` and every `Errors[]` entry (validation errors included, since `Save()`
253
+ * runs `Validate()` and records its errors there), so there is no separate `Validate()` call.
254
+ * When a subclass refused the write without recording anything, say so explicitly instead of
255
+ * "unknown error", which is what hid MJ#4791.
256
+ *
257
+ * Only a NULL `LatestResult` proves the refusal happened before the provider (nothing was
258
+ * registered at all). A SUCCESS `LatestResult` is an earlier save on the same instance — the
259
+ * refusal registered nothing — so its text must never be reported as this failure's reason.
260
+ */
261
+ describeSaveFailure(entity) {
262
+ const latest = entity.LatestResult;
263
+ if (!latest) {
264
+ return `no failure detail recorded (LatestResult null; ResultHistory length ${entity.ResultHistory.length}) ` +
265
+ '— the write was refused before reaching the provider';
266
+ }
267
+ if (latest.Success) {
268
+ return 'no failure detail recorded (latest result entry is a prior success; ' +
269
+ `ResultHistory length ${entity.ResultHistory.length})`;
270
+ }
271
+ const detail = latest.CompleteMessage?.trim();
272
+ return detail || `no failure detail recorded (LatestResult empty; ResultHistory length ${entity.ResultHistory.length})`;
273
+ }
273
274
  /**
274
275
  * Start a client-direct realtime voice session targeting `targetAgentId`.
275
276
  *
@@ -278,7 +279,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
278
279
  * session is created.
279
280
  * 2. Resolve the co-agent id via the metadata-driven resolution chain (runtime `coAgentId` →
280
281
  * agent's `DefaultCoAgentID` → type-level `AIAgentCoAgent` default row → global Realtime Co-Agent) —
281
- * see {@link RealtimeClientSessionResolver.resolveCoAgentID} for the full contract.
282
+ * see {@link ResolveRealtimeCoAgentID} for the full contract.
282
283
  * 3. Create the durable `AIAgentSession` (run by the co-agent), storing `targetAgentID` in its
283
284
  * config server-side — this is the authoritative target for all later relays.
284
285
  * 4. Mint the {@link import('@memberjunction/ai').ClientRealtimeSessionConfig} via the service.
@@ -311,7 +312,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
311
312
  */
312
313
  async StartRealtimeClientSession(targetAgentId, { userPayload, providers }, conversationId, lastSessionId, preferredModelId, clientToolsJson, coAgentId, configOverridesJson, recordingStartedAt, recordingConsent, mediaCollectionId, applicationId, appContextJson) {
313
314
  const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
314
- const coAgentID = await this.resolveCoAgentID(targetAgentId, coAgentId, contextUser, provider);
315
+ const coAgentID = await ResolveRealtimeCoAgentID(targetAgentId, coAgentId, contextUser, provider);
315
316
  // PAIRING CONSTRAINTS: a co-agent with pairing rows is restricted to that target list
316
317
  // (with the IsDefault row standing in when no runtime target was supplied); zero rows =
317
318
  // universal, today's behavior untouched. Resolves the AUTHORITATIVE target id.
@@ -398,7 +399,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
398
399
  TargetAgentID: config.targetAgentID,
399
400
  // Multi-target (Move 4): the session's persisted allowed-agent union — a model-named
400
401
  // colleague in the call is validated against this; absent ⇒ single-target behavior.
401
- AllowedAgents: await this.filterAllowedAgentsByCanRun(config.allowedAgents, contextUser),
402
+ AllowedAgents: await FilterAllowedAgentsByCanRun(config.allowedAgents, contextUser),
402
403
  DirectActions: config.directActions,
403
404
  // Attribution follows the VISITOR even when `runUser` is elevated: the delegated run
404
405
  // row and its context-memory scope must stay the person's, not the system user's.
@@ -421,7 +422,9 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
421
422
  // Junction-link any artifacts the delegated run produced into the session's conversation
422
423
  // history (best-effort) — so chat, session review, and resume carryover can all see them.
423
424
  // Runs as `runUser`: the junction entity is not among an anonymous caller's relay grants.
424
- await this.linkDelegatedArtifactsToConversation(session, Artifacts, runUser, provider);
425
+ // The hidden anchor detail it may need to create, though, is written by the CALLER (#4791)
426
+ // — same reason `persistDirectActionTurn` is: the System user is not the conversation owner.
427
+ await this.linkDelegatedArtifactsToConversation(session, Artifacts, runUser, contextUser, provider);
425
428
  await this.sessionManager.Heartbeat(agentSessionId, contextUser, provider);
426
429
  return ResultJson;
427
430
  }
@@ -472,7 +475,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
472
475
  };
473
476
  session.Config_ = JSON.stringify(next);
474
477
  if (!(await session.Save())) {
475
- LogError(`RealtimeClientSessionResolver.updatePendingFeedbackRunID save failed: ${session.LatestResult?.CompleteMessage ?? 'unknown error'}`);
478
+ LogError(`RealtimeClientSessionResolver.updatePendingFeedbackRunID save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`);
476
479
  }
477
480
  }
478
481
  /**
@@ -906,33 +909,6 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
906
909
  throw new Error(`Not authorized: you are not permitted to run the target agent ${targetAgentId}`);
907
910
  }
908
911
  }
909
- /**
910
- * Narrows a session's colleague union (`allowedAgents`) to the agents the CALLER may run.
911
- *
912
- * {@link assertCanRunTarget} gates the LEAD target at session start, but the union it travels
913
- * with was never gated at all — a model-named colleague resolves straight to a delegated run.
914
- * That was survivable while the run carried the caller's own identity, because base-agent
915
- * re-checks `CanRun` against `contextUser`. Once the run user is elevated for a scoped anonymous
916
- * caller (issue #3371) that check sees the SYSTEM user, so this is the only remaining place the
917
- * caller's own authority is applied to a colleague. It therefore runs for EVERY caller, elevated
918
- * or not — the authorization identity must never depend on the elevation decision.
919
- *
920
- * `HasPermission` reads AIEngineBase's in-memory caches (no DB round trip) and already fails
921
- * closed on error, so an unresolvable agent drops OUT of the union rather than becoming runnable.
922
- * A filtered-out colleague is not an error: the delegation layer reports it as "not available in
923
- * this session" and lists what remains, which is the same answer the model gets for a typo.
924
- *
925
- * @param allowedAgents The session's persisted colleague union (absent/empty ⇒ single-target).
926
- * @param contextUser The ORIGINAL caller — never the elevated run user.
927
- * @returns The subset the caller may run, preserving order.
928
- */
929
- async filterAllowedAgentsByCanRun(allowedAgents, contextUser) {
930
- if (!allowedAgents || allowedAgents.length === 0) {
931
- return allowedAgents;
932
- }
933
- const verdicts = await Promise.all(allowedAgents.map((a) => AIAgentPermissionHelper.HasPermission(a.agentId, contextUser, 'run')));
934
- return allowedAgents.filter((_, i) => verdicts[i]);
935
- }
936
912
  /**
937
913
  * Resolves the AUTHORITATIVE target agent id under the co-agent's PAIRING CONSTRAINTS
938
914
  * (`MJ: AI Agent Co Agents`, ordered by `Sequence`):
@@ -1136,158 +1112,6 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1136
1112
  return false;
1137
1113
  }
1138
1114
  }
1139
- /**
1140
- * Resolves the co-agent (the Realtime-type agent that voices the target agent) for a new
1141
- * client-direct session via the metadata-driven **CO-AGENT RESOLUTION CHAIN** — first match
1142
- * wins, evaluated in precedence order:
1143
- *
1144
- * 1. **Runtime parameter** — the mutation's explicit `coAgentId`. A per-call override; an
1145
- * invalid candidate (unknown, not Active, or not of the Realtime agent type) **throws**
1146
- * (fail loud — the caller asked for something specific, mirroring `preferredModelId`).
1147
- * 2. **Per-agent persona** — the target agent's `AIAgent.DefaultCoAgentID`. An invalid
1148
- * reference logs a warning and **falls through** to the next step (stale metadata should
1149
- * degrade gracefully, never break calls).
1150
- * 3. **Per-type default** — an Active `AIAgentCoAgent` row of Type `'CoAgent'` whose
1151
- * `TargetAgentTypeID` is the target agent's type and `IsDefault = 1` (lowest `Sequence`
1152
- * wins a tie). Same tolerant warn-and-fall-through semantics as step 2.
1153
- * 4. **Global default** — the seeded {@link REALTIME_CO_AGENT_NAME} agent, looked up by name
1154
- * (with a deprecated fallback to {@link LEGACY_REALTIME_CO_AGENT_NAME}). Throws when absent
1155
- * entirely (the realtime feature is unconfigured in this deployment).
1156
- *
1157
- * Every candidate from steps 1–3 is validated by {@link findValidCoAgent}: it must exist in
1158
- * {@link AIEngine}'s cached agents, have Status `Active`, and be of the
1159
- * {@link REALTIME_AGENT_TYPE_NAME} agent type. Step 4 keeps its original name-lookup contract.
1160
- *
1161
- * @param targetAgentId The agent the co-agent will voice on behalf of (drives steps 2–3).
1162
- * @param explicitCoAgentId The runtime `coAgentId` mutation argument, when supplied (step 1).
1163
- * @returns The resolved co-agent's id (canonical casing from the metadata cache).
1164
- */
1165
- async resolveCoAgentID(targetAgentId, explicitCoAgentId, contextUser, provider) {
1166
- await AIEngine.Instance.Config(false, contextUser, provider);
1167
- // Step 1 — explicit runtime parameter: fail LOUD on any problem.
1168
- if (explicitCoAgentId) {
1169
- const { agent, problem } = this.findValidCoAgent(explicitCoAgentId);
1170
- if (!agent) {
1171
- throw new Error(`Invalid coAgentId '${explicitCoAgentId}': ${problem}`);
1172
- }
1173
- return agent.ID;
1174
- }
1175
- const targetAgent = targetAgentId
1176
- ? (AIEngine.Instance.Agents ?? []).find(a => UUIDsEqual(a.ID, targetAgentId))
1177
- : undefined;
1178
- // Step 2 — the target agent's own DefaultCoAgentID (per-agent persona): warn + fall through.
1179
- const fromAgent = this.resolveMetadataDefault(targetAgent?.DefaultCoAgentID, `agent '${targetAgent?.Name}' (DefaultCoAgentID)`);
1180
- if (fromAgent) {
1181
- return fromAgent;
1182
- }
1183
- // Step 3 — the type-level AIAgentCoAgent default row (per-type default): warn + fall through.
1184
- const agentType = targetAgent?.TypeID
1185
- ? (AIEngine.Instance.AgentTypes ?? []).find(t => UUIDsEqual(t.ID, targetAgent.TypeID))
1186
- : undefined;
1187
- const typeDefaultCoAgentID = agentType
1188
- ? this.findTypeDefaultCoAgentID(agentType.ID, contextUser, provider)
1189
- : undefined;
1190
- const fromType = this.resolveMetadataDefault(await typeDefaultCoAgentID, `agent type '${agentType?.Name}' (AIAgentCoAgent type-default row)`);
1191
- if (fromType) {
1192
- return fromType;
1193
- }
1194
- // Step 4 — global default: the seeded Realtime Co-Agent, by name (original behavior).
1195
- return this.resolveGlobalCoAgentID();
1196
- }
1197
- /**
1198
- * Chain step 3 lookup: the TYPE-LEVEL default co-agent for an agent type — the Active
1199
- * `AIAgentCoAgent` row of Type `'CoAgent'` whose `TargetAgentTypeID` matches, with
1200
- * `IsDefault = 1` preferred and the lowest `Sequence` breaking ties (so a deployment can
1201
- * stage multiple type-level candidates deterministically). Reads {@link AIEngineBase}'s
1202
- * cached rows — no RunView. Tolerant: a failed cache read logs and returns `undefined`
1203
- * (the chain falls through to the global default).
1204
- */
1205
- async findTypeDefaultCoAgentID(agentTypeID, contextUser, provider) {
1206
- try {
1207
- const engine = await this.configuredAIEngineBase(contextUser, provider);
1208
- const candidates = (engine.AgentCoAgents ?? [])
1209
- .filter((r) => r.Type === 'CoAgent' &&
1210
- r.Status === 'Active' &&
1211
- r.TargetAgentTypeID != null &&
1212
- UUIDsEqual(r.TargetAgentTypeID, agentTypeID))
1213
- .sort((a, b) => (a.IsDefault === b.IsDefault ? 0 : a.IsDefault ? -1 : 1) ||
1214
- (a.Sequence ?? 0) - (b.Sequence ?? 0));
1215
- return candidates[0]?.CoAgentID;
1216
- }
1217
- catch (error) {
1218
- LogError(`StartRealtimeClientSession: ${CO_AGENT_ENTITY} cache read failed while resolving the type-level ` +
1219
- `default co-agent for agent type ${agentTypeID} (${error.message}) — falling through.`);
1220
- return undefined;
1221
- }
1222
- }
1223
- /**
1224
- * Validates one metadata-level co-agent default (chain steps 2/3). Returns the resolved
1225
- * co-agent id when the reference is valid; logs a warning and returns `null` (caller falls
1226
- * through to the next chain step) when the reference is set but invalid — metadata drift must
1227
- * degrade, not break live calls. A `null`/absent reference returns `null` silently.
1228
- */
1229
- resolveMetadataDefault(candidateId, source) {
1230
- if (!candidateId) {
1231
- return null;
1232
- }
1233
- const { agent, problem } = this.findValidCoAgent(candidateId);
1234
- if (agent) {
1235
- return agent.ID;
1236
- }
1237
- LogError(`StartRealtimeClientSession: ignoring co-agent default '${candidateId}' from ${source} — ${problem} ` +
1238
- 'Falling through to the next step of the co-agent resolution chain.');
1239
- return null;
1240
- }
1241
- /**
1242
- * Validates a co-agent candidate against {@link AIEngine}'s cached metadata. A valid co-agent
1243
- * must (a) exist, (b) have Status `Active`, and (c) be of the {@link REALTIME_AGENT_TYPE_NAME}
1244
- * agent type. Returns the cached agent on success, or a human-readable `problem` on failure —
1245
- * the CALLER decides whether that's fatal (explicit runtime param) or tolerated (metadata default).
1246
- */
1247
- findValidCoAgent(candidateId) {
1248
- const agent = (AIEngine.Instance.Agents ?? []).find(a => UUIDsEqual(a.ID, candidateId));
1249
- if (!agent) {
1250
- return { problem: 'no agent with that ID exists.' };
1251
- }
1252
- if (agent.Status !== 'Active') {
1253
- return { problem: `agent '${agent.Name}' is not Active (Status: ${agent.Status}).` };
1254
- }
1255
- const realtimeType = (AIEngine.Instance.AgentTypes ?? []).find(t => t.Name?.trim().toLowerCase() === REALTIME_AGENT_TYPE_NAME.toLowerCase());
1256
- if (!realtimeType) {
1257
- return { problem: `the '${REALTIME_AGENT_TYPE_NAME}' agent type is not configured in this deployment.` };
1258
- }
1259
- if (!agent.TypeID || !UUIDsEqual(agent.TypeID, realtimeType.ID)) {
1260
- return { problem: `agent '${agent.Name}' is not of the '${REALTIME_AGENT_TYPE_NAME}' agent type.` };
1261
- }
1262
- return { agent };
1263
- }
1264
- /**
1265
- * Resolves the seeded Realtime Co-Agent's id from {@link AIEngine}'s cached agents — the GLOBAL
1266
- * DEFAULT (step 4) of the co-agent resolution chain. The engine is already configured by
1267
- * {@link resolveCoAgentID}. Looks up the current {@link REALTIME_CO_AGENT_NAME} first, then
1268
- * falls back to the DEPRECATED {@link LEGACY_REALTIME_CO_AGENT_NAME} (pre-rename seed) with a
1269
- * deprecation log so un-resynced deployments keep working. Throws a clear error when neither
1270
- * name is present in metadata.
1271
- */
1272
- resolveGlobalCoAgentID() {
1273
- const coAgent = this.findAgentByName(REALTIME_CO_AGENT_NAME);
1274
- if (coAgent) {
1275
- return coAgent.ID;
1276
- }
1277
- const legacy = this.findAgentByName(LEGACY_REALTIME_CO_AGENT_NAME);
1278
- if (legacy) {
1279
- LogStatus(`StartRealtimeClientSession: resolved the global co-agent via its DEPRECATED legacy name ` +
1280
- `'${LEGACY_REALTIME_CO_AGENT_NAME}'. Re-sync the agent seed metadata to rename it to ` +
1281
- `'${REALTIME_CO_AGENT_NAME}'.`);
1282
- return legacy.ID;
1283
- }
1284
- throw new Error(`The '${REALTIME_CO_AGENT_NAME}' agent is not configured; cannot start a realtime voice session.`);
1285
- }
1286
- /** Case/whitespace-insensitive agent lookup by Name in {@link AIEngine}'s cached agents. */
1287
- findAgentByName(name) {
1288
- const wanted = name.toLowerCase();
1289
- return (AIEngine.Instance.Agents ?? []).find(a => a.Name?.trim().toLowerCase() === wanted);
1290
- }
1291
1115
  /**
1292
1116
  * Mints the client session config for a just-created session. On failure, closes the session so
1293
1117
  * no half-open record leaks, then throws the service's error message.
@@ -1378,7 +1202,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1378
1202
  session.Config_ = JSON.stringify(config);
1379
1203
  const saved = await session.Save();
1380
1204
  if (!saved) {
1381
- LogError(`RealtimeClientSessionResolver.persistObservabilityRunIDs save failed: ${session.LatestResult?.CompleteMessage ?? 'unknown error'}`);
1205
+ LogError(`RealtimeClientSessionResolver.persistObservabilityRunIDs save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`);
1382
1206
  }
1383
1207
  }
1384
1208
  /**
@@ -1462,7 +1286,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1462
1286
  const saved = await row.Save();
1463
1287
  if (!saved) {
1464
1288
  LogError(`SaveSessionChannelState: save failed for session ${agentSessionID} / channel ${channelID}: ` +
1465
- `${row.LatestResult?.CompleteMessage ?? 'unknown error'}`);
1289
+ `${this.describeSaveFailure(row)}`);
1466
1290
  }
1467
1291
  return saved;
1468
1292
  }
@@ -1765,7 +1589,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1765
1589
  artifact.UserID = contextUser.ID;
1766
1590
  artifact.Visibility = 'Always';
1767
1591
  if (!(await artifact.Save())) {
1768
- const message = `SaveSessionChannelArtifact: artifact save failed: ${artifact.LatestResult?.CompleteMessage ?? 'unknown error'}`;
1592
+ const message = `SaveSessionChannelArtifact: artifact save failed for session ${session.ID}: ${this.describeSaveFailure(artifact)}`;
1769
1593
  LogError(message);
1770
1594
  return { Success: false, ErrorMessage: message, ConversationDetailLinked: false };
1771
1595
  }
@@ -1776,7 +1600,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1776
1600
  version.Content = contentJson;
1777
1601
  version.UserID = contextUser.ID;
1778
1602
  if (!(await version.Save())) {
1779
- const message = `SaveSessionChannelArtifact: artifact version save failed: ${version.LatestResult?.CompleteMessage ?? 'unknown error'}`;
1603
+ const message = `SaveSessionChannelArtifact: artifact version save failed for session ${session.ID} (artifact ${artifact.ID}): ${this.describeSaveFailure(version)}`;
1780
1604
  LogError(message);
1781
1605
  return { Success: false, ErrorMessage: message, ArtifactID: artifact.ID, ConversationDetailLinked: false };
1782
1606
  }
@@ -1826,21 +1650,29 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1826
1650
  * queries (which scan junctions across ALL details) still surface the artifact. This is the
1827
1651
  * least-invasive correct anchor — no fake visible message, no orphaned artifact.
1828
1652
  *
1653
+ * TWO PRINCIPALS, deliberately: the lookup (`findLatestSessionDetailID`) and the junction row
1654
+ * itself (`saveDetailArtifactJunction`, `MJ: Conversation Detail Artifacts`) run as `runUser`
1655
+ * (elevated for a scoped-anonymous caller) — that junction entity is outside an anonymous
1656
+ * caller's relay grants. The hidden ANCHOR `Conversation Detail` this may need to create runs as
1657
+ * `callerUser` instead: like every other `MJ: Conversation Details` write, the System user is
1658
+ * refused because it is not the conversation's owner (see {@link persistDirectActionTurn},
1659
+ * issue #4791) — only the actual caller can create it.
1660
+ *
1829
1661
  * Strictly best-effort: every failure path logs and returns; a relayed tool call NEVER fails
1830
1662
  * because history linking did.
1831
1663
  */
1832
- async linkDelegatedArtifactsToConversation(session, artifacts, contextUser, provider) {
1664
+ async linkDelegatedArtifactsToConversation(session, artifacts, runUser, callerUser, provider) {
1833
1665
  if (!artifacts || artifacts.length === 0 || !session.ConversationID) {
1834
1666
  return;
1835
1667
  }
1836
1668
  try {
1837
- const detailID = (await this.findLatestSessionDetailID(session, contextUser, provider)) ??
1838
- (await this.createHiddenSessionAnchorDetail(session, contextUser, provider));
1669
+ const detailID = (await this.findLatestSessionDetailID(session, runUser, provider)) ??
1670
+ (await this.createHiddenSessionAnchorDetail(session, callerUser, provider));
1839
1671
  if (!detailID) {
1840
1672
  return; // anchor unavailable — logged in the helpers
1841
1673
  }
1842
1674
  for (const artifact of artifacts) {
1843
- await this.saveDetailArtifactJunction(detailID, artifact.ArtifactVersionID, contextUser, provider);
1675
+ await this.saveDetailArtifactJunction(detailID, artifact.ArtifactVersionID, runUser, provider);
1844
1676
  }
1845
1677
  }
1846
1678
  catch (error) {
@@ -1871,22 +1703,22 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1871
1703
  * {@link linkDelegatedArtifactsToConversation}). Returns the new detail's id, or `null`
1872
1704
  * (logged) when the save fails.
1873
1705
  */
1874
- async createHiddenSessionAnchorDetail(session, contextUser, provider) {
1875
- const detail = await provider.GetEntityObject(CONVERSATION_DETAIL_ENTITY, contextUser);
1706
+ async createHiddenSessionAnchorDetail(session, callerUser, provider) {
1707
+ const detail = await provider.GetEntityObject(CONVERSATION_DETAIL_ENTITY, callerUser);
1876
1708
  detail.NewRecord();
1877
1709
  detail.ConversationID = session.ConversationID;
1878
1710
  detail.Role = 'AI';
1879
1711
  detail.HiddenToUser = true;
1880
1712
  detail.Message = 'Artifacts produced during a realtime session (system anchor).';
1881
1713
  detail.AgentSessionID = session.ID;
1882
- // Attribute the anchor to the SESSION owner, not the (possibly elevated) writer — identical
1883
- // for every non-elevated caller, whose ownership of the session is already proven.
1714
+ // Attribute to the SESSION owner — the caller writing this row, whose ownership
1715
+ // `loadOwnedActiveSession` already proved.
1884
1716
  detail.UserID = session.UserID;
1885
1717
  if (await detail.Save()) {
1886
1718
  return detail.ID;
1887
1719
  }
1888
1720
  LogError(`ExecuteRealtimeSessionTool: hidden anchor detail save failed for session ${session.ID}: ` +
1889
- `${detail.LatestResult?.CompleteMessage ?? 'unknown error'}`);
1721
+ `${this.describeSaveFailure(detail)}`);
1890
1722
  return null;
1891
1723
  }
1892
1724
  /**
@@ -1902,7 +1734,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1902
1734
  const saved = await junction.Save();
1903
1735
  if (!saved) {
1904
1736
  LogError(`RealtimeClientSessionResolver: artifact junction save failed for detail ${conversationDetailID} / version ${artifactVersionID}: ` +
1905
- `${junction.LatestResult?.CompleteMessage ?? 'unknown error'}`);
1737
+ `${this.describeSaveFailure(junction)}`);
1906
1738
  }
1907
1739
  return saved;
1908
1740
  }
@@ -1931,7 +1763,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1931
1763
  row.LastActiveAt = new Date();
1932
1764
  if (!(await row.Save())) {
1933
1765
  LogError(`SaveSessionChannelArtifact: LastActiveAt stamp failed for session ${agentSessionID} / channel ${channelID}: ` +
1934
- `${row.LatestResult?.CompleteMessage ?? 'unknown error'}`);
1766
+ `${this.describeSaveFailure(row)}`);
1935
1767
  }
1936
1768
  }
1937
1769
  catch (error) {
@@ -2064,12 +1896,6 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
2064
1896
  const minutes = elevation?.Widget.VoiceMaxSessionMinutes;
2065
1897
  return minutes && minutes > 0 ? minutes * 60 : undefined;
2066
1898
  }
2067
- /**
2068
- * Persists a single transcript turn as a `Conversation Detail` stamped with the session's
2069
- * conversation, the mapped role, the turn text, the session id, and the owning user.
2070
- *
2071
- * @returns The boolean save result (logs `CompleteMessage` on failure).
2072
- */
2073
1899
  /**
2074
1900
  * Stamps `RecordingStartedAt` (the recording `t0` alignment origin) + `RecordingMedia` on a
2075
1901
  * just-started session when the browser captured WITH consent. Best-effort: a parse/save failure
@@ -2091,9 +1917,16 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
2091
1917
  session.RecordingStartedAt = startedAt;
2092
1918
  session.RecordingMedia = 'Audio';
2093
1919
  if (!(await session.Save())) {
2094
- LogError(`RealtimeClientSessionResolver.stampRecordingStart save failed: ${session.LatestResult?.CompleteMessage ?? 'unknown error'}`);
1920
+ LogError(`RealtimeClientSessionResolver.stampRecordingStart save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`);
2095
1921
  }
2096
1922
  }
1923
+ /**
1924
+ * Persists a single transcript turn as a `Conversation Detail` stamped with the session's
1925
+ * conversation, the mapped role, the turn text, the session id, and the owning user.
1926
+ *
1927
+ * @returns The boolean save result. On failure it logs the session, the write user and the
1928
+ * reason from {@link describeSaveFailure}.
1929
+ */
2097
1930
  async persistTranscriptTurn(session, role, text, contextUser, provider, utteranceStartMs, utteranceEndMs) {
2098
1931
  const detail = await provider.GetEntityObject(CONVERSATION_DETAIL_ENTITY, contextUser);
2099
1932
  detail.NewRecord();
@@ -2110,7 +1943,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
2110
1943
  }
2111
1944
  const saved = await detail.Save();
2112
1945
  if (!saved) {
2113
- LogError(`RealtimeClientSessionResolver.persistTranscriptTurn save failed: ${detail.LatestResult?.CompleteMessage ?? 'unknown error'}`);
1946
+ LogError(`RealtimeClientSessionResolver.persistTranscriptTurn save failed for session ${session.ID} (write user ${contextUser.ID}): ${this.describeSaveFailure(detail)}`);
2114
1947
  }
2115
1948
  return saved;
2116
1949
  }
@@ -2141,8 +1974,13 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
2141
1974
  // ⚠️ ELEVATING INSTEAD WAS TRIED AND IS A TRAP. `ResolveScopedAnonymousRunUser` returns
2142
1975
  // `UserCache.GetSystemUser()`, which on this deployment resolves to the unconfigured
2143
1976
  // placeholder `not.set@nowhere.com` ("Configured provisioning user not found; falling back
2144
- // to an Owner"). Saving as a user that does not exist returns false with a NULL
2145
- // `LatestResult` — indistinguishable from a permission denial, and just as silent.
1977
+ // to an Owner"). That user EXISTS — it is not a missing/invalid principal — and even carries
1978
+ // full CRUD on `MJ: Conversation Details`. It is refused anyway:
1979
+ // `MJConversationDetailEntityExtended`'s owner gate accepts only the conversation's OWNER or
1980
+ // an Edit/Owner Resource Permission grantee, and the System user is neither. Measured on
1981
+ // MJ#4791 (before the core-entities fix that now records the denial): `Save()` → `false`,
1982
+ // `LatestResult` null — indistinguishable from a permission denial, and just as silent. The
1983
+ // denial itself still happens; that fix only made it stop being silent.
2146
1984
  const writeUser = contextUser;
2147
1985
  const rv = RunView.FromMetadataProvider(provider);
2148
1986
  const result = await rv.RunView({
@@ -2160,8 +1998,8 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
2160
1998
  //
2161
1999
  // `ResultType: 'entity_object'` hands back hydrated entities, but they do not carry the
2162
2000
  // context user the way `GetEntityObject(entity, user)` does — so `Save()` ran with no
2163
- // principal and failed with an EMPTY message ("unknown error"), which is what the elevation
2164
- // fix above looked like when it was still broken. The insert path beside this one has
2001
+ // principal and failed with an EMPTY message ("unknown error"), which is what an empty-message
2002
+ // failure here looked like before the re-load. The insert path beside this one has
2165
2003
  // always used `GetEntityObject`; this now matches it, which is also why they now succeed
2166
2004
  // and fail for the same reasons.
2167
2005
  const previous = await provider.GetEntityObject(CONVERSATION_DETAIL_ENTITY, writeUser);
@@ -2187,7 +2025,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
2187
2025
  LogError(`RealtimeClientSessionResolver.replacePreviousTranscriptTurn save failed for session `
2188
2026
  + `${session.ID} — the ${mappedRole} turn keeps its PREVIOUS, shorter text and the `
2189
2027
  + `transcript now understates what was said: `
2190
- + `${previous.LatestResult?.CompleteMessage || JSON.stringify(previous.LatestResult ?? null)}`);
2028
+ + `${this.describeSaveFailure(previous)}`);
2191
2029
  }
2192
2030
  return saved;
2193
2031
  }
@@ -2204,14 +2042,30 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
2204
2042
  * Stored as stable facts (Role: 'AI', HiddenToUser: true, ExternalID: callId, CompletionTime: durationMs,
2205
2043
  * Message: JSON-serialized tool facts) so that session review can reconstruct the action card dynamically
2206
2044
  * on load (Option 1) without polluting user-facing speech chat bubbles.
2045
+ *
2046
+ * Writes as the CALLER, never the elevated run user (issue #4791). Owning the session
2047
+ * (`loadOwnedActiveSession`'s gate has already run on every path here) does NOT imply owning
2048
+ * the conversation: the conversation id was client-supplied at session start. What authorizes
2049
+ * this write is the save itself, evaluated for the caller — the conversation owner gate in
2050
+ * `MJConversationDetailEntityExtended`, plus the caller's own entity permission and Create RLS
2051
+ * on `MJ: Conversation Details` (for the magic-link role that RLS scopes by the invite's
2052
+ * `ScopeResourceID`, and the role belongs to a downstream app, not MJ core). That is why
2053
+ * writing as the caller is safe even though the conversation id originated client-side.
2054
+ * Elevating to the System user would bypass RLS entirely, and the owner gate refuses it anyway
2055
+ * because the System user is not the conversation owner. Measured on MJ#4791 (before the
2056
+ * core-entities fix that now records the denial): `Save()` returned `false` with a NULL `LatestResult` and an empty
2057
+ * `ResultHistory` — indistinguishable from any other silent failure. Elevating this write is
2058
+ * what silently dropped both hidden tool-execution turns of a real magic-link session; the
2059
+ * denial itself still happens today, only its silence was fixed separately.
2060
+ * `ResolveScopedAnonymousRunUser` elevates AI-**run**-entity writes only (prompt runs, agent
2061
+ * runs, the relayed dispatch itself) — never this one.
2207
2062
  */
2208
2063
  async persistDirectActionTurn(session, callId, toolName, argsJson, resultJson, durationMs, contextUser, provider) {
2209
2064
  if (!session.ConversationID) {
2210
2065
  return false;
2211
2066
  }
2212
2067
  try {
2213
- const writeUser = ResolveScopedAnonymousRunUser(contextUser);
2214
- const detail = await provider.GetEntityObject(CONVERSATION_DETAIL_ENTITY, writeUser);
2068
+ const detail = await provider.GetEntityObject(CONVERSATION_DETAIL_ENTITY, contextUser);
2215
2069
  detail.NewRecord();
2216
2070
  detail.ConversationID = session.ConversationID;
2217
2071
  detail.Role = 'AI';
@@ -2249,13 +2103,13 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
2249
2103
  detail.Message = JSON.stringify(factPayload);
2250
2104
  const saved = await detail.Save();
2251
2105
  if (!saved) {
2252
- LogError(`RealtimeClientSessionResolver.persistDirectActionTurn save failed: ${detail.LatestResult?.CompleteMessage ?? 'unknown error'}`);
2106
+ LogError(`RealtimeClientSessionResolver.persistDirectActionTurn save failed for session ${session.ID} (tool '${toolName}', call ${callId ?? 'n/a'}, write user ${contextUser.ID}): ${this.describeSaveFailure(detail)}`);
2253
2107
  }
2254
2108
  return saved;
2255
2109
  }
2256
2110
  catch (error) {
2257
2111
  const message = error instanceof Error ? error.message : String(error);
2258
- LogError(`RealtimeClientSessionResolver.persistDirectActionTurn unexpected error: ${message}`);
2112
+ LogError(`RealtimeClientSessionResolver.persistDirectActionTurn unexpected error for session ${session.ID}: ${message}`);
2259
2113
  return false;
2260
2114
  }
2261
2115
  }