@memberjunction/server 6.2.0-edge.4 → 6.2.0-edge.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 (560) hide show
  1. package/README.md +47 -1
  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 +17 -3
  8. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  9. package/dist/agentSessions/SessionManager.js +44 -10
  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 +27 -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 +52 -10
  25. package/dist/auth/index.js.map +1 -1
  26. package/dist/auth/initializeProviders.d.ts +10 -0
  27. package/dist/auth/initializeProviders.d.ts.map +1 -1
  28. package/dist/auth/initializeProviders.js +37 -1
  29. package/dist/auth/initializeProviders.js.map +1 -1
  30. package/dist/auth/localDevAuth.d.ts +79 -0
  31. package/dist/auth/localDevAuth.d.ts.map +1 -0
  32. package/dist/auth/localDevAuth.js +439 -0
  33. package/dist/auth/localDevAuth.js.map +1 -0
  34. package/dist/auth/magicLink/MagicLinkKeys.d.ts +2 -2
  35. package/dist/auth/magicLink/MagicLinkKeys.d.ts.map +1 -1
  36. package/dist/auth/magicLink/MagicLinkKeys.js +1 -1
  37. package/dist/auth/magicLink/MagicLinkKeys.js.map +1 -1
  38. package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
  39. package/dist/auth/magicLink/MagicLinkRouter.js +3 -2
  40. package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
  41. package/dist/auth/magicLink/types.d.ts +12 -0
  42. package/dist/auth/magicLink/types.d.ts.map +1 -1
  43. package/dist/auth/sessionUserClone.d.ts +1 -1
  44. package/dist/auth/sessionUserClone.d.ts.map +1 -1
  45. package/dist/auth/sessionUserClone.js +3 -2
  46. package/dist/auth/sessionUserClone.js.map +1 -1
  47. package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.d.ts +292 -0
  48. package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.d.ts.map +1 -0
  49. package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.js +442 -0
  50. package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.js.map +1 -0
  51. package/dist/broadcast/index.d.ts +6 -0
  52. package/dist/broadcast/index.d.ts.map +1 -0
  53. package/dist/broadcast/index.js +6 -0
  54. package/dist/broadcast/index.js.map +1 -0
  55. package/dist/broadcast/providers/AWSIoTCoreBroadcastProvider.d.ts +43 -0
  56. package/dist/broadcast/providers/AWSIoTCoreBroadcastProvider.d.ts.map +1 -0
  57. package/dist/broadcast/providers/AWSIoTCoreBroadcastProvider.js +144 -0
  58. package/dist/broadcast/providers/AWSIoTCoreBroadcastProvider.js.map +1 -0
  59. package/dist/broadcast/providers/AzureWebPubSubBroadcastProvider.d.ts +46 -0
  60. package/dist/broadcast/providers/AzureWebPubSubBroadcastProvider.d.ts.map +1 -0
  61. package/dist/broadcast/providers/AzureWebPubSubBroadcastProvider.js +149 -0
  62. package/dist/broadcast/providers/AzureWebPubSubBroadcastProvider.js.map +1 -0
  63. package/dist/broadcast/providers/GCPPubSubBroadcastProvider.d.ts +49 -0
  64. package/dist/broadcast/providers/GCPPubSubBroadcastProvider.d.ts.map +1 -0
  65. package/dist/broadcast/providers/GCPPubSubBroadcastProvider.js +131 -0
  66. package/dist/broadcast/providers/GCPPubSubBroadcastProvider.js.map +1 -0
  67. package/dist/broadcast/providers/SequencedRoleBucketBroadcastProvider.d.ts +24 -0
  68. package/dist/broadcast/providers/SequencedRoleBucketBroadcastProvider.d.ts.map +1 -0
  69. package/dist/broadcast/providers/SequencedRoleBucketBroadcastProvider.js +51 -0
  70. package/dist/broadcast/providers/SequencedRoleBucketBroadcastProvider.js.map +1 -0
  71. package/dist/cache.d.ts +3 -2
  72. package/dist/cache.d.ts.map +1 -1
  73. package/dist/cache.js.map +1 -1
  74. package/dist/config.d.ts +632 -22
  75. package/dist/config.d.ts.map +1 -1
  76. package/dist/config.js +104 -4
  77. package/dist/config.js.map +1 -1
  78. package/dist/context.d.ts +3 -1
  79. package/dist/context.d.ts.map +1 -1
  80. package/dist/context.js +29 -8
  81. package/dist/context.js.map +1 -1
  82. package/dist/coreRoutes.d.ts +29 -0
  83. package/dist/coreRoutes.d.ts.map +1 -0
  84. package/dist/coreRoutes.js +156 -0
  85. package/dist/coreRoutes.js.map +1 -0
  86. package/dist/generated/graphql-schemas/__mj.d.ts +1016 -29
  87. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  88. package/dist/generated/graphql-schemas/__mj.js +5125 -434
  89. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  90. package/dist/generic/CacheInvalidationResolver.d.ts +85 -30
  91. package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
  92. package/dist/generic/CacheInvalidationResolver.js +175 -60
  93. package/dist/generic/CacheInvalidationResolver.js.map +1 -1
  94. package/dist/generic/OrphanedConversationDetailReconciler.d.ts.map +1 -1
  95. package/dist/generic/OrphanedConversationDetailReconciler.js +2 -0
  96. package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -1
  97. package/dist/generic/PubSubManager.d.ts +10 -0
  98. package/dist/generic/PubSubManager.d.ts.map +1 -1
  99. package/dist/generic/PubSubManager.js +44 -1
  100. package/dist/generic/PubSubManager.js.map +1 -1
  101. package/dist/generic/PushStatusResolver.d.ts +39 -6
  102. package/dist/generic/PushStatusResolver.d.ts.map +1 -1
  103. package/dist/generic/PushStatusResolver.js +94 -12
  104. package/dist/generic/PushStatusResolver.js.map +1 -1
  105. package/dist/generic/ResolverBase.d.ts +168 -69
  106. package/dist/generic/ResolverBase.d.ts.map +1 -1
  107. package/dist/generic/ResolverBase.js +464 -405
  108. package/dist/generic/ResolverBase.js.map +1 -1
  109. package/dist/generic/RunViewResolver.d.ts +11 -3
  110. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  111. package/dist/generic/RunViewResolver.js +91 -13
  112. package/dist/generic/RunViewResolver.js.map +1 -1
  113. package/dist/generic/actionOutputFieldSecurity.d.ts +26 -0
  114. package/dist/generic/actionOutputFieldSecurity.d.ts.map +1 -0
  115. package/dist/generic/actionOutputFieldSecurity.js +47 -0
  116. package/dist/generic/actionOutputFieldSecurity.js.map +1 -0
  117. package/dist/generic/fieldSecurityTransport.d.ts +30 -0
  118. package/dist/generic/fieldSecurityTransport.d.ts.map +1 -0
  119. package/dist/generic/fieldSecurityTransport.js +30 -0
  120. package/dist/generic/fieldSecurityTransport.js.map +1 -0
  121. package/dist/globalCors.d.ts +33 -0
  122. package/dist/globalCors.d.ts.map +1 -0
  123. package/dist/globalCors.js +58 -0
  124. package/dist/globalCors.js.map +1 -0
  125. package/dist/index.d.ts +14 -0
  126. package/dist/index.d.ts.map +1 -1
  127. package/dist/index.js +230 -222
  128. package/dist/index.js.map +1 -1
  129. package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
  130. package/dist/integration/CustomColumnPromoter.js +6 -1
  131. package/dist/integration/CustomColumnPromoter.js.map +1 -1
  132. package/dist/logging/variablesLoggingMiddleware.d.ts +12 -0
  133. package/dist/logging/variablesLoggingMiddleware.d.ts.map +1 -1
  134. package/dist/logging/variablesLoggingMiddleware.js +50 -1
  135. package/dist/logging/variablesLoggingMiddleware.js.map +1 -1
  136. package/dist/multiTenancy/index.d.ts.map +1 -1
  137. package/dist/multiTenancy/index.js +22 -2
  138. package/dist/multiTenancy/index.js.map +1 -1
  139. package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts.map +1 -1
  140. package/dist/realtimeSessions/RealtimeSessionVerificationService.js +2 -1
  141. package/dist/realtimeSessions/RealtimeSessionVerificationService.js.map +1 -1
  142. package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts.map +1 -1
  143. package/dist/realtimeSessions/RealtimeVerifyRouter.js +2 -1
  144. package/dist/realtimeSessions/RealtimeVerifyRouter.js.map +1 -1
  145. package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts +100 -0
  146. package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts.map +1 -0
  147. package/dist/realtimeSessions/RecordingAssemblyScheduler.js +173 -0
  148. package/dist/realtimeSessions/RecordingAssemblyScheduler.js.map +1 -0
  149. package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts.map +1 -1
  150. package/dist/realtimeSessions/SessionVerificationPortsBase.js +1 -0
  151. package/dist/realtimeSessions/SessionVerificationPortsBase.js.map +1 -1
  152. package/dist/realtimeSessions/index.d.ts +2 -0
  153. package/dist/realtimeSessions/index.d.ts.map +1 -1
  154. package/dist/realtimeSessions/index.js +2 -0
  155. package/dist/realtimeSessions/index.js.map +1 -1
  156. package/dist/realtimeSessions/ownedRealtimeSession.d.ts +32 -0
  157. package/dist/realtimeSessions/ownedRealtimeSession.d.ts.map +1 -0
  158. package/dist/realtimeSessions/ownedRealtimeSession.js +56 -0
  159. package/dist/realtimeSessions/ownedRealtimeSession.js.map +1 -0
  160. package/dist/realtimeSessions/sessionAccess.d.ts +91 -8
  161. package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -1
  162. package/dist/realtimeSessions/sessionAccess.js +127 -20
  163. package/dist/realtimeSessions/sessionAccess.js.map +1 -1
  164. package/dist/realtimeSessions/sessionConfigStamps.js +1 -1
  165. package/dist/realtimeSessions/sessionConfigStamps.js.map +1 -1
  166. package/dist/realtimeSessions/subscriptionAuthorization.d.ts.map +1 -1
  167. package/dist/realtimeSessions/subscriptionAuthorization.js +5 -2
  168. package/dist/realtimeSessions/subscriptionAuthorization.js.map +1 -1
  169. package/dist/realtimeSessions/verificationWorkflow.d.ts +2 -0
  170. package/dist/realtimeSessions/verificationWorkflow.d.ts.map +1 -1
  171. package/dist/realtimeSessions/verificationWorkflow.js +1 -1
  172. package/dist/realtimeSessions/verificationWorkflow.js.map +1 -1
  173. package/dist/realtimeWidget/WidgetRouter.d.ts.map +1 -1
  174. package/dist/realtimeWidget/WidgetRouter.js +19 -3
  175. package/dist/realtimeWidget/WidgetRouter.js.map +1 -1
  176. package/dist/realtimeWidget/WidgetSessionService.d.ts +67 -7
  177. package/dist/realtimeWidget/WidgetSessionService.d.ts.map +1 -1
  178. package/dist/realtimeWidget/WidgetSessionService.js +201 -19
  179. package/dist/realtimeWidget/WidgetSessionService.js.map +1 -1
  180. package/dist/realtimeWidget/host-identity.d.ts +1 -1
  181. package/dist/realtimeWidget/host-identity.js +1 -1
  182. package/dist/realtimeWidget/widgetCore.d.ts +20 -1
  183. package/dist/realtimeWidget/widgetCore.d.ts.map +1 -1
  184. package/dist/realtimeWidget/widgetCore.js +17 -1
  185. package/dist/realtimeWidget/widgetCore.js.map +1 -1
  186. package/dist/realtimeWidget/widgetGuestElevation.d.ts +1 -1
  187. package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
  188. package/dist/realtimeWidget/widgetGuestElevation.js +2 -1
  189. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
  190. package/dist/resolvers/ActionResolver.d.ts +19 -1
  191. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  192. package/dist/resolvers/ActionResolver.js +63 -12
  193. package/dist/resolvers/ActionResolver.js.map +1 -1
  194. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
  195. package/dist/resolvers/AdhocQueryResolver.js +1 -0
  196. package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
  197. package/dist/resolvers/AgentRunStatusPublisher.d.ts +18 -4
  198. package/dist/resolvers/AgentRunStatusPublisher.d.ts.map +1 -1
  199. package/dist/resolvers/AgentRunStatusPublisher.js +32 -8
  200. package/dist/resolvers/AgentRunStatusPublisher.js.map +1 -1
  201. package/dist/resolvers/AgentSessionResolver.d.ts +10 -4
  202. package/dist/resolvers/AgentSessionResolver.d.ts.map +1 -1
  203. package/dist/resolvers/AgentSessionResolver.js +40 -17
  204. package/dist/resolvers/AgentSessionResolver.js.map +1 -1
  205. package/dist/resolvers/ArtifactFileResolver.d.ts +5 -1
  206. package/dist/resolvers/ArtifactFileResolver.d.ts.map +1 -1
  207. package/dist/resolvers/ArtifactFileResolver.js +9 -7
  208. package/dist/resolvers/ArtifactFileResolver.js.map +1 -1
  209. package/dist/resolvers/ConversationRunEventsResolver.d.ts +8 -4
  210. package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -1
  211. package/dist/resolvers/ConversationRunEventsResolver.js +6 -6
  212. package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -1
  213. package/dist/resolvers/DatasetResolver.d.ts +24 -3
  214. package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
  215. package/dist/resolvers/DatasetResolver.js +57 -8
  216. package/dist/resolvers/DatasetResolver.js.map +1 -1
  217. package/dist/resolvers/EntityCommunicationsResolver.d.ts +13 -1
  218. package/dist/resolvers/EntityCommunicationsResolver.d.ts.map +1 -1
  219. package/dist/resolvers/EntityCommunicationsResolver.js +50 -14
  220. package/dist/resolvers/EntityCommunicationsResolver.js.map +1 -1
  221. package/dist/resolvers/EntityRecordNameResolver.d.ts +2 -1
  222. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  223. package/dist/resolvers/EntityRecordNameResolver.js +3 -2
  224. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  225. package/dist/resolvers/FetchEntityVectorsResolver.d.ts +46 -3
  226. package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
  227. package/dist/resolvers/FetchEntityVectorsResolver.js +211 -50
  228. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  229. package/dist/resolvers/FileCategoryResolver.d.ts.map +1 -1
  230. package/dist/resolvers/FileCategoryResolver.js +1 -0
  231. package/dist/resolvers/FileCategoryResolver.js.map +1 -1
  232. package/dist/resolvers/FileResolver.d.ts +87 -11
  233. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  234. package/dist/resolvers/FileResolver.js +292 -105
  235. package/dist/resolvers/FileResolver.js.map +1 -1
  236. package/dist/resolvers/GetDataContextDataResolver.d.ts +33 -7
  237. package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
  238. package/dist/resolvers/GetDataContextDataResolver.js +117 -38
  239. package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
  240. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  241. package/dist/resolvers/GetDataResolver.js +6 -3
  242. package/dist/resolvers/GetDataResolver.js.map +1 -1
  243. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  244. package/dist/resolvers/IntegrationDiscoveryResolver.js +49 -9
  245. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  246. package/dist/resolvers/MergeRecordsResolver.d.ts +8 -1
  247. package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
  248. package/dist/resolvers/MergeRecordsResolver.js +31 -3
  249. package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
  250. package/dist/resolvers/PipelineProgressResolver.d.ts +6 -0
  251. package/dist/resolvers/PipelineProgressResolver.d.ts.map +1 -1
  252. package/dist/resolvers/PipelineProgressResolver.js +5 -3
  253. package/dist/resolvers/PipelineProgressResolver.js.map +1 -1
  254. package/dist/resolvers/QueryResolver.d.ts +2 -0
  255. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  256. package/dist/resolvers/QueryResolver.js +33 -6
  257. package/dist/resolvers/QueryResolver.js.map +1 -1
  258. package/dist/resolvers/QuerySystemUserResolver.d.ts +2 -0
  259. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  260. package/dist/resolvers/QuerySystemUserResolver.js +15 -6
  261. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  262. package/dist/resolvers/RSUResolver.d.ts.map +1 -1
  263. package/dist/resolvers/RSUResolver.js +12 -0
  264. package/dist/resolvers/RSUResolver.js.map +1 -1
  265. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +18 -10
  266. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  267. package/dist/resolvers/RealtimeClientSessionResolver.js +92 -54
  268. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  269. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
  270. package/dist/resolvers/RemoteBrowserActionResolver.js +11 -9
  271. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
  272. package/dist/resolvers/RunAIAgentResolver.d.ts +56 -8
  273. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  274. package/dist/resolvers/RunAIAgentResolver.js +241 -67
  275. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  276. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
  277. package/dist/resolvers/RunTemplateResolver.js +4 -4
  278. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  279. package/dist/resolvers/RunTestResolver.d.ts.map +1 -1
  280. package/dist/resolvers/RunTestResolver.js +3 -1
  281. package/dist/resolvers/RunTestResolver.js.map +1 -1
  282. package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts +12 -1
  283. package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts.map +1 -1
  284. package/dist/resolvers/SearchKnowledgeStreamResolver.js +15 -2
  285. package/dist/resolvers/SearchKnowledgeStreamResolver.js.map +1 -1
  286. package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
  287. package/dist/resolvers/SqlLoggingConfigResolver.js +2 -3
  288. package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
  289. package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
  290. package/dist/resolvers/SyncDataResolver.js +4 -0
  291. package/dist/resolvers/SyncDataResolver.js.map +1 -1
  292. package/dist/resolvers/TaskGraphFrameResolver.d.ts +2 -1
  293. package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
  294. package/dist/resolvers/TaskGraphFrameResolver.js +28 -1
  295. package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
  296. package/dist/resolvers/TestQuerySQLResolver.d.ts +24 -0
  297. package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
  298. package/dist/resolvers/TestQuerySQLResolver.js +66 -13
  299. package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
  300. package/dist/resolvers/TransactionGroupResolver.d.ts +34 -2
  301. package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
  302. package/dist/resolvers/TransactionGroupResolver.js +150 -27
  303. package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
  304. package/dist/resolvers/UserViewResolver.d.ts +1 -1
  305. package/dist/resolvers/UserViewResolver.d.ts.map +1 -1
  306. package/dist/resolvers/UserViewResolver.js +2 -1
  307. package/dist/resolvers/UserViewResolver.js.map +1 -1
  308. package/dist/resolvers/agent-run-data-guard.d.ts +31 -0
  309. package/dist/resolvers/agent-run-data-guard.d.ts.map +1 -0
  310. package/dist/resolvers/agent-run-data-guard.js +72 -0
  311. package/dist/resolvers/agent-run-data-guard.js.map +1 -0
  312. package/dist/rest/EntityCRUDHandler.d.ts +6 -0
  313. package/dist/rest/EntityCRUDHandler.d.ts.map +1 -1
  314. package/dist/rest/EntityCRUDHandler.js +19 -5
  315. package/dist/rest/EntityCRUDHandler.js.map +1 -1
  316. package/dist/rest/RESTEndpointHandler.d.ts.map +1 -1
  317. package/dist/rest/RESTEndpointHandler.js +8 -5
  318. package/dist/rest/RESTEndpointHandler.js.map +1 -1
  319. package/dist/rest/RealtimeRecordingHandler.d.ts +49 -0
  320. package/dist/rest/RealtimeRecordingHandler.d.ts.map +1 -0
  321. package/dist/rest/RealtimeRecordingHandler.js +383 -0
  322. package/dist/rest/RealtimeRecordingHandler.js.map +1 -0
  323. package/dist/rest/ViewOperationsHandler.d.ts +9 -1
  324. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  325. package/dist/rest/ViewOperationsHandler.js +43 -17
  326. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  327. package/dist/rest/setupRESTEndpoints.d.ts +6 -0
  328. package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
  329. package/dist/rest/setupRESTEndpoints.js +6 -0
  330. package/dist/rest/setupRESTEndpoints.js.map +1 -1
  331. package/dist/serverExtensionReservedRoots.d.ts +12 -10
  332. package/dist/serverExtensionReservedRoots.d.ts.map +1 -1
  333. package/dist/serverExtensionReservedRoots.js +14 -17
  334. package/dist/serverExtensionReservedRoots.js.map +1 -1
  335. package/dist/serverRoleConfigUnits.d.ts +29 -0
  336. package/dist/serverRoleConfigUnits.d.ts.map +1 -0
  337. package/dist/serverRoleConfigUnits.js +33 -0
  338. package/dist/serverRoleConfigUnits.js.map +1 -0
  339. package/dist/services/MJStorageBlobStore.d.ts +8 -1
  340. package/dist/services/MJStorageBlobStore.d.ts.map +1 -1
  341. package/dist/services/MJStorageBlobStore.js +25 -19
  342. package/dist/services/MJStorageBlobStore.js.map +1 -1
  343. package/dist/services/StartTaskGraphDispatcher.js +2 -2
  344. package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
  345. package/dist/services/TaskGraphActionRunner.d.ts +5 -3
  346. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
  347. package/dist/services/TaskGraphActionRunner.js +27 -5
  348. package/dist/services/TaskGraphActionRunner.js.map +1 -1
  349. package/dist/services/TaskGraphAgentRunner.d.ts +14 -2
  350. package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
  351. package/dist/services/TaskGraphAgentRunner.js +157 -9
  352. package/dist/services/TaskGraphAgentRunner.js.map +1 -1
  353. package/dist/services/TaskGraphContinuationDeliverer.d.ts +59 -2
  354. package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
  355. package/dist/services/TaskGraphContinuationDeliverer.js +626 -16
  356. package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
  357. package/dist/sharedCache.d.ts.map +1 -1
  358. package/dist/sharedCache.js +3 -0
  359. package/dist/sharedCache.js.map +1 -1
  360. package/dist/types.d.ts +23 -1
  361. package/dist/types.d.ts.map +1 -1
  362. package/dist/types.js.map +1 -1
  363. package/package.json +99 -98
  364. package/src/__tests__/ActionResolver.fieldSecurity.test.ts +237 -0
  365. package/src/__tests__/AdhocQueryResolver.dialect.test.ts +137 -0
  366. package/src/__tests__/AgentRunStatusPublisher.test.ts +49 -0
  367. package/src/__tests__/AgentSessionResolver.ownership.test.ts +137 -0
  368. package/src/__tests__/CustomColumnPromoterOnePass.test.ts +15 -1
  369. package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
  370. package/src/__tests__/EntityCommunicationsResolver.actingUser.test.ts +151 -0
  371. package/src/__tests__/EntityRecordNameResolver.rowSecurity.test.ts +152 -0
  372. package/src/__tests__/EvolutionActsOnWhatItDetects.test.ts +113 -0
  373. package/src/__tests__/FetchEntityVectorsResolver.test.ts +369 -18
  374. package/src/__tests__/FileResolver.fieldSecurity.test.ts +82 -0
  375. package/src/__tests__/FileResolver.storageAccess.test.ts +562 -0
  376. package/src/__tests__/GetDataContextDataResolver.sqlItems.test.ts +416 -0
  377. package/src/__tests__/ListOperationsResolver.adhocScreen.test.ts +137 -0
  378. package/src/__tests__/LoginScreenEngine.test.ts +86 -0
  379. package/src/__tests__/MJStorageBlobStore.storageGate.test.ts +63 -0
  380. package/src/__tests__/PipelineProgressResolver.filter.test.ts +58 -0
  381. package/src/__tests__/QuerySystemUserResolver.fieldSecurity.test.ts +49 -0
  382. package/src/__tests__/RESTEndpointHandler.test.ts +25 -0
  383. package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +23 -3
  384. package/src/__tests__/RealtimeClientSessionResolver.test.ts +324 -46
  385. package/src/__tests__/RecordingAssemblyScheduler.test.ts +178 -0
  386. package/src/__tests__/RemoteBrowserActionResolver.ownership.test.ts +98 -0
  387. package/src/__tests__/ResolverBase.adhocReadScope.test.ts +133 -2
  388. package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
  389. package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +127 -0
  390. package/src/__tests__/ResolverBase.clientOldValues.test.ts +243 -7
  391. package/src/__tests__/ResolverBase.deleteReturnProjection.test.ts +52 -0
  392. package/src/__tests__/ResolverBase.entityMessageRelay.test.ts +176 -0
  393. package/src/__tests__/ResolverBase.filterEscaping.test.ts +5 -2
  394. package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +23 -1
  395. package/src/__tests__/ResolverBase.publishStatusUpdate.test.ts +68 -0
  396. package/src/__tests__/ResolverBase.userViewCacheAndDedup.test.ts +130 -0
  397. package/src/__tests__/RunAIAgentResolver.branch.test.ts +69 -13
  398. package/src/__tests__/RunAIAgentResolver.conversationWriter.test.ts +210 -0
  399. package/src/__tests__/RunAIAgentResolver.fireAndForgetCompletion.test.ts +209 -0
  400. package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +20 -28
  401. package/src/__tests__/RunAIAgentResolver.runDataGuard.test.ts +264 -0
  402. package/src/__tests__/RunAIAgentResolver.serverReply.test.ts +358 -0
  403. package/src/__tests__/RunAIAgentResolver.stoppedPredecessor.test.ts +4 -1
  404. package/src/__tests__/RunTemplateResolver.test.ts +35 -2
  405. package/src/__tests__/RunViewResolver.cacheCheckForwarding.test.ts +11 -0
  406. package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +165 -0
  407. package/src/__tests__/SearchKnowledgeStreamResolver.test.ts +161 -0
  408. package/src/__tests__/SessionJanitor.test.ts +365 -10
  409. package/src/__tests__/SessionManager.test.ts +122 -0
  410. package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +25 -0
  411. package/src/__tests__/TaskGraphAgentRunner.test.ts +266 -0
  412. package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +468 -23
  413. package/src/__tests__/TaskGraphFrameResolver.test.ts +22 -0
  414. package/src/__tests__/TestQuerySQLResolver.test.ts +274 -0
  415. package/src/__tests__/TransactionGroupResolver.fieldSecurity.test.ts +199 -0
  416. package/src/__tests__/TransactionGroupResolver.refusals.test.ts +98 -1
  417. package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
  418. package/src/__tests__/UserViewResolver.updateWhereClause.test.ts +15 -0
  419. package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +157 -0
  420. package/src/__tests__/ViewOperationsHandler.test.ts +7 -2
  421. package/src/__tests__/WidgetSessionService.test.ts +583 -4
  422. package/src/__tests__/agent-run-data-guard.test.ts +130 -0
  423. package/src/__tests__/apiRootBoot.test.ts +202 -0
  424. package/src/__tests__/buildMagicLinkSessionUser.test.ts +96 -0
  425. package/src/__tests__/cacheBroadcastProviders.test.ts +403 -0
  426. package/src/__tests__/cacheInvalidationRecordData.test.ts +109 -0
  427. package/src/__tests__/conversationRunEvents.test.ts +56 -14
  428. package/src/__tests__/fieldSecurityFixtures.ts +91 -0
  429. package/src/__tests__/globalCors.test.ts +217 -0
  430. package/src/__tests__/localDevAuth.test.ts +196 -0
  431. package/src/__tests__/multiTenancy.postgresRewrite.test.ts +181 -0
  432. package/src/__tests__/multiTenancy.security.test.ts +51 -0
  433. package/src/__tests__/multiTenancy.test.ts +1 -1
  434. package/src/__tests__/orphanedConversationDetailReconciler.test.ts +10 -0
  435. package/src/__tests__/ownedRealtimeSession.test.ts +39 -0
  436. package/src/__tests__/pushStatusResolver.filter.test.ts +64 -1
  437. package/src/__tests__/realtimeRecordingHandler.test.ts +669 -0
  438. package/src/__tests__/realtimeSessions/RealtimeSessionEventsResolver.test.ts +29 -1
  439. package/src/__tests__/realtimeSessions/ownedRealtimeSession.anonymous.test.ts +31 -0
  440. package/src/__tests__/realtimeSessions/ownedRealtimeSession.namedScoped.test.ts +37 -0
  441. package/src/__tests__/realtimeSessions/sessionAccess.test.ts +146 -7
  442. package/src/__tests__/realtimeSessions/sessionConfigStamps.test.ts +1 -1
  443. package/src/__tests__/realtimeSessions/verificationWorkflow.test.ts +25 -4
  444. package/src/__tests__/resolverBase.cloudEventChangedFields.test.ts +157 -0
  445. package/src/__tests__/resolverBase.fieldSecurityTransport.test.ts +222 -0
  446. package/src/__tests__/resolverBase.fls.test.ts +146 -97
  447. package/src/__tests__/restTransport.fieldSecurity.test.ts +148 -0
  448. package/src/__tests__/runParamsTimeZone.test.ts +179 -0
  449. package/src/__tests__/runStreams.fieldSecurity.test.ts +100 -0
  450. package/src/__tests__/runViewProperties.test.ts +114 -0
  451. package/src/__tests__/scalabilityBenchmarks.test.ts +765 -0
  452. package/src/__tests__/scalabilityDatabaseIntegration.test.ts +207 -0
  453. package/src/__tests__/serverExtensionHostOverlay.test.ts +66 -0
  454. package/src/__tests__/serverExtensionReservedRoots.test.ts +11 -98
  455. package/src/__tests__/sessionUserClone.test.ts +5 -1
  456. package/src/__tests__/sharedCache.test.ts +5 -3
  457. package/src/__tests__/startupSweepOrder.test.ts +40 -0
  458. package/src/__tests__/unifiedAuth.test.ts +28 -0
  459. package/src/__tests__/widgetGuestElevation.test.ts +2 -0
  460. package/src/agentSessions/SessionJanitor.ts +369 -93
  461. package/src/agentSessions/SessionManager.ts +45 -10
  462. package/src/apiRoot.ts +78 -0
  463. package/src/auth/AuthProviderCatalogRouter.ts +29 -0
  464. package/src/auth/LoginScreenEngine.ts +50 -0
  465. package/src/auth/index.ts +57 -12
  466. package/src/auth/initializeProviders.ts +43 -1
  467. package/src/auth/localDevAuth.ts +528 -0
  468. package/src/auth/magicLink/MagicLinkKeys.ts +2 -2
  469. package/src/auth/magicLink/MagicLinkRouter.ts +3 -2
  470. package/src/auth/magicLink/types.ts +10 -0
  471. package/src/auth/sessionUserClone.ts +3 -2
  472. package/src/broadcast/BaseCacheInvalidationBroadcastProvider.ts +674 -0
  473. package/src/broadcast/index.ts +5 -0
  474. package/src/broadcast/providers/AWSIoTCoreBroadcastProvider.ts +180 -0
  475. package/src/broadcast/providers/AzureWebPubSubBroadcastProvider.ts +182 -0
  476. package/src/broadcast/providers/GCPPubSubBroadcastProvider.ts +169 -0
  477. package/src/broadcast/providers/SequencedRoleBucketBroadcastProvider.ts +57 -0
  478. package/src/cache.ts +2 -1
  479. package/src/config.ts +115 -5
  480. package/src/context.ts +32 -8
  481. package/src/coreRoutes.ts +181 -0
  482. package/src/generated/graphql-schemas/__mj.ts +3684 -284
  483. package/src/generic/CacheInvalidationResolver.ts +172 -60
  484. package/src/generic/OrphanedConversationDetailReconciler.ts +4 -2
  485. package/src/generic/PubSubManager.ts +54 -1
  486. package/src/generic/PushStatusResolver.ts +109 -12
  487. package/src/generic/ResolverBase.ts +540 -431
  488. package/src/generic/RunViewResolver.ts +97 -15
  489. package/src/generic/actionOutputFieldSecurity.ts +75 -0
  490. package/src/generic/fieldSecurityTransport.ts +62 -0
  491. package/src/globalCors.ts +71 -0
  492. package/src/index.ts +239 -238
  493. package/src/integration/CustomColumnPromoter.ts +6 -1
  494. package/src/logging/variablesLoggingMiddleware.ts +59 -3
  495. package/src/multiTenancy/index.ts +24 -2
  496. package/src/realtimeSessions/README.md +7 -3
  497. package/src/realtimeSessions/RealtimeSessionVerificationService.ts +2 -1
  498. package/src/realtimeSessions/RealtimeVerifyRouter.ts +2 -1
  499. package/src/realtimeSessions/RecordingAssemblyScheduler.ts +234 -0
  500. package/src/realtimeSessions/SessionVerificationPortsBase.ts +1 -0
  501. package/src/realtimeSessions/index.ts +2 -0
  502. package/src/realtimeSessions/ownedRealtimeSession.ts +69 -0
  503. package/src/realtimeSessions/sessionAccess.ts +183 -23
  504. package/src/realtimeSessions/sessionConfigStamps.ts +1 -1
  505. package/src/realtimeSessions/subscriptionAuthorization.ts +4 -2
  506. package/src/realtimeSessions/verificationWorkflow.ts +3 -1
  507. package/src/realtimeWidget/WidgetRouter.ts +22 -4
  508. package/src/realtimeWidget/WidgetSessionService.ts +252 -17
  509. package/src/realtimeWidget/host-identity.ts +1 -1
  510. package/src/realtimeWidget/widgetCore.ts +30 -1
  511. package/src/realtimeWidget/widgetGuestElevation.ts +2 -1
  512. package/src/resolvers/ActionResolver.ts +78 -15
  513. package/src/resolvers/AdhocQueryResolver.ts +1 -0
  514. package/src/resolvers/AgentRunStatusPublisher.ts +36 -8
  515. package/src/resolvers/AgentSessionResolver.ts +46 -18
  516. package/src/resolvers/ArtifactFileResolver.ts +9 -8
  517. package/src/resolvers/ConversationRunEventsResolver.ts +15 -11
  518. package/src/resolvers/DatasetResolver.ts +70 -10
  519. package/src/resolvers/EntityCommunicationsResolver.ts +56 -15
  520. package/src/resolvers/EntityRecordNameResolver.ts +3 -2
  521. package/src/resolvers/FetchEntityVectorsResolver.ts +248 -55
  522. package/src/resolvers/FileCategoryResolver.ts +1 -0
  523. package/src/resolvers/FileResolver.ts +334 -109
  524. package/src/resolvers/GetDataContextDataResolver.ts +139 -41
  525. package/src/resolvers/GetDataResolver.ts +6 -3
  526. package/src/resolvers/IntegrationDiscoveryResolver.ts +53 -9
  527. package/src/resolvers/MergeRecordsResolver.ts +23 -2
  528. package/src/resolvers/PipelineProgressResolver.ts +12 -3
  529. package/src/resolvers/QueryResolver.ts +31 -6
  530. package/src/resolvers/QuerySystemUserResolver.ts +15 -6
  531. package/src/resolvers/RSUResolver.ts +12 -0
  532. package/src/resolvers/RealtimeClientSessionResolver.ts +114 -66
  533. package/src/resolvers/RemoteBrowserActionResolver.ts +15 -10
  534. package/src/resolvers/RunAIAgentResolver.ts +296 -85
  535. package/src/resolvers/RunTemplateResolver.ts +6 -4
  536. package/src/resolvers/RunTestResolver.ts +3 -1
  537. package/src/resolvers/SearchKnowledgeStreamResolver.ts +19 -3
  538. package/src/resolvers/SqlLoggingConfigResolver.ts +2 -4
  539. package/src/resolvers/SyncDataResolver.ts +4 -0
  540. package/src/resolvers/TaskGraphFrameResolver.ts +33 -1
  541. package/src/resolvers/TestQuerySQLResolver.ts +69 -13
  542. package/src/resolvers/TransactionGroupResolver.ts +185 -30
  543. package/src/resolvers/UserViewResolver.ts +2 -1
  544. package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
  545. package/src/resolvers/__tests__/MergeRecordsResolver.test.ts +43 -0
  546. package/src/resolvers/agent-run-data-guard.ts +89 -0
  547. package/src/rest/EntityCRUDHandler.ts +22 -5
  548. package/src/rest/RESTEndpointHandler.ts +8 -5
  549. package/src/rest/RealtimeRecordingHandler.ts +467 -0
  550. package/src/rest/ViewOperationsHandler.ts +52 -18
  551. package/src/rest/setupRESTEndpoints.ts +6 -0
  552. package/src/serverExtensionReservedRoots.ts +14 -17
  553. package/src/serverRoleConfigUnits.ts +48 -0
  554. package/src/services/MJStorageBlobStore.ts +31 -30
  555. package/src/services/StartTaskGraphDispatcher.ts +2 -2
  556. package/src/services/TaskGraphActionRunner.ts +25 -5
  557. package/src/services/TaskGraphAgentRunner.ts +188 -11
  558. package/src/services/TaskGraphContinuationDeliverer.ts +738 -16
  559. package/src/sharedCache.ts +3 -0
  560. package/src/types.ts +23 -1
