@memberjunction/server 6.2.0-edge.3 → 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 (543) hide show
  1. package/README.md +20 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/agentSessions/BridgeHostInstance.d.ts +25 -0
  4. package/dist/agentSessions/BridgeHostInstance.d.ts.map +1 -0
  5. package/dist/agentSessions/BridgeHostInstance.js +30 -0
  6. package/dist/agentSessions/BridgeHostInstance.js.map +1 -0
  7. package/dist/agentSessions/HostInstance.d.ts +75 -11
  8. package/dist/agentSessions/HostInstance.d.ts.map +1 -1
  9. package/dist/agentSessions/HostInstance.js +99 -28
  10. package/dist/agentSessions/HostInstance.js.map +1 -1
  11. package/dist/agentSessions/ReturningVisitorRecap.d.ts.map +1 -1
  12. package/dist/agentSessions/ReturningVisitorRecap.js +7 -3
  13. package/dist/agentSessions/ReturningVisitorRecap.js.map +1 -1
  14. package/dist/agentSessions/SessionJanitor.d.ts +236 -27
  15. package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
  16. package/dist/agentSessions/SessionJanitor.js +682 -54
  17. package/dist/agentSessions/SessionJanitor.js.map +1 -1
  18. package/dist/agentSessions/SessionManager.d.ts +48 -3
  19. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  20. package/dist/agentSessions/SessionManager.js +112 -9
  21. package/dist/agentSessions/SessionManager.js.map +1 -1
  22. package/dist/agentSessions/SessionRunIDVerifier.d.ts +63 -0
  23. package/dist/agentSessions/SessionRunIDVerifier.d.ts.map +1 -0
  24. package/dist/agentSessions/SessionRunIDVerifier.js +129 -0
  25. package/dist/agentSessions/SessionRunIDVerifier.js.map +1 -0
  26. package/dist/agentSessions/index.d.ts +2 -0
  27. package/dist/agentSessions/index.d.ts.map +1 -1
  28. package/dist/agentSessions/index.js +10 -3
  29. package/dist/agentSessions/index.js.map +1 -1
  30. package/dist/apiRoot.d.ts +32 -0
  31. package/dist/apiRoot.d.ts.map +1 -0
  32. package/dist/apiRoot.js +67 -0
  33. package/dist/apiRoot.js.map +1 -0
  34. package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -1
  35. package/dist/auth/AuthProviderCatalogRouter.js +25 -0
  36. package/dist/auth/AuthProviderCatalogRouter.js.map +1 -1
  37. package/dist/auth/LoginScreenEngine.d.ts +22 -0
  38. package/dist/auth/LoginScreenEngine.d.ts.map +1 -0
  39. package/dist/auth/LoginScreenEngine.js +57 -0
  40. package/dist/auth/LoginScreenEngine.js.map +1 -0
  41. package/dist/auth/actionAuthorization.d.ts +40 -0
  42. package/dist/auth/actionAuthorization.d.ts.map +1 -0
  43. package/dist/auth/actionAuthorization.js +94 -0
  44. package/dist/auth/actionAuthorization.js.map +1 -0
  45. package/dist/auth/index.d.ts +1 -0
  46. package/dist/auth/index.d.ts.map +1 -1
  47. package/dist/auth/index.js +4 -10
  48. package/dist/auth/index.js.map +1 -1
  49. package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
  50. package/dist/auth/magicLink/MagicLinkRouter.js +3 -2
  51. package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
  52. package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
  53. package/dist/auth/magicLink/MagicLinkService.js +6 -3
  54. package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
  55. package/dist/auth/newUsers.d.ts +10 -0
  56. package/dist/auth/newUsers.d.ts.map +1 -1
  57. package/dist/auth/newUsers.js +80 -11
  58. package/dist/auth/newUsers.js.map +1 -1
  59. package/dist/config.d.ts +2514 -22
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/config.js +166 -4
  62. package/dist/config.js.map +1 -1
  63. package/dist/context.d.ts +8 -0
  64. package/dist/context.d.ts.map +1 -1
  65. package/dist/context.js +14 -11
  66. package/dist/context.js.map +1 -1
  67. package/dist/coreRoutes.d.ts +29 -0
  68. package/dist/coreRoutes.d.ts.map +1 -0
  69. package/dist/coreRoutes.js +142 -0
  70. package/dist/coreRoutes.js.map +1 -0
  71. package/dist/generated/graphql-schemas/__mj.d.ts +504 -3
  72. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  73. package/dist/generated/graphql-schemas/__mj.js +2456 -7
  74. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  75. package/dist/generic/CacheInvalidationResolver.d.ts +43 -8
  76. package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
  77. package/dist/generic/CacheInvalidationResolver.js +59 -19
  78. package/dist/generic/CacheInvalidationResolver.js.map +1 -1
  79. package/dist/generic/OrphanedConversationDetailReconciler.js +3 -3
  80. package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -1
  81. package/dist/generic/PushStatusResolver.js +1 -1
  82. package/dist/generic/PushStatusResolver.js.map +1 -1
  83. package/dist/generic/ResolverBase.d.ts +123 -48
  84. package/dist/generic/ResolverBase.d.ts.map +1 -1
  85. package/dist/generic/ResolverBase.js +293 -305
  86. package/dist/generic/ResolverBase.js.map +1 -1
  87. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  88. package/dist/generic/RunViewResolver.js +16 -0
  89. package/dist/generic/RunViewResolver.js.map +1 -1
  90. package/dist/globalCors.d.ts +33 -0
  91. package/dist/globalCors.d.ts.map +1 -0
  92. package/dist/globalCors.js +58 -0
  93. package/dist/globalCors.js.map +1 -0
  94. package/dist/index.d.ts +17 -0
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +219 -278
  97. package/dist/index.js.map +1 -1
  98. package/dist/isolatedProvider.d.ts +20 -0
  99. package/dist/isolatedProvider.d.ts.map +1 -0
  100. package/dist/isolatedProvider.js +22 -0
  101. package/dist/isolatedProvider.js.map +1 -0
  102. package/dist/multiTenancy/index.d.ts.map +1 -1
  103. package/dist/multiTenancy/index.js +22 -2
  104. package/dist/multiTenancy/index.js.map +1 -1
  105. package/dist/realtimeProxy/RealtimeProxyServer.d.ts +44 -4
  106. package/dist/realtimeProxy/RealtimeProxyServer.d.ts.map +1 -1
  107. package/dist/realtimeProxy/RealtimeProxyServer.js +121 -16
  108. package/dist/realtimeProxy/RealtimeProxyServer.js.map +1 -1
  109. package/dist/realtimeProxy/RealtimeRelayTunnel.d.ts +171 -0
  110. package/dist/realtimeProxy/RealtimeRelayTunnel.d.ts.map +1 -0
  111. package/dist/realtimeProxy/RealtimeRelayTunnel.js +532 -0
  112. package/dist/realtimeProxy/RealtimeRelayTunnel.js.map +1 -0
  113. package/dist/realtimeSessions/RealtimeSessionEventService.d.ts +148 -0
  114. package/dist/realtimeSessions/RealtimeSessionEventService.d.ts.map +1 -0
  115. package/dist/realtimeSessions/RealtimeSessionEventService.js +191 -0
  116. package/dist/realtimeSessions/RealtimeSessionEventService.js.map +1 -0
  117. package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts +66 -0
  118. package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts.map +1 -0
  119. package/dist/realtimeSessions/RealtimeSessionVerificationService.js +139 -0
  120. package/dist/realtimeSessions/RealtimeSessionVerificationService.js.map +1 -0
  121. package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts +38 -0
  122. package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts.map +1 -0
  123. package/dist/realtimeSessions/RealtimeVerifyRouter.js +99 -0
  124. package/dist/realtimeSessions/RealtimeVerifyRouter.js.map +1 -0
  125. package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts +100 -0
  126. package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts.map +1 -0
  127. package/dist/realtimeSessions/RecordingAssemblyScheduler.js +173 -0
  128. package/dist/realtimeSessions/RecordingAssemblyScheduler.js.map +1 -0
  129. package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts +61 -0
  130. package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts.map +1 -0
  131. package/dist/realtimeSessions/SessionVerificationPortsBase.js +149 -0
  132. package/dist/realtimeSessions/SessionVerificationPortsBase.js.map +1 -0
  133. package/dist/realtimeSessions/SlidingWindowRateLimiter.d.ts +57 -0
  134. package/dist/realtimeSessions/SlidingWindowRateLimiter.d.ts.map +1 -0
  135. package/dist/realtimeSessions/SlidingWindowRateLimiter.js +69 -0
  136. package/dist/realtimeSessions/SlidingWindowRateLimiter.js.map +1 -0
  137. package/dist/realtimeSessions/index.d.ts +19 -0
  138. package/dist/realtimeSessions/index.d.ts.map +1 -0
  139. package/dist/realtimeSessions/index.js +19 -0
  140. package/dist/realtimeSessions/index.js.map +1 -0
  141. package/dist/realtimeSessions/ownedRealtimeSession.d.ts +30 -0
  142. package/dist/realtimeSessions/ownedRealtimeSession.d.ts.map +1 -0
  143. package/dist/realtimeSessions/ownedRealtimeSession.js +47 -0
  144. package/dist/realtimeSessions/ownedRealtimeSession.js.map +1 -0
  145. package/dist/realtimeSessions/sessionAccess.d.ts +92 -0
  146. package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -0
  147. package/dist/realtimeSessions/sessionAccess.js +94 -0
  148. package/dist/realtimeSessions/sessionAccess.js.map +1 -0
  149. package/dist/realtimeSessions/sessionConfigStamps.d.ts +70 -0
  150. package/dist/realtimeSessions/sessionConfigStamps.d.ts.map +1 -0
  151. package/dist/realtimeSessions/sessionConfigStamps.js +93 -0
  152. package/dist/realtimeSessions/sessionConfigStamps.js.map +1 -0
  153. package/dist/realtimeSessions/subscriptionAuthorization.d.ts +65 -0
  154. package/dist/realtimeSessions/subscriptionAuthorization.d.ts.map +1 -0
  155. package/dist/realtimeSessions/subscriptionAuthorization.js +71 -0
  156. package/dist/realtimeSessions/subscriptionAuthorization.js.map +1 -0
  157. package/dist/realtimeSessions/verificationCore.d.ts +243 -0
  158. package/dist/realtimeSessions/verificationCore.d.ts.map +1 -0
  159. package/dist/realtimeSessions/verificationCore.js +356 -0
  160. package/dist/realtimeSessions/verificationCore.js.map +1 -0
  161. package/dist/realtimeSessions/verificationPages.d.ts +66 -0
  162. package/dist/realtimeSessions/verificationPages.d.ts.map +1 -0
  163. package/dist/realtimeSessions/verificationPages.js +110 -0
  164. package/dist/realtimeSessions/verificationPages.js.map +1 -0
  165. package/dist/realtimeSessions/verificationPolicy.d.ts +147 -0
  166. package/dist/realtimeSessions/verificationPolicy.d.ts.map +1 -0
  167. package/dist/realtimeSessions/verificationPolicy.js +249 -0
  168. package/dist/realtimeSessions/verificationPolicy.js.map +1 -0
  169. package/dist/realtimeSessions/verificationWorkflow.d.ts +263 -0
  170. package/dist/realtimeSessions/verificationWorkflow.d.ts.map +1 -0
  171. package/dist/realtimeSessions/verificationWorkflow.js +435 -0
  172. package/dist/realtimeSessions/verificationWorkflow.js.map +1 -0
  173. package/dist/realtimeWidget/widgetGuestElevation.d.ts +20 -2
  174. package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
  175. package/dist/realtimeWidget/widgetGuestElevation.js +31 -2
  176. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
  177. package/dist/resolvers/ActionResolver.d.ts +31 -5
  178. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  179. package/dist/resolvers/ActionResolver.js +75 -14
  180. package/dist/resolvers/ActionResolver.js.map +1 -1
  181. package/dist/resolvers/AdhocQueryResolver.d.ts +14 -1
  182. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
  183. package/dist/resolvers/AdhocQueryResolver.js +21 -5
  184. package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
  185. package/dist/resolvers/ArtifactFileResolver.d.ts +5 -1
  186. package/dist/resolvers/ArtifactFileResolver.d.ts.map +1 -1
  187. package/dist/resolvers/ArtifactFileResolver.js +9 -7
  188. package/dist/resolvers/ArtifactFileResolver.js.map +1 -1
  189. package/dist/resolvers/CacheStatsResolver.d.ts +27 -0
  190. package/dist/resolvers/CacheStatsResolver.d.ts.map +1 -1
  191. package/dist/resolvers/CacheStatsResolver.js +113 -2
  192. package/dist/resolvers/CacheStatsResolver.js.map +1 -1
  193. package/dist/resolvers/ClientToolRequestResolver.d.ts +4 -2
  194. package/dist/resolvers/ClientToolRequestResolver.d.ts.map +1 -1
  195. package/dist/resolvers/ClientToolRequestResolver.js +28 -10
  196. package/dist/resolvers/ClientToolRequestResolver.js.map +1 -1
  197. package/dist/resolvers/ConversationRunEventsResolver.d.ts +8 -4
  198. package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -1
  199. package/dist/resolvers/ConversationRunEventsResolver.js +6 -6
  200. package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -1
  201. package/dist/resolvers/DatasetResolver.d.ts +24 -3
  202. package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
  203. package/dist/resolvers/DatasetResolver.js +57 -8
  204. package/dist/resolvers/DatasetResolver.js.map +1 -1
  205. package/dist/resolvers/EntityRecordNameResolver.d.ts +2 -1
  206. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  207. package/dist/resolvers/EntityRecordNameResolver.js +3 -2
  208. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  209. package/dist/resolvers/FetchEntityVectorsResolver.d.ts +27 -1
  210. package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
  211. package/dist/resolvers/FetchEntityVectorsResolver.js +148 -2
  212. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  213. package/dist/resolvers/FileResolver.d.ts +82 -11
  214. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  215. package/dist/resolvers/FileResolver.js +279 -101
  216. package/dist/resolvers/FileResolver.js.map +1 -1
  217. package/dist/resolvers/GetDataContextDataResolver.d.ts +33 -7
  218. package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
  219. package/dist/resolvers/GetDataContextDataResolver.js +117 -38
  220. package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
  221. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  222. package/dist/resolvers/GetDataResolver.js +6 -3
  223. package/dist/resolvers/GetDataResolver.js.map +1 -1
  224. package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -1
  225. package/dist/resolvers/HumanHandoffResolver.js +7 -1
  226. package/dist/resolvers/HumanHandoffResolver.js.map +1 -1
  227. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  228. package/dist/resolvers/IntegrationDiscoveryResolver.js +4 -2
  229. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  230. package/dist/resolvers/MCPResolver.d.ts.map +1 -1
  231. package/dist/resolvers/MCPResolver.js +9 -4
  232. package/dist/resolvers/MCPResolver.js.map +1 -1
  233. package/dist/resolvers/QueryResolver.d.ts +2 -0
  234. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  235. package/dist/resolvers/QueryResolver.js +13 -0
  236. package/dist/resolvers/QueryResolver.js.map +1 -1
  237. package/dist/resolvers/RealtimeBridgeResolver.d.ts +54 -2
  238. package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
  239. package/dist/resolvers/RealtimeBridgeResolver.js +155 -10
  240. package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
  241. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +157 -33
  242. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  243. package/dist/resolvers/RealtimeClientSessionResolver.js +404 -116
  244. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  245. package/dist/resolvers/RealtimeSessionEventsResolver.d.ts +61 -0
  246. package/dist/resolvers/RealtimeSessionEventsResolver.d.ts.map +1 -0
  247. package/dist/resolvers/RealtimeSessionEventsResolver.js +130 -0
  248. package/dist/resolvers/RealtimeSessionEventsResolver.js.map +1 -0
  249. package/dist/resolvers/RealtimeSessionVerificationResolver.d.ts +85 -0
  250. package/dist/resolvers/RealtimeSessionVerificationResolver.d.ts.map +1 -0
  251. package/dist/resolvers/RealtimeSessionVerificationResolver.js +188 -0
  252. package/dist/resolvers/RealtimeSessionVerificationResolver.js.map +1 -0
  253. package/dist/resolvers/RemoteBrowserActionResolver.d.ts +2 -0
  254. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
  255. package/dist/resolvers/RemoteBrowserActionResolver.js +5 -1
  256. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
  257. package/dist/resolvers/RunAIAgentResolver.d.ts +10 -0
  258. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  259. package/dist/resolvers/RunAIAgentResolver.js +100 -14
  260. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  261. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
  262. package/dist/resolvers/RunTemplateResolver.js +12 -11
  263. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  264. package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts +12 -1
  265. package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts.map +1 -1
  266. package/dist/resolvers/SearchKnowledgeStreamResolver.js +15 -2
  267. package/dist/resolvers/SearchKnowledgeStreamResolver.js.map +1 -1
  268. package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
  269. package/dist/resolvers/SqlLoggingConfigResolver.js +2 -3
  270. package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
  271. package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
  272. package/dist/resolvers/SyncDataResolver.js +6 -2
  273. package/dist/resolvers/SyncDataResolver.js.map +1 -1
  274. package/dist/resolvers/SyncRolesUsersResolver.d.ts +2 -0
  275. package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
  276. package/dist/resolvers/SyncRolesUsersResolver.js +23 -6
  277. package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
  278. package/dist/resolvers/TaskGraphFrameResolver.d.ts +2 -1
  279. package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
  280. package/dist/resolvers/TaskGraphFrameResolver.js +28 -1
  281. package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
  282. package/dist/resolvers/TestQuerySQLResolver.d.ts +24 -0
  283. package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
  284. package/dist/resolvers/TestQuerySQLResolver.js +65 -13
  285. package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
  286. package/dist/resolvers/TransactionGroupResolver.d.ts +19 -2
  287. package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
  288. package/dist/resolvers/TransactionGroupResolver.js +102 -24
  289. package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
  290. package/dist/resolvers/UserAvatarResolver.d.ts +16 -0
  291. package/dist/resolvers/UserAvatarResolver.d.ts.map +1 -0
  292. package/dist/resolvers/UserAvatarResolver.js +124 -0
  293. package/dist/resolvers/UserAvatarResolver.js.map +1 -0
  294. package/dist/resolvers/UserResolver.d.ts.map +1 -1
  295. package/dist/resolvers/UserResolver.js +2 -17
  296. package/dist/resolvers/UserResolver.js.map +1 -1
  297. package/dist/resolvers/agent-run-data-guard.d.ts +31 -0
  298. package/dist/resolvers/agent-run-data-guard.d.ts.map +1 -0
  299. package/dist/resolvers/agent-run-data-guard.js +72 -0
  300. package/dist/resolvers/agent-run-data-guard.js.map +1 -0
  301. package/dist/resolvers/agentVisionConsentAudit.d.ts +37 -0
  302. package/dist/resolvers/agentVisionConsentAudit.d.ts.map +1 -0
  303. package/dist/resolvers/agentVisionConsentAudit.js +63 -0
  304. package/dist/resolvers/agentVisionConsentAudit.js.map +1 -0
  305. package/dist/resolvers/avatarInputValidation.d.ts +40 -0
  306. package/dist/resolvers/avatarInputValidation.d.ts.map +1 -0
  307. package/dist/resolvers/avatarInputValidation.js +97 -0
  308. package/dist/resolvers/avatarInputValidation.js.map +1 -0
  309. package/dist/resolvers/currentUserRoles.d.ts +29 -0
  310. package/dist/resolvers/currentUserRoles.d.ts.map +1 -1
  311. package/dist/resolvers/currentUserRoles.js +30 -0
  312. package/dist/resolvers/currentUserRoles.js.map +1 -1
  313. package/dist/resolvers/meetings/meetingMutations.d.ts +6 -0
  314. package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -1
  315. package/dist/resolvers/meetings/meetingMutations.js +21 -0
  316. package/dist/resolvers/meetings/meetingMutations.js.map +1 -1
  317. package/dist/rest/RealtimeRecordingHandler.d.ts +49 -0
  318. package/dist/rest/RealtimeRecordingHandler.d.ts.map +1 -0
  319. package/dist/rest/RealtimeRecordingHandler.js +383 -0
  320. package/dist/rest/RealtimeRecordingHandler.js.map +1 -0
  321. package/dist/rest/ViewOperationsHandler.d.ts +3 -1
  322. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  323. package/dist/rest/ViewOperationsHandler.js +21 -12
  324. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  325. package/dist/rest/setupRESTEndpoints.d.ts +6 -0
  326. package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
  327. package/dist/rest/setupRESTEndpoints.js +6 -0
  328. package/dist/rest/setupRESTEndpoints.js.map +1 -1
  329. package/dist/serverExtensionReservedRoots.d.ts +12 -10
  330. package/dist/serverExtensionReservedRoots.d.ts.map +1 -1
  331. package/dist/serverExtensionReservedRoots.js +14 -17
  332. package/dist/serverExtensionReservedRoots.js.map +1 -1
  333. package/dist/services/MJStorageBlobStore.d.ts +8 -1
  334. package/dist/services/MJStorageBlobStore.d.ts.map +1 -1
  335. package/dist/services/MJStorageBlobStore.js +25 -19
  336. package/dist/services/MJStorageBlobStore.js.map +1 -1
  337. package/dist/services/StartTaskGraphDispatcher.js +2 -2
  338. package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
  339. package/dist/services/TaskGraphActionRunner.d.ts +5 -3
  340. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
  341. package/dist/services/TaskGraphActionRunner.js +27 -5
  342. package/dist/services/TaskGraphActionRunner.js.map +1 -1
  343. package/dist/services/TaskGraphAgentRunner.d.ts +14 -2
  344. package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
  345. package/dist/services/TaskGraphAgentRunner.js +157 -9
  346. package/dist/services/TaskGraphAgentRunner.js.map +1 -1
  347. package/dist/services/TaskGraphContinuationDeliverer.d.ts +57 -4
  348. package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
  349. package/dist/services/TaskGraphContinuationDeliverer.js +607 -18
  350. package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
  351. package/dist/sharedCache.d.ts +105 -0
  352. package/dist/sharedCache.d.ts.map +1 -0
  353. package/dist/sharedCache.js +279 -0
  354. package/dist/sharedCache.js.map +1 -0
  355. package/dist/types.d.ts +6 -0
  356. package/dist/types.d.ts.map +1 -1
  357. package/dist/types.js.map +1 -1
  358. package/package.json +99 -96
  359. package/src/__tests__/ActionResolver.authorization.test.ts +353 -0
  360. package/src/__tests__/AdhocQueryResolver.sqlAuthorizer.test.ts +104 -0
  361. package/src/__tests__/BridgeHostInstance.test.ts +232 -0
  362. package/src/__tests__/ClientToolRequestResolver.test.ts +116 -0
  363. package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
  364. package/src/__tests__/EntityRecordNameResolver.rowSecurity.test.ts +152 -0
  365. package/src/__tests__/FetchEntityVectorsResolver.test.ts +433 -0
  366. package/src/__tests__/FileResolver.storageAccess.test.ts +562 -0
  367. package/src/__tests__/GetDataContextDataResolver.sqlItems.test.ts +416 -0
  368. package/src/__tests__/HostInstance.test.ts +84 -0
  369. package/src/__tests__/HumanHandoffResolver.test.ts +17 -0
  370. package/src/__tests__/ListOperationsResolver.adhocScreen.test.ts +137 -0
  371. package/src/__tests__/LoginScreenEngine.test.ts +86 -0
  372. package/src/__tests__/MJStorageBlobStore.storageGate.test.ts +63 -0
  373. package/src/__tests__/MeetingResolver.test.ts +75 -1
  374. package/src/__tests__/RealtimeBridgeResolver.test.ts +415 -6
  375. package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +383 -0
  376. package/src/__tests__/RealtimeClientSessionResolver.test.ts +682 -58
  377. package/src/__tests__/RealtimeProxyServer.test.ts +16 -1
  378. package/src/__tests__/RealtimeRelay.test.ts +876 -0
  379. package/src/__tests__/RecordingAssemblyScheduler.test.ts +178 -0
  380. package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +129 -0
  381. package/src/__tests__/ResolverBase.adhocReadScope.test.ts +133 -2
  382. package/src/__tests__/ResolverBase.aggregateScreen.test.ts +174 -0
  383. package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
  384. package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +127 -0
  385. package/src/__tests__/ResolverBase.clientOldValues.test.ts +212 -6
  386. package/src/__tests__/ResolverBase.filterEscaping.test.ts +5 -2
  387. package/src/__tests__/ReturningVisitorRecap.test.ts +115 -0
  388. package/src/__tests__/RunAIAgentResolver.branch.test.ts +170 -0
  389. package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +6 -4
  390. package/src/__tests__/RunAIAgentResolver.runDataGuard.test.ts +258 -0
  391. package/src/__tests__/RunAIAgentResolver.stoppedPredecessor.test.ts +166 -0
  392. package/src/__tests__/RunTemplateResolver.test.ts +132 -0
  393. package/src/__tests__/RunViewResolver.cacheCheckForwarding.test.ts +63 -0
  394. package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +165 -0
  395. package/src/__tests__/SearchKnowledgeStreamResolver.test.ts +161 -0
  396. package/src/__tests__/SessionJanitor.test.ts +1203 -21
  397. package/src/__tests__/SessionManager.test.ts +240 -17
  398. package/src/__tests__/SessionRunIDVerifier.test.ts +251 -0
  399. package/src/__tests__/SyncRolesUsersResolver.userRoles.test.ts +97 -0
  400. package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +25 -0
  401. package/src/__tests__/TaskGraphAgentRunner.test.ts +266 -0
  402. package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +403 -22
  403. package/src/__tests__/TaskGraphFrameResolver.test.ts +22 -0
  404. package/src/__tests__/TestQuerySQLResolver.test.ts +274 -0
  405. package/src/__tests__/TransactionGroupResolver.refusals.test.ts +98 -1
  406. package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
  407. package/src/__tests__/UserAvatarResolver.test.ts +255 -0
  408. package/src/__tests__/UserResolver.currentUser.test.ts +16 -1
  409. package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +157 -0
  410. package/src/__tests__/actionAuthorization.test.ts +130 -0
  411. package/src/__tests__/agent-run-data-guard.test.ts +130 -0
  412. package/src/__tests__/agentVisionConsentAudit.test.ts +118 -0
  413. package/src/__tests__/apiRootBoot.test.ts +202 -0
  414. package/src/__tests__/avatarInputValidation.test.ts +160 -0
  415. package/src/__tests__/cacheInvalidationRecordData.test.ts +64 -0
  416. package/src/__tests__/conversationRunEvents.test.ts +56 -14
  417. package/src/__tests__/globalCors.test.ts +217 -0
  418. package/src/__tests__/isolatedProviderTransactions.test.ts +169 -0
  419. package/src/__tests__/magicLinkService.consume.test.ts +6 -0
  420. package/src/__tests__/magicLinkService.provisionLog.test.ts +5 -0
  421. package/src/__tests__/multiTenancy.postgresRewrite.test.ts +181 -0
  422. package/src/__tests__/multiTenancy.security.test.ts +51 -0
  423. package/src/__tests__/multiTenancy.test.ts +1 -1
  424. package/src/__tests__/newUsers.template.test.ts +151 -0
  425. package/src/__tests__/newUsers.test.ts +74 -5
  426. package/src/__tests__/orphanedConversationDetailReconciler.test.ts +17 -2
  427. package/src/__tests__/ownedRealtimeSession.test.ts +39 -0
  428. package/src/__tests__/pushStatusResolver.filter.test.ts +7 -0
  429. package/src/__tests__/realtimeRecordingHandler.test.ts +652 -0
  430. package/src/__tests__/realtimeSessions/RealtimeSessionEventService.test.ts +201 -0
  431. package/src/__tests__/realtimeSessions/RealtimeSessionEventsResolver.test.ts +215 -0
  432. package/src/__tests__/realtimeSessions/RealtimeSessionVerificationResolver.test.ts +102 -0
  433. package/src/__tests__/realtimeSessions/RealtimeSessionVerificationService.test.ts +349 -0
  434. package/src/__tests__/realtimeSessions/RealtimeVerifyRouter.test.ts +131 -0
  435. package/src/__tests__/realtimeSessions/SlidingWindowRateLimiter.test.ts +56 -0
  436. package/src/__tests__/realtimeSessions/helpers.ts +4 -0
  437. package/src/__tests__/realtimeSessions/sessionAccess.test.ts +129 -0
  438. package/src/__tests__/realtimeSessions/sessionConfigStamps.test.ts +124 -0
  439. package/src/__tests__/realtimeSessions/verificationCore.test.ts +355 -0
  440. package/src/__tests__/realtimeSessions/verificationPages.test.ts +84 -0
  441. package/src/__tests__/realtimeSessions/verificationPolicy.test.ts +226 -0
  442. package/src/__tests__/realtimeSessions/verificationWorkflow.test.ts +633 -0
  443. package/src/__tests__/resolverBase.fls.test.ts +87 -4
  444. package/src/__tests__/serverExtensionHostOverlay.test.ts +66 -0
  445. package/src/__tests__/serverExtensionReservedRoots.test.ts +11 -96
  446. package/src/__tests__/sharedCache.test.ts +312 -0
  447. package/src/__tests__/widgetGuestElevation.test.ts +44 -0
  448. package/src/agentSessions/BridgeHostInstance.ts +32 -0
  449. package/src/agentSessions/HostInstance.ts +124 -28
  450. package/src/agentSessions/ReturningVisitorRecap.ts +7 -3
  451. package/src/agentSessions/SessionJanitor.ts +821 -59
  452. package/src/agentSessions/SessionManager.ts +138 -26
  453. package/src/agentSessions/SessionRunIDVerifier.ts +171 -0
  454. package/src/agentSessions/index.ts +10 -3
  455. package/src/apiRoot.ts +78 -0
  456. package/src/auth/AuthProviderCatalogRouter.ts +26 -0
  457. package/src/auth/LoginScreenEngine.ts +50 -0
  458. package/src/auth/actionAuthorization.ts +116 -0
  459. package/src/auth/index.ts +4 -8
  460. package/src/auth/magicLink/MagicLinkRouter.ts +3 -2
  461. package/src/auth/magicLink/MagicLinkService.ts +6 -3
  462. package/src/auth/newUsers.ts +99 -11
  463. package/src/config.ts +171 -5
  464. package/src/context.ts +15 -10
  465. package/src/coreRoutes.ts +166 -0
  466. package/src/generated/graphql-schemas/__mj.ts +1815 -57
  467. package/src/generic/CacheInvalidationResolver.ts +73 -18
  468. package/src/generic/OrphanedConversationDetailReconciler.ts +3 -3
  469. package/src/generic/PushStatusResolver.ts +1 -1
  470. package/src/generic/ResolverBase.ts +335 -326
  471. package/src/generic/RunViewResolver.ts +21 -0
  472. package/src/globalCors.ts +71 -0
  473. package/src/index.ts +231 -300
  474. package/src/isolatedProvider.ts +22 -0
  475. package/src/multiTenancy/index.ts +24 -2
  476. package/src/realtimeProxy/RealtimeProxyServer.ts +148 -16
  477. package/src/realtimeProxy/RealtimeRelayTunnel.ts +645 -0
  478. package/src/realtimeSessions/README.md +179 -0
  479. package/src/realtimeSessions/RealtimeSessionEventService.ts +274 -0
  480. package/src/realtimeSessions/RealtimeSessionVerificationService.ts +168 -0
  481. package/src/realtimeSessions/RealtimeVerifyRouter.ts +109 -0
  482. package/src/realtimeSessions/RecordingAssemblyScheduler.ts +234 -0
  483. package/src/realtimeSessions/SessionVerificationPortsBase.ts +189 -0
  484. package/src/realtimeSessions/SlidingWindowRateLimiter.ts +92 -0
  485. package/src/realtimeSessions/index.ts +18 -0
  486. package/src/realtimeSessions/ownedRealtimeSession.ts +55 -0
  487. package/src/realtimeSessions/sessionAccess.ts +145 -0
  488. package/src/realtimeSessions/sessionConfigStamps.ts +123 -0
  489. package/src/realtimeSessions/subscriptionAuthorization.ts +116 -0
  490. package/src/realtimeSessions/verificationCore.ts +502 -0
  491. package/src/realtimeSessions/verificationPages.ts +149 -0
  492. package/src/realtimeSessions/verificationPolicy.ts +321 -0
  493. package/src/realtimeSessions/verificationWorkflow.ts +702 -0
  494. package/src/realtimeWidget/widgetGuestElevation.ts +34 -2
  495. package/src/resolvers/ActionResolver.ts +84 -21
  496. package/src/resolvers/AdhocQueryResolver.ts +24 -6
  497. package/src/resolvers/ArtifactFileResolver.ts +9 -8
  498. package/src/resolvers/CacheStatsResolver.ts +89 -2
  499. package/src/resolvers/ClientToolRequestResolver.ts +25 -7
  500. package/src/resolvers/ConversationRunEventsResolver.ts +15 -11
  501. package/src/resolvers/DatasetResolver.ts +70 -10
  502. package/src/resolvers/EntityRecordNameResolver.ts +3 -2
  503. package/src/resolvers/FetchEntityVectorsResolver.ts +179 -2
  504. package/src/resolvers/FileResolver.ts +321 -106
  505. package/src/resolvers/GetDataContextDataResolver.ts +139 -41
  506. package/src/resolvers/GetDataResolver.ts +6 -3
  507. package/src/resolvers/HumanHandoffResolver.ts +8 -0
  508. package/src/resolvers/IntegrationDiscoveryResolver.ts +4 -2
  509. package/src/resolvers/MCPResolver.ts +8 -5
  510. package/src/resolvers/QueryResolver.ts +11 -0
  511. package/src/resolvers/RealtimeBridgeResolver.ts +163 -11
  512. package/src/resolvers/RealtimeClientSessionResolver.ts +511 -129
  513. package/src/resolvers/RealtimeSessionEventsResolver.ts +133 -0
  514. package/src/resolvers/RealtimeSessionVerificationResolver.ts +174 -0
  515. package/src/resolvers/RemoteBrowserActionResolver.ts +11 -1
  516. package/src/resolvers/RunAIAgentResolver.ts +116 -14
  517. package/src/resolvers/RunTemplateResolver.ts +13 -12
  518. package/src/resolvers/SearchKnowledgeStreamResolver.ts +19 -3
  519. package/src/resolvers/SqlLoggingConfigResolver.ts +2 -4
  520. package/src/resolvers/SyncDataResolver.ts +6 -2
  521. package/src/resolvers/SyncRolesUsersResolver.ts +24 -6
  522. package/src/resolvers/TaskGraphFrameResolver.ts +33 -1
  523. package/src/resolvers/TestQuerySQLResolver.ts +68 -13
  524. package/src/resolvers/TransactionGroupResolver.ts +131 -27
  525. package/src/resolvers/UserAvatarResolver.ts +111 -0
  526. package/src/resolvers/UserResolver.ts +2 -16
  527. package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
  528. package/src/resolvers/agent-run-data-guard.ts +89 -0
  529. package/src/resolvers/agentVisionConsentAudit.ts +82 -0
  530. package/src/resolvers/avatarInputValidation.ts +123 -0
  531. package/src/resolvers/currentUserRoles.ts +32 -1
  532. package/src/resolvers/meetings/meetingMutations.ts +21 -0
  533. package/src/rest/RealtimeRecordingHandler.ts +464 -0
  534. package/src/rest/ViewOperationsHandler.ts +29 -13
  535. package/src/rest/setupRESTEndpoints.ts +6 -0
  536. package/src/serverExtensionReservedRoots.ts +14 -17
  537. package/src/services/MJStorageBlobStore.ts +31 -30
  538. package/src/services/StartTaskGraphDispatcher.ts +2 -2
  539. package/src/services/TaskGraphActionRunner.ts +25 -5
  540. package/src/services/TaskGraphAgentRunner.ts +188 -11
  541. package/src/services/TaskGraphContinuationDeliverer.ts +713 -17
  542. package/src/sharedCache.ts +305 -0
  543. package/src/types.ts +7 -0
