@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
@@ -3,7 +3,7 @@
3
3
  // bring it in automatically — this line MUST precede any import that pulls in the resolver file.
4
4
  import 'reflect-metadata';
5
5
 
6
- import { describe, it, expect, vi, beforeEach } from 'vitest';
6
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
7
7
 
8
8
  // --- Mock CanRun authorization (AIAgentPermissionHelper.HasPermission) plus the
9
9
  // provider-scoped AIEngineBase whose cached metadata serves the resolver's pairing-row
@@ -210,9 +210,23 @@ interface FakeSession {
210
210
  NewRecord: () => void;
211
211
  Save: () => Promise<boolean>;
212
212
  Load: (id: string) => Promise<boolean>;
213
- LatestResult?: { CompleteMessage?: string };
213
+ LatestResult?: { CompleteMessage?: string; Success?: boolean };
214
+ /** Only set on the fakes exercising `describeSaveFailure`'s "no failure detail recorded" branches. */
215
+ ResultHistory?: unknown[];
214
216
  }
215
217
 
218
+ /**
219
+ * A `console.error` spy shared by the save-failure-log tests (#4791) below. Assigned with
220
+ * `vi.spyOn(console, 'error').mockImplementation(() => undefined)` inside each test that needs it
221
+ * — restored here so no test leaks a mocked `console.error` into the ones after it.
222
+ */
223
+ let errSpy: ReturnType<typeof vi.spyOn> | undefined;
224
+
225
+ afterEach(() => {
226
+ errSpy?.mockRestore();
227
+ errSpy = undefined;
228
+ });
229
+
216
230
  function makeSessionEntity(overrides: Partial<FakeSession> = {}): FakeSession {
217
231
  return {
218
232
  ID: 'session-1',
@@ -228,13 +242,44 @@ function makeSessionEntity(overrides: Partial<FakeSession> = {}): FakeSession {
228
242
  };
229
243
  }
230
244
 
231
- /** Provider whose GetEntityObject returns a fixed entity per entity name. */
232
- function makeProvider(factory: (entityName: string) => FakeSession): unknown {
233
- return { GetEntityObject: vi.fn(async (name: string) => factory(name)) };
245
+ /** Co-agent observability and paused-run ids, as a voice session's Config holds them. */
246
+ const CO_RUN_ID = 'c0c0c0c0-0000-4000-8000-000000000001';
247
+ const PROMPT_RUN_ID = 'c0c0c0c0-0000-4000-8000-000000000002';
248
+ const PAUSED_RUN_ID = 'c0c0c0c0-0000-4000-8000-000000000003';
249
+
250
+ /** `RunView`/`RunViews` stubs for a provider. */
251
+ interface RunLookups {
252
+ RunView: ReturnType<typeof vi.fn>;
253
+ RunViews: ReturnType<typeof vi.fn>;
254
+ }
255
+
256
+ /**
257
+ * Answers the session run-id lookups: each id in `runIDs` is a run of the session, and
258
+ * `promptRunID` (when given) is the TargetLogID of the co-agent run's Prompt step.
259
+ */
260
+ function sessionRunLookups(runIDs: string[], promptRunID?: string): RunLookups {
261
+ const runView = vi.fn(async (params: { EntityName?: string }) => {
262
+ if (params.EntityName === 'MJ: AI Agent Runs') {
263
+ return { Success: true, Results: runIDs.map((ID) => ({ ID })) };
264
+ }
265
+ if (params.EntityName === 'MJ: AI Agent Run Steps' && promptRunID) {
266
+ return { Success: true, Results: [{ ID: 'c0c0c0c0-0000-4000-8000-0000000000a1', StepType: 'Prompt', TargetLogID: promptRunID }] };
267
+ }
268
+ return { Success: true, Results: [] };
269
+ });
270
+ const runViews = vi.fn(async (paramsList: Array<{ EntityName?: string }>) => Promise.all(paramsList.map((params) => runView(params))));
271
+ return { RunView: runView, RunViews: runViews };
272
+ }
273
+
274
+ /** Provider whose GetEntityObject returns a fixed entity per entity name, plus optional run-id lookups. */
275
+ function makeProvider(factory: (entityName: string) => FakeSession, lookups?: RunLookups): unknown {
276
+ return { GetEntityObject: vi.fn(async (name: string) => factory(name)), ...lookups };
234
277
  }
235
278
 
236
279
  beforeEach(() => {
237
280
  hasPermissionMock.mockReset();
281
+ // Callers may run the agents in play unless a test says otherwise; relays re-check CanRun on the target.
282
+ hasPermissionMock.mockResolvedValue(true);
238
283
  engineCoAgentsMock.mockReset();
239
284
  engineCoAgentsMock.mockReturnValue([]);
240
285
  engineChannelsMock.mockReset();
@@ -671,10 +716,12 @@ describe('RealtimeClientSessionResolver.ExecuteRealtimeSessionTool', () => {
671
716
  });
672
717
 
673
718
  it('reads the target + co-agent run id from the session config and returns ResultJson', async () => {
674
- currentProvider = makeProvider(() =>
675
- makeSessionEntity({
676
- Config_: JSON.stringify({ targetAgentID: 'target-from-session', coAgentRunID: 'co-run-77' }),
677
- }),
719
+ currentProvider = makeProvider(
720
+ () =>
721
+ makeSessionEntity({
722
+ Config_: JSON.stringify({ targetAgentID: 'target-from-session', coAgentRunID: CO_RUN_ID }),
723
+ }),
724
+ sessionRunLookups([CO_RUN_ID]),
678
725
  );
679
726
  executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"ok":true}', Success: true });
680
727
  const resolver = makeResolver();
@@ -697,7 +744,7 @@ describe('RealtimeClientSessionResolver.ExecuteRealtimeSessionTool', () => {
697
744
  // Target comes from the session, NOT the client.
698
745
  expect(relayArg.TargetAgentID).toBe('target-from-session');
699
746
  // Delegated run nests under the co-agent observability run from the session config.
700
- expect(relayArg.ParentRunID).toBe('co-run-77');
747
+ expect(relayArg.ParentRunID).toBe(CO_RUN_ID);
701
748
  expect(relayArg.Call).toEqual({ CallID: 'call-1', ToolName: 'invoke-target-agent', Arguments: '{"request":"do it"}' });
702
749
  expect(heartbeatMock).toHaveBeenCalledWith('session-1', USER, currentProvider);
703
750
  });
@@ -761,9 +808,9 @@ describe('RealtimeClientSessionResolver.ExecuteRealtimeSessionTool', () => {
761
808
 
762
809
  it('consumes pendingFeedbackRunID as ResumeRunID and clears it on the next call', async () => {
763
810
  const session = makeSessionEntity({
764
- Config_: JSON.stringify({ targetAgentID: 'target-1', pendingFeedbackRunID: 'paused-1' }),
811
+ Config_: JSON.stringify({ targetAgentID: 'target-1', pendingFeedbackRunID: PAUSED_RUN_ID }),
765
812
  });
766
- currentProvider = makeProvider(() => session);
813
+ currentProvider = makeProvider(() => session, sessionRunLookups([PAUSED_RUN_ID]));
767
814
  // Resumed run completes (no new pause).
768
815
  executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"done":1}', Success: true });
769
816
  const resolver = makeResolver();
@@ -772,7 +819,7 @@ describe('RealtimeClientSessionResolver.ExecuteRealtimeSessionTool', () => {
772
819
 
773
820
  // The paused id was passed in to resume the run...
774
821
  const relayArg = executeRelayedToolMock.mock.calls[0][0] as { ResumeRunID?: string };
775
- expect(relayArg.ResumeRunID).toBe('paused-1');
822
+ expect(relayArg.ResumeRunID).toBe(PAUSED_RUN_ID);
776
823
  // ...and cleared from the session config afterward.
777
824
  expect(JSON.parse(session.Config_ as string).pendingFeedbackRunID).toBeUndefined();
778
825
  expect(session.Save).toHaveBeenCalled();
@@ -780,16 +827,16 @@ describe('RealtimeClientSessionResolver.ExecuteRealtimeSessionTool', () => {
780
827
 
781
828
  it('re-stores a new pendingFeedbackRunID when a resumed run pauses again', async () => {
782
829
  const session = makeSessionEntity({
783
- Config_: JSON.stringify({ targetAgentID: 'target-1', pendingFeedbackRunID: 'paused-1' }),
830
+ Config_: JSON.stringify({ targetAgentID: 'target-1', pendingFeedbackRunID: PAUSED_RUN_ID }),
784
831
  });
785
- currentProvider = makeProvider(() => session);
832
+ currentProvider = makeProvider(() => session, sessionRunLookups([PAUSED_RUN_ID]));
786
833
  executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"q":2}', Success: true, PausedRunID: 'paused-2' });
787
834
  const resolver = makeResolver();
788
835
 
789
836
  await resolver.ExecuteRealtimeSessionTool('session-1', 'call-3', 'invoke-target-agent', '{"request":"yes"}', makeCtx(), makePubSub());
790
837
 
791
838
  const relayArg = executeRelayedToolMock.mock.calls[0][0] as { ResumeRunID?: string };
792
- expect(relayArg.ResumeRunID).toBe('paused-1');
839
+ expect(relayArg.ResumeRunID).toBe(PAUSED_RUN_ID);
793
840
  expect(JSON.parse(session.Config_ as string).pendingFeedbackRunID).toBe('paused-2');
794
841
  });
795
842
 
@@ -848,6 +895,28 @@ describe('RealtimeClientSessionResolver.ExecuteRealtimeSessionTool', () => {
848
895
  resolver.ExecuteRealtimeSessionTool('session-1', 'call-1', 'x', '{}', makeCtx(), makePubSub()),
849
896
  ).rejects.toThrow(/no target agent configured/i);
850
897
  });
898
+
899
+ it('logs the session id and "no failure detail recorded" when updatePendingFeedbackRunID\'s save is refused silently (#4791)', async () => {
900
+ errSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
901
+ const session = makeSessionEntity({
902
+ Config_: JSON.stringify({ targetAgentID: 'target-1' }),
903
+ Save: vi.fn(async () => false),
904
+ LatestResult: undefined,
905
+ ResultHistory: [],
906
+ });
907
+ currentProvider = makeProvider(() => session);
908
+ executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"q":1}', Success: true, PausedRunID: 'paused-1' });
909
+ const resolver = makeResolver();
910
+
911
+ await resolver.ExecuteRealtimeSessionTool('session-1', 'call-1', 'invoke-target-agent', '{}', makeCtx(), makePubSub());
912
+
913
+ const loggedLines = errSpy.mock.calls.map((call) => String(call[0]));
914
+ const failureLine = loggedLines.find((line) => line.includes('updatePendingFeedbackRunID'));
915
+ expect(failureLine).toBeDefined();
916
+ expect(failureLine).toContain('session-1');
917
+ expect(failureLine).toContain('no failure detail recorded');
918
+ expect(failureLine).not.toContain('unknown error');
919
+ });
851
920
  });
852
921
 
853
922
  describe('RealtimeClientSessionResolver.RelayRealtimeTranscript', () => {
@@ -1690,6 +1759,7 @@ describe('RealtimeClientSessionResolver.SaveSessionChannelArtifact', () => {
1690
1759
  });
1691
1760
 
1692
1761
  it('returns a structured failure when the artifact header save fails (no version attempted)', async () => {
1762
+ errSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
1693
1763
  const failingArtifact = makeSessionEntity({
1694
1764
  ID: 'artifact-fail',
1695
1765
  Save: vi.fn(async () => false),
@@ -1706,10 +1776,14 @@ describe('RealtimeClientSessionResolver.SaveSessionChannelArtifact', () => {
1706
1776
 
1707
1777
  expect(result.Success).toBe(false);
1708
1778
  expect(result.ErrorMessage).toMatch(/artifact save failed/i);
1779
+ expect(result.ErrorMessage).toContain('session session-1');
1780
+ expect(result.ErrorMessage).toContain('db down');
1781
+ expect(errSpy.mock.calls.map((call) => String(call[0]))).toContain(result.ErrorMessage);
1709
1782
  expect(version.Save).not.toHaveBeenCalled();
1710
1783
  });
1711
1784
 
1712
1785
  it('returns a structured failure (carrying the orphaned ArtifactID) when the version save fails', async () => {
1786
+ errSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
1713
1787
  const failingVersion = makeSessionEntity({
1714
1788
  ID: 'version-fail',
1715
1789
  Save: vi.fn(async () => false),
@@ -1728,6 +1802,10 @@ describe('RealtimeClientSessionResolver.SaveSessionChannelArtifact', () => {
1728
1802
  expect(result.Success).toBe(false);
1729
1803
  expect(result.ArtifactID).toBe('artifact-1');
1730
1804
  expect(result.ArtifactVersionID).toBeUndefined();
1805
+ expect(result.ErrorMessage).toMatch(/artifact version save failed/i);
1806
+ expect(result.ErrorMessage).toContain('session session-1');
1807
+ expect(result.ErrorMessage).toContain('too big');
1808
+ expect(errSpy.mock.calls.map((call) => String(call[0]))).toContain(result.ErrorMessage);
1731
1809
  expect(junction.Save).not.toHaveBeenCalled();
1732
1810
  });
1733
1811
 
@@ -2247,10 +2325,11 @@ describe('RealtimeClientSessionResolver.RelayRealtimeUsage', () => {
2247
2325
  }): { provider: unknown; promptRun: FakeSession } {
2248
2326
  const session =
2249
2327
  opts.session ??
2250
- makeSessionEntity({ Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID: 'prompt-run-1' }) });
2251
- const promptRun = opts.promptRun ?? makeSessionEntity({ ID: 'prompt-run-1', TokensPrompt: null, TokensCompletion: null, TokensUsed: null });
2328
+ makeSessionEntity({ Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }) });
2329
+ const promptRun = opts.promptRun ?? makeSessionEntity({ ID: PROMPT_RUN_ID, TokensPrompt: null, TokensCompletion: null, TokensUsed: null });
2252
2330
  const provider = {
2253
2331
  GetEntityObject: vi.fn(async (name: string) => (name === 'MJ: AI Prompt Runs' ? promptRun : session)),
2332
+ ...sessionRunLookups([CO_RUN_ID], PROMPT_RUN_ID),
2254
2333
  };
2255
2334
  return { provider, promptRun };
2256
2335
  }
@@ -2276,7 +2355,7 @@ describe('RealtimeClientSessionResolver.RelayRealtimeUsage', () => {
2276
2355
  const ok = await resolver.RelayRealtimeUsage('session-1', 30, 5, makeCtx());
2277
2356
 
2278
2357
  expect(ok).toBe(true);
2279
- expect(promptRun.Load).toHaveBeenCalledWith('prompt-run-1');
2358
+ expect(promptRun.Load).toHaveBeenCalledWith(PROMPT_RUN_ID);
2280
2359
  expect(promptRun.TokensPrompt).toBe(130);
2281
2360
  expect(promptRun.TokensCompletion).toBe(45);
2282
2361
  expect(promptRun.TokensUsed).toBe(175);
@@ -2373,7 +2452,7 @@ describe('RealtimeClientSessionResolver.RelayRealtimeUsage', () => {
2373
2452
  const { provider, promptRun } = makeUsageProvider({
2374
2453
  session: makeSessionEntity({
2375
2454
  Status: 'Closed',
2376
- Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID: 'prompt-run-1' }),
2455
+ Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }),
2377
2456
  }),
2378
2457
  });
2379
2458
  currentProvider = provider;
@@ -2770,7 +2849,7 @@ describe('RealtimeClientSessionResolver — app awareness (applicationId / appCo
2770
2849
  expect(relayArg.AllowedAgents?.map(a => a.agentId)).toEqual(['skip-1']);
2771
2850
  });
2772
2851
 
2773
- it('passes the persisted directActions from the session config into ExecuteRelayedTool', async () => {
2852
+ it('does not pass the directActions stored in the session config into ExecuteRelayedTool', async () => {
2774
2853
  currentProvider = makeProvider(() =>
2775
2854
  makeSessionEntity({
2776
2855
  Config_: JSON.stringify({
@@ -2787,7 +2866,8 @@ describe('RealtimeClientSessionResolver — app awareness (applicationId / appCo
2787
2866
  );
2788
2867
 
2789
2868
  const relayArg = executeRelayedToolMock.mock.calls[0][0] as { DirectActions?: { enabled: boolean; actionNames: string[] } };
2790
- expect(relayArg.DirectActions).toEqual({ enabled: true, actionNames: ['SendEmail'] });
2869
+ // The owner can edit Config, so the service takes direct actions from server-side state instead.
2870
+ expect(relayArg.DirectActions).toBeUndefined();
2791
2871
  });
2792
2872
  });
2793
2873
 
@@ -2880,14 +2960,52 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
2880
2960
  expect(executeRelayedToolMock.mock.calls[0][1]).toBe(ANON_USER);
2881
2961
  });
2882
2962
 
2963
+ it('refuses a direct action from a scoped anonymous caller without dispatching it or writing a turn as the system user (#4791)', async () => {
2964
+ const detail = makeSessionEntity({ ID: 'detail-1' });
2965
+ currentProvider = {
2966
+ GetEntityObject: vi.fn(async (name: string) =>
2967
+ name === 'MJ: Conversation Details' ? detail : makeSessionEntity({ UserID: 'anon-1' }),
2968
+ ),
2969
+ };
2970
+ executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"success":true}', Success: true });
2971
+ const resolver = makeAnonResolver();
2972
+
2973
+ const out = await resolver.ExecuteRealtimeSessionTool(
2974
+ 'session-1', 'call-1', 'Some Direct Action', '{}', makeCtx(), makePubSub(),
2975
+ );
2976
+
2977
+ // Scope-limited callers get no direct actions, so nothing is dispatched, elevated or not…
2978
+ expect(JSON.parse(out)).toMatchObject({ success: false });
2979
+ expect(executeRelayedToolMock).not.toHaveBeenCalled();
2980
+ // …and no hidden Conversation Detail turn is written, least of all as the system user.
2981
+ expect(getEntityObjectSpy()).not.toHaveBeenCalledWith('MJ: Conversation Details', SYSTEM_USER);
2982
+ expect(detail.Save).not.toHaveBeenCalled();
2983
+ });
2984
+
2985
+ it('writes the direct-action tool turn as the caller unchanged for a normal authenticated user', async () => {
2986
+ const detail = makeSessionEntity({ ID: 'detail-2' });
2987
+ currentProvider = {
2988
+ GetEntityObject: vi.fn(async (name: string) => (name === 'MJ: Conversation Details' ? detail : makeSessionEntity())),
2989
+ };
2990
+ executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"success":true}', Success: true });
2991
+ const resolver = makeResolver();
2992
+
2993
+ await resolver.ExecuteRealtimeSessionTool(
2994
+ 'session-1', 'call-1', 'Some Direct Action', '{}', makeCtx(), makePubSub(),
2995
+ );
2996
+
2997
+ expect(getEntityObjectSpy()).toHaveBeenCalledWith('MJ: Conversation Details', USER);
2998
+ });
2999
+
2883
3000
  it('accumulates relayed usage onto the prompt run under the SYSTEM user', async () => {
2884
3001
  const session = makeSessionEntity({
2885
3002
  UserID: 'anon-1',
2886
- Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID: 'prompt-run-1' }),
3003
+ Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }),
2887
3004
  });
2888
- const promptRun = makeSessionEntity({ ID: 'prompt-run-1', TokensPrompt: null, TokensCompletion: null, TokensUsed: null });
3005
+ const promptRun = makeSessionEntity({ ID: PROMPT_RUN_ID, TokensPrompt: null, TokensCompletion: null, TokensUsed: null });
2889
3006
  currentProvider = {
2890
3007
  GetEntityObject: vi.fn(async (name: string) => (name === 'MJ: AI Prompt Runs' ? promptRun : session)),
3008
+ ...sessionRunLookups([CO_RUN_ID], PROMPT_RUN_ID),
2891
3009
  };
2892
3010
  const resolver = makeAnonResolver();
2893
3011
 
@@ -2902,9 +3020,9 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
2902
3020
  it('mirrors the transcript turn onto the prompt run as SYSTEM while the Conversation Detail stays the caller', async () => {
2903
3021
  const session = makeSessionEntity({
2904
3022
  UserID: 'anon-1',
2905
- Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID: 'prompt-run-1' }),
3023
+ Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }),
2906
3024
  });
2907
- const promptRun = makeSessionEntity({ ID: 'prompt-run-1', Messages: null });
3025
+ const promptRun = makeSessionEntity({ ID: PROMPT_RUN_ID, Messages: null });
2908
3026
  const detail = makeSessionEntity({ ID: 'detail-1' });
2909
3027
  currentProvider = {
2910
3028
  GetEntityObject: vi.fn(async (name: string) => {
@@ -2912,6 +3030,7 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
2912
3030
  if (name === 'MJ: Conversation Details') return detail;
2913
3031
  return session;
2914
3032
  }),
3033
+ ...sessionRunLookups([CO_RUN_ID], PROMPT_RUN_ID),
2915
3034
  };
2916
3035
  const resolver = makeAnonResolver();
2917
3036
 
@@ -2928,18 +3047,46 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
2928
3047
  it('records the co-agent tool turn (RelayRealtimeToolTurn) under the SYSTEM user', async () => {
2929
3048
  const session = makeSessionEntity({
2930
3049
  UserID: 'anon-1',
2931
- Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID: 'prompt-run-1' }),
3050
+ Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }),
2932
3051
  });
2933
- const promptRun = makeSessionEntity({ ID: 'prompt-run-1', Messages: null });
3052
+ const promptRun = makeSessionEntity({ ID: PROMPT_RUN_ID, Messages: null });
2934
3053
  currentProvider = {
2935
3054
  GetEntityObject: vi.fn(async (name: string) => (name === 'MJ: AI Prompt Runs' ? promptRun : session)),
3055
+ ...sessionRunLookups([CO_RUN_ID], PROMPT_RUN_ID),
3056
+ };
3057
+ const resolver = makeAnonResolver();
3058
+
3059
+ const ok = await resolver.RelayRealtimeToolTurn('session-1', 'browser_navigate', makeCtx(), '{"url":"x"}');
3060
+
3061
+ expect(ok).toBe(true);
3062
+ expect(getEntityObjectSpy()).toHaveBeenCalledWith('MJ: AI Prompt Runs', SYSTEM_USER);
3063
+ });
3064
+
3065
+ it('writes the hidden co-agent tool turn (RelayRealtimeToolTurn) as the CALLER (#4791)', async () => {
3066
+ const session = makeSessionEntity({
3067
+ UserID: 'anon-1',
3068
+ Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }),
3069
+ });
3070
+ const promptRun = makeSessionEntity({ ID: PROMPT_RUN_ID, Messages: null });
3071
+ const detail = makeSessionEntity({ ID: 'detail-3' });
3072
+ currentProvider = {
3073
+ GetEntityObject: vi.fn(async (name: string) => {
3074
+ if (name === 'MJ: AI Prompt Runs') return promptRun;
3075
+ if (name === 'MJ: Conversation Details') return detail;
3076
+ return session;
3077
+ }),
3078
+ ...sessionRunLookups([CO_RUN_ID], PROMPT_RUN_ID),
2936
3079
  };
2937
3080
  const resolver = makeAnonResolver();
2938
3081
 
2939
3082
  const ok = await resolver.RelayRealtimeToolTurn('session-1', 'browser_navigate', makeCtx(), '{"url":"x"}');
2940
3083
 
2941
3084
  expect(ok).toBe(true);
3085
+ // The co-agent run mirror stays SYSTEM…
2942
3086
  expect(getEntityObjectSpy()).toHaveBeenCalledWith('MJ: AI Prompt Runs', SYSTEM_USER);
3087
+ // …but the hidden Conversation Detail tool-turn is written as the CALLER, never SYSTEM.
3088
+ expect(getEntityObjectSpy()).toHaveBeenCalledWith('MJ: Conversation Details', ANON_USER);
3089
+ expect(getEntityObjectSpy()).not.toHaveBeenCalledWith('MJ: Conversation Details', SYSTEM_USER);
2943
3090
  });
2944
3091
 
2945
3092
  it('prepares the session with observability under the SYSTEM user while UserID stays the visitor', async () => {
@@ -2971,6 +3118,30 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
2971
3118
  expect(prepInput.UserID).toBe('anon-1');
2972
3119
  });
2973
3120
 
3121
+ /** Starts a session as `resolver`'s caller and returns the input the prepare service received. */
3122
+ async function startAndCapturePrepInput(resolver: RealtimeClientSessionResolver, userID: string): Promise<{ ExcludeDirectActions?: boolean }> {
3123
+ currentProvider = makeProvider(() => makeSessionEntity({ UserID: userID }));
3124
+ createSessionMock.mockResolvedValue(makeSessionEntity({ ID: 'session-start', UserID: userID }));
3125
+ prepareClientSessionMock.mockResolvedValue({
3126
+ Success: true,
3127
+ ClientConfig: { Provider: 'openai', Model: 'gpt-realtime', EphemeralToken: 'ek_abc', ExpiresAt: '2026-01-01T00:00:00Z', SessionConfig: {} },
3128
+ });
3129
+ await resolver.StartRealtimeClientSession('target-1', makeCtx());
3130
+ return prepareClientSessionMock.mock.calls[0][0] as { ExcludeDirectActions?: boolean };
3131
+ }
3132
+
3133
+ it('prepares a scope-limited caller\'s session with no direct actions', async () => {
3134
+ const prepInput = await startAndCapturePrepInput(makeAnonResolver(), 'anon-1');
3135
+
3136
+ expect(prepInput.ExcludeDirectActions).toBe(true);
3137
+ });
3138
+
3139
+ it('prepares a caller that is not scope-limited with direct actions', async () => {
3140
+ const prepInput = await startAndCapturePrepInput(makeResolver(), 'user-1');
3141
+
3142
+ expect(prepInput.ExcludeDirectActions).toBe(false);
3143
+ });
3144
+
2974
3145
  it('junction-links delegated artifacts under the SYSTEM user and stamps the hidden anchor with the SESSION owner', async () => {
2975
3146
  const session = makeSessionEntity({ UserID: 'anon-1' });
2976
3147
  const junctions: FakeSession[] = [];
@@ -3003,6 +3174,10 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
3003
3174
  // The junction write (an entity the anon role does NOT hold) runs as the SYSTEM user.
3004
3175
  expect(getEntityObjectSpy()).toHaveBeenCalledWith('MJ: Conversation Detail Artifacts', SYSTEM_USER);
3005
3176
  expect(junctions).toHaveLength(1);
3177
+ // The hidden anchor `Conversation Detail` is written as the CALLER (#4791) — like every
3178
+ // other `MJ: Conversation Details` write, the System user is refused as not the owner.
3179
+ expect(getEntityObjectSpy()).toHaveBeenCalledWith('MJ: Conversation Details', ANON_USER);
3180
+ expect(getEntityObjectSpy()).not.toHaveBeenCalledWith('MJ: Conversation Details', SYSTEM_USER);
3006
3181
  // The hidden anchor is attributed to the SESSION owner, not the elevated principal.
3007
3182
  expect(anchors).toHaveLength(1);
3008
3183
  expect(anchors[0].UserID).toBe('anon-1');
@@ -3102,6 +3277,108 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
3102
3277
  expect((storeRecordingMock.mock.calls[0][0] as { ContextUser: unknown }).ContextUser).toBe(USER);
3103
3278
  expect(getSystemUserMock).not.toHaveBeenCalled();
3104
3279
  });
3280
+
3281
+ /** Provider routing a hidden-tool-turn RelayRealtimeToolTurn call: session, prompt run, detail. */
3282
+ function makeToolTurnProvider(detail: FakeSession): unknown {
3283
+ const session = makeSessionEntity({
3284
+ UserID: 'anon-1',
3285
+ Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }),
3286
+ });
3287
+ const promptRun = makeSessionEntity({ ID: PROMPT_RUN_ID, Messages: null });
3288
+ return {
3289
+ GetEntityObject: vi.fn(async (name: string) => {
3290
+ if (name === 'MJ: AI Prompt Runs') return promptRun;
3291
+ if (name === 'MJ: Conversation Details') return detail;
3292
+ return session;
3293
+ }),
3294
+ ...sessionRunLookups([CO_RUN_ID], PROMPT_RUN_ID),
3295
+ };
3296
+ }
3297
+
3298
+ it('logs the session id, write user id and "no failure detail recorded" when a hidden tool turn save is refused without a LatestResult (#4791)', async () => {
3299
+ errSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
3300
+ const detail = makeSessionEntity({
3301
+ ID: 'detail-1',
3302
+ Save: vi.fn(async () => false),
3303
+ LatestResult: undefined,
3304
+ ResultHistory: [],
3305
+ });
3306
+ currentProvider = makeToolTurnProvider(detail);
3307
+ const resolver = makeAnonResolver();
3308
+
3309
+ await resolver.RelayRealtimeToolTurn('session-1', 'browser_navigate', makeCtx(), '{}');
3310
+
3311
+ const loggedLines = errSpy.mock.calls.map((call) => String(call[0]));
3312
+ const failureLine = loggedLines.find((line) => line.includes('persistDirectActionTurn'));
3313
+ expect(failureLine).toBeDefined();
3314
+ // Exact substrings: 'anon-1' alone is the caller, session.UserID AND detail.UserID at once,
3315
+ // so only the labelled form proves the WRITE user is what got logged.
3316
+ expect(failureLine).toContain('session session-1');
3317
+ expect(failureLine).toContain('write user anon-1');
3318
+ expect(failureLine).toContain('no failure detail recorded (LatestResult null; ResultHistory length 0)');
3319
+ expect(failureLine).toContain('the write was refused before reaching the provider');
3320
+ expect(failureLine).not.toContain('unknown error');
3321
+ });
3322
+
3323
+ it('does not claim WHERE the write was refused when a result was registered with an empty message (#4791)', async () => {
3324
+ errSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
3325
+ const detail = makeSessionEntity({
3326
+ ID: 'detail-1',
3327
+ Save: vi.fn(async () => false),
3328
+ LatestResult: { Success: false, CompleteMessage: ' ' },
3329
+ ResultHistory: [{}],
3330
+ });
3331
+ currentProvider = makeToolTurnProvider(detail);
3332
+ const resolver = makeAnonResolver();
3333
+
3334
+ await resolver.RelayRealtimeToolTurn('session-1', 'browser_navigate', makeCtx(), '{}');
3335
+
3336
+ const loggedLines = errSpy.mock.calls.map((call) => String(call[0]));
3337
+ const failureLine = loggedLines.find((line) => line.includes('persistDirectActionTurn'));
3338
+ expect(failureLine).toBeDefined();
3339
+ expect(failureLine).toContain('no failure detail recorded (LatestResult empty; ResultHistory length 1)');
3340
+ expect(failureLine).not.toContain('refused before reaching the provider');
3341
+ });
3342
+
3343
+ it('never logs a PRIOR SUCCESS entry\'s text as the failure reason when the refusal registered nothing (#4791)', async () => {
3344
+ errSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
3345
+ const detail = makeSessionEntity({
3346
+ ID: 'detail-1',
3347
+ Save: vi.fn(async () => false),
3348
+ LatestResult: { Success: true, CompleteMessage: 'previous save ok' },
3349
+ ResultHistory: [{}, {}],
3350
+ });
3351
+ currentProvider = makeToolTurnProvider(detail);
3352
+ const resolver = makeAnonResolver();
3353
+
3354
+ await resolver.RelayRealtimeToolTurn('session-1', 'browser_navigate', makeCtx(), '{}');
3355
+
3356
+ const loggedLines = errSpy.mock.calls.map((call) => String(call[0]));
3357
+ const failureLine = loggedLines.find((line) => line.includes('persistDirectActionTurn'));
3358
+ expect(failureLine).toBeDefined();
3359
+ expect(failureLine).toContain(
3360
+ 'no failure detail recorded (latest result entry is a prior success; ResultHistory length 2)',
3361
+ );
3362
+ expect(failureLine).not.toContain('previous save ok');
3363
+ });
3364
+
3365
+ it('logs the real CompleteMessage when present (#4791)', async () => {
3366
+ errSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
3367
+ const detail = makeSessionEntity({
3368
+ ID: 'detail-1',
3369
+ Save: vi.fn(async () => false),
3370
+ LatestResult: { CompleteMessage: 'You do not have access to this conversation.' },
3371
+ });
3372
+ currentProvider = makeToolTurnProvider(detail);
3373
+ const resolver = makeAnonResolver();
3374
+
3375
+ await resolver.RelayRealtimeToolTurn('session-1', 'browser_navigate', makeCtx(), '{}');
3376
+
3377
+ const loggedLines = errSpy.mock.calls.map((call) => String(call[0]));
3378
+ const failureLine = loggedLines.find((line) => line.includes('persistDirectActionTurn'));
3379
+ expect(failureLine).toBeDefined();
3380
+ expect(failureLine).toContain('You do not have access to this conversation.');
3381
+ });
3105
3382
  });