@@ -1,5 +1,6 @@
1
1
  import { BaseSingleton, ShutdownRegistry, IShutdownable } from '@memberjunction/global';
2
2
  import {
3
+ DatabaseProviderBase,
3
4
  IMetadataProvider,
4
5
  UserInfo,
5
6
  CompositeKey,
@@ -10,10 +11,11 @@ import {
10
11
  } from '@memberjunction/core';
11
12
  import { MJAIAgentEntity, MJAIAgentSessionEntity } from '@memberjunction/core-entities';
12
13
  import {
14
+ AssembleRealtimeRecordingFromLedger,
13
15
  RecoverRealtimeRecordingFromSegments,
14
16
  ResolveRecordingStorageAccountID,
15
- type RecoverRealtimeRecordingInput,
16
- type RecoverRealtimeRecordingResult,
17
+ SqlRealtimeRecordingLedger,
18
+ type IRealtimeRecordingLedger,
17
19
  } from '@memberjunction/ai-agents';
18
20
  import { GetCurrentHostInstance, GetHostInstanceID, HostInstanceIdentity } from './HostInstance.js';
19
21
  import { SessionManager, SessionCloseReason } from './SessionManager.js';
@@ -30,9 +32,19 @@ const AGENT_ENTITY = 'MJ: AI Agents';
30
32
  /** A session whose recording recovery has failed this many times is given up on for the life of the process. */
31
33
  const MAX_RECOVERY_ATTEMPTS = 8;
32
34
 
33
- /** Recovery attempts (calls into the recovery routine) per sweep; bounds the I/O one tick can trigger. */
35
+ /**
36
+ * Recovery attempts (calls into the recovery routine) per sweep; bounds the I/O one tick can trigger.
37
+ * The ledger sweep and the legacy segment sweep each get this many: a backlog in one must not starve
38
+ * the other, so a pass makes at most twice this many attempts.
39
+ */
34
40
  const MAX_RECOVERIES_PER_SWEEP = 5;
35
41
 
42
+ /** The shard-ledger view (core schema); a session with rows in it is assembled by the ledger sweep. */
43
+ const RECORDING_SHARD_VIEW = 'vwAIAgentSessionRecordingShards';
44
+
45
+ /** Ledger shard rows each recovery pass deletes, at most, for sessions closed before the lookback window. */
46
+ const MAX_PURGED_SHARDS_PER_PASS = 500;
47
+
36
48
  /**
37
49
  * Abandoned (timed-out, still running) recovery attempts allowed at once. Each keeps its socket and any
38
50
  * shard buffers it had read (up to the 384 MiB recovery cap) until it settles, so recovery pauses at this
@@ -98,6 +110,71 @@ interface RecoveryBackoff {
98
110
  Deferred: boolean;
99
111
  }
100
112
 
113
+ /**
114
+ * Per-session retry state of one recovery sweep, keyed by lowercase session id (process-local: a restart
115
+ * gets fresh attempts). A failing session backs off exponentially (`min(2^k * 5 min, 6 h)`) and is given
116
+ * up on after {@link MAX_RECOVERY_ATTEMPTS} failures with one log line.
117
+ */
118
+ class RecoveryBackoffTracker {
119
+ private readonly states = new Map<string, RecoveryBackoff>();
120
+
121
+ /** True while a session's backoff (or give-up) window has not elapsed. */
122
+ public IsBackedOff(sessionID: string): boolean {
123
+ const state = this.states.get(sessionID.toLowerCase());
124
+ return state != null && Date.now() < state.NextAttemptAt;
125
+ }
126
+
127
+ /** Forgets a session's failures (it succeeded, or another writer took care of it). */
128
+ public Clear(sessionID: string): void {
129
+ this.states.delete(sessionID.toLowerCase());
130
+ }
131
+
132
+ /**
133
+ * Defers a session that cannot be recovered until configuration changes (its agent has no recording
134
+ * storage account). That is not a failed attempt, so it never counts toward the give-up: the session
135
+ * is re-checked every {@link RECOVERY_BACKOFF_MAX_MS} and logged only the first time.
136
+ */
137
+ public Defer(sessionID: string, reason: string): void {
138
+ const key = sessionID.toLowerCase();
139
+ const state = this.states.get(key);
140
+ if (!state?.Deferred) {
141
+ LogStatus(`[SessionJanitor] Recording recovery skipped for session ${sessionID}: ${reason}; re-checked every ${RECOVERY_BACKOFF_MAX_MS / 3_600_000} h`);
142
+ }
143
+ this.states.set(key, { Failures: state?.Failures ?? 0, NextAttemptAt: Date.now() + RECOVERY_BACKOFF_MAX_MS, Deferred: true });
144
+ }
145
+
146
+ /**
147
+ * Counts one more unsuccessful recovery for a session and schedules its next attempt
148
+ * (`min(2^k * 5 min, 6 h)`); at {@link MAX_RECOVERY_ATTEMPTS} it stops checking the session for this
149
+ * process with a single log line: a `LogError` when the last attempt failed, a `LogStatus` when it
150
+ * simply found no shards (nothing to recover is not an error).
151
+ */
152
+ public RecordFailure(sessionID: string, kind: 'Failed' | 'NoSegments'): void {
153
+ const key = sessionID.toLowerCase();
154
+ const failures = (this.states.get(key)?.Failures ?? 0) + 1;
155
+ if (failures >= MAX_RECOVERY_ATTEMPTS) {
156
+ this.states.set(key, { Failures: failures, NextAttemptAt: Infinity, Deferred: false });
157
+ if (kind === 'NoSegments') {
158
+ LogStatus(`[SessionJanitor] Session ${sessionID} had no recoverable segments in ${failures} checks; not checking it again until restart`);
159
+ } else {
160
+ LogError(`[SessionJanitor] Giving up on recording recovery for session ${sessionID} after ${failures} failed attempts`);
161
+ }
162
+ } else {
163
+ const waitMs = Math.min(2 ** failures * RECOVERY_BACKOFF_BASE_MS, RECOVERY_BACKOFF_MAX_MS);
164
+ this.states.set(key, { Failures: failures, NextAttemptAt: Date.now() + waitMs, Deferred: false });
165
+ }
166
+ }
167
+
168
+ /** Drops state for sessions that were not in the latest complete candidate set (lowercase ids). */
169
+ public Prune(candidateKeys: ReadonlySet<string>): void {
170
+ for (const key of this.states.keys()) {
171
+ if (!candidateKeys.has(key)) {
172
+ this.states.delete(key);
173
+ }
174
+ }
175
+ }
176
+ }
177
+
101
178
  /** Per-sweep working state shared by every candidate in one recovery pass. */
102
179
  interface RecoverySweepContext {
103
180
  provider: IMetadataProvider;
@@ -141,13 +218,17 @@ interface RecoveryVisit {
141
218
  * invoked from {@link Shutdown} during the graceful ShutdownRegistry drain — closes this exact host
142
219
  * instance's own live sessions with `CloseReason = 'Shutdown'`.
143
220
  *
144
- * Separately from closing, {@link RunRecordingRecoverySweep} runs after the close sweeps on every
145
- * periodic tick, without blocking them, and rebuilds the recording of already-`Closed` sessions whose
146
- * end-of-call upload never arrived (it closes nothing).
221
+ * Separately from closing, a recording recovery pass runs after the close sweeps on every periodic tick,
222
+ * without blocking them, and closes nothing. It purges shard-ledger rows past the lookback window, then
223
+ * {@link RunRecordingLedgerSweep} assembles the recordings of `Closed` sessions on the shard ledger that
224
+ * no terminal signal (or a dead assembler) left unassembled, then {@link RunRecordingRecoverySweep}
225
+ * rebuilds, from legacy storage segments, the recording of `Closed` sessions not on the ledger whose
226
+ * end-of-call upload never arrived.
147
227
  *
148
228
  * The close sweeps are **idempotent and safe to run concurrently** on every instance: closing an
149
- * already-`Closed` session is a no-op, and the close path is last-writer-wins. Recording recovery is not
150
- * strictly so: two instances can recover the same session at once (there is no claim column). Each
229
+ * already-`Closed` session is a no-op, and the close path is last-writer-wins. Ledger assembly is
230
+ * arbitrated by the ledger's atomic claim. Legacy recovery is not: two instances can recover the same
231
+ * session at once (there is no claim column for it). Each
151
232
  * attempt uploads its own file and defers to an already-stamped recording on a best-effort basis, so
152
233
  * the cost is duplicate work and an extra file, not a lost recording. The extra file is an orphan when the
153
234
  * second attempt's re-check sees the first stamp; when both re-checks pass first, both files are stamped in
@@ -166,11 +247,16 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
166
247
  /** Set by {@link Stop}, cleared by {@link Start}: no new recovery pass starts while stopped. */
167
248
  private _stopped = false;
168
249
  private _registered = false;
169
- private _provider: IMetadataProvider | null = null;
250
+ private _provider: DatabaseProviderBase | null = null;
170
251
  private _systemUser: UserInfo | null = null;
171
252
  private readonly sessionManager = new SessionManager();
172
- /** Recovery retry state keyed by lowercase session id. Process-local: a restart gets fresh attempts. */
173
- private readonly recoveryBackoff = new Map<string, RecoveryBackoff>();
253
+ /** Retry state of the legacy storage-segment recovery. */
254
+ private readonly recoveryBackoff = new RecoveryBackoffTracker();
255
+ /**
256
+ * Retry state of the ledger sweep, kept apart from {@link recoveryBackoff}: each sweep prunes its
257
+ * tracker by its own candidate set, so a shared one would forget the other sweep's failures.
258
+ */
259
+ private readonly ledgerBackoff = new RecoveryBackoffTracker();
174
260
  /**
175
261
  * Lowercase ids of sessions whose recovery attempt timed out but has not settled. Skipped by later
176
262
  * sweeps so a second attempt never runs alongside the abandoned one; removed when it settles.
@@ -217,9 +303,10 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
217
303
  * close sweeps, then recording recovery). Idempotent: a second call does not stack a second timer.
218
304
  * Captures the provider + system user so the timer callback can run without re-supplying them, and
219
305
  * re-enables recording recovery after a {@link Stop}. Throws, before doing any work, when
220
- * `intervalMs` is invalid (see {@link Configure}).
306
+ * `intervalMs` is invalid (see {@link Configure}). Takes the server's database provider because
307
+ * recording recovery calls the shard ledger's stored procedures.
221
308
  */
222
- public async Start(provider: IMetadataProvider, systemUser: UserInfo, intervalMs?: number): Promise<void> {
309
+ public async Start(provider: DatabaseProviderBase, systemUser: UserInfo, intervalMs?: number): Promise<void> {
223
310
  if (intervalMs != null) {
224
311
  this.Configure({ sweepIntervalMs: intervalMs });
225
312
  }
@@ -365,20 +452,102 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
365
452
  * sessions that left the candidate set are pruned, but only after a pass that saw every candidate, so
366
453
  * a capped pass never forgets sessions it did not reach.
367
454
  *
455
+ * Sessions on the shard ledger (a `RecordingStatus`, or any ledger row) are left to
456
+ * {@link RunRecordingLedgerSweep}: their audio is in SQL, not in storage segments.
457
+ *
368
458
  * Never throws for a single bad session. Returns the number of recordings recovered.
369
459
  */
370
- public async RunRecordingRecoverySweep(provider: IMetadataProvider, systemUser: UserInfo): Promise<number> {
460
+ public async RunRecordingRecoverySweep(provider: DatabaseProviderBase, systemUser: UserInfo): Promise<number> {
371
461
  if (this.isRecoveryPausedForAbandonedAttempts()) {
372
462
  return 0;
373
463
  }
374
- const nowMs = Date.now();
375
- const graceCutoff = new Date(nowMs - this._config.recordingRecoveryGraceMinutes * 60_000).toISOString();
376
- const lookbackCutoff = new Date(nowMs - this._config.recordingRecoveryLookbackHours * 3_600_000).toISOString();
464
+ const { graceCutoff, lookbackCutoff } = this.recoveryWindow();
377
465
  const filter =
378
466
  `Status = 'Closed' AND RecordingStartedAt IS NOT NULL AND RecordingFileID IS NULL ` +
379
- `AND ClosedAt < '${graceCutoff}' AND ClosedAt >= '${lookbackCutoff}'`;
467
+ `AND ClosedAt < '${graceCutoff.toISOString()}' AND ClosedAt >= '${lookbackCutoff.toISOString()}' ` +
468
+ `AND RecordingStatus IS NULL AND ID NOT IN (SELECT AgentSessionID FROM ${this.shardViewName(provider)})`;
380
469
 
381
470
  const context: RecoverySweepContext = { provider, systemUser, storage: new Map() };
471
+ const recovered = await this.sweepRecoveryCandidates(filter, provider, systemUser, this.recoveryBackoff,
472
+ session => this.recoverSessionRecording(session, context));
473
+ if (recovered > 0) {
474
+ LogStatus(`[SessionJanitor] Recording recovery rebuilt ${recovered} recording(s)`);
475
+ }
476
+ return recovered;
477
+ }
478
+
479
+ /**
480
+ * Backstop for client-direct recordings on the shard ledger: assembles the recording of `Closed`
481
+ * sessions that closed between the lookback window and the grace period and are still
482
+ * - `Pending` (shards, or a terminal signal, with no assembly since: the browser died before its
483
+ * terminal request, or the in-process assembly request was lost with its host),
484
+ * - `Assembling` with no write for `recordingRecoveryAttemptTimeoutMinutes` (the assembler died, or
485
+ * it hit a transient error and left the claim for a re-claim), or
486
+ * - status-less while the ledger holds shards for them (see {@link restoreLostRecordingStatus}).
487
+ *
488
+ * Each candidate is claimed with the ledger's atomic, stale-aware claim (a lost claim is skipped: another
489
+ * assembler has it) and assembled with `ForcePartial`, so missing shards become silence. An attempt is
490
+ * bounded by `recordingRecoveryAttemptTimeoutMinutes` and shares the legacy sweep's abandoned-attempt
491
+ * pause; failures back off and are given up on exactly like legacy recovery, in their own tracker.
492
+ * At most {@link MAX_RECOVERIES_PER_SWEEP} assemblies per call. Never throws for a single bad session.
493
+ * Returns the number of recordings assembled (`Complete` or `Partial`).
494
+ */
495
+ public async RunRecordingLedgerSweep(provider: DatabaseProviderBase, systemUser: UserInfo): Promise<number> {
496
+ if (this.isRecoveryPausedForAbandonedAttempts()) {
497
+ return 0;
498
+ }
499
+ const { graceCutoff, lookbackCutoff } = this.recoveryWindow();
500
+ const staleCutoff = new Date(Date.now() - this._config.recordingRecoveryAttemptTimeoutMinutes * 60_000);
501
+ // Every Assembling session is fetched, not only stale ones, and a fresh one is skipped below: a
502
+ // session whose assembly left a Retry claim is fresh for a while, and dropping it from the walk
503
+ // would prune its backoff, so the give-up cap would never be reached.
504
+ const filter =
505
+ `Status = 'Closed' AND ClosedAt < '${graceCutoff.toISOString()}' AND ClosedAt >= '${lookbackCutoff.toISOString()}' ` +
506
+ `AND (RecordingStatus IN ('Pending','Assembling') ` +
507
+ `OR (RecordingStatus IS NULL AND ID IN (SELECT AgentSessionID FROM ${this.shardViewName(provider)})))`;
508
+
509
+ const ledger = new SqlRealtimeRecordingLedger(provider, systemUser);
510
+ const assembled = await this.sweepRecoveryCandidates(
511
+ filter, provider, systemUser, this.ledgerBackoff,
512
+ session => this.assembleSessionFromLedger(session, ledger, staleCutoff, provider, systemUser),
513
+ // The claim proc re-checks staleness atomically; this only spares a claim that would lose.
514
+ session => session.RecordingStatus === 'Assembling' && session.__mj_UpdatedAt.getTime() >= staleCutoff.getTime(),
515
+ );
516
+ if (assembled > 0) {
517
+ LogStatus(`[SessionJanitor] Recording ledger sweep assembled ${assembled} recording(s)`);
518
+ }
519
+ return assembled;
520
+ }
521
+
522
+ /** The closed-at window recording recovery considers: older than the grace, newer than the lookback. */
523
+ private recoveryWindow(): { graceCutoff: Date; lookbackCutoff: Date } {
524
+ const nowMs = Date.now();
525
+ return {
526
+ graceCutoff: new Date(nowMs - this._config.recordingRecoveryGraceMinutes * 60_000),
527
+ lookbackCutoff: new Date(nowMs - this._config.recordingRecoveryLookbackHours * 3_600_000),
528
+ };
529
+ }
530
+
531
+ /** The schema-qualified, dialect-quoted shard-ledger view, for filter sub-queries. */
532
+ private shardViewName(provider: DatabaseProviderBase): string {
533
+ return provider.QuoteSchemaAndView(provider.MJCoreSchemaName, RECORDING_SHARD_VIEW);
534
+ }
535
+
536
+ /**
537
+ * Walks every session matching `filter` and runs `recover` on each one that is not backed off, not
538
+ * abandoned, and not `isBusy`, sequentially, stopping at {@link MAX_RECOVERIES_PER_SWEEP} attempts or
539
+ * {@link MAX_ABANDONED_RECOVERIES} abandoned attempts. Backoff entries of sessions that left the
540
+ * candidate set are pruned, but only after a walk that saw every candidate, so a capped walk never
541
+ * forgets sessions it did not reach. Returns the number recovered.
542
+ */
543
+ private async sweepRecoveryCandidates(
544
+ filter: string,
545
+ provider: IMetadataProvider,
546
+ systemUser: UserInfo,
547
+ backoff: RecoveryBackoffTracker,
548
+ recover: (session: MJAIAgentSessionEntity) => Promise<RecoveryVisit>,
549
+ isBusy: (session: MJAIAgentSessionEntity) => boolean = () => false,
550
+ ): Promise<number> {
382
551
  const seen = new Set<string>();
383
552
  let attempts = 0;
384
553
  let recovered = 0;
@@ -386,13 +555,13 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
386
555
  const walk = await this.forEachSessionPage(filter, provider, systemUser, async page => {
387
556
  for (const session of page) {
388
557
  seen.add(session.ID.toLowerCase());
389
- if (this.isRecoveryBackedOff(session.ID) || this.abandonedRecoveries.has(session.ID.toLowerCase())) {
558
+ if (backoff.IsBackedOff(session.ID) || this.abandonedRecoveries.has(session.ID.toLowerCase()) || isBusy(session)) {
390
559
  continue;
391
560
  }
392
561
  if (attempts >= MAX_RECOVERIES_PER_SWEEP || this.abandonedRecoveries.size >= MAX_ABANDONED_RECOVERIES) {
393
562
  return false; // per-sweep cap, or too many abandoned attempts still running: stop paging
394
563
  }
395
- const visit = await this.recoverSessionRecording(session, context);
564
+ const visit = await recover(session);
396
565
  attempts += visit.Attempted ? 1 : 0;
397
566
  recovered += visit.Recovered ? 1 : 0;
398
567
  }
@@ -401,14 +570,141 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
401
570
  // Only a walk that reached the end saw every candidate. A capped or failed-load walk has an
402
571
  // incomplete `seen`; pruning on it would wipe backoff and give-up state for unseen sessions.
403
572
  if (walk === 'complete') {
404
- this.pruneRecoveryBackoff(seen);
405
- }
406
- if (recovered > 0) {
407
- LogStatus(`[SessionJanitor] Recording recovery rebuilt ${recovered} recording(s)`);
573
+ backoff.Prune(seen);
408
574
  }
409
575
  return recovered;
410
576
  }
411
577
 
578
+ /**
579
+ * Claims and assembles one ledger candidate and updates its backoff. Never throws: any exception is
580
+ * logged with the session id and counted as a failure.
581
+ */
582
+ private async assembleSessionFromLedger(
583
+ candidate: MJAIAgentSessionEntity,
584
+ ledger: IRealtimeRecordingLedger,
585
+ staleCutoff: Date,
586
+ provider: DatabaseProviderBase,
587
+ systemUser: UserInfo,
588
+ ): Promise<RecoveryVisit> {
589
+ const sessionID = candidate.ID;
590
+ let attempted = false;
591
+ try {
592
+ if (candidate.RecordingStatus == null) {
593
+ // The restore is a write; a failing one counts toward the per-sweep cap like an assembly.
594
+ attempted = true;
595
+ if (!(await this.restoreLostRecordingStatus(candidate, ledger))) {
596
+ return this.recordLedgerFailure(sessionID, true);
597
+ }
598
+ attempted = false;
599
+ }
600
+ const version = await ledger.ClaimAssembly(sessionID, staleCutoff);
601
+ if (version === null) {
602
+ LogStatus(`[SessionJanitor] Recording ledger sweep skipped session ${sessionID}: another assembler holds its claim`);
603
+ return { Attempted: false, Recovered: false };
604
+ }
605
+ attempted = true;
606
+ const outcome = await this.runWithinAttemptTimeout(
607
+ sessionID,
608
+ AssembleRealtimeRecordingFromLedger({
609
+ SessionID: sessionID, Version: version, Ledger: ledger,
610
+ ContextUser: systemUser, Provider: provider, ForcePartial: true,
611
+ }),
612
+ late => late,
613
+ );
614
+ switch (outcome) {
615
+ case 'Complete':
616
+ case 'Partial':
617
+ this.ledgerBackoff.Clear(sessionID);
618
+ LogStatus(`[SessionJanitor] Assembled the recording of session ${sessionID} from its shard ledger: ${outcome} (version ${version})`);
619
+ return { Attempted: true, Recovered: true };
620
+ case 'Superseded':
621
+ this.ledgerBackoff.Clear(sessionID);
622
+ LogStatus(`[SessionJanitor] Ledger assembly of session ${sessionID} (version ${version}) was Superseded by a newer claim; left to it`);
623
+ return { Attempted: true, Recovered: false };
624
+ case 'Retry':
625
+ // The assembler already logged the error; the claim stays Assembling for a re-claim after the backoff.
626
+ LogStatus(`[SessionJanitor] Ledger assembly of session ${sessionID} (version ${version}) hit a transient error; retried after its backoff`);
627
+ return this.recordLedgerFailure(sessionID, true);
628
+ case 'Deferred':
629
+ LogError(`[SessionJanitor] Forced ledger assembly of session ${sessionID} (version ${version}) returned Deferred, which only an unforced assembly should; backing off`);
630
+ return this.recordLedgerFailure(sessionID, true);
631
+ case 'Failed':
632
+ LogError(`[SessionJanitor] Ledger assembly of session ${sessionID} (version ${version}) Failed; its recording status is now Failed`);
633
+ return this.recordLedgerFailure(sessionID, true);
634
+ case 'TimedOut':
635
+ LogError(
636
+ `[SessionJanitor] Ledger assembly of session ${sessionID} (version ${version}) did not finish within ` +
637
+ `${this._config.recordingRecoveryAttemptTimeoutMinutes} minutes; abandoning this attempt (it may still finish) ` +
638
+ `and continuing the sweep. The session is not retried until it settles.`,
639
+ );
640
+ return this.recordLedgerFailure(sessionID, true);
641
+ }
642
+ } catch (err) {
643
+ LogError(`[SessionJanitor] Recording ledger sweep failed for session ${sessionID}: ${err instanceof Error ? err.message : String(err)}`);
644
+ return this.recordLedgerFailure(sessionID, attempted);
645
+ }
646
+ }
647
+
648
+ /**
649
+ * Gives a status-less session with ledger shards back a `Pending` status so it can be claimed (the
650
+ * claim only takes Pending/Partial/stale Assembling). That is how statuses were lost before #6010
651
+ * made the ledger columns not updatable through an entity save (a stale `Save` of the session, e.g. its
652
+ * close, put back the NULL status and counts). The restore stays for rows hit before then.
653
+ *
654
+ * The terminal proc is the existing way to set a NULL status to `Pending`. It is stamped with the
655
+ * counts the session still stores, or 0 for a NULL count (the proc accepts any count over NULL), and
656
+ * never with counts invented from the ledger: a made-up terminal could make a recording whose tail
657
+ * never arrived assemble as Complete, which deletes its shards. With no real terminal the stored shard
658
+ * count differs from the stamped one, so the assembly is Partial and keeps the shards; a later real
659
+ * terminal (higher counts) or late shard re-opens it intact.
660
+ * Returns false (logged) when the status could not be restored.
661
+ */
662
+ private async restoreLostRecordingStatus(candidate: MJAIAgentSessionEntity, ledger: IRealtimeRecordingLedger): Promise<boolean> {
663
+ const sessionID = candidate.ID;
664
+ const expectedShards = candidate.RecordingExpectedShards ?? 0;
665
+ // Number(): the candidate comes from a RunView entity page, which keeps the driver's value for a
666
+ // BIGINT column, and the SQL Server driver returns BIGINT as a string (the ledger refuses strings).
667
+ const capturedSamples = Number(candidate.RecordingCapturedSamples ?? 0);
668
+ const stamped = await ledger.StampTerminal(sessionID, expectedShards, capturedSamples);
669
+ if (stamped.RecordingStatus == null) {
670
+ LogError(
671
+ `[SessionJanitor] Could not restore the recording status of session ${sessionID} ` +
672
+ `(terminal stamp of ${expectedShards} shard(s), ${capturedSamples} sample(s) was ${stamped.Outcome}); backing off`,
673
+ );
674
+ return false;
675
+ }
676
+ LogStatus(
677
+ `[SessionJanitor] Restored recording status ${stamped.RecordingStatus} for session ${sessionID} ` +
678
+ `(stamped its stored counts: ${expectedShards} shard(s), ${capturedSamples} sample(s))`,
679
+ );
680
+ return true;
681
+ }
682
+
683
+ /** Counts a failed ledger assembly in its backoff (see {@link RecoveryBackoffTracker.RecordFailure}). */
684
+ private recordLedgerFailure(sessionID: string, attempted: boolean): RecoveryVisit {
685
+ this.ledgerBackoff.RecordFailure(sessionID, 'Failed');
686
+ return { Attempted: attempted, Recovered: false };
687
+ }
688
+
689
+ /**
690
+ * Deletes up to {@link MAX_PURGED_SHARDS_PER_PASS} ledger shards of sessions closed before the lookback
691
+ * window, which no sweep assembles any more. Never throws: a failure is logged and retried next pass.
692
+ */
693
+ private async purgeExpiredLedgerShards(provider: DatabaseProviderBase, systemUser: UserInfo): Promise<void> {
694
+ const { lookbackCutoff } = this.recoveryWindow();
695
+ try {
696
+ const purged = await new SqlRealtimeRecordingLedger(provider, systemUser).PurgeClosedBefore(lookbackCutoff, MAX_PURGED_SHARDS_PER_PASS);
697
+ if (purged > 0) {
698
+ LogStatus(`[SessionJanitor] Purged ${purged} recording shard(s) of sessions closed before ${lookbackCutoff.toISOString()}`);
699
+ }
700
+ } catch (err) {
701
+ LogError(
702
+ `[SessionJanitor] Recording shard purge (sessions closed before ${lookbackCutoff.toISOString()}) failed: ` +
703
+ `${err instanceof Error ? err.message : String(err)}`,
704
+ );
705
+ }
706
+ }
707
+
412
708
  /** Parses the absolute deadline (ms) from a session's `Config_` JSON, or null when absent/malformed. */
413
709
  private parseSessionDeadlineMs(configJson: string | null | undefined): number | null {
414
710
  if (!configJson) {
@@ -473,15 +769,38 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
473
769
  this.startRecoveryPass(this._provider, this._systemUser);
474
770
  }
475
771
 
772
+ /**
773
+ * One recording recovery pass: purge expired ledger shards, then the ledger sweep, then legacy
774
+ * segment recovery. The ledger sweep goes first because its sessions are the current recording path;
775
+ * a failed ledger sweep is logged here so legacy recovery still runs. A legacy failure rejects (logged
776
+ * by {@link startRecoveryPass}).
777
+ */
778
+ private async runRecoveryPass(provider: DatabaseProviderBase, systemUser: UserInfo): Promise<void> {
779
+ await this.purgeExpiredLedgerShards(provider, systemUser);
780
+ // A Stop() during a step ends the pass before the next one, so Shutdown waits on less work.
781
+ if (this._stopped) {
782
+ return;
783
+ }
784
+ try {
785
+ await this.RunRecordingLedgerSweep(provider, systemUser);
786
+ } catch (err) {
787
+ LogError(`SessionJanitor recording ledger sweep failed: ${err instanceof Error ? err.message : String(err)}`);
788
+ }
789
+ if (this._stopped) {
790
+ return;
791
+ }
792
+ await this.RunRecordingRecoverySweep(provider, systemUser);
793
+ }
794
+
476
795
  /**
477
796
  * Fire-and-forget recording recovery, skipped when the previous pass is still running or the janitor
478
797
  * has been stopped (a tick that was mid-close-sweep when {@link Stop} ran must not start one).
479
798
  */
480
- private startRecoveryPass(provider: IMetadataProvider, systemUser: UserInfo): void {
799
+ private startRecoveryPass(provider: DatabaseProviderBase, systemUser: UserInfo): void {
481
800
  if (this._stopped || this._recoveryPass) {
482
801
  return;
483
802
  }
484
- this._recoveryPass = this.RunRecordingRecoverySweep(provider, systemUser)
803
+ this._recoveryPass = this.runRecoveryPass(provider, systemUser)
485
804
  .then(
486
805
  () => undefined,
487
806
  err => {
@@ -544,7 +863,8 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
544
863
  }
545
864
  const storage = await this.resolveRecoveryStorage(candidate.AgentID, context);
546
865
  if (storage.Kind === 'NoAccount') {
547
- return this.deferRecovery(candidate.ID, `no recording storage account for agent ${candidate.AgentID}`);
866
+ this.recoveryBackoff.Defer(candidate.ID, `no recording storage account for agent ${candidate.AgentID}`);
867
+ return { Attempted: false, Recovered: false };
548
868
  }
549
869
  if (storage.Kind === 'AgentUnavailable') {
550
870
  LogStatus(`[SessionJanitor] Recording recovery skipped for session ${candidate.ID}: agent ${candidate.AgentID} could not be loaded`);
@@ -552,13 +872,17 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
552
872
  }
553
873
  const accountID = storage.AccountID;
554
874
  attempted = true;
555
- const result = await this.recoverWithinTimeout({
556
- SessionID: candidate.ID,
557
- StorageAccountID: accountID,
558
- StartedAt: startedAt,
559
- ContextUser: context.systemUser,
560
- Provider: context.provider,
561
- });
875
+ const result = await this.runWithinAttemptTimeout(
876
+ candidate.ID,
877
+ RecoverRealtimeRecordingFromSegments({
878
+ SessionID: candidate.ID,
879
+ StorageAccountID: accountID,
880
+ StartedAt: startedAt,
881
+ ContextUser: context.systemUser,
882
+ Provider: context.provider,
883
+ }),
884
+ late => late.Outcome,
885
+ );
562
886
  if (result === 'TimedOut') {
563
887
  LogError(
564
888
  `[SessionJanitor] Recording recovery for session ${candidate.ID} did not finish within ` +
@@ -569,11 +893,11 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
569
893
  }
570
894
  switch (result.Outcome) {
571
895
  case 'Recovered':
572
- this.recoveryBackoff.delete(candidate.ID.toLowerCase());
896
+ this.recoveryBackoff.Clear(candidate.ID);
573
897
  LogStatus(`[SessionJanitor] Recovered recording for session ${candidate.ID} from ${result.SegmentCount} segment(s) (file ${result.FileID})`);
574
898
  return { Attempted: true, Recovered: true };
575
899
  case 'Superseded':
576
- this.recoveryBackoff.delete(candidate.ID.toLowerCase());
900
+ this.recoveryBackoff.Clear(candidate.ID);
577
901
  LogStatus(`[SessionJanitor] Session ${candidate.ID} was given a recording by another writer; recovery left it alone`);
578
902
  return { Attempted: true, Recovered: false };
579
903
  case 'NoSegments':
@@ -592,13 +916,12 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
592
916
  }
593
917
 
594
918
  /**
595
- * Runs one session's recovery, giving up on waiting after `recordingRecoveryAttemptTimeoutMinutes`.
596
- * A call cannot be cancelled, so a timed-out one keeps running: the session is marked abandoned until
597
- * it settles (later sweeps skip it), and its late outcome is logged then.
919
+ * Waits for one session's recovery attempt for at most `recordingRecoveryAttemptTimeoutMinutes`.
920
+ * An attempt cannot be cancelled, so a timed-out one keeps running: the session is marked abandoned
921
+ * until it settles (later sweeps skip it), and its late outcome (`describeLate`) is logged then.
598
922
  */
599
- private async recoverWithinTimeout(input: RecoverRealtimeRecordingInput): Promise<RecoverRealtimeRecordingResult | 'TimedOut'> {
600
- const key = input.SessionID.toLowerCase();
601
- const attempt = RecoverRealtimeRecordingFromSegments(input);
923
+ private async runWithinAttemptTimeout<T>(sessionID: string, attempt: Promise<T>, describeLate: (late: T) => string): Promise<T | 'TimedOut'> {
924
+ const key = sessionID.toLowerCase();
602
925
  let timer: ReturnType<typeof setTimeout> | undefined;
603
926
  const timedOut = new Promise<'TimedOut'>(resolve => {
604
927
  timer = setTimeout(() => resolve('TimedOut'), this._config.recordingRecoveryAttemptTimeoutMinutes * 60_000);
@@ -609,8 +932,8 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
609
932
  if (outcome === 'TimedOut') {
610
933
  this.abandonedRecoveries.add(key);
611
934
  attempt.then(
612
- late => LogStatus(`[SessionJanitor] Abandoned recording recovery for session ${input.SessionID} finished late: ${late.Outcome}`),
613
- err => LogError(`[SessionJanitor] Abandoned recording recovery for session ${input.SessionID} failed late: ${err instanceof Error ? err.message : String(err)}`),
935
+ late => LogStatus(`[SessionJanitor] Abandoned recording recovery for session ${sessionID} finished late: ${describeLate(late)}`),
936
+ err => LogError(`[SessionJanitor] Abandoned recording recovery for session ${sessionID} failed late: ${err instanceof Error ? err.message : String(err)}`),
614
937
  ).finally(() => this.abandonedRecoveries.delete(key));
615
938
  }
616
939
  return outcome;
@@ -638,21 +961,6 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
638
961
  return true;
639
962
  }
640
963
 
641
- /**
642
- * Defers a session that cannot be recovered until configuration changes (its agent has no recording
643
- * storage account). That is not a failed attempt, so it never counts toward the give-up: the session
644
- * is re-checked every {@link RECOVERY_BACKOFF_MAX_MS} and logged only the first time.
645
- */
646
- private deferRecovery(sessionID: string, reason: string): RecoveryVisit {
647
- const key = sessionID.toLowerCase();
648
- const state = this.recoveryBackoff.get(key);
649
- if (!state?.Deferred) {
650
- LogStatus(`[SessionJanitor] Recording recovery skipped for session ${sessionID}: ${reason}; re-checked every ${RECOVERY_BACKOFF_MAX_MS / 3_600_000} h`);
651
- }
652
- this.recoveryBackoff.set(key, { Failures: state?.Failures ?? 0, NextAttemptAt: Date.now() + RECOVERY_BACKOFF_MAX_MS, Deferred: true });
653
- return { Attempted: false, Recovered: false };
654
- }
655
-
656
964
  /**
657
965
  * Loads the agent carrying a session's recording storage config and resolves its storage account,
658
966
  * memoized per agent for the sweep: the resolve may force a storage-engine reload, so it must not
@@ -677,44 +985,12 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
677
985
  return storage;
678
986
  }
679
987
 
680
- /** True while a session's backoff (or give-up) window has not elapsed. */
681
- private isRecoveryBackedOff(sessionID: string): boolean {
682
- const state = this.recoveryBackoff.get(sessionID.toLowerCase());
683
- return state != null && Date.now() < state.NextAttemptAt;
684
- }
685
-
686
- /**
687
- * Counts one more unsuccessful recovery for a session and schedules its next attempt
688
- * (`min(2^k * 5 min, 6 h)`); at {@link MAX_RECOVERY_ATTEMPTS} it stops checking the session for this
689
- * process with a single log line: a `LogError` when the last attempt failed, a `LogStatus` when it
690
- * simply found no shards (nothing to recover is not an error).
691
- */
988
+ /** Counts a failed legacy recovery in its backoff (see {@link RecoveryBackoffTracker.RecordFailure}). */
692
989
  private recordRecoveryFailure(sessionID: string, attempted: boolean, kind: 'Failed' | 'NoSegments' = 'Failed'): RecoveryVisit {
693
- const key = sessionID.toLowerCase();
694
- const failures = (this.recoveryBackoff.get(key)?.Failures ?? 0) + 1;
695
- if (failures >= MAX_RECOVERY_ATTEMPTS) {
696
- this.recoveryBackoff.set(key, { Failures: failures, NextAttemptAt: Infinity, Deferred: false });
697
- if (kind === 'NoSegments') {
698
- LogStatus(`[SessionJanitor] Session ${sessionID} had no recoverable segments in ${failures} checks; not checking it again until restart`);
699
- } else {
700
- LogError(`[SessionJanitor] Giving up on recording recovery for session ${sessionID} after ${failures} failed attempts`);
701
- }
702
- } else {
703
- const waitMs = Math.min(2 ** failures * RECOVERY_BACKOFF_BASE_MS, RECOVERY_BACKOFF_MAX_MS);
704
- this.recoveryBackoff.set(key, { Failures: failures, NextAttemptAt: Date.now() + waitMs, Deferred: false });
705
- }
990
+ this.recoveryBackoff.RecordFailure(sessionID, kind);
706
991
  return { Attempted: attempted, Recovered: false };
707
992
  }
708
993
 
709
- /** Drops backoff state for sessions that were not in the latest complete candidate set. */
710
- private pruneRecoveryBackoff(candidateKeys: ReadonlySet<string>): void {
711
- for (const key of this.recoveryBackoff.keys()) {
712
- if (!candidateKeys.has(key)) {
713
- this.recoveryBackoff.delete(key);
714
- }
715
- }
716
- }
717
-
718
994
  /**
719
995
  * Page through every session matching `filter` with keyset (`AfterKey`) pagination and close each
720
996
  * via {@link SessionManager.CloseSession}, stamping `closeReason` on every row it transitions.