@@ -1,36 +1,156 @@
1
1
  import { BaseSingleton, ShutdownRegistry } from '@memberjunction/global';
2
2
  import { CompositeKey, RunView, LogError, LogStatus, } from '@memberjunction/core';
3
- import { GetHostInstanceID, GetHostNamePrefix } from './HostInstance.js';
3
+ import { AssembleRealtimeRecordingFromLedger, RecoverRealtimeRecordingFromSegments, ResolveRecordingStorageAccountID, SqlRealtimeRecordingLedger, } from '@memberjunction/ai-agents';
4
+ import { GetCurrentHostInstance, GetHostInstanceID } from './HostInstance.js';
4
5
  import { SessionManager } from './SessionManager.js';
5
6
  /** Entity name for session rows (kept in sync with {@link SessionManager}). */
6
7
  const SESSION_ENTITY = 'MJ: AI Agent Sessions';
7
8
  /** Rows fetched per keyset page during a sweep. Small enough to bound memory on a large backlog. */
8
9
  const SWEEP_PAGE_SIZE = 200;
10
+ /** Entity name for agent rows (the recording storage account is configured on the agent). */
11
+ const AGENT_ENTITY = 'MJ: AI Agents';
12
+ /** A session whose recording recovery has failed this many times is given up on for the life of the process. */
13
+ const MAX_RECOVERY_ATTEMPTS = 8;
14
+ /**
15
+ * Recovery attempts (calls into the recovery routine) per sweep; bounds the I/O one tick can trigger.
16
+ * The ledger sweep and the legacy segment sweep each get this many: a backlog in one must not starve
17
+ * the other, so a pass makes at most twice this many attempts.
18
+ */
19
+ const MAX_RECOVERIES_PER_SWEEP = 5;
20
+ /** The shard-ledger view (core schema); a session with rows in it is assembled by the ledger sweep. */
21
+ const RECORDING_SHARD_VIEW = 'vwAIAgentSessionRecordingShards';
22
+ /** Ledger shard rows each recovery pass deletes, at most, for sessions closed before the lookback window. */
23
+ const MAX_PURGED_SHARDS_PER_PASS = 500;
24
+ /**
25
+ * Abandoned (timed-out, still running) recovery attempts allowed at once. Each keeps its socket and any
26
+ * shard buffers it had read (up to the 384 MiB recovery cap) until it settles, so recovery pauses at this
27
+ * many instead of adding one more every timeout while storage keeps hanging.
28
+ */
29
+ const MAX_ABANDONED_RECOVERIES = 2;
30
+ /** Backoff after the k-th failed recovery is `2^k * base`, capped at the max. */
31
+ const RECOVERY_BACKOFF_BASE_MS = 5 * 60_000;
32
+ const RECOVERY_BACKOFF_MAX_MS = 6 * 60 * 60_000;
33
+ /**
34
+ * How long {@link SessionJanitor.Shutdown} waits for an in-flight recording recovery pass. A pass can
35
+ * spend minutes on large downloads/uploads, and graceful shutdown must not hang on it indefinitely.
36
+ */
37
+ const RECOVERY_SHUTDOWN_WAIT_MS = 10_000;
9
38
  const DEFAULT_CONFIG = {
10
39
  closeThresholdMinutes: 15,
11
40
  sweepIntervalMs: 60_000,
41
+ recordingRecoveryGraceMinutes: 10,
42
+ recordingRecoveryLookbackHours: 168,
43
+ recordingRecoveryAttemptTimeoutMinutes: 10,
12
44
  };