3106
3383
 
3107
3384
  describe('RealtimeClientSessionResolver — colleague authorization (allowedAgents)', () => {
@@ -3174,7 +3451,8 @@ describe('RealtimeClientSessionResolver — colleague authorization (allowedAgen
3174
3451
 
3175
3452
  it('excludes every colleague when the caller can run none of them', async () => {
3176
3453
  currentProvider = makeProvider(() => makeMultiTargetSession('user-1'));
3177
- hasPermissionMock.mockResolvedValue(false);
3454
+ // The caller can run the lead target, and none of the colleagues.
3455
+ hasPermissionMock.mockImplementation((...args: unknown[]) => Promise.resolve(args[0] === 'target-1'));
3178
3456
 
3179
3457
  await makeResolver().ExecuteRealtimeSessionTool(
3180
3458
  'session-1', 'call-1', 'invoke-target-agent', '{}', makeCtx(), makePubSub(),
@@ -3183,7 +3461,7 @@ describe('RealtimeClientSessionResolver — colleague authorization (allowedAgen
3183
3461
  expect(dispatchedAgentIDs()).toEqual([]);
3184
3462
  });
3185
3463
 
3186
- it('passes a single-target session through with no permission checks', async () => {
3464
+ it('passes a single-target session through with only the target permission check', async () => {
3187
3465
  currentProvider = makeProvider(() => makeSessionEntity());
3188
3466
  hasPermissionMock.mockResolvedValue(true);
3189
3467
 
@@ -3193,6 +3471,7 @@ describe('RealtimeClientSessionResolver — colleague authorization (allowedAgen
3193
3471
 
3194
3472
  const input = executeRelayedToolMock.mock.calls[0][0] as { AllowedAgents?: unknown };
3195
3473
  expect(input.AllowedAgents).toBeUndefined();
3196
- expect(hasPermissionMock).not.toHaveBeenCalled();
3474
+ expect(hasPermissionMock).toHaveBeenCalledTimes(1);
3475
+ expect((hasPermissionMock.mock.calls[0] as unknown[])[0]).toBe('target-1');
3197
3476
  });
3198
3477
  });