@memberjunction/server 6.2.0-edge.4 → 6.2.0-edge.5

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 (287) hide show
  1. package/README.md +20 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/agentSessions/SessionJanitor.d.ts +91 -30
  4. package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
  5. package/dist/agentSessions/SessionJanitor.js +311 -81
  6. package/dist/agentSessions/SessionJanitor.js.map +1 -1
  7. package/dist/agentSessions/SessionManager.d.ts +5 -1
  8. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  9. package/dist/agentSessions/SessionManager.js +13 -1
  10. package/dist/agentSessions/SessionManager.js.map +1 -1
  11. package/dist/apiRoot.d.ts +32 -0
  12. package/dist/apiRoot.d.ts.map +1 -0
  13. package/dist/apiRoot.js +67 -0
  14. package/dist/apiRoot.js.map +1 -0
  15. package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -1
  16. package/dist/auth/AuthProviderCatalogRouter.js +25 -0
  17. package/dist/auth/AuthProviderCatalogRouter.js.map +1 -1
  18. package/dist/auth/LoginScreenEngine.d.ts +22 -0
  19. package/dist/auth/LoginScreenEngine.d.ts.map +1 -0
  20. package/dist/auth/LoginScreenEngine.js +57 -0
  21. package/dist/auth/LoginScreenEngine.js.map +1 -0
  22. package/dist/auth/index.d.ts +1 -0
  23. package/dist/auth/index.d.ts.map +1 -1
  24. package/dist/auth/index.js +1 -0
  25. package/dist/auth/index.js.map +1 -1
  26. package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
  27. package/dist/auth/magicLink/MagicLinkRouter.js +3 -2
  28. package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
  29. package/dist/config.d.ts +136 -22
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/config.js +43 -4
  32. package/dist/config.js.map +1 -1
  33. package/dist/coreRoutes.d.ts +29 -0
  34. package/dist/coreRoutes.d.ts.map +1 -0
  35. package/dist/coreRoutes.js +142 -0
  36. package/dist/coreRoutes.js.map +1 -0
  37. package/dist/generated/graphql-schemas/__mj.d.ts +369 -3
  38. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  39. package/dist/generated/graphql-schemas/__mj.js +1750 -7
  40. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  41. package/dist/generic/CacheInvalidationResolver.d.ts +43 -8
  42. package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
  43. package/dist/generic/CacheInvalidationResolver.js +59 -19
  44. package/dist/generic/CacheInvalidationResolver.js.map +1 -1
  45. package/dist/generic/PushStatusResolver.js +1 -1
  46. package/dist/generic/PushStatusResolver.js.map +1 -1
  47. package/dist/generic/ResolverBase.d.ts +103 -44
  48. package/dist/generic/ResolverBase.d.ts.map +1 -1
  49. package/dist/generic/ResolverBase.js +264 -305
  50. package/dist/generic/ResolverBase.js.map +1 -1
  51. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  52. package/dist/generic/RunViewResolver.js +12 -0
  53. package/dist/generic/RunViewResolver.js.map +1 -1
  54. package/dist/globalCors.d.ts +33 -0
  55. package/dist/globalCors.d.ts.map +1 -0
  56. package/dist/globalCors.js +58 -0
  57. package/dist/globalCors.js.map +1 -0
  58. package/dist/index.d.ts +11 -0
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +112 -160
  61. package/dist/index.js.map +1 -1
  62. package/dist/multiTenancy/index.d.ts.map +1 -1
  63. package/dist/multiTenancy/index.js +22 -2
  64. package/dist/multiTenancy/index.js.map +1 -1
  65. package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts.map +1 -1
  66. package/dist/realtimeSessions/RealtimeSessionVerificationService.js +2 -1
  67. package/dist/realtimeSessions/RealtimeSessionVerificationService.js.map +1 -1
  68. package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts.map +1 -1
  69. package/dist/realtimeSessions/RealtimeVerifyRouter.js +2 -1
  70. package/dist/realtimeSessions/RealtimeVerifyRouter.js.map +1 -1
  71. package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts +100 -0
  72. package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts.map +1 -0
  73. package/dist/realtimeSessions/RecordingAssemblyScheduler.js +173 -0
  74. package/dist/realtimeSessions/RecordingAssemblyScheduler.js.map +1 -0
  75. package/dist/realtimeSessions/index.d.ts +2 -0
  76. package/dist/realtimeSessions/index.d.ts.map +1 -1
  77. package/dist/realtimeSessions/index.js +2 -0
  78. package/dist/realtimeSessions/index.js.map +1 -1
  79. package/dist/realtimeSessions/ownedRealtimeSession.d.ts +30 -0
  80. package/dist/realtimeSessions/ownedRealtimeSession.d.ts.map +1 -0
  81. package/dist/realtimeSessions/ownedRealtimeSession.js +47 -0
  82. package/dist/realtimeSessions/ownedRealtimeSession.js.map +1 -0
  83. package/dist/realtimeSessions/sessionAccess.d.ts +6 -0
  84. package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -1
  85. package/dist/realtimeSessions/sessionAccess.js +7 -3
  86. package/dist/realtimeSessions/sessionAccess.js.map +1 -1
  87. package/dist/resolvers/ArtifactFileResolver.d.ts +5 -1
  88. package/dist/resolvers/ArtifactFileResolver.d.ts.map +1 -1
  89. package/dist/resolvers/ArtifactFileResolver.js +9 -7
  90. package/dist/resolvers/ArtifactFileResolver.js.map +1 -1
  91. package/dist/resolvers/ConversationRunEventsResolver.d.ts +8 -4
  92. package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -1
  93. package/dist/resolvers/ConversationRunEventsResolver.js +6 -6
  94. package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -1
  95. package/dist/resolvers/DatasetResolver.d.ts +24 -3
  96. package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
  97. package/dist/resolvers/DatasetResolver.js +57 -8
  98. package/dist/resolvers/DatasetResolver.js.map +1 -1
  99. package/dist/resolvers/EntityRecordNameResolver.d.ts +2 -1
  100. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  101. package/dist/resolvers/EntityRecordNameResolver.js +3 -2
  102. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  103. package/dist/resolvers/FetchEntityVectorsResolver.d.ts +18 -3
  104. package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
  105. package/dist/resolvers/FetchEntityVectorsResolver.js +100 -35
  106. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  107. package/dist/resolvers/FileResolver.d.ts +82 -11
  108. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  109. package/dist/resolvers/FileResolver.js +279 -101
  110. package/dist/resolvers/FileResolver.js.map +1 -1
  111. package/dist/resolvers/GetDataContextDataResolver.d.ts +33 -7
  112. package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
  113. package/dist/resolvers/GetDataContextDataResolver.js +117 -38
  114. package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
  115. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  116. package/dist/resolvers/GetDataResolver.js +6 -3
  117. package/dist/resolvers/GetDataResolver.js.map +1 -1
  118. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  119. package/dist/resolvers/IntegrationDiscoveryResolver.js +4 -2
  120. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  121. package/dist/resolvers/QueryResolver.d.ts +2 -0
  122. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  123. package/dist/resolvers/QueryResolver.js +13 -0
  124. package/dist/resolvers/QueryResolver.js.map +1 -1
  125. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +2 -8
  126. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  127. package/dist/resolvers/RealtimeClientSessionResolver.js +10 -26
  128. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  129. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  130. package/dist/resolvers/RunAIAgentResolver.js +35 -6
  131. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  132. package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts +12 -1
  133. package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts.map +1 -1
  134. package/dist/resolvers/SearchKnowledgeStreamResolver.js +15 -2
  135. package/dist/resolvers/SearchKnowledgeStreamResolver.js.map +1 -1
  136. package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
  137. package/dist/resolvers/SqlLoggingConfigResolver.js +2 -3
  138. package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
  139. package/dist/resolvers/TaskGraphFrameResolver.d.ts +2 -1
  140. package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
  141. package/dist/resolvers/TaskGraphFrameResolver.js +28 -1
  142. package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
  143. package/dist/resolvers/TestQuerySQLResolver.d.ts +24 -0
  144. package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
  145. package/dist/resolvers/TestQuerySQLResolver.js +65 -13
  146. package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
  147. package/dist/resolvers/TransactionGroupResolver.d.ts +19 -2
  148. package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
  149. package/dist/resolvers/TransactionGroupResolver.js +102 -24
  150. package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
  151. package/dist/resolvers/agent-run-data-guard.d.ts +31 -0
  152. package/dist/resolvers/agent-run-data-guard.d.ts.map +1 -0
  153. package/dist/resolvers/agent-run-data-guard.js +72 -0
  154. package/dist/resolvers/agent-run-data-guard.js.map +1 -0
  155. package/dist/rest/RealtimeRecordingHandler.d.ts +49 -0
  156. package/dist/rest/RealtimeRecordingHandler.d.ts.map +1 -0
  157. package/dist/rest/RealtimeRecordingHandler.js +383 -0
  158. package/dist/rest/RealtimeRecordingHandler.js.map +1 -0
  159. package/dist/rest/ViewOperationsHandler.d.ts +3 -1
  160. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  161. package/dist/rest/ViewOperationsHandler.js +21 -12
  162. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  163. package/dist/rest/setupRESTEndpoints.d.ts +6 -0
  164. package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
  165. package/dist/rest/setupRESTEndpoints.js +6 -0
  166. package/dist/rest/setupRESTEndpoints.js.map +1 -1
  167. package/dist/serverExtensionReservedRoots.d.ts +12 -10
  168. package/dist/serverExtensionReservedRoots.d.ts.map +1 -1
  169. package/dist/serverExtensionReservedRoots.js +14 -17
  170. package/dist/serverExtensionReservedRoots.js.map +1 -1
  171. package/dist/services/MJStorageBlobStore.d.ts +8 -1
  172. package/dist/services/MJStorageBlobStore.d.ts.map +1 -1
  173. package/dist/services/MJStorageBlobStore.js +25 -19
  174. package/dist/services/MJStorageBlobStore.js.map +1 -1
  175. package/dist/services/StartTaskGraphDispatcher.js +2 -2
  176. package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
  177. package/dist/services/TaskGraphActionRunner.d.ts +5 -3
  178. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
  179. package/dist/services/TaskGraphActionRunner.js +27 -5
  180. package/dist/services/TaskGraphActionRunner.js.map +1 -1
  181. package/dist/services/TaskGraphAgentRunner.d.ts +14 -2
  182. package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
  183. package/dist/services/TaskGraphAgentRunner.js +157 -9
  184. package/dist/services/TaskGraphAgentRunner.js.map +1 -1
  185. package/dist/services/TaskGraphContinuationDeliverer.d.ts +54 -2
  186. package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
  187. package/dist/services/TaskGraphContinuationDeliverer.js +598 -15
  188. package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
  189. package/dist/sharedCache.d.ts.map +1 -1
  190. package/dist/sharedCache.js +3 -0
  191. package/dist/sharedCache.js.map +1 -1
  192. package/package.json +99 -98
  193. package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
  194. package/src/__tests__/EntityRecordNameResolver.rowSecurity.test.ts +152 -0
  195. package/src/__tests__/FetchEntityVectorsResolver.test.ts +163 -15
  196. package/src/__tests__/FileResolver.storageAccess.test.ts +562 -0
  197. package/src/__tests__/GetDataContextDataResolver.sqlItems.test.ts +416 -0
  198. package/src/__tests__/ListOperationsResolver.adhocScreen.test.ts +137 -0
  199. package/src/__tests__/LoginScreenEngine.test.ts +86 -0
  200. package/src/__tests__/MJStorageBlobStore.storageGate.test.ts +63 -0
  201. package/src/__tests__/RecordingAssemblyScheduler.test.ts +178 -0
  202. package/src/__tests__/ResolverBase.adhocReadScope.test.ts +133 -2
  203. package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
  204. package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +127 -0
  205. package/src/__tests__/ResolverBase.clientOldValues.test.ts +209 -6
  206. package/src/__tests__/ResolverBase.filterEscaping.test.ts +5 -2
  207. package/src/__tests__/RunAIAgentResolver.branch.test.ts +1 -0
  208. package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +5 -3
  209. package/src/__tests__/RunAIAgentResolver.runDataGuard.test.ts +258 -0
  210. package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +165 -0
  211. package/src/__tests__/SearchKnowledgeStreamResolver.test.ts +161 -0
  212. package/src/__tests__/SessionJanitor.test.ts +365 -10
  213. package/src/__tests__/SessionManager.test.ts +27 -0
  214. package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +25 -0
  215. package/src/__tests__/TaskGraphAgentRunner.test.ts +266 -0
  216. package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +356 -22
  217. package/src/__tests__/TaskGraphFrameResolver.test.ts +22 -0
  218. package/src/__tests__/TestQuerySQLResolver.test.ts +274 -0
  219. package/src/__tests__/TransactionGroupResolver.refusals.test.ts +98 -1
  220. package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
  221. package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +157 -0
  222. package/src/__tests__/agent-run-data-guard.test.ts +130 -0
  223. package/src/__tests__/apiRootBoot.test.ts +202 -0
  224. package/src/__tests__/cacheInvalidationRecordData.test.ts +64 -0
  225. package/src/__tests__/conversationRunEvents.test.ts +56 -14
  226. package/src/__tests__/globalCors.test.ts +217 -0
  227. package/src/__tests__/multiTenancy.postgresRewrite.test.ts +181 -0
  228. package/src/__tests__/multiTenancy.security.test.ts +51 -0
  229. package/src/__tests__/multiTenancy.test.ts +1 -1
  230. package/src/__tests__/ownedRealtimeSession.test.ts +39 -0
  231. package/src/__tests__/pushStatusResolver.filter.test.ts +7 -0
  232. package/src/__tests__/realtimeRecordingHandler.test.ts +652 -0
  233. package/src/__tests__/serverExtensionHostOverlay.test.ts +66 -0
  234. package/src/__tests__/serverExtensionReservedRoots.test.ts +11 -98
  235. package/src/__tests__/sharedCache.test.ts +5 -3
  236. package/src/agentSessions/SessionJanitor.ts +369 -93
  237. package/src/agentSessions/SessionManager.ts +13 -1
  238. package/src/apiRoot.ts +78 -0
  239. package/src/auth/AuthProviderCatalogRouter.ts +26 -0
  240. package/src/auth/LoginScreenEngine.ts +50 -0
  241. package/src/auth/index.ts +1 -0
  242. package/src/auth/magicLink/MagicLinkRouter.ts +3 -2
  243. package/src/config.ts +46 -5
  244. package/src/coreRoutes.ts +166 -0
  245. package/src/generated/graphql-schemas/__mj.ts +1273 -10
  246. package/src/generic/CacheInvalidationResolver.ts +73 -18
  247. package/src/generic/PushStatusResolver.ts +1 -1
  248. package/src/generic/ResolverBase.ts +304 -325
  249. package/src/generic/RunViewResolver.ts +17 -0
  250. package/src/globalCors.ts +71 -0
  251. package/src/index.ts +116 -174
  252. package/src/multiTenancy/index.ts +24 -2
  253. package/src/realtimeSessions/RealtimeSessionVerificationService.ts +2 -1
  254. package/src/realtimeSessions/RealtimeVerifyRouter.ts +2 -1
  255. package/src/realtimeSessions/RecordingAssemblyScheduler.ts +234 -0
  256. package/src/realtimeSessions/index.ts +2 -0
  257. package/src/realtimeSessions/ownedRealtimeSession.ts +55 -0
  258. package/src/realtimeSessions/sessionAccess.ts +7 -3
  259. package/src/resolvers/ArtifactFileResolver.ts +9 -8
  260. package/src/resolvers/ConversationRunEventsResolver.ts +15 -11
  261. package/src/resolvers/DatasetResolver.ts +70 -10
  262. package/src/resolvers/EntityRecordNameResolver.ts +3 -2
  263. package/src/resolvers/FetchEntityVectorsResolver.ts +121 -39
  264. package/src/resolvers/FileResolver.ts +321 -106
  265. package/src/resolvers/GetDataContextDataResolver.ts +139 -41
  266. package/src/resolvers/GetDataResolver.ts +6 -3
  267. package/src/resolvers/IntegrationDiscoveryResolver.ts +4 -2
  268. package/src/resolvers/QueryResolver.ts +11 -0
  269. package/src/resolvers/RealtimeClientSessionResolver.ts +10 -33
  270. package/src/resolvers/RunAIAgentResolver.ts +39 -4
  271. package/src/resolvers/SearchKnowledgeStreamResolver.ts +19 -3
  272. package/src/resolvers/SqlLoggingConfigResolver.ts +2 -4
  273. package/src/resolvers/TaskGraphFrameResolver.ts +33 -1
  274. package/src/resolvers/TestQuerySQLResolver.ts +68 -13
  275. package/src/resolvers/TransactionGroupResolver.ts +131 -27
  276. package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
  277. package/src/resolvers/agent-run-data-guard.ts +89 -0
  278. package/src/rest/RealtimeRecordingHandler.ts +464 -0
  279. package/src/rest/ViewOperationsHandler.ts +29 -13
  280. package/src/rest/setupRESTEndpoints.ts +6 -0
  281. package/src/serverExtensionReservedRoots.ts +14 -17
  282. package/src/services/MJStorageBlobStore.ts +31 -30
  283. package/src/services/StartTaskGraphDispatcher.ts +2 -2
  284. package/src/services/TaskGraphActionRunner.ts +25 -5
  285. package/src/services/TaskGraphAgentRunner.ts +188 -11
  286. package/src/services/TaskGraphContinuationDeliverer.ts +704 -14
  287. package/src/sharedCache.ts +3 -0