45
+ /** Every knob {@link SessionJanitor.Configure} accepts; each must be a finite number greater than zero. */
46
+ const CONFIG_KEYS = [
47
+ 'closeThresholdMinutes',
48
+ 'sweepIntervalMs',
49
+ 'recordingRecoveryGraceMinutes',
50
+ 'recordingRecoveryLookbackHours',
51
+ 'recordingRecoveryAttemptTimeoutMinutes',
52
+ ];
53
+ /**
54
+ * Per-session retry state of one recovery sweep, keyed by lowercase session id (process-local: a restart
55
+ * gets fresh attempts). A failing session backs off exponentially (`min(2^k * 5 min, 6 h)`) and is given
56
+ * up on after {@link MAX_RECOVERY_ATTEMPTS} failures with one log line.
57
+ */
58
+ class RecoveryBackoffTracker {
59
+ constructor() {
60
+ this.states = new Map();
61
+ }
62
+ /** True while a session's backoff (or give-up) window has not elapsed. */
63
+ IsBackedOff(sessionID) {
64
+ const state = this.states.get(sessionID.toLowerCase());
65
+ return state != null && Date.now() < state.NextAttemptAt;
66
+ }
67
+ /** Forgets a session's failures (it succeeded, or another writer took care of it). */
68
+ Clear(sessionID) {
69
+ this.states.delete(sessionID.toLowerCase());
70
+ }
71
+ /**
72
+ * Defers a session that cannot be recovered until configuration changes (its agent has no recording
73
+ * storage account). That is not a failed attempt, so it never counts toward the give-up: the session
74
+ * is re-checked every {@link RECOVERY_BACKOFF_MAX_MS} and logged only the first time.
75
+ */
76
+ Defer(sessionID, reason) {
77
+ const key = sessionID.toLowerCase();
78
+ const state = this.states.get(key);
79
+ if (!state?.Deferred) {
80
+ LogStatus(`[SessionJanitor] Recording recovery skipped for session ${sessionID}: ${reason}; re-checked every ${RECOVERY_BACKOFF_MAX_MS / 3_600_000} h`);
81
+ }
82
+ this.states.set(key, { Failures: state?.Failures ?? 0, NextAttemptAt: Date.now() + RECOVERY_BACKOFF_MAX_MS, Deferred: true });
83
+ }
84
+ /**
85
+ * Counts one more unsuccessful recovery for a session and schedules its next attempt
86
+ * (`min(2^k * 5 min, 6 h)`); at {@link MAX_RECOVERY_ATTEMPTS} it stops checking the session for this
87
+ * process with a single log line: a `LogError` when the last attempt failed, a `LogStatus` when it
88
+ * simply found no shards (nothing to recover is not an error).
89
+ */
90
+ RecordFailure(sessionID, kind) {
91
+ const key = sessionID.toLowerCase();
92
+ const failures = (this.states.get(key)?.Failures ?? 0) + 1;
93
+ if (failures >= MAX_RECOVERY_ATTEMPTS) {
94
+ this.states.set(key, { Failures: failures, NextAttemptAt: Infinity, Deferred: false });
95
+ if (kind === 'NoSegments') {
96
+ LogStatus(`[SessionJanitor] Session ${sessionID} had no recoverable segments in ${failures} checks; not checking it again until restart`);
97
+ }
98
+ else {
99
+ LogError(`[SessionJanitor] Giving up on recording recovery for session ${sessionID} after ${failures} failed attempts`);
100
+ }
101
+ }
102
+ else {
103
+ const waitMs = Math.min(2 ** failures * RECOVERY_BACKOFF_BASE_MS, RECOVERY_BACKOFF_MAX_MS);
104
+ this.states.set(key, { Failures: failures, NextAttemptAt: Date.now() + waitMs, Deferred: false });
105
+ }
106
+ }
107
+ /** Drops state for sessions that were not in the latest complete candidate set (lowercase ids). */
108
+ Prune(candidateKeys) {
109
+ for (const key of this.states.keys()) {
110
+ if (!candidateKeys.has(key)) {
111
+ this.states.delete(key);
112
+ }
113
+ }
114
+ }
115
+ }
13
116
  /**
14
117
  * `BaseSingleton` background reconciler that keeps the durable `AIAgentSession` state from drifting
15
118
  * away from volatile process reality. A crash/redeploy vaporizes a host's in-memory sockets but
16
119
  * leaves its session rows reading `Active`/`Idle` forever; the janitor force-closes those orphans.
17
120
  *
18
- * Two sweeps, both writing through {@link SessionManager.CloseSession} so Record Changes captures
121
+ * Three close sweeps, all writing through {@link SessionManager.CloseSession} so Record Changes captures
19
122
  * each transition and channel rows are disconnected consistently:
20
123
  *
21
- * 1. **Own-host recovery ({@link RunStartupRecovery})** — run once at boot. Closes any `Active`/`Idle`
22
- * session whose `HostInstanceID` belongs to a *previous* boot of *this* host (same hostname prefix,
23
- * different `bootId`). Primary defense against the "Active forever" leak after a restart.
24
- * 2. **Global staleness sweep ({@link RunStalenessSweep})** — run periodically on every instance.
25
- * Closes any `Active`/`Idle` session whose `LastActiveAt` is older than `closeThresholdMinutes`,
26
- * regardless of host. Catches sessions whose owner died without a clean reboot (OOM, scaled-down pod).
124
+ * 1. **Own-instance recovery ({@link RunStartupRecovery})** — run once at boot. Closes any `Active`/`Idle`
125
+ * session whose `HostInstanceID` belongs to an *earlier* boot of *this* MJAPI instance (same host and port,
126
+ * different boot; see {@link HostInstanceIdentity}). Another MJAPI on the same host serves on another port,
127
+ * so its live sessions are left alone. Primary defense against the "Active forever" leak after a restart.
128
+ * 2. **Global staleness sweep ({@link RunStalenessSweep})** — run on every periodic tick, on every
129
+ * instance. Closes any `Active`/`Idle` session whose `LastActiveAt` is older than
130
+ * `closeThresholdMinutes`, regardless of host. Catches sessions whose owner died without a clean
131
+ * reboot (OOM, scaled-down pod).
132
+ * 3. **Max-duration sweep ({@link RunMaxDurationSweep})** — run on every periodic tick after the
133
+ * staleness sweep. Closes any session past its stored hard deadline (the public voice cost cap).
27
134
  *
28
- * Both sweeps stamp `CloseReason = 'Janitor'`. A third, shutdown-time path —
29
- * {@link RunShutdownDrain}, invoked from {@link Shutdown} during the graceful ShutdownRegistry
30
- * drain — closes this exact host instance's own live sessions with `CloseReason = 'Shutdown'`.
135
+ * The close sweeps stamp `CloseReason = 'Janitor'`. A shutdown-time path — {@link RunShutdownDrain},
136
+ * invoked from {@link Shutdown} during the graceful ShutdownRegistry drain — closes this exact host
137
+ * instance's own live sessions with `CloseReason = 'Shutdown'`.
31
138
  *
32
- * Both sweeps are **idempotent and safe to run concurrently** on every instance: closing an
33
- * already-`Closed` session is a no-op, and the close path is last-writer-wins. Both page with keyset
139
+ * Separately from closing, a recording recovery pass runs after the close sweeps on every periodic tick,
140
+ * without blocking them, and closes nothing. It purges shard-ledger rows past the lookback window, then
141
+ * {@link RunRecordingLedgerSweep} assembles the recordings of `Closed` sessions on the shard ledger that
142
+ * no terminal signal (or a dead assembler) left unassembled, then {@link RunRecordingRecoverySweep}
143
+ * rebuilds, from legacy storage segments, the recording of `Closed` sessions not on the ledger whose
144
+ * end-of-call upload never arrived.
145
+ *
146
+ * The close sweeps are **idempotent and safe to run concurrently** on every instance: closing an
147
+ * already-`Closed` session is a no-op, and the close path is last-writer-wins. Ledger assembly is
148
+ * arbitrated by the ledger's atomic claim. Legacy recovery is not: two instances can recover the same
149
+ * session at once (there is no claim column for it). Each
150
+ * attempt uploads its own file and defers to an already-stamped recording on a best-effort basis, so
151
+ * the cost is duplicate work and an extra file, not a lost recording. The extra file is an orphan when the
152
+ * second attempt's re-check sees the first stamp; when both re-checks pass first, both files are stamped in
153
+ * turn and both are linked to the session (the later stamp wins `RecordingFileID`). Every sweep pages with keyset
34
154
  * (`AfterKey`) pagination per the deep-pagination guide so a large backlog can't blow up memory.
35
155
  */