@@ -6,12 +6,20 @@ vi.mock('@memberjunction/ai-engine-base', () => ({
6
6
  }));
7
7
 
8
8
  // Capture the ExtraFilter passed to each sweep page and return controllable rows.
9
- const { runViewMock, logErrorMock, logStatusMock, recoverMock, resolveAccountMock } = vi.hoisted(() => ({
9
+ const { runViewMock, logErrorMock, logStatusMock, recoverMock, resolveAccountMock, assembleMock, ledgerMock } = vi.hoisted(() => ({
10
10
  runViewMock: vi.fn(),
11
11
  logErrorMock: vi.fn(),
12
12
  logStatusMock: vi.fn(),
13
13
  recoverMock: vi.fn(),
14
14
  resolveAccountMock: vi.fn(),
15
+ assembleMock: vi.fn(),
16
+ /** The one fake shard ledger every `new SqlRealtimeRecordingLedger(...)` returns. */
17
+ ledgerMock: {
18
+ ClaimAssembly: vi.fn(),
19
+ PurgeClosedBefore: vi.fn(),
20
+ LoadShardMetadata: vi.fn(),
21
+ StampTerminal: vi.fn(),
22
+ },
15
23
  }));
16
24
  vi.mock('@memberjunction/core', async (importOriginal) => {
17
25
  const actual = await importOriginal<typeof import('@memberjunction/core')>();
@@ -34,6 +42,8 @@ vi.mock('@memberjunction/ai-agents', async (importOriginal) => {
34
42
  ...actual,
35
43
  RecoverRealtimeRecordingFromSegments: recoverMock,
36
44
  ResolveRecordingStorageAccountID: resolveAccountMock,
45
+ AssembleRealtimeRecordingFromLedger: assembleMock,
46
+ SqlRealtimeRecordingLedger: vi.fn(function FakeLedger() { return ledgerMock; }),
37
47
  };
38
48
  });
39
49
 
@@ -44,7 +54,7 @@ import {
44
54
  HostInstanceIdentity,
45
55
  SetHostInstancePort,
46
56
  } from '../agentSessions/HostInstance.js';
47
- import type { UserInfo, IMetadataProvider } from '@memberjunction/core';
57
+ import type { UserInfo, IMetadataProvider, DatabaseProviderBase } from '@memberjunction/core';
48
58
  import type { MJAIAgentSessionEntity } from '@memberjunction/core-entities';
49
59
 
50
60
  /** The port this test process "serves" on, set the way `Serve` sets it, before anything reads the identity. */
@@ -53,6 +63,16 @@ const OUR_PORT = 4000;
53
63
  const OTHER_PORT = 4100;
54
64
  SetHostInstancePort(OUR_PORT);
55
65
 
66
+ /**
67
+ * The janitor builds the shard-ledger view name from the provider's core schema and dialect quoting;
68
+ * fakes carry SQL Server's so filter assertions read naturally.
69
+ */
70
+ const LEDGER_SCHEMA_PROVIDER_FIELDS = {
71
+ MJCoreSchemaName: '__mj',
72
+ QuoteSchemaAndView: (schema: string, view: string) => `[${schema}].[${view}]`,
73
+ };
74
+ const LEDGER_VIEW = '[__mj].[vwAIAgentSessionRecordingShards]';
75
+
56
76
  interface FakeSession {
57
77
  ID: string;
58
78
  Status: 'Active' | 'Idle' | 'Closed';
@@ -73,7 +93,7 @@ function makeUser(): UserInfo {
73
93
  * inside CloseSession uses runViewMock, which we default to an empty channel list.
74
94
  */
75
95
  function makeProvider(store: Map<string, 'Active' | 'Idle' | 'Closed'>): {
76
- provider: IMetadataProvider;
96
+ provider: DatabaseProviderBase;
77
97
  closedIds: string[];
78
98
  closedReasons: Map<string, string | null>;
79
99
  } {
@@ -105,7 +125,8 @@ function makeProvider(store: Map<string, 'Active' | 'Idle' | 'Closed'>): {
105
125
  };
106
126
  return entity as unknown as Awaited<ReturnType<IMetadataProvider['GetEntityObject']>>;
107
127
  }),
108
- } as unknown as IMetadataProvider;
128
+ ...LEDGER_SCHEMA_PROVIDER_FIELDS,
129
+ } as unknown as DatabaseProviderBase;
109
130
  return { provider, closedIds, closedReasons };
110
131
  }
111
132
 
@@ -129,6 +150,11 @@ beforeEach(() => {
129
150
  logStatusMock.mockReset();
130
151
  recoverMock.mockReset();
131
152
  resolveAccountMock.mockReset();
153
+ assembleMock.mockReset();
154
+ for (const method of Object.values(ledgerMock)) method.mockReset();
155
+ // Quiet ledger by default: nothing to purge, every claim lost.
156
+ ledgerMock.PurgeClosedBefore.mockResolvedValue(0);
157
+ ledgerMock.ClaimAssembly.mockResolvedValue(null);
132
158
  sessionPages = [];
133
159
  runViewMock.mockImplementation(async (params: { EntityName: string }) => {
134
160
  if (params.EntityName === 'MJ: AI Agent Session Channels') {
@@ -379,7 +405,7 @@ describe('SessionJanitor.RunRecordingRecoverySweep', () => {
379
405
  let nextSessionNumber = 0;
380
406
 
381
407
  interface RecoveryHarness {
382
- provider: IMetadataProvider;
408
+ provider: DatabaseProviderBase;
383
409
  /** Session id -> RecordingFileID the reload will report (default null). */
384
410
  fileIds: Map<string, string | null>;
385
411
  agentLoads: string[];
@@ -414,7 +440,8 @@ describe('SessionJanitor.RunRecordingRecoverySweep', () => {
414
440
  };
415
441
  return entity;
416
442
  }),
417
- } as unknown as IMetadataProvider;
443
+ ...LEDGER_SCHEMA_PROVIDER_FIELDS,
444
+ } as unknown as DatabaseProviderBase;
418
445
  return { provider, fileIds, agentLoads, failingAgents };
419
446
  }
420
447
 
@@ -425,7 +452,7 @@ describe('SessionJanitor.RunRecordingRecoverySweep', () => {
425
452
  }
426
453
 
427
454
  /** Runs one sweep whose candidate page holds exactly `rows`. */
428
- async function sweepWith(provider: IMetadataProvider, rows: unknown[]): Promise<number> {
455
+ async function sweepWith(provider: DatabaseProviderBase, rows: unknown[]): Promise<number> {
429
456
  sessionPages = [{ Success: true, Results: rows }];
430
457
  return SessionJanitor.Instance.RunRecordingRecoverySweep(provider, makeUser());
431
458
  }
@@ -455,6 +482,14 @@ describe('SessionJanitor.RunRecordingRecoverySweep', () => {
455
482
  expect(filter).toContain(`ClosedAt >= '${new Date(NOW.getTime() - 168 * HOUR).toISOString()}'`);
456
483
  });
457
484
 
485
+ it('leaves sessions on the shard ledger to the ledger sweep', async () => {
486
+ const { provider } = makeRecoveryProvider();
487
+ await sweepWith(provider, []);
488
+
489
+ const filter = recoveryFilter();
490
+ expect(filter).toContain(`RecordingStatus IS NULL AND ID NOT IN (SELECT AgentSessionID FROM ${LEDGER_VIEW})`);
491
+ });
492
+
458
493
  it('honours Configure() overrides for grace and lookback', async () => {
459
494
  SessionJanitor.Instance.Configure({ recordingRecoveryGraceMinutes: 30, recordingRecoveryLookbackHours: 24 });
460
495
  const { provider } = makeRecoveryProvider();
@@ -878,7 +913,7 @@ describe('SessionJanitor.RunRecordingRecoverySweep', () => {
878
913
  }
879
914
  });
880
915
 
881
- it('runs recovery after the close sweeps', async () => {
916
+ it('runs recovery after the close sweeps: ledger assembly first, then legacy segment recovery', async () => {
882
917
  const { provider } = makeRecoveryProvider();
883
918
  await SessionJanitor.Instance.Start(provider, makeUser(), 60_000);
884
919
  runViewMock.mockClear();
@@ -886,10 +921,69 @@ describe('SessionJanitor.RunRecordingRecoverySweep', () => {
886
921
  await vi.advanceTimersByTimeAsync(60_000);
887
922
 
888
923
  const filters = sessionSweepCalls().map((c) => c[0].ExtraFilter);
889
- expect(filters).toHaveLength(3);
924
+ expect(filters).toHaveLength(4);
890
925
  expect(filters[0]).toContain('LastActiveAt <');
891
926
  expect(filters[1]).toContain('maxSessionDeadlineIso');
892
- expect(filters[2]).toContain('RecordingStartedAt');
927
+ expect(filters[2]).toContain("RecordingStatus IN ('Pending','Assembling')");
928
+ expect(filters[3]).toContain('RecordingStartedAt');
929
+ });
930
+
931
+ it('purges ledger shards past the lookback window once per pass', async () => {
932
+ const { provider } = makeRecoveryProvider();
933
+ await SessionJanitor.Instance.Start(provider, makeUser(), 60_000);
934
+ ledgerMock.PurgeClosedBefore.mockResolvedValueOnce(42);
935
+
936
+ await vi.advanceTimersByTimeAsync(60_000);
937
+
938
+ expect(ledgerMock.PurgeClosedBefore).toHaveBeenCalledTimes(1);
939
+ const [cutoff, maxRows] = ledgerMock.PurgeClosedBefore.mock.calls[0] as [Date, number];
940
+ expect(cutoff.toISOString()).toBe(new Date(Date.now() - 168 * HOUR).toISOString()); // now = the tick
941
+ expect(maxRows).toBe(500);
942
+ expect(logStatusMock).toHaveBeenCalledWith(expect.stringMatching(/purged 42 recording shard/i));
943
+ });
944
+
945
+ it('a Stop() during the pass ends it before the next step', async () => {
946
+ const { provider } = makeRecoveryProvider();
947
+ await SessionJanitor.Instance.Start(provider, makeUser(), 60_000);
948
+ runViewMock.mockClear();
949
+ let releasePurge: () => void = () => undefined;
950
+ ledgerMock.PurgeClosedBefore.mockImplementationOnce(
951
+ () => new Promise<number>((resolve) => { releasePurge = () => resolve(0); }),
952
+ );
953
+
954
+ await vi.advanceTimersByTimeAsync(60_000); // the pass parks in the purge
955
+ SessionJanitor.Instance.Stop();
956
+ releasePurge();
957
+ await vi.advanceTimersByTimeAsync(0);
958
+
959
+ expect(sessionSweepCalls().filter((c) => c[0].ExtraFilter.includes('RecordingStatus'))).toHaveLength(0);
960
+ });
961
+
962
+ it('a failed purge is logged with context and the sweeps still run', async () => {
963
+ const { provider } = makeRecoveryProvider();
964
+ await SessionJanitor.Instance.Start(provider, makeUser(), 60_000);
965
+ runViewMock.mockClear();
966
+ ledgerMock.PurgeClosedBefore.mockRejectedValueOnce(new Error('purge deadlock'));
967
+
968
+ await vi.advanceTimersByTimeAsync(60_000);
969
+
970
+ expect(logErrorMock).toHaveBeenCalledWith(expect.stringMatching(/purge.*purge deadlock/i));
971
+ expect(sessionSweepCalls().some((c) => c[0].ExtraFilter.includes('RecordingStartedAt'))).toBe(true);
972
+ });
973
+
974
+ it('a failed ledger sweep is logged and legacy recovery still runs', async () => {
975
+ const { provider } = makeRecoveryProvider();
976
+ await SessionJanitor.Instance.Start(provider, makeUser(), 60_000);
977
+ runViewMock.mockClear();
978
+ runViewMock.mockImplementation(async (params: { ExtraFilter?: string }) => {
979
+ if (params.ExtraFilter?.includes("RecordingStatus IN ('Pending','Assembling')")) throw new Error('ledger query blew up');
980
+ return { Success: true, Results: [] };
981
+ });
982
+
983
+ await vi.advanceTimersByTimeAsync(60_000);
984
+
985
+ expect(logErrorMock).toHaveBeenCalledWith(expect.stringMatching(/ledger.*ledger query blew up/i));
986
+ expect(sessionSweepCalls().some((c) => c[0].ExtraFilter.includes('RecordingStartedAt'))).toBe(true);
893
987
  });
894
988
 
895
989
  it('a slow recovery does not block the close sweeps on later ticks and is not started twice', async () => {
@@ -1018,6 +1112,267 @@ describe('SessionJanitor.RunRecordingRecoverySweep', () => {
1018
1112
  });
1019
1113
  });
1020
1114
 
1115
+ describe('SessionJanitor.RunRecordingLedgerSweep', () => {
1116
+ const NOW = new Date('2026-10-12T12:00:00.000Z');
1117
+ const MINUTE = 60_000;
1118
+ const HOUR = 60 * MINUTE;
1119
+ const STALE_CUTOFF = new Date(NOW.getTime() - 10 * MINUTE); // recordingRecoveryAttemptTimeoutMinutes
1120
+ let nextSessionNumber = 0;
1121
+
1122
+ type LedgerCandidate = {
1123
+ ID: string;
1124
+ AgentID: string;
1125
+ RecordingStatus: 'Pending' | 'Assembling' | null;
1126
+ RecordingExpectedShards: number | null;
1127
+ RecordingCapturedSamples: number | null;
1128
+ __mj_UpdatedAt: Date;
1129
+ };
1130
+
1131
+ /** Session ids are unique per test: the janitor is a singleton whose backoff outlives a test. */
1132
+ function ledgerCandidate(overrides: Partial<LedgerCandidate> = {}): LedgerCandidate {
1133
+ return {
1134
+ ID: `ledger-session-${nextSessionNumber++}`,
1135
+ AgentID: 'agent-1',
1136
+ RecordingStatus: 'Pending',
1137
+ RecordingExpectedShards: null,
1138
+ RecordingCapturedSamples: null,
1139
+ __mj_UpdatedAt: new Date(NOW.getTime() - HOUR),
1140
+ ...overrides,
1141
+ };
1142
+ }
1143
+
1144
+ function ledgerProvider(): DatabaseProviderBase {
1145
+ return { GetEntityObject: vi.fn(), ...LEDGER_SCHEMA_PROVIDER_FIELDS } as unknown as DatabaseProviderBase;
1146
+ }
1147
+
1148
+ /** Runs one ledger sweep whose candidate page holds exactly `rows`. */
1149
+ async function ledgerSweepWith(rows: LedgerCandidate[], provider = ledgerProvider()): Promise<number> {
1150
+ sessionPages = [{ Success: true, Results: rows }];
1151
+ return SessionJanitor.Instance.RunRecordingLedgerSweep(provider, makeUser());
1152
+ }
1153
+
1154
+ function ledgerFilter(): string {
1155
+ const call = sessionSweepCalls().find((c) => c[0].ExtraFilter.includes('RecordingStatus'));
1156
+ if (!call) throw new Error('ledger sweep never queried sessions');
1157
+ return call[0].ExtraFilter;
1158
+ }
1159
+
1160
+ function claimedIDs(): string[] {
1161
+ return ledgerMock.ClaimAssembly.mock.calls.map((c) => c[0] as string);
1162
+ }
1163
+
1164
+ beforeEach(() => {
1165
+ vi.useFakeTimers();
1166
+ vi.setSystemTime(NOW);
1167
+ SessionJanitor.Instance.Configure({ recordingRecoveryGraceMinutes: 10, recordingRecoveryLookbackHours: 168, recordingRecoveryAttemptTimeoutMinutes: 10 });
1168
+ ledgerMock.ClaimAssembly.mockResolvedValue(7);
1169
+ assembleMock.mockResolvedValue('Partial');
1170
+ });
1171
+
1172
+ afterEach(() => {
1173
+ SessionJanitor.Instance.Stop();
1174
+ vi.useRealTimers();
1175
+ });
1176
+
1177
+ it('queries closed sessions past the grace and inside the lookback that are Pending, Assembling, or lost their status with shards on the ledger', async () => {
1178
+ await ledgerSweepWith([]);
1179
+
1180
+ const filter = ledgerFilter();
1181
+ expect(filter).toContain("Status = 'Closed'");
1182
+ expect(filter).toContain(`ClosedAt < '${new Date(NOW.getTime() - 10 * MINUTE).toISOString()}'`);
1183
+ expect(filter).toContain(`ClosedAt >= '${new Date(NOW.getTime() - 168 * HOUR).toISOString()}'`);
1184
+ expect(filter).toContain("RecordingStatus IN ('Pending','Assembling')");
1185
+ expect(filter).toContain(`RecordingStatus IS NULL AND ID IN (SELECT AgentSessionID FROM ${LEDGER_VIEW})`);
1186
+ expect(filter).not.toContain('RecordingStartedAt');
1187
+ });
1188
+
1189
+ it('assembles a closed Pending session with no terminal signal as Partial: stale-aware claim, then a forced assembly', async () => {
1190
+ const session = ledgerCandidate();
1191
+
1192
+ expect(await ledgerSweepWith([session])).toBe(1);
1193
+
1194
+ expect(ledgerMock.ClaimAssembly).toHaveBeenCalledWith(session.ID, STALE_CUTOFF);
1195
+ expect(assembleMock).toHaveBeenCalledTimes(1);
1196
+ expect(assembleMock).toHaveBeenCalledWith(expect.objectContaining({
1197
+ SessionID: session.ID, Version: 7, Ledger: ledgerMock, ForcePartial: true,
1198
+ }));
1199
+ expect(logStatusMock).toHaveBeenCalledWith(expect.stringMatching(new RegExp(`${session.ID}.*Partial`)));
1200
+ });
1201
+
1202
+ it('re-claims an Assembling session whose assembler went quiet past the attempt timeout, but not a fresh one', async () => {
1203
+ const stale = ledgerCandidate({ RecordingStatus: 'Assembling', __mj_UpdatedAt: new Date(STALE_CUTOFF.getTime() - 1) });
1204
+ const fresh = ledgerCandidate({ RecordingStatus: 'Assembling', __mj_UpdatedAt: new Date(NOW.getTime() - MINUTE) });
1205
+
1206
+ expect(await ledgerSweepWith([fresh, stale])).toBe(1);
1207
+
1208
+ expect(claimedIDs()).toEqual([stale.ID]);
1209
+ expect(ledgerMock.ClaimAssembly).toHaveBeenCalledWith(stale.ID, STALE_CUTOFF);
1210
+ });
1211
+
1212
+ it('skips a session whose claim another assembler won, without backing it off', async () => {
1213
+ const session = ledgerCandidate();
1214
+ ledgerMock.ClaimAssembly.mockResolvedValueOnce(null);
1215
+
1216
+ expect(await ledgerSweepWith([session])).toBe(0);
1217
+ expect(assembleMock).not.toHaveBeenCalled();
1218
+ expect(logErrorMock).not.toHaveBeenCalled();
1219
+
1220
+ await ledgerSweepWith([session]); // not backed off: the next sweep tries the claim again
1221
+ expect(claimedIDs()).toEqual([session.ID, session.ID]);
1222
+ });
1223
+
1224
+ it('backs off a session whose assembly hit a transient error (Retry), and retries after the backoff', async () => {
1225
+ const session = ledgerCandidate();
1226
+ assembleMock.mockResolvedValueOnce('Retry');
1227
+
1228
+ expect(await ledgerSweepWith([session])).toBe(0);
1229
+ await ledgerSweepWith([session]);
1230
+ expect(claimedIDs()).toEqual([session.ID]);
1231
+
1232
+ vi.setSystemTime(NOW.getTime() + 11 * MINUTE); // first backoff is 2^1 * 5 min
1233
+ await ledgerSweepWith([session]);
1234
+ expect(claimedIDs()).toEqual([session.ID, session.ID]);
1235
+ });
1236
+
1237
+ it('gives up on a session that keeps hitting Retry after 8 attempts, with one LogError', async () => {
1238
+ const session = ledgerCandidate();
1239
+ assembleMock.mockResolvedValue('Retry');
1240
+
1241
+ for (let i = 0; i < 12; i++) {
1242
+ await ledgerSweepWith([session]);
1243
+ vi.setSystemTime(Date.now() + 7 * HOUR); // past the longest (6 h) backoff
1244
+ }
1245
+
1246
+ expect(assembleMock).toHaveBeenCalledTimes(8);
1247
+ expect(logErrorMock.mock.calls.filter((c) => /Giving up/.test(String(c[0])) && String(c[0]).includes(session.ID))).toHaveLength(1);
1248
+ });
1249
+
1250
+ it('LogErrors a Failed assembly with the session id and counts it as a failure', async () => {
1251
+ const session = ledgerCandidate();
1252
+ assembleMock.mockResolvedValueOnce('Failed');
1253
+
1254
+ expect(await ledgerSweepWith([session])).toBe(0);
1255
+ expect(logErrorMock).toHaveBeenCalledWith(expect.stringMatching(new RegExp(`${session.ID}.*Failed`)));
1256
+ await ledgerSweepWith([session]);
1257
+ expect(claimedIDs()).toEqual([session.ID]); // backed off
1258
+ });
1259
+
1260
+ it('logs a Superseded assembly without treating it as a failure', async () => {
1261
+ const session = ledgerCandidate();
1262
+ assembleMock.mockResolvedValueOnce('Superseded');
1263
+
1264
+ expect(await ledgerSweepWith([session])).toBe(0);
1265
+ expect(logErrorMock).not.toHaveBeenCalled();
1266
+ expect(logStatusMock).toHaveBeenCalledWith(expect.stringMatching(new RegExp(`${session.ID}.*Superseded`)));
1267
+ await ledgerSweepWith([session]);
1268
+ expect(claimedIDs()).toEqual([session.ID, session.ID]);
1269
+ });
1270
+
1271
+ it('LogErrors an unexpected Deferred from a forced assembly and backs off', async () => {
1272
+ const session = ledgerCandidate();
1273
+ assembleMock.mockResolvedValueOnce('Deferred');
1274
+
1275
+ await ledgerSweepWith([session]);
1276
+ expect(logErrorMock).toHaveBeenCalledWith(expect.stringMatching(new RegExp(`${session.ID}.*Deferred`)));
1277
+ await ledgerSweepWith([session]);
1278
+ expect(claimedIDs()).toEqual([session.ID]);
1279
+ });
1280
+
1281
+ it('logs and backs off a claim that throws, then moves on to the next candidate', async () => {
1282
+ const [bad, good] = [ledgerCandidate(), ledgerCandidate()];
1283
+ ledgerMock.ClaimAssembly.mockRejectedValueOnce(new Error('claim deadlock'));
1284
+
1285
+ expect(await ledgerSweepWith([bad, good])).toBe(1);
1286
+ expect(logErrorMock).toHaveBeenCalledWith(expect.stringMatching(new RegExp(`${bad.ID}.*claim deadlock`)));
1287
+ });
1288
+
1289
+ it('restores the status a racing session save wiped with a (0, 0) stamp, never counts invented from the ledger, before claiming', async () => {
1290
+ // No terminal counts survive: stamping 0/0 sets NULL -> Pending, and the assembly (shard count != 0)
1291
+ // is Partial and keeps the shards, instead of a made-up terminal labelling it Complete.
1292
+ const session = ledgerCandidate({ RecordingStatus: null });
1293
+ ledgerMock.StampTerminal.mockResolvedValueOnce({ Outcome: 'Stamped', RecordingStatus: 'Pending' });
1294
+
1295
+ expect(await ledgerSweepWith([session])).toBe(1);
1296
+
1297
+ expect(ledgerMock.StampTerminal).toHaveBeenCalledWith(session.ID, 0, 0);
1298
+ expect(ledgerMock.LoadShardMetadata).not.toHaveBeenCalled();
1299
+ expect(ledgerMock.StampTerminal.mock.invocationCallOrder[0]).toBeLessThan(ledgerMock.ClaimAssembly.mock.invocationCallOrder[0]);
1300
+ expect(assembleMock).toHaveBeenCalledWith(expect.objectContaining({ SessionID: session.ID, ForcePartial: true }));
1301
+ });
1302
+
1303
+ it('restores a lost status with the counts the session still stores', async () => {
1304
+ const session = ledgerCandidate({ RecordingStatus: null, RecordingExpectedShards: 9, RecordingCapturedSamples: 2_000_000 });
1305
+ ledgerMock.StampTerminal.mockResolvedValueOnce({ Outcome: 'Stamped', RecordingStatus: 'Pending' });
1306
+
1307
+ await ledgerSweepWith([session]);
1308
+
1309
+ expect(ledgerMock.StampTerminal).toHaveBeenCalledWith(session.ID, 9, 2_000_000);
1310
+ });
1311
+
1312
+ it('stamps a BIGINT capture count that the candidate page delivers as a string as a number', async () => {
1313
+ // A RunView entity page keeps the driver's value for a BIGINT column: the SQL Server driver
1314
+ // returns it as a string, and the ledger's integer guard refused it (#5294 gauntlet, row S13 J3).
1315
+ const session = ledgerCandidate({ RecordingStatus: null, RecordingExpectedShards: 5, RecordingCapturedSamples: '1137600' as unknown as number });
1316
+ ledgerMock.StampTerminal.mockResolvedValueOnce({ Outcome: 'Stamped', RecordingStatus: 'Pending' });
1317
+
1318
+ await ledgerSweepWith([session]);
1319
+
1320
+ expect(ledgerMock.StampTerminal).toHaveBeenCalledWith(session.ID, 5, 1_137_600);
1321
+ });
1322
+
1323
+ it('LogErrors and backs off a lost status the stamp could not restore', async () => {
1324
+ const session = ledgerCandidate({ RecordingStatus: null });
1325
+ ledgerMock.StampTerminal.mockResolvedValueOnce({ Outcome: 'Ignored', RecordingStatus: null });
1326
+
1327
+ expect(await ledgerSweepWith([session])).toBe(0);
1328
+ expect(ledgerMock.ClaimAssembly).not.toHaveBeenCalled();
1329
+ expect(logErrorMock).toHaveBeenCalledWith(expect.stringContaining(session.ID));
1330
+ await ledgerSweepWith([session]);
1331
+ expect(ledgerMock.StampTerminal).toHaveBeenCalledTimes(1); // backed off
1332
+ });
1333
+
1334
+ it('counts failed status restores toward the per-sweep cap', async () => {
1335
+ const sessions = Array.from({ length: 7 }, () => ledgerCandidate({ RecordingStatus: null }));
1336
+ ledgerMock.StampTerminal.mockRejectedValue(new Error('stamp deadlock'));
1337
+
1338
+ await ledgerSweepWith(sessions);
1339
+
1340
+ expect(ledgerMock.StampTerminal).toHaveBeenCalledTimes(5);
1341
+ });
1342
+
1343
+ it('attempts at most 5 assemblies per sweep', async () => {
1344
+ const sessions = Array.from({ length: 7 }, () => ledgerCandidate());
1345
+
1346
+ expect(await ledgerSweepWith(sessions)).toBe(5);
1347
+ expect(assembleMock).toHaveBeenCalledTimes(5);
1348
+ });
1349
+
1350
+ it('abandons an assembly that outlives the attempt timeout, backs it off, and continues with the next candidate', async () => {
1351
+ const [hung, next] = [ledgerCandidate(), ledgerCandidate()];
1352
+ let release: () => void = () => undefined;
1353
+ assembleMock.mockImplementation(async (input: { SessionID: string }) => {
1354
+ if (input.SessionID === hung.ID) {
1355
+ await new Promise<void>((resolve) => { release = resolve; });
1356
+ return 'Retry';
1357
+ }
1358
+ return 'Complete';
1359
+ });
1360
+ try {
1361
+ const sweep = ledgerSweepWith([hung, next]);
1362
+ await vi.advanceTimersByTimeAsync(10 * MINUTE);
1363
+ expect(await sweep).toBe(1);
1364
+ expect(logErrorMock).toHaveBeenCalledWith(expect.stringMatching(new RegExp(`${hung.ID}.*did not finish within 10 minutes`)));
1365
+
1366
+ vi.setSystemTime(Date.now() + 11 * MINUTE); // past the backoff, but the abandoned call is still running
1367
+ await ledgerSweepWith([hung]);
1368
+ expect(assembleMock.mock.calls.map((c) => (c[0] as { SessionID: string }).SessionID)).toEqual([hung.ID, next.ID]);
1369
+ } finally {
1370
+ release();
1371
+ await vi.advanceTimersByTimeAsync(0);
1372
+ }
1373
+ });
1374
+ });
1375
+
1021
1376
  describe('SessionJanitor.Configure', () => {
1022
1377
  const DEFAULTS: SessionJanitorConfig = {
1023
1378
  closeThresholdMinutes: 15, sweepIntervalMs: 60_000, recordingRecoveryGraceMinutes: 10, recordingRecoveryLookbackHours: 168,
@@ -29,6 +29,16 @@ vi.mock('@memberjunction/ai-agents', () => ({
29
29
  },
30
30
  }));
31
31
 
32
+ // --- Mock the recording assembly scheduler so a close's assembly request is observable (#5294) ---
33
+ const scheduleAssemblyMock = vi.fn();
34
+ vi.mock('../realtimeSessions/RecordingAssemblyScheduler.js', () => ({
35
+ RecordingAssemblyScheduler: {
36
+ get Instance() {
37
+ return { ScheduleAssembly: scheduleAssemblyMock };
38
+ },
39
+ },
40
+ }));
41
+
32
42
  // --- Mock RunView.FromMetadataProvider so channel sweeps and run-id lookups are controllable,
33
43
  // while leaving the rest of @memberjunction/core intact. RunViews runs each view through runViewMock. ---
34
44
  const runViewMock = vi.fn();
@@ -135,6 +145,7 @@ beforeEach(() => {
135
145
  hostSessionClosedMock.mockResolvedValue(undefined);
136
146
  runViewMock.mockReset();
137
147
  runViewMock.mockResolvedValue({ Success: true, Results: [] });
148
+ scheduleAssemblyMock.mockReset();
138
149
  getSystemUserMock.mockReset();
139
150
  getSystemUserMock.mockReturnValue(undefined);
140
151
  });
@@ -566,6 +577,22 @@ describe('SessionManager.Heartbeat', () => {
566
577
  });
567
578
  });
568
579
 
580
+ describe('SessionManager.CloseSession — recording assembly (#5294)', () => {
581
+ it('CloseSession of a recorded session asks for its assembly now (the hidden-tab deferral ends with the call)', async () => {
582
+ const session = makeSessionEntity({ ID: 'session-1', Status: 'Active', RecordingStartedAt: new Date(0) });
583
+ const { provider } = makeProvider(() => session);
584
+ await new SessionManager().CloseSession('session-1', makeUser(), provider);
585
+ expect(scheduleAssemblyMock).toHaveBeenCalledWith('session-1', 0, false);
586
+ });
587
+
588
+ it('CloseSession of a session that was not recorded asks for no assembly', async () => {
589
+ const session = makeSessionEntity({ ID: 'session-1', Status: 'Active', RecordingStartedAt: null });
590
+ const { provider } = makeProvider(() => session);
591
+ await new SessionManager().CloseSession('session-1', makeUser(), provider);
592
+ expect(scheduleAssemblyMock).not.toHaveBeenCalled();
593
+ });
594
+ });
595
+
569
596
  describe('SessionManager.Heartbeat — the session\'s co-agent run stays alive', () => {
570
597
  beforeEach(() => {
571
598
  keepCoAgentRunAliveMock.mockClear();
@@ -169,4 +169,29 @@ describe('TaskGraphActionRunner: outputs must be storable', () => {
169
169
 
170
170
  expect(result).toMatchObject({ Success: true, Output: { AgentRunID: 'RUN-1' }, ActionLogID: 'LOG-2' });
171
171
  });
172
+
173
+ it('merges structured JSON Message properties into Output and aliases handle/Handle', async () => {
174
+ vi.mocked(ActionEngineServer.Instance.RunAction).mockResolvedValue({
175
+ Success: true,
176
+ Message: JSON.stringify({
177
+ message: 'Created new PDF document.',
178
+ handle: 'doc-handle-99',
179
+ documentType: 'pdf',
180
+ fileName: 'Sales_Demo_Report.pdf',
181
+ }),
182
+ Params: [],
183
+ LogEntry: { ID: 'LOG-3' },
184
+ } as never);
185
+
186
+ const result = await new TaskGraphActionRunner().RunActionForTask(baseRunParams({ DocumentType: 'pdf' }));
187
+
188
+ expect(result.Success).toBe(true);
189
+ expect(result.Output).toMatchObject({
190
+ handle: 'doc-handle-99',
191
+ Handle: 'doc-handle-99',
192
+ documentType: 'pdf',
193
+ fileName: 'Sales_Demo_Report.pdf',
194
+ });
195
+ });
172
196
  });
197
+