36
156
  export class SessionJanitor extends BaseSingleton {
@@ -39,10 +159,31 @@ export class SessionJanitor extends BaseSingleton {
39
159
  this._config = DEFAULT_CONFIG;
40
160
  this._sweepTimer = null;
41
161
  this._sweepRunning = false;
162
+ /**
163
+ * The in-flight recording recovery pass, or null. Separate from {@link _sweepRunning}: slow recovery
164
+ * must never block the close sweeps. Kept as a promise so {@link Shutdown} can wait for it.
165
+ */
166
+ this._recoveryPass = null;
167
+ /** Set by {@link Stop}, cleared by {@link Start}: no new recovery pass starts while stopped. */
168
+ this._stopped = false;
42
169
  this._registered = false;
43
170
  this._provider = null;
44
171
  this._systemUser = null;
45
172
  this.sessionManager = new SessionManager();
173
+ /** Retry state of the legacy storage-segment recovery. */
174
+ this.recoveryBackoff = new RecoveryBackoffTracker();
175
+ /**
176
+ * Retry state of the ledger sweep, kept apart from {@link recoveryBackoff}: each sweep prunes its
177
+ * tracker by its own candidate set, so a shared one would forget the other sweep's failures.
178
+ */
179
+ this.ledgerBackoff = new RecoveryBackoffTracker();
180
+ /**
181
+ * Lowercase ids of sessions whose recovery attempt timed out but has not settled. Skipped by later
182
+ * sweeps so a second attempt never runs alongside the abandoned one; removed when it settles.
183
+ */
184
+ this.abandonedRecoveries = new Set();
185
+ /** True once the "recovery paused" error has been logged for the current pause; reset when it lifts. */
186
+ this._recoveryPauseLogged = false;
46
187
  /** Identifier surfaced in graceful-shutdown logs. */
47
188
  this.ShutdownName = 'SessionJanitor';
48
189
  }
@@ -50,35 +191,61 @@ export class SessionJanitor extends BaseSingleton {
50
191
  static get Instance() {
51
192
  return super.getInstance();
52
193
  }
53
- /** Overrides default tuning. Safe to call before or after {@link Start}. */
194
+ /**
195
+ * Overrides default tuning. Safe to call before or after {@link Start}. Every value given must be a
196
+ * finite number greater than zero: a NaN or non-positive duration would otherwise build an invalid
197
+ * cutoff date and make every later sweep throw. All-or-nothing: on an invalid value it throws, naming
198
+ * the field and value, and applies nothing. `undefined` values are ignored.
199
+ */
54
200
  Configure(config) {
55
- this._config = { ...this._config, ...config };
201
+ const next = { ...this._config };
202
+ for (const key of CONFIG_KEYS) {
203
+ const value = config[key];
204
+ if (value === undefined) {
205
+ continue;
206
+ }
207
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
208
+ throw new Error(`SessionJanitor.Configure: ${key} must be a finite number greater than 0, got ${String(value)}`);
209
+ }
210
+ next[key] = value;
211
+ }
212
+ this._config = next;
56
213
  }
57
214
  /**
58
- * Run own-host startup recovery once, then schedule the periodic staleness sweep. Idempotent:
59
- * a second call does not stack a second timer. Captures the provider + system user so the timer
60
- * callback can run without re-supplying them.
215
+ * Run own-host startup recovery once, then schedule the periodic tick (staleness and max-duration
216
+ * close sweeps, then recording recovery). Idempotent: a second call does not stack a second timer.
217
+ * Captures the provider + system user so the timer callback can run without re-supplying them, and
218
+ * re-enables recording recovery after a {@link Stop}. Throws, before doing any work, when
219
+ * `intervalMs` is invalid (see {@link Configure}). Takes the server's database provider because
220
+ * recording recovery calls the shard ledger's stored procedures.
61
221
  */
62
222
  async Start(provider, systemUser, intervalMs) {
63
- this._provider = provider;
64
- this._systemUser = systemUser;
65
223
  if (intervalMs != null) {
66
- this._config = { ...this._config, sweepIntervalMs: intervalMs };
224
+ this.Configure({ sweepIntervalMs: intervalMs });
67
225
  }
226
+ this._provider = provider;
227
+ this._systemUser = systemUser;
228
+ this._stopped = false;
68
229
  this.ensureRegistered();
69
230
  await this.RunStartupRecovery(provider, systemUser);
70
231
  this.scheduleSweep();
71
232
  }
72
- /** Stops the periodic sweep timer. Idempotent. Part of {@link IShutdownable}. */
233
+ /**
234
+ * Stops the periodic tick and keeps any new recording recovery pass from starting until the next
235
+ * {@link Start}, including one a tick already in progress would have started. A pass that is already
236
+ * running is not interrupted ({@link Shutdown} waits for it). Idempotent. Part of {@link IShutdownable}.
237
+ */
73
238
  Stop() {
239
+ this._stopped = true;
74
240
  if (this._sweepTimer) {
75
241
  clearInterval(this._sweepTimer);
76
242
  this._sweepTimer = null;
77
243
  }
78
244
  }
79
245
  /**
80
- * {@link IShutdownable}: clear the timer, then drain this host's own live sessions so a
81
- * graceful stop never strands `Active`/`Idle` rows for the next boot's janitor to mop up.
246
+ * {@link IShutdownable}: stop the timer, then drain this host's own live sessions so a
247
+ * graceful stop never strands `Active`/`Idle` rows for the next boot's janitor to mop up, then wait
248
+ * (at most {@link RECOVERY_SHUTDOWN_WAIT_MS}) for an in-flight recording recovery pass.
82
249
  * Drained sessions are stamped `CloseReason = 'Shutdown'` (vs. `'Janitor'` for crash orphans),
83
250
  * so the dashboards can tell a clean redeploy from a reconciled crash. Never throws; the drain
84
251
  * is skipped when {@link Start} was never called (no captured provider/user).
@@ -93,6 +260,7 @@ export class SessionJanitor extends BaseSingleton {
93
260
  LogError(`SessionJanitor shutdown drain failed: ${err instanceof Error ? err.message : String(err)}`);
94
261
  }
95
262
  }
263
+ await this.waitForRecoveryPass();
96
264
  }
97
265
  /**
98
266
  * Close every `Active`/`Idle` session owned by **this exact host instance** (current boot),
@@ -110,18 +278,27 @@ export class SessionJanitor extends BaseSingleton {
110
278
  return closed;
111
279
  }
112
280
  /**
113
- * Force-close any `Active`/`Idle` session left behind by a *previous boot of this host*
114
- * (matching hostname prefix, differing `bootId`). Returns the number of sessions closed.
281
+ * Force-close any `Active`/`Idle` session left behind by an earlier boot of this MJAPI instance: its
282
+ * `HostInstanceID` has this instance's prefix (`hostname:port:`) and another boot. The sessions of another
283
+ * MJAPI on the same host carry another port and stay open. Returns the number of sessions closed.
284
+ *
285
+ * @param provider The metadata provider for the reads and writes.
286
+ * @param systemUser The user the closes run as.
287
+ * @param hostInstance The instance whose earlier boots to close. Defaults to this process's
288
+ * ({@link GetCurrentHostInstance}).
115
289
  */
116
- async RunStartupRecovery(provider, systemUser) {
117
- const prefix = GetHostNamePrefix().replace(/'/g, "''");
118
- const current = GetHostInstanceID().replace(/'/g, "''");
290
+ async RunStartupRecovery(provider, systemUser, hostInstance = GetCurrentHostInstance()) {
291
+ const prefix = hostInstance.GetInstancePrefix().replace(/'/g, "''");
292
+ const current = hostInstance.GetHostInstanceID().replace(/'/g, "''");
119
293
  const filter = `Status IN ('Active','Idle') ` +
120
294
  `AND HostInstanceID LIKE '${prefix}%' ` +
121
295
  `AND HostInstanceID <> '${current}'`;
122
- const closed = await this.sweepAndClose(filter, provider, systemUser, 'Janitor');
296
+ // The LIKE narrows the read; the exact check decides, since LIKE reads a `_` in a host name as any character.
297
+ const isPriorBoot = (session) => hostInstance.IsPriorBoot(session.HostInstanceID);
298
+ const closed = await this.sweepAndClose(filter, provider, systemUser, 'Janitor', isPriorBoot);
123
299
  if (closed > 0) {
124
- LogStatus(`[SessionJanitor] Startup recovery closed ${closed} orphaned session(s) from a prior boot of this host`);
300
+ LogStatus(`[SessionJanitor] Startup recovery closed ${closed} orphaned session(s) from an earlier boot of this instance ` +
301
+ `(${hostInstance.GetInstancePrefix()})`);
125
302
  }
126
303
  return closed;
127
304
  }
@@ -163,6 +340,229 @@ export class SessionJanitor extends BaseSingleton {
163
340
  }
164
341
  return closed;
165
342
  }
343
+ /**
344
+ * Rebuilds the recording of sessions whose end-of-call upload never arrived (browser died, tab
345
+ * closed). A session is a candidate when it is `Closed`, has `RecordingStartedAt` (consent is implied:
346
+ * recording only starts after the guest agreed), still has no `RecordingFileID`, and closed between
347
+ * the lookback window and the grace period (see {@link SessionJanitorConfig.recordingRecoveryGraceMinutes}).
348
+ *
349
+ * Work is bounded: candidates are handled sequentially, at most {@link MAX_RECOVERIES_PER_SWEEP}
350
+ * recovery attempts per call, and a failing session backs off exponentially (`min(2^k * 5 min, 6 h)`)
351
+ * and is abandoned after {@link MAX_RECOVERY_ATTEMPTS} failures with one `LogError`. Backoff entries of
352
+ * sessions that left the candidate set are pruned, but only after a pass that saw every candidate, so
353
+ * a capped pass never forgets sessions it did not reach.
354
+ *
355
+ * Sessions on the shard ledger (a `RecordingStatus`, or any ledger row) are left to
356
+ * {@link RunRecordingLedgerSweep}: their audio is in SQL, not in storage segments.
357
+ *
358
+ * Never throws for a single bad session. Returns the number of recordings recovered.
359
+ */
360
+ async RunRecordingRecoverySweep(provider, systemUser) {
361
+ if (this.isRecoveryPausedForAbandonedAttempts()) {
362
+ return 0;
363
+ }
364
+ const { graceCutoff, lookbackCutoff } = this.recoveryWindow();
365
+ const filter = `Status = 'Closed' AND RecordingStartedAt IS NOT NULL AND RecordingFileID IS NULL ` +
366
+ `AND ClosedAt < '${graceCutoff.toISOString()}' AND ClosedAt >= '${lookbackCutoff.toISOString()}' ` +
367
+ `AND RecordingStatus IS NULL AND ID NOT IN (SELECT AgentSessionID FROM ${this.shardViewName(provider)})`;
368
+ const context = { provider, systemUser, storage: new Map() };
369
+ const recovered = await this.sweepRecoveryCandidates(filter, provider, systemUser, this.recoveryBackoff, session => this.recoverSessionRecording(session, context));
370
+ if (recovered > 0) {
371
+ LogStatus(`[SessionJanitor] Recording recovery rebuilt ${recovered} recording(s)`);
372
+ }
373
+ return recovered;
374
+ }
375
+ /**
376
+ * Backstop for client-direct recordings on the shard ledger: assembles the recording of `Closed`
377
+ * sessions that closed between the lookback window and the grace period and are still
378
+ * - `Pending` (shards, or a terminal signal, with no assembly since: the browser died before its
379
+ * terminal request, or the in-process assembly request was lost with its host),
380
+ * - `Assembling` with no write for `recordingRecoveryAttemptTimeoutMinutes` (the assembler died, or
381
+ * it hit a transient error and left the claim for a re-claim), or
382
+ * - status-less while the ledger holds shards for them (see {@link restoreLostRecordingStatus}).
383
+ *
384
+ * Each candidate is claimed with the ledger's atomic, stale-aware claim (a lost claim is skipped: another
385
+ * assembler has it) and assembled with `ForcePartial`, so missing shards become silence. An attempt is
386
+ * bounded by `recordingRecoveryAttemptTimeoutMinutes` and shares the legacy sweep's abandoned-attempt
387
+ * pause; failures back off and are given up on exactly like legacy recovery, in their own tracker.
388
+ * At most {@link MAX_RECOVERIES_PER_SWEEP} assemblies per call. Never throws for a single bad session.
389
+ * Returns the number of recordings assembled (`Complete` or `Partial`).
390
+ */
391
+ async RunRecordingLedgerSweep(provider, systemUser) {
392
+ if (this.isRecoveryPausedForAbandonedAttempts()) {
393
+ return 0;
394
+ }
395
+ const { graceCutoff, lookbackCutoff } = this.recoveryWindow();
396
+ const staleCutoff = new Date(Date.now() - this._config.recordingRecoveryAttemptTimeoutMinutes * 60_000);
397
+ // Every Assembling session is fetched, not only stale ones, and a fresh one is skipped below: a
398
+ // session whose assembly left a Retry claim is fresh for a while, and dropping it from the walk
399
+ // would prune its backoff, so the give-up cap would never be reached.
400
+ const filter = `Status = 'Closed' AND ClosedAt < '${graceCutoff.toISOString()}' AND ClosedAt >= '${lookbackCutoff.toISOString()}' ` +
401
+ `AND (RecordingStatus IN ('Pending','Assembling') ` +
402
+ `OR (RecordingStatus IS NULL AND ID IN (SELECT AgentSessionID FROM ${this.shardViewName(provider)})))`;
403
+ const ledger = new SqlRealtimeRecordingLedger(provider, systemUser);
404
+ const assembled = await this.sweepRecoveryCandidates(filter, provider, systemUser, this.ledgerBackoff, session => this.assembleSessionFromLedger(session, ledger, staleCutoff, provider, systemUser),
405
+ // The claim proc re-checks staleness atomically; this only spares a claim that would lose.
406
+ session => session.RecordingStatus === 'Assembling' && session.__mj_UpdatedAt.getTime() >= staleCutoff.getTime());
407
+ if (assembled > 0) {
408
+ LogStatus(`[SessionJanitor] Recording ledger sweep assembled ${assembled} recording(s)`);
409
+ }
410
+ return assembled;
411
+ }
412
+ /** The closed-at window recording recovery considers: older than the grace, newer than the lookback. */
413
+ recoveryWindow() {
414
+ const nowMs = Date.now();
415
+ return {
416
+ graceCutoff: new Date(nowMs - this._config.recordingRecoveryGraceMinutes * 60_000),
417
+ lookbackCutoff: new Date(nowMs - this._config.recordingRecoveryLookbackHours * 3_600_000),
418
+ };
419
+ }
420
+ /** The schema-qualified, dialect-quoted shard-ledger view, for filter sub-queries. */
421
+ shardViewName(provider) {
422
+ return provider.QuoteSchemaAndView(provider.MJCoreSchemaName, RECORDING_SHARD_VIEW);
423
+ }
424
+ /**
425
+ * Walks every session matching `filter` and runs `recover` on each one that is not backed off, not
426
+ * abandoned, and not `isBusy`, sequentially, stopping at {@link MAX_RECOVERIES_PER_SWEEP} attempts or
427
+ * {@link MAX_ABANDONED_RECOVERIES} abandoned attempts. Backoff entries of sessions that left the
428
+ * candidate set are pruned, but only after a walk that saw every candidate, so a capped walk never
429
+ * forgets sessions it did not reach. Returns the number recovered.
430
+ */
431
+ async sweepRecoveryCandidates(filter, provider, systemUser, backoff, recover, isBusy = () => false) {
432
+ const seen = new Set();
433
+ let attempts = 0;
434
+ let recovered = 0;
435
+ const walk = await this.forEachSessionPage(filter, provider, systemUser, async (page) => {
436
+ for (const session of page) {
437
+ seen.add(session.ID.toLowerCase());
438
+ if (backoff.IsBackedOff(session.ID) || this.abandonedRecoveries.has(session.ID.toLowerCase()) || isBusy(session)) {
439
+ continue;
440
+ }
441
+ if (attempts >= MAX_RECOVERIES_PER_SWEEP || this.abandonedRecoveries.size >= MAX_ABANDONED_RECOVERIES) {
442
+ return false; // per-sweep cap, or too many abandoned attempts still running: stop paging
443
+ }
444
+ const visit = await recover(session);
445
+ attempts += visit.Attempted ? 1 : 0;
446
+ recovered += visit.Recovered ? 1 : 0;
447
+ }
448
+ });
449
+ // Only a walk that reached the end saw every candidate. A capped or failed-load walk has an
450
+ // incomplete `seen`; pruning on it would wipe backoff and give-up state for unseen sessions.
451
+ if (walk === 'complete') {
452
+ backoff.Prune(seen);
453
+ }
454
+ return recovered;
455
+ }
456
+ /**
457
+ * Claims and assembles one ledger candidate and updates its backoff. Never throws: any exception is
458
+ * logged with the session id and counted as a failure.
459
+ */
460
+ async assembleSessionFromLedger(candidate, ledger, staleCutoff, provider, systemUser) {
461
+ const sessionID = candidate.ID;
462
+ let attempted = false;
463
+ try {
464
+ if (candidate.RecordingStatus == null) {
465
+ // The restore is a write; a failing one counts toward the per-sweep cap like an assembly.
466
+ attempted = true;
467
+ if (!(await this.restoreLostRecordingStatus(candidate, ledger))) {
468
+ return this.recordLedgerFailure(sessionID, true);
469
+ }
470
+ attempted = false;
471
+ }
472
+ const version = await ledger.ClaimAssembly(sessionID, staleCutoff);
473
+ if (version === null) {
474
+ LogStatus(`[SessionJanitor] Recording ledger sweep skipped session ${sessionID}: another assembler holds its claim`);
475
+ return { Attempted: false, Recovered: false };
476
+ }
477
+ attempted = true;
478
+ const outcome = await this.runWithinAttemptTimeout(sessionID, AssembleRealtimeRecordingFromLedger({
479
+ SessionID: sessionID, Version: version, Ledger: ledger,
480
+ ContextUser: systemUser, Provider: provider, ForcePartial: true,
481
+ }), late => late);
482
+ switch (outcome) {
483
+ case 'Complete':
484
+ case 'Partial':
485
+ this.ledgerBackoff.Clear(sessionID);
486
+ LogStatus(`[SessionJanitor] Assembled the recording of session ${sessionID} from its shard ledger: ${outcome} (version ${version})`);
487
+ return { Attempted: true, Recovered: true };
488
+ case 'Superseded':
489
+ this.ledgerBackoff.Clear(sessionID);
490
+ LogStatus(`[SessionJanitor] Ledger assembly of session ${sessionID} (version ${version}) was Superseded by a newer claim; left to it`);
491
+ return { Attempted: true, Recovered: false };
492
+ case 'Retry':
493
+ // The assembler already logged the error; the claim stays Assembling for a re-claim after the backoff.
494
+ LogStatus(`[SessionJanitor] Ledger assembly of session ${sessionID} (version ${version}) hit a transient error; retried after its backoff`);
495
+ return this.recordLedgerFailure(sessionID, true);
496
+ case 'Deferred':
497
+ LogError(`[SessionJanitor] Forced ledger assembly of session ${sessionID} (version ${version}) returned Deferred, which only an unforced assembly should; backing off`);
498
+ return this.recordLedgerFailure(sessionID, true);
499
+ case 'Failed':
500
+ LogError(`[SessionJanitor] Ledger assembly of session ${sessionID} (version ${version}) Failed; its recording status is now Failed`);
501
+ return this.recordLedgerFailure(sessionID, true);
502
+ case 'TimedOut':
503
+ LogError(`[SessionJanitor] Ledger assembly of session ${sessionID} (version ${version}) did not finish within ` +
504
+ `${this._config.recordingRecoveryAttemptTimeoutMinutes} minutes; abandoning this attempt (it may still finish) ` +
505
+ `and continuing the sweep. The session is not retried until it settles.`);
506
+ return this.recordLedgerFailure(sessionID, true);
507
+ }
508
+ }
509
+ catch (err) {
510
+ LogError(`[SessionJanitor] Recording ledger sweep failed for session ${sessionID}: ${err instanceof Error ? err.message : String(err)}`);
511
+ return this.recordLedgerFailure(sessionID, attempted);
512
+ }
513
+ }
514
+ /**
515
+ * Gives a status-less session with ledger shards back a `Pending` status so it can be claimed (the
516
+ * claim only takes Pending/Partial/stale Assembling). The status is lost when a `BaseEntity.Save` of
517
+ * the session (e.g. its close) loaded the row before a shard or terminal proc wrote it and saved
518
+ * after: `spUpdate` rewrites every column, so it puts back the stale NULL status and counts.
519
+ *
520
+ * The terminal proc is the existing way to set a NULL status to `Pending`. It is stamped with the
521
+ * counts the session still stores, or 0 for a NULL count (the proc accepts any count over NULL), and
522
+ * never with counts invented from the ledger: a made-up terminal could make a recording whose tail
523
+ * never arrived assemble as Complete, which deletes its shards. With no real terminal the stored shard
524
+ * count differs from the stamped one, so the assembly is Partial and keeps the shards; a later real
525
+ * terminal (higher counts) or late shard re-opens it intact.
526
+ * Returns false (logged) when the status could not be restored.
527
+ */
528
+ async restoreLostRecordingStatus(candidate, ledger) {
529
+ const sessionID = candidate.ID;
530
+ const expectedShards = candidate.RecordingExpectedShards ?? 0;
531
+ // Number(): the candidate comes from a RunView entity page, which keeps the driver's value for a
532
+ // BIGINT column, and the SQL Server driver returns BIGINT as a string (the ledger refuses strings).
533
+ const capturedSamples = Number(candidate.RecordingCapturedSamples ?? 0);
534
+ const stamped = await ledger.StampTerminal(sessionID, expectedShards, capturedSamples);
535
+ if (stamped.RecordingStatus == null) {
536
+ LogError(`[SessionJanitor] Could not restore the recording status of session ${sessionID} ` +
537
+ `(terminal stamp of ${expectedShards} shard(s), ${capturedSamples} sample(s) was ${stamped.Outcome}); backing off`);
538
+ return false;
539
+ }
540
+ LogStatus(`[SessionJanitor] Restored recording status ${stamped.RecordingStatus} for session ${sessionID} ` +
541
+ `(stamped its stored counts: ${expectedShards} shard(s), ${capturedSamples} sample(s))`);
542
+ return true;
543
+ }
544
+ /** Counts a failed ledger assembly in its backoff (see {@link RecoveryBackoffTracker.RecordFailure}). */
545
+ recordLedgerFailure(sessionID, attempted) {
546
+ this.ledgerBackoff.RecordFailure(sessionID, 'Failed');
547
+ return { Attempted: attempted, Recovered: false };
548
+ }
549
+ /**
550
+ * Deletes up to {@link MAX_PURGED_SHARDS_PER_PASS} ledger shards of sessions closed before the lookback
551
+ * window, which no sweep assembles any more. Never throws: a failure is logged and retried next pass.
552
+ */
553
+ async purgeExpiredLedgerShards(provider, systemUser) {
554
+ const { lookbackCutoff } = this.recoveryWindow();
555
+ try {
556
+ const purged = await new SqlRealtimeRecordingLedger(provider, systemUser).PurgeClosedBefore(lookbackCutoff, MAX_PURGED_SHARDS_PER_PASS);
557
+ if (purged > 0) {
558
+ LogStatus(`[SessionJanitor] Purged ${purged} recording shard(s) of sessions closed before ${lookbackCutoff.toISOString()}`);
559
+ }
560
+ }
561
+ catch (err) {
562
+ LogError(`[SessionJanitor] Recording shard purge (sessions closed before ${lookbackCutoff.toISOString()}) failed: ` +
563
+ `${err instanceof Error ? err.message : String(err)}`);
564
+ }
565
+ }
166
566
  /** Parses the absolute deadline (ms) from a session's `Config_` JSON, or null when absent/malformed. */
167
567
  parseSessionDeadlineMs(configJson) {
168
568
  if (!configJson) {
@@ -188,7 +588,7 @@ export class SessionJanitor extends BaseSingleton {
188
588
  this._registered = true;
189
589
  }
190
590
  }
191
- /** Schedule the periodic staleness sweep, exactly once. Timer is unref'd so it never blocks exit. */
591
+ /** Schedule the periodic sweep tick (close sweeps + recording recovery), exactly once. Timer is unref'd so it never blocks exit. */
192
592
  scheduleSweep() {
193
593
  if (this._sweepTimer) {
194
594
  return;
@@ -196,7 +596,16 @@ export class SessionJanitor extends BaseSingleton {
196
596
  this._sweepTimer = setInterval(() => void this.periodicSweep(), this._config.sweepIntervalMs);
197
597
  this._sweepTimer.unref?.();
198
598
  }
199
- /** Timer tick: run the staleness sweep under the captured provider/user, guarding overlap. */
599
+ /**
600
+ * Timer tick: run the close sweeps (staleness, then max-duration) under the captured
601
+ * provider/user, guarding overlap, then kick off recording recovery.
602
+ *
603
+ * Recovery is started WITHOUT awaiting and has its own overlap guard: it can spend minutes on
604
+ * large downloads/uploads, and awaiting it under the close sweeps' guard would delay the
605
+ * max-duration cost cap and make every later tick return early. Each tick skips recovery while a pass
606
+ * is still running; because every session's attempt is bounded by
607
+ * `recordingRecoveryAttemptTimeoutMinutes`, a hung storage call delays later passes, never stops them.
608
+ */
200
609
  async periodicSweep() {
201
610
  if (this._sweepRunning || !this._provider || !this._systemUser) {
202
611
  return;
@@ -214,6 +623,210 @@ export class SessionJanitor extends BaseSingleton {
214
623
  finally {
215
624
  this._sweepRunning = false;
216
625
  }
626
+ this.startRecoveryPass(this._provider, this._systemUser);
627
+ }
628
+ /**
629
+ * One recording recovery pass: purge expired ledger shards, then the ledger sweep, then legacy
630
+ * segment recovery. The ledger sweep goes first because its sessions are the current recording path;
631
+ * a failed ledger sweep is logged here so legacy recovery still runs. A legacy failure rejects (logged
632
+ * by {@link startRecoveryPass}).
633
+ */
634
+ async runRecoveryPass(provider, systemUser) {
635
+ await this.purgeExpiredLedgerShards(provider, systemUser);
636
+ // A Stop() during a step ends the pass before the next one, so Shutdown waits on less work.
637
+ if (this._stopped) {
638
+ return;
639
+ }
640
+ try {
641
+ await this.RunRecordingLedgerSweep(provider, systemUser);
642
+ }
643
+ catch (err) {
644
+ LogError(`SessionJanitor recording ledger sweep failed: ${err instanceof Error ? err.message : String(err)}`);
645
+ }
646
+ if (this._stopped) {
647
+ return;
648
+ }
649
+ await this.RunRecordingRecoverySweep(provider, systemUser);
650
+ }
651
+ /**
652
+ * Fire-and-forget recording recovery, skipped when the previous pass is still running or the janitor
653
+ * has been stopped (a tick that was mid-close-sweep when {@link Stop} ran must not start one).
654
+ */
655
+ startRecoveryPass(provider, systemUser) {
656
+ if (this._stopped || this._recoveryPass) {
657
+ return;
658
+ }
659
+ this._recoveryPass = this.runRecoveryPass(provider, systemUser)
660
+ .then(() => undefined, err => {
661
+ LogError(`SessionJanitor recording recovery sweep failed: ${err instanceof Error ? err.message : String(err)}`);
662
+ })
663
+ .finally(() => {
664
+ this._recoveryPass = null;
665
+ });
666
+ }
667
+ /**
668
+ * Waits for the in-flight recovery pass, if any, for at most {@link RECOVERY_SHUTDOWN_WAIT_MS}; logs
669
+ * once when it gives up. Never throws: the pass promise already logs and absorbs its own errors.
670
+ */
671
+ async waitForRecoveryPass() {
672
+ const pass = this._recoveryPass;
673
+ if (!pass) {
674
+ return;
675
+ }
676
+ let timer;
677
+ const timedOut = new Promise(resolve => {
678
+ timer = setTimeout(() => resolve('timeout'), RECOVERY_SHUTDOWN_WAIT_MS);
679
+ });
680
+ try {
681
+ const outcome = await Promise.race([pass.then(() => 'finished'), timedOut]);
682
+ if (outcome === 'timeout') {
683
+ LogError(`SessionJanitor shutdown (host instance ${GetHostInstanceID()}): recording recovery pass still running after ` +
684
+ `${RECOVERY_SHUTDOWN_WAIT_MS} ms; shutting down without it. A session it had not yet stamped is retried by a later sweep.`);
685
+ }
686
+ }
687
+ finally {
688
+ clearTimeout(timer);
689
+ }
690
+ }
691
+ /**
692
+ * Recovers one candidate's recording and updates its backoff. Never throws: any exception is logged
693
+ * with the session id and counted as a failure.
694
+ */
695
+ async recoverSessionRecording(candidate, context) {
696
+ let attempted = false;
697
+ // The sweep filter requires RecordingStartedAt, so this only fires on drifted data; recovery would
698
+ // otherwise stamp a recording with no t0. Counted as a failure so it backs off rather than logging every tick.
699
+ const startedAt = candidate.RecordingStartedAt;
700
+ if (!startedAt) {
701
+ LogError(`[SessionJanitor] Recording recovery skipped for session ${candidate.ID}: RecordingStartedAt is empty`);
702
+ return this.recordRecoveryFailure(candidate.ID, false);
703
+ }
704
+ try {
705
+ // Re-read: the late upload may have landed since the candidate page was fetched.
706
+ const session = await context.provider.GetEntityObject(SESSION_ENTITY, context.systemUser);
707
+ if (!(await session.Load(candidate.ID))) {
708
+ LogError(`[SessionJanitor] Recording recovery could not reload session ${candidate.ID}`);
709
+ return this.recordRecoveryFailure(candidate.ID, false);
710
+ }
711
+ if (session.RecordingFileID) {
712
+ return { Attempted: false, Recovered: false };
713
+ }
714
+ const storage = await this.resolveRecoveryStorage(candidate.AgentID, context);
715
+ if (storage.Kind === 'NoAccount') {
716
+ this.recoveryBackoff.Defer(candidate.ID, `no recording storage account for agent ${candidate.AgentID}`);
717
+ return { Attempted: false, Recovered: false };
718
+ }
719
+ if (storage.Kind === 'AgentUnavailable') {
720
+ LogStatus(`[SessionJanitor] Recording recovery skipped for session ${candidate.ID}: agent ${candidate.AgentID} could not be loaded`);
721
+ return this.recordRecoveryFailure(candidate.ID, false);
722
+ }
723
+ const accountID = storage.AccountID;
724
+ attempted = true;
725
+ const result = await this.runWithinAttemptTimeout(candidate.ID, RecoverRealtimeRecordingFromSegments({
726
+ SessionID: candidate.ID,
727
+ StorageAccountID: accountID,
728
+ StartedAt: startedAt,
729
+ ContextUser: context.systemUser,
730
+ Provider: context.provider,
731
+ }), late => late.Outcome);
732
+ if (result === 'TimedOut') {
733
+ LogError(`[SessionJanitor] Recording recovery for session ${candidate.ID} did not finish within ` +
734
+ `${this._config.recordingRecoveryAttemptTimeoutMinutes} minutes; abandoning this attempt (its storage call may ` +
735
+ `still complete) and continuing the sweep. The session is not retried until that call settles.`);
736
+ return this.recordRecoveryFailure(candidate.ID, true);
737
+ }
738
+ switch (result.Outcome) {
739
+ case 'Recovered':
740
+ this.recoveryBackoff.Clear(candidate.ID);
741
+ LogStatus(`[SessionJanitor] Recovered recording for session ${candidate.ID} from ${result.SegmentCount} segment(s) (file ${result.FileID})`);
742
+ return { Attempted: true, Recovered: true };
743
+ case 'Superseded':
744
+ this.recoveryBackoff.Clear(candidate.ID);
745
+ LogStatus(`[SessionJanitor] Session ${candidate.ID} was given a recording by another writer; recovery left it alone`);
746
+ return { Attempted: true, Recovered: false };
747
+ case 'NoSegments':
748
+ LogStatus(`[SessionJanitor] No recoverable segments for session ${candidate.ID} (agent ${candidate.AgentID}, storage account ${accountID})`);
749
+ // Retried with backoff: a storage outage can also read as an empty folder (drivers that swallow
750
+ // list errors), but a session that never has shards is not an error when checking stops.
751
+ return this.recordRecoveryFailure(candidate.ID, true, 'NoSegments');
752
+ default:
753
+ LogError(`[SessionJanitor] Recording recovery failed for session ${candidate.ID}: ${result.ErrorMessage ?? 'unknown error'}`);
754
+ return this.recordRecoveryFailure(candidate.ID, true);
755
+ }
756
+ }
757
+ catch (err) {
758
+ LogError(`[SessionJanitor] Recording recovery threw for session ${candidate.ID}: ${err instanceof Error ? err.message : String(err)}`);
759
+ return this.recordRecoveryFailure(candidate.ID, attempted);
760
+ }
761
+ }
762
+ /**
763
+ * Waits for one session's recovery attempt for at most `recordingRecoveryAttemptTimeoutMinutes`.
764
+ * An attempt cannot be cancelled, so a timed-out one keeps running: the session is marked abandoned
765
+ * until it settles (later sweeps skip it), and its late outcome (`describeLate`) is logged then.
766
+ */
767
+ async runWithinAttemptTimeout(sessionID, attempt, describeLate) {
768
+ const key = sessionID.toLowerCase();
769
+ let timer;
770
+ const timedOut = new Promise(resolve => {
771
+ timer = setTimeout(() => resolve('TimedOut'), this._config.recordingRecoveryAttemptTimeoutMinutes * 60_000);
772
+ timer.unref?.();
773
+ });
774
+ try {
775
+ const outcome = await Promise.race([attempt, timedOut]);
776
+ if (outcome === 'TimedOut') {
777
+ this.abandonedRecoveries.add(key);
778
+ attempt.then(late => LogStatus(`[SessionJanitor] Abandoned recording recovery for session ${sessionID} finished late: ${describeLate(late)}`), err => LogError(`[SessionJanitor] Abandoned recording recovery for session ${sessionID} failed late: ${err instanceof Error ? err.message : String(err)}`)).finally(() => this.abandonedRecoveries.delete(key));
779
+ }
780
+ return outcome;
781
+ }
782
+ finally {
783
+ clearTimeout(timer);
784
+ }
785
+ }
786
+ /**
787
+ * True while {@link MAX_ABANDONED_RECOVERIES} timed-out attempts are still running: no new attempt
788
+ * starts until one settles. Logs one `LogError` per pause.
789
+ */
790
+ isRecoveryPausedForAbandonedAttempts() {
791
+ if (this.abandonedRecoveries.size < MAX_ABANDONED_RECOVERIES) {
792
+ this._recoveryPauseLogged = false;
793
+ return false;
794
+ }
795
+ if (!this._recoveryPauseLogged) {
796
+ LogError(`[SessionJanitor] Recording recovery paused: ${this.abandonedRecoveries.size} abandoned attempts are still waiting on storage ` +
797
+ `(sessions ${[...this.abandonedRecoveries].join(', ')}); no new attempt starts until one of them settles`);
798
+ this._recoveryPauseLogged = true;
799
+ }
800
+ return true;
801
+ }
802
+ /**
803
+ * Loads the agent carrying a session's recording storage config and resolves its storage account,
804
+ * memoized per agent for the sweep: the resolve may force a storage-engine reload, so it must not
805
+ * repeat for every session of the same agent.
806
+ */
807
+ async resolveRecoveryStorage(agentID, context) {
808
+ const key = agentID.toLowerCase();
809
+ const cached = context.storage.get(key);
810
+ if (cached) {
811
+ return cached;
812
+ }
813
+ const agent = await context.provider.GetEntityObject(AGENT_ENTITY, context.systemUser);
814
+ let storage;
815
+ if (!(await agent.Load(agentID))) {
816
+ LogError(`[SessionJanitor] Recording recovery could not load agent ${agentID}`);
817
+ storage = { Kind: 'AgentUnavailable' };
818
+ }
819
+ else {
820
+ const accountID = await ResolveRecordingStorageAccountID(agent, context.systemUser, context.provider);
821
+ storage = accountID ? { Kind: 'Resolved', AccountID: accountID } : { Kind: 'NoAccount' };
822
+ }
823
+ context.storage.set(key, storage);
824
+ return storage;
825
+ }
826
+ /** Counts a failed legacy recovery in its backoff (see {@link RecoveryBackoffTracker.RecordFailure}). */
827
+ recordRecoveryFailure(sessionID, attempted, kind = 'Failed') {
828
+ this.recoveryBackoff.RecordFailure(sessionID, kind);
829
+ return { Attempted: attempted, Recovered: false };
217
830
  }
218
831
  /**
219
832
  * Page through every session matching `filter` with keyset (`AfterKey`) pagination and close each
@@ -223,28 +836,11 @@ export class SessionJanitor extends BaseSingleton {
223
836
  */
224
837
  async sweepAndClose(filter, provider, systemUser, closeReason, shouldClose) {
225
838
  let closedCount = 0;
226
- let afterKey;
227
- // eslint-disable-next-line no-constant-condition
228
- while (true) {
229
- const fetched = await this.fetchPage(filter, afterKey, provider, systemUser);
230
- if (fetched == null) {
231
- break; // load failure already logged
232
- }
233
- if (fetched.length === 0) {
234
- break;
235
- }
236
- // Advance the keyset BEFORE the optional JS predicate narrows the page (we must page by the
237
- // SQL-matched set, not the post-filtered subset, or pagination would stall/skip).
238
- const lastFetchedId = fetched[fetched.length - 1].ID;
239
- const fetchedCount = fetched.length;
839
+ await this.forEachSessionPage(filter, provider, systemUser, async (fetched) => {
240
840
  // Optional in-JS narrowing (e.g. exact deadline check the SQL pre-filter can't express).
241
841
  const page = shouldClose ? fetched.filter(shouldClose) : fetched;
242
842
  if (page.length === 0) {
243
- if (fetchedCount < SWEEP_PAGE_SIZE) {
244
- break;
245
- }
246
- afterKey = CompositeKey.FromID(lastFetchedId); // first-pk-ok: keyset AfterKey on SESSION_ENTITY = MJ: AI Agent Sessions, a core entity keyed by ID
247
- continue;
843
+ return;
248
844
  }
249
845
  // Batch-load every channel for this whole page of sessions in ONE query, then hand each
250
846
  // session its own slice to CloseSession — avoids the N+1 channel read (one RunView per
@@ -256,12 +852,44 @@ export class SessionJanitor extends BaseSingleton {
256
852
  closedCount++;
257
853
  }
258
854
  }
855
+ });
856
+ return closedCount;
857
+ }
858
+ /**
859
+ * Keyset-page (`AfterKey`) through every session matching `filter`, handing each full fetched page
860
+ * to `visit`. Stops on a load failure (already logged), an empty page, a partial page, or when
861
+ * `visit` returns `false`.
862
+ *
863
+ * `visit` receives the page exactly as the SQL filter matched it. The keyset advances from the
864
+ * LAST FETCHED row, so any narrowing `visit` does in JS can never stall or skip pagination.
865
+ *
866
+ * Returns how the walk ended: `'complete'` (reached the end of the matching set), `'stopped'`
867
+ * (`visit` returned `false`) or `'failed'` (a page failed to load). Callers that reason about the
868
+ * WHOLE set (e.g. pruning state for absent rows) must act only on `'complete'`.
869
+ */
870
+ async forEachSessionPage(filter, provider, systemUser, visit) {
871
+ let afterKey;
872
+ // eslint-disable-next-line no-constant-condition
873
+ while (true) {
874
+ const fetched = await this.fetchPage(filter, afterKey, provider, systemUser);
875
+ if (fetched == null) {
876
+ return 'failed'; // load failure already logged
877
+ }
878
+ if (fetched.length === 0) {
879
+ return 'complete';
880
+ }
881
+ // Advance from the last FETCHED row (before `visit` narrows anything): we must page by the
882
+ // SQL-matched set, not a post-filtered subset, or pagination would stall/skip.
883
+ const lastFetchedId = fetched[fetched.length - 1].ID;
884
+ const fetchedCount = fetched.length;
885
+ if ((await visit(fetched)) === false) {
886
+ return 'stopped';
887
+ }
259
888
  if (fetchedCount < SWEEP_PAGE_SIZE) {
260
- break; // partial page (by the SQL-matched set) => end of data
889
+ return 'complete'; // partial page (by the SQL-matched set) => end of data
261
890
  }
262
891
  afterKey = CompositeKey.FromID(lastFetchedId); // first-pk-ok: keyset AfterKey on SESSION_ENTITY = MJ: AI Agent Sessions, a core entity keyed by ID
263
892
  }
264
- return closedCount;
265
893
  }
266
894
  /** Fetch one keyset page of matching sessions, or null on a load failure (logged). */
267
895
  async fetchPage(filter, afterKey, provider, systemUser) {