@smthrs/gateway 0.35.0 → 1.0.0-rc.4

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 (605) hide show
  1. package/CHANGELOG.md +198 -0
  2. package/LICENSE +1 -1
  3. package/README.md +92 -0
  4. package/dist/cjs/Diagnosis.d.ts +275 -0
  5. package/dist/cjs/Diagnosis.d.ts.map +1 -0
  6. package/dist/cjs/Diagnosis.js +476 -0
  7. package/dist/cjs/Diagnosis.js.map +7 -0
  8. package/dist/cjs/EngineTrace.d.ts +76 -0
  9. package/dist/cjs/EngineTrace.d.ts.map +1 -0
  10. package/dist/cjs/EngineTrace.js +509 -0
  11. package/dist/cjs/EngineTrace.js.map +7 -0
  12. package/dist/cjs/GatewayError.d.ts +54 -0
  13. package/dist/cjs/GatewayError.d.ts.map +1 -0
  14. package/dist/cjs/GatewayError.js +51 -0
  15. package/dist/cjs/GatewayError.js.map +7 -0
  16. package/dist/cjs/GatewayProjection.d.ts +480 -0
  17. package/dist/cjs/GatewayProjection.d.ts.map +1 -0
  18. package/dist/cjs/GatewayProjection.js +471 -0
  19. package/dist/cjs/GatewayProjection.js.map +7 -0
  20. package/dist/cjs/GatewayRpcs.d.ts +1886 -0
  21. package/dist/cjs/GatewayRpcs.d.ts.map +1 -0
  22. package/dist/cjs/GatewayRpcs.js +121 -0
  23. package/dist/cjs/GatewayRpcs.js.map +7 -0
  24. package/dist/cjs/GatewaySchema.d.ts +3274 -0
  25. package/dist/cjs/GatewaySchema.d.ts.map +1 -0
  26. package/dist/cjs/GatewaySchema.js +141 -0
  27. package/dist/cjs/GatewaySchema.js.map +7 -0
  28. package/dist/cjs/GatewayServer.d.ts +427 -0
  29. package/dist/cjs/GatewayServer.d.ts.map +1 -0
  30. package/dist/cjs/GatewayServer.js +428 -0
  31. package/dist/cjs/GatewayServer.js.map +7 -0
  32. package/dist/cjs/HostBranchPresence.d.ts +19 -0
  33. package/dist/cjs/HostBranchPresence.d.ts.map +1 -0
  34. package/dist/cjs/HostBranchPresence.js +82 -0
  35. package/dist/cjs/HostBranchPresence.js.map +7 -0
  36. package/dist/cjs/Projections.d.ts +209 -0
  37. package/dist/cjs/Projections.d.ts.map +1 -0
  38. package/dist/cjs/Projections.js +906 -0
  39. package/dist/cjs/Projections.js.map +7 -0
  40. package/dist/cjs/RunDevTools.d.ts +181 -0
  41. package/dist/cjs/RunDevTools.d.ts.map +1 -0
  42. package/dist/cjs/RunDevTools.js +195 -0
  43. package/dist/cjs/RunDevTools.js.map +7 -0
  44. package/dist/cjs/RunHistory.d.ts +185 -0
  45. package/dist/cjs/RunHistory.d.ts.map +1 -0
  46. package/dist/cjs/RunHistory.js +68 -0
  47. package/dist/cjs/RunHistory.js.map +7 -0
  48. package/dist/cjs/RunTrace.d.ts +753 -0
  49. package/dist/cjs/RunTrace.d.ts.map +1 -0
  50. package/dist/cjs/RunTrace.js +1595 -0
  51. package/dist/cjs/RunTrace.js.map +7 -0
  52. package/dist/cjs/RuntimeBridge.d.ts +1386 -0
  53. package/dist/cjs/RuntimeBridge.d.ts.map +1 -0
  54. package/dist/cjs/RuntimeBridge.js +598 -0
  55. package/dist/cjs/RuntimeBridge.js.map +7 -0
  56. package/dist/cjs/bun/BunGateway.d.ts +20 -0
  57. package/dist/cjs/bun/BunGateway.d.ts.map +1 -0
  58. package/dist/cjs/bun/BunGateway.js +62 -0
  59. package/dist/cjs/bun/BunGateway.js.map +7 -0
  60. package/dist/cjs/index.d.ts +65 -0
  61. package/dist/cjs/index.d.ts.map +1 -0
  62. package/dist/cjs/index.js +59 -0
  63. package/dist/cjs/index.js.map +7 -0
  64. package/dist/cjs/internal/NativeGateway.d.ts +176 -0
  65. package/dist/cjs/internal/NativeGateway.d.ts.map +1 -0
  66. package/dist/cjs/internal/NativeGateway.js +187 -0
  67. package/dist/cjs/internal/NativeGateway.js.map +7 -0
  68. package/dist/cjs/internal/actionRenderings.d.ts +12 -0
  69. package/dist/cjs/internal/actionRenderings.d.ts.map +1 -0
  70. package/dist/cjs/internal/actionRenderings.js +224 -0
  71. package/dist/cjs/internal/actionRenderings.js.map +7 -0
  72. package/dist/cjs/internal/callEvents.d.ts +97 -0
  73. package/dist/cjs/internal/callEvents.d.ts.map +1 -0
  74. package/dist/cjs/internal/callEvents.js +169 -0
  75. package/dist/cjs/internal/callEvents.js.map +7 -0
  76. package/dist/cjs/internal/digestIndex.d.ts +31 -0
  77. package/dist/cjs/internal/digestIndex.d.ts.map +1 -0
  78. package/dist/cjs/internal/digestIndex.js +105 -0
  79. package/dist/cjs/internal/digestIndex.js.map +7 -0
  80. package/dist/cjs/internal/digestMemory.d.ts +19 -0
  81. package/dist/cjs/internal/digestMemory.d.ts.map +1 -0
  82. package/dist/cjs/internal/digestMemory.js +33 -0
  83. package/dist/cjs/internal/digestMemory.js.map +7 -0
  84. package/dist/cjs/internal/inspectLabels.d.ts +13 -0
  85. package/dist/cjs/internal/inspectLabels.d.ts.map +1 -0
  86. package/dist/cjs/internal/inspectLabels.js +29 -0
  87. package/dist/cjs/internal/inspectLabels.js.map +7 -0
  88. package/dist/cjs/internal/nativeResolution.d.ts +41 -0
  89. package/dist/cjs/internal/nativeResolution.d.ts.map +1 -0
  90. package/dist/cjs/internal/nativeResolution.js +62 -0
  91. package/dist/cjs/internal/nativeResolution.js.map +7 -0
  92. package/dist/cjs/internal/nodeEvents.d.ts +47 -0
  93. package/dist/cjs/internal/nodeEvents.d.ts.map +1 -0
  94. package/dist/cjs/internal/nodeEvents.js +60 -0
  95. package/dist/cjs/internal/nodeEvents.js.map +7 -0
  96. package/dist/cjs/node/NodeGateway.d.ts +16 -0
  97. package/dist/cjs/node/NodeGateway.d.ts.map +1 -0
  98. package/dist/cjs/node/NodeGateway.js +48 -0
  99. package/dist/cjs/node/NodeGateway.js.map +7 -0
  100. package/dist/cjs/package.json +1 -0
  101. package/dist/esm/Diagnosis.d.ts +275 -0
  102. package/dist/esm/Diagnosis.d.ts.map +1 -0
  103. package/dist/esm/Diagnosis.js +697 -0
  104. package/dist/esm/Diagnosis.js.map +1 -0
  105. package/dist/esm/EngineTrace.d.ts +76 -0
  106. package/dist/esm/EngineTrace.d.ts.map +1 -0
  107. package/dist/esm/EngineTrace.js +557 -0
  108. package/dist/esm/EngineTrace.js.map +1 -0
  109. package/dist/esm/GatewayError.d.ts +54 -0
  110. package/dist/esm/GatewayError.d.ts.map +1 -0
  111. package/dist/esm/GatewayError.js +60 -0
  112. package/dist/esm/GatewayError.js.map +1 -0
  113. package/dist/esm/GatewayProjection.d.ts +480 -0
  114. package/dist/esm/GatewayProjection.d.ts.map +1 -0
  115. package/dist/esm/GatewayProjection.js +799 -0
  116. package/dist/esm/GatewayProjection.js.map +1 -0
  117. package/dist/esm/GatewayRpcs.d.ts +1886 -0
  118. package/dist/esm/GatewayRpcs.d.ts.map +1 -0
  119. package/dist/esm/GatewayRpcs.js +145 -0
  120. package/dist/esm/GatewayRpcs.js.map +1 -0
  121. package/dist/esm/GatewaySchema.d.ts +3274 -0
  122. package/dist/esm/GatewaySchema.d.ts.map +1 -0
  123. package/dist/esm/GatewaySchema.js +244 -0
  124. package/dist/esm/GatewaySchema.js.map +1 -0
  125. package/dist/esm/GatewayServer.d.ts +427 -0
  126. package/dist/esm/GatewayServer.d.ts.map +1 -0
  127. package/dist/esm/GatewayServer.js +725 -0
  128. package/dist/esm/GatewayServer.js.map +1 -0
  129. package/dist/esm/HostBranchPresence.d.ts +19 -0
  130. package/dist/esm/HostBranchPresence.d.ts.map +1 -0
  131. package/dist/esm/HostBranchPresence.js +57 -0
  132. package/dist/esm/HostBranchPresence.js.map +1 -0
  133. package/dist/esm/Projections.d.ts +209 -0
  134. package/dist/esm/Projections.d.ts.map +1 -0
  135. package/dist/esm/Projections.js +1254 -0
  136. package/dist/esm/Projections.js.map +1 -0
  137. package/dist/esm/RunDevTools.d.ts +181 -0
  138. package/dist/esm/RunDevTools.d.ts.map +1 -0
  139. package/dist/esm/RunDevTools.js +281 -0
  140. package/dist/esm/RunDevTools.js.map +1 -0
  141. package/dist/esm/RunHistory.d.ts +185 -0
  142. package/dist/esm/RunHistory.d.ts.map +1 -0
  143. package/dist/esm/RunHistory.js +109 -0
  144. package/dist/esm/RunHistory.js.map +1 -0
  145. package/dist/esm/RunTrace.d.ts +753 -0
  146. package/dist/esm/RunTrace.d.ts.map +1 -0
  147. package/dist/esm/RunTrace.js +2106 -0
  148. package/dist/esm/RunTrace.js.map +1 -0
  149. package/dist/esm/RuntimeBridge.d.ts +1386 -0
  150. package/dist/esm/RuntimeBridge.d.ts.map +1 -0
  151. package/dist/esm/RuntimeBridge.js +659 -0
  152. package/dist/esm/RuntimeBridge.js.map +1 -0
  153. package/dist/esm/bun/BunGateway.d.ts +20 -0
  154. package/dist/esm/bun/BunGateway.d.ts.map +1 -0
  155. package/dist/esm/bun/BunGateway.js +32 -0
  156. package/dist/esm/bun/BunGateway.js.map +1 -0
  157. package/dist/esm/index.d.ts +65 -0
  158. package/dist/esm/index.d.ts.map +1 -0
  159. package/dist/esm/index.js +65 -0
  160. package/dist/esm/index.js.map +1 -0
  161. package/dist/esm/internal/NativeGateway.d.ts +176 -0
  162. package/dist/esm/internal/NativeGateway.d.ts.map +1 -0
  163. package/dist/esm/internal/NativeGateway.js +268 -0
  164. package/dist/esm/internal/NativeGateway.js.map +1 -0
  165. package/dist/esm/internal/actionRenderings.d.ts +12 -0
  166. package/dist/esm/internal/actionRenderings.d.ts.map +1 -0
  167. package/dist/esm/internal/actionRenderings.js +212 -0
  168. package/dist/esm/internal/actionRenderings.js.map +1 -0
  169. package/dist/esm/internal/callEvents.d.ts +97 -0
  170. package/dist/esm/internal/callEvents.d.ts.map +1 -0
  171. package/dist/esm/internal/callEvents.js +206 -0
  172. package/dist/esm/internal/callEvents.js.map +1 -0
  173. package/dist/esm/internal/digestIndex.d.ts +31 -0
  174. package/dist/esm/internal/digestIndex.d.ts.map +1 -0
  175. package/dist/esm/internal/digestIndex.js +87 -0
  176. package/dist/esm/internal/digestIndex.js.map +1 -0
  177. package/dist/esm/internal/digestMemory.d.ts +19 -0
  178. package/dist/esm/internal/digestMemory.d.ts.map +1 -0
  179. package/dist/esm/internal/digestMemory.js +23 -0
  180. package/dist/esm/internal/digestMemory.js.map +1 -0
  181. package/dist/esm/internal/inspectLabels.d.ts +13 -0
  182. package/dist/esm/internal/inspectLabels.d.ts.map +1 -0
  183. package/dist/esm/internal/inspectLabels.js +17 -0
  184. package/dist/esm/internal/inspectLabels.js.map +1 -0
  185. package/dist/esm/internal/nativeResolution.d.ts +41 -0
  186. package/dist/esm/internal/nativeResolution.d.ts.map +1 -0
  187. package/dist/esm/internal/nativeResolution.js +78 -0
  188. package/dist/esm/internal/nativeResolution.js.map +1 -0
  189. package/dist/esm/internal/nodeEvents.d.ts +47 -0
  190. package/dist/esm/internal/nodeEvents.d.ts.map +1 -0
  191. package/dist/esm/internal/nodeEvents.js +60 -0
  192. package/dist/esm/internal/nodeEvents.js.map +1 -0
  193. package/dist/esm/node/NodeGateway.d.ts +16 -0
  194. package/dist/esm/node/NodeGateway.d.ts.map +1 -0
  195. package/dist/esm/node/NodeGateway.js +13 -0
  196. package/dist/esm/node/NodeGateway.js.map +1 -0
  197. package/dist/vendor/adapters.json +2707 -0
  198. package/dist/vendor/bun/BunChildProcessSpawner.d.ts +7 -0
  199. package/dist/vendor/bun/BunChildProcessSpawner.js +14 -0
  200. package/dist/vendor/bun/BunClusterHttp.d.ts +83 -0
  201. package/dist/vendor/bun/BunClusterHttp.js +39 -0
  202. package/dist/vendor/bun/BunClusterSocket.d.ts +65 -0
  203. package/dist/vendor/bun/BunClusterSocket.js +22 -0
  204. package/dist/vendor/bun/BunCrypto.d.ts +10 -0
  205. package/dist/vendor/bun/BunCrypto.js +8 -0
  206. package/dist/vendor/bun/BunFileSystem.d.ts +10 -0
  207. package/dist/vendor/bun/BunFileSystem.js +9 -0
  208. package/dist/vendor/bun/BunHttpClient.d.ts +9 -0
  209. package/dist/vendor/bun/BunHttpClient.js +1 -0
  210. package/dist/vendor/bun/BunHttpPlatform.d.ts +10 -0
  211. package/dist/vendor/bun/BunHttpPlatform.js +11 -0
  212. package/dist/vendor/bun/BunHttpServer.d.ts +142 -0
  213. package/dist/vendor/bun/BunHttpServer.js +38 -0
  214. package/dist/vendor/bun/BunHttpServerRequest.d.ts +18 -0
  215. package/dist/vendor/bun/BunHttpServerRequest.js +7 -0
  216. package/dist/vendor/bun/BunMultipart.d.ts +31 -0
  217. package/dist/vendor/bun/BunMultipart.js +12 -0
  218. package/dist/vendor/bun/BunPath.d.ts +24 -0
  219. package/dist/vendor/bun/BunPath.js +12 -0
  220. package/dist/vendor/bun/BunRedis.d.ts +49 -0
  221. package/dist/vendor/bun/BunRedis.js +11 -0
  222. package/dist/vendor/bun/BunRuntime.d.ts +85 -0
  223. package/dist/vendor/bun/BunRuntime.js +8 -0
  224. package/dist/vendor/bun/BunServices.d.ts +34 -0
  225. package/dist/vendor/bun/BunServices.js +23 -0
  226. package/dist/vendor/bun/BunSink.d.ts +8 -0
  227. package/dist/vendor/bun/BunSink.js +12 -0
  228. package/dist/vendor/bun/BunSocket.d.ts +40 -0
  229. package/dist/vendor/bun/BunSocket.js +29 -0
  230. package/dist/vendor/bun/BunSocketServer.d.ts +8 -0
  231. package/dist/vendor/bun/BunSocketServer.js +21 -0
  232. package/dist/vendor/bun/BunStdio.d.ts +11 -0
  233. package/dist/vendor/bun/BunStdio.js +10 -0
  234. package/dist/vendor/bun/BunStream.d.ts +19 -0
  235. package/dist/vendor/bun/BunStream.js +30 -0
  236. package/dist/vendor/bun/BunTerminal.d.ts +21 -0
  237. package/dist/vendor/bun/BunTerminal.js +10 -0
  238. package/dist/vendor/bun/BunWorker.d.ts +20 -0
  239. package/dist/vendor/bun/BunWorker.js +9 -0
  240. package/dist/vendor/bun/BunWorkerRunner.d.ts +12 -0
  241. package/dist/vendor/bun/BunWorkerRunner.js +7 -0
  242. package/dist/vendor/bun/LICENSE +21 -0
  243. package/dist/vendor/bun/index.d.ts +92 -0
  244. package/dist/vendor/bun/index.js +107 -0
  245. package/dist/vendor/chunk-2GCOZTTW.js +48 -0
  246. package/dist/vendor/chunk-37CUR3PO.js +81 -0
  247. package/dist/vendor/chunk-3ARKKENZ.js +45 -0
  248. package/dist/vendor/chunk-3NV2UDBI.js +7 -0
  249. package/dist/vendor/chunk-3OSW7CT7.js +143 -0
  250. package/dist/vendor/chunk-62R63L74.js +62 -0
  251. package/dist/vendor/chunk-6AQLBC2I.js +118 -0
  252. package/dist/vendor/chunk-6C5H7S6B.js +18 -0
  253. package/dist/vendor/chunk-6DY5RBZD.js +85 -0
  254. package/dist/vendor/chunk-6UPBD47P.js +53 -0
  255. package/dist/vendor/chunk-AVVVN75Z.js +96 -0
  256. package/dist/vendor/chunk-B4DHEMBB.js +14 -0
  257. package/dist/vendor/chunk-BCK3NJOX.js +15 -0
  258. package/dist/vendor/chunk-BFBIHJ6X.js +0 -0
  259. package/dist/vendor/chunk-CES4OUM4.js +63 -0
  260. package/dist/vendor/chunk-D64L6YND.js +59 -0
  261. package/dist/vendor/chunk-DS2R6WOM.js +18 -0
  262. package/dist/vendor/chunk-DX4Z23DM.js +65 -0
  263. package/dist/vendor/chunk-DZ2VOUDP.js +564 -0
  264. package/dist/vendor/chunk-FOXN6JPJ.js +61 -0
  265. package/dist/vendor/chunk-FPQF7CR5.js +55 -0
  266. package/dist/vendor/chunk-FUTCTCEG.js +20 -0
  267. package/dist/vendor/chunk-GUPAV2YZ.js +18 -0
  268. package/dist/vendor/chunk-H2Z2Y4QH.js +70 -0
  269. package/dist/vendor/chunk-HAEDVSSM.js +129 -0
  270. package/dist/vendor/chunk-HY7BBL3K.js +93 -0
  271. package/dist/vendor/chunk-IFTTT6D5.js +31 -0
  272. package/dist/vendor/chunk-KQDSNB7M.js +26 -0
  273. package/dist/vendor/chunk-LIHLXXGQ.js +26 -0
  274. package/dist/vendor/chunk-LKCGQEE5.js +28 -0
  275. package/dist/vendor/chunk-MH2J3BYB.js +0 -0
  276. package/dist/vendor/chunk-ML2NAFH2.js +131 -0
  277. package/dist/vendor/chunk-MLFIZ5O2.js +0 -0
  278. package/dist/vendor/chunk-MLKGABMK.js +9 -0
  279. package/dist/vendor/chunk-MSCLEMYX.js +71 -0
  280. package/dist/vendor/chunk-OBWXQKVP.js +57 -0
  281. package/dist/vendor/chunk-QR557K66.js +20 -0
  282. package/dist/vendor/chunk-R4QUY7DH.js +18 -0
  283. package/dist/vendor/chunk-RGECXXJS.js +18 -0
  284. package/dist/vendor/chunk-TFY7W6D4.js +36 -0
  285. package/dist/vendor/chunk-TGPIVY4A.js +104 -0
  286. package/dist/vendor/chunk-TGXCX6W3.js +398 -0
  287. package/dist/vendor/chunk-TOHSFXMV.js +437 -0
  288. package/dist/vendor/chunk-UHIDPUFF.js +34 -0
  289. package/dist/vendor/chunk-ULT6CWUI.js +109 -0
  290. package/dist/vendor/chunk-UXBGYQR3.js +18 -0
  291. package/dist/vendor/chunk-V6LUGQ7O.js +21 -0
  292. package/dist/vendor/chunk-W4OASVX6.js +92 -0
  293. package/dist/vendor/chunk-WAII54GA.js +18 -0
  294. package/dist/vendor/chunk-WECQDT36.js +426 -0
  295. package/dist/vendor/chunk-WHKMOC7Z.js +28 -0
  296. package/dist/vendor/chunk-WVFOZ2UL.js +40 -0
  297. package/dist/vendor/chunk-X73E2UPI.js +18 -0
  298. package/dist/vendor/chunk-XCYARUII.js +63 -0
  299. package/dist/vendor/chunk-XFEVTDVD.js +34 -0
  300. package/dist/vendor/chunk-XFZZYSJ4.js +22 -0
  301. package/dist/vendor/chunk-XJWJXLXJ.js +18 -0
  302. package/dist/vendor/chunk-XY3Q62EO.js +18 -0
  303. package/dist/vendor/chunk-YDASSC75.js +394 -0
  304. package/dist/vendor/chunk-Z6C7SO5Z.js +507 -0
  305. package/dist/vendor/chunk-Z7QNPJEB.js +229 -0
  306. package/dist/vendor/chunk-Z7QPP4YH.js +249 -0
  307. package/dist/vendor/chunk-ZCHYLHJH.js +86 -0
  308. package/dist/vendor/chunk-ZIQVYVMA.js +22 -0
  309. package/dist/vendor/chunk-ZPIG2CUA.js +34 -0
  310. package/dist/vendor/cjs/bun/BunChildProcessSpawner.d.ts +7 -0
  311. package/dist/vendor/cjs/bun/BunChildProcessSpawner.js +1 -0
  312. package/dist/vendor/cjs/bun/BunClusterHttp.d.ts +83 -0
  313. package/dist/vendor/cjs/bun/BunClusterHttp.js +1 -0
  314. package/dist/vendor/cjs/bun/BunClusterSocket.d.ts +65 -0
  315. package/dist/vendor/cjs/bun/BunClusterSocket.js +1 -0
  316. package/dist/vendor/cjs/bun/BunCrypto.d.ts +10 -0
  317. package/dist/vendor/cjs/bun/BunCrypto.js +1 -0
  318. package/dist/vendor/cjs/bun/BunFileSystem.d.ts +10 -0
  319. package/dist/vendor/cjs/bun/BunFileSystem.js +1 -0
  320. package/dist/vendor/cjs/bun/BunHttpClient.d.ts +9 -0
  321. package/dist/vendor/cjs/bun/BunHttpClient.js +1 -0
  322. package/dist/vendor/cjs/bun/BunHttpPlatform.d.ts +10 -0
  323. package/dist/vendor/cjs/bun/BunHttpPlatform.js +1 -0
  324. package/dist/vendor/cjs/bun/BunHttpServer.d.ts +142 -0
  325. package/dist/vendor/cjs/bun/BunHttpServer.js +1 -0
  326. package/dist/vendor/cjs/bun/BunHttpServerRequest.d.ts +18 -0
  327. package/dist/vendor/cjs/bun/BunHttpServerRequest.js +1 -0
  328. package/dist/vendor/cjs/bun/BunMultipart.d.ts +31 -0
  329. package/dist/vendor/cjs/bun/BunMultipart.js +1 -0
  330. package/dist/vendor/cjs/bun/BunPath.d.ts +24 -0
  331. package/dist/vendor/cjs/bun/BunPath.js +1 -0
  332. package/dist/vendor/cjs/bun/BunRedis.d.ts +49 -0
  333. package/dist/vendor/cjs/bun/BunRedis.js +1 -0
  334. package/dist/vendor/cjs/bun/BunRuntime.d.ts +85 -0
  335. package/dist/vendor/cjs/bun/BunRuntime.js +1 -0
  336. package/dist/vendor/cjs/bun/BunServices.d.ts +34 -0
  337. package/dist/vendor/cjs/bun/BunServices.js +1 -0
  338. package/dist/vendor/cjs/bun/BunSink.d.ts +8 -0
  339. package/dist/vendor/cjs/bun/BunSink.js +1 -0
  340. package/dist/vendor/cjs/bun/BunSocket.d.ts +40 -0
  341. package/dist/vendor/cjs/bun/BunSocket.js +1 -0
  342. package/dist/vendor/cjs/bun/BunSocketServer.d.ts +8 -0
  343. package/dist/vendor/cjs/bun/BunSocketServer.js +1 -0
  344. package/dist/vendor/cjs/bun/BunStdio.d.ts +11 -0
  345. package/dist/vendor/cjs/bun/BunStdio.js +1 -0
  346. package/dist/vendor/cjs/bun/BunStream.d.ts +19 -0
  347. package/dist/vendor/cjs/bun/BunStream.js +1 -0
  348. package/dist/vendor/cjs/bun/BunTerminal.d.ts +21 -0
  349. package/dist/vendor/cjs/bun/BunTerminal.js +1 -0
  350. package/dist/vendor/cjs/bun/BunWorker.d.ts +20 -0
  351. package/dist/vendor/cjs/bun/BunWorker.js +1 -0
  352. package/dist/vendor/cjs/bun/BunWorkerRunner.d.ts +12 -0
  353. package/dist/vendor/cjs/bun/BunWorkerRunner.js +1 -0
  354. package/dist/vendor/cjs/bun/index.d.ts +92 -0
  355. package/dist/vendor/cjs/bun/index.js +1 -0
  356. package/dist/vendor/cjs/node/NodeChildProcessSpawner.d.ts +7 -0
  357. package/dist/vendor/cjs/node/NodeChildProcessSpawner.js +1 -0
  358. package/dist/vendor/cjs/node/NodeClusterHttp.d.ts +63 -0
  359. package/dist/vendor/cjs/node/NodeClusterHttp.js +1 -0
  360. package/dist/vendor/cjs/node/NodeClusterSocket.d.ts +79 -0
  361. package/dist/vendor/cjs/node/NodeClusterSocket.js +1 -0
  362. package/dist/vendor/cjs/node/NodeCrypto.d.ts +10 -0
  363. package/dist/vendor/cjs/node/NodeCrypto.js +1 -0
  364. package/dist/vendor/cjs/node/NodeFileSystem.d.ts +10 -0
  365. package/dist/vendor/cjs/node/NodeFileSystem.js +1 -0
  366. package/dist/vendor/cjs/node/NodeHttpClient.d.ts +179 -0
  367. package/dist/vendor/cjs/node/NodeHttpClient.js +1 -0
  368. package/dist/vendor/cjs/node/NodeHttpIncomingMessage.d.ts +62 -0
  369. package/dist/vendor/cjs/node/NodeHttpIncomingMessage.js +1 -0
  370. package/dist/vendor/cjs/node/NodeHttpPlatform.d.ts +36 -0
  371. package/dist/vendor/cjs/node/NodeHttpPlatform.js +1 -0
  372. package/dist/vendor/cjs/node/NodeHttpServer.d.ts +120 -0
  373. package/dist/vendor/cjs/node/NodeHttpServer.js +1 -0
  374. package/dist/vendor/cjs/node/NodeHttpServerRequest.d.ts +29 -0
  375. package/dist/vendor/cjs/node/NodeHttpServerRequest.js +1 -0
  376. package/dist/vendor/cjs/node/NodeMultipart.d.ts +46 -0
  377. package/dist/vendor/cjs/node/NodeMultipart.js +1 -0
  378. package/dist/vendor/cjs/node/NodeMultipartParser.d.ts +97 -0
  379. package/dist/vendor/cjs/node/NodeMultipartParser.js +1 -0
  380. package/dist/vendor/cjs/node/NodePath.d.ts +27 -0
  381. package/dist/vendor/cjs/node/NodePath.js +1 -0
  382. package/dist/vendor/cjs/node/NodeRedis.d.ts +74 -0
  383. package/dist/vendor/cjs/node/NodeRedis.js +1 -0
  384. package/dist/vendor/cjs/node/NodeRuntime.d.ts +85 -0
  385. package/dist/vendor/cjs/node/NodeRuntime.js +1 -0
  386. package/dist/vendor/cjs/node/NodeServices.d.ts +34 -0
  387. package/dist/vendor/cjs/node/NodeServices.js +1 -0
  388. package/dist/vendor/cjs/node/NodeSink.d.ts +8 -0
  389. package/dist/vendor/cjs/node/NodeSink.js +1 -0
  390. package/dist/vendor/cjs/node/NodeSocket.d.ts +38 -0
  391. package/dist/vendor/cjs/node/NodeSocket.js +1 -0
  392. package/dist/vendor/cjs/node/NodeSocketServer.d.ts +8 -0
  393. package/dist/vendor/cjs/node/NodeSocketServer.js +1 -0
  394. package/dist/vendor/cjs/node/NodeStdio.d.ts +11 -0
  395. package/dist/vendor/cjs/node/NodeStdio.js +1 -0
  396. package/dist/vendor/cjs/node/NodeStream.d.ts +8 -0
  397. package/dist/vendor/cjs/node/NodeStream.js +1 -0
  398. package/dist/vendor/cjs/node/NodeTerminal.d.ts +21 -0
  399. package/dist/vendor/cjs/node/NodeTerminal.js +1 -0
  400. package/dist/vendor/cjs/node/NodeWorker.d.ts +22 -0
  401. package/dist/vendor/cjs/node/NodeWorker.js +1 -0
  402. package/dist/vendor/cjs/node/NodeWorkerRunner.d.ts +12 -0
  403. package/dist/vendor/cjs/node/NodeWorkerRunner.js +1 -0
  404. package/dist/vendor/cjs/node/Undici.d.ts +28 -0
  405. package/dist/vendor/cjs/node/Undici.js +1 -0
  406. package/dist/vendor/cjs/node/index.d.ts +100 -0
  407. package/dist/vendor/cjs/node/index.js +1 -0
  408. package/dist/vendor/cjs/node-shared/NodeChildProcessSpawner.d.ts +57 -0
  409. package/dist/vendor/cjs/node-shared/NodeChildProcessSpawner.js +1 -0
  410. package/dist/vendor/cjs/node-shared/NodeClusterSocket.d.ts +22 -0
  411. package/dist/vendor/cjs/node-shared/NodeClusterSocket.js +1 -0
  412. package/dist/vendor/cjs/node-shared/NodeCrypto.d.ts +27 -0
  413. package/dist/vendor/cjs/node-shared/NodeCrypto.js +1 -0
  414. package/dist/vendor/cjs/node-shared/NodeFileSystem.d.ts +11 -0
  415. package/dist/vendor/cjs/node-shared/NodeFileSystem.js +1 -0
  416. package/dist/vendor/cjs/node-shared/NodeHttpCompression.d.ts +36 -0
  417. package/dist/vendor/cjs/node-shared/NodeHttpCompression.js +1 -0
  418. package/dist/vendor/cjs/node-shared/NodePath.d.ts +27 -0
  419. package/dist/vendor/cjs/node-shared/NodePath.js +1 -0
  420. package/dist/vendor/cjs/node-shared/NodeRuntime.d.ts +47 -0
  421. package/dist/vendor/cjs/node-shared/NodeRuntime.js +1 -0
  422. package/dist/vendor/cjs/node-shared/NodeSink.d.ts +69 -0
  423. package/dist/vendor/cjs/node-shared/NodeSink.js +1 -0
  424. package/dist/vendor/cjs/node-shared/NodeSocket.d.ts +134 -0
  425. package/dist/vendor/cjs/node-shared/NodeSocket.js +1 -0
  426. package/dist/vendor/cjs/node-shared/NodeSocketServer.d.ts +80 -0
  427. package/dist/vendor/cjs/node-shared/NodeSocketServer.js +1 -0
  428. package/dist/vendor/cjs/node-shared/NodeStdio.d.ts +12 -0
  429. package/dist/vendor/cjs/node-shared/NodeStdio.js +1 -0
  430. package/dist/vendor/cjs/node-shared/NodeStream.d.ts +179 -0
  431. package/dist/vendor/cjs/node-shared/NodeStream.js +1 -0
  432. package/dist/vendor/cjs/node-shared/NodeTerminal.d.ts +22 -0
  433. package/dist/vendor/cjs/node-shared/NodeTerminal.js +1 -0
  434. package/dist/vendor/cjs/node-shared/index.d.ts +56 -0
  435. package/dist/vendor/cjs/node-shared/index.js +1 -0
  436. package/dist/vendor/cjs/node-shared/internal/nodeChildProcessSpawner.d.ts +4 -0
  437. package/dist/vendor/cjs/node-shared/internal/nodeChildProcessSpawner.js +1 -0
  438. package/dist/vendor/cjs/node-shared/internal/utils.d.ts +2 -0
  439. package/dist/vendor/cjs/node-shared/internal/utils.js +1 -0
  440. package/dist/vendor/cjs/package.json +1 -0
  441. package/dist/vendor/cjs/platform-bun.d.ts +92 -0
  442. package/dist/vendor/cjs/platform-bun.js +1 -0
  443. package/dist/vendor/cjs/platform-node-shared.d.ts +56 -0
  444. package/dist/vendor/cjs/platform-node-shared.js +1 -0
  445. package/dist/vendor/cjs/platform-node.d.ts +100 -0
  446. package/dist/vendor/cjs/platform-node.js +1 -0
  447. package/dist/vendor/node/LICENSE +21 -0
  448. package/dist/vendor/node/NodeChildProcessSpawner.d.ts +7 -0
  449. package/dist/vendor/node/NodeChildProcessSpawner.js +14 -0
  450. package/dist/vendor/node/NodeClusterHttp.d.ts +63 -0
  451. package/dist/vendor/node/NodeClusterHttp.js +43 -0
  452. package/dist/vendor/node/NodeClusterSocket.d.ts +79 -0
  453. package/dist/vendor/node/NodeClusterSocket.js +30 -0
  454. package/dist/vendor/node/NodeCrypto.d.ts +10 -0
  455. package/dist/vendor/node/NodeCrypto.js +8 -0
  456. package/dist/vendor/node/NodeFileSystem.d.ts +10 -0
  457. package/dist/vendor/node/NodeFileSystem.js +9 -0
  458. package/dist/vendor/node/NodeHttpClient.d.ts +179 -0
  459. package/dist/vendor/node/NodeHttpClient.js +46 -0
  460. package/dist/vendor/node/NodeHttpIncomingMessage.d.ts +62 -0
  461. package/dist/vendor/node/NodeHttpIncomingMessage.js +10 -0
  462. package/dist/vendor/node/NodeHttpPlatform.d.ts +36 -0
  463. package/dist/vendor/node/NodeHttpPlatform.js +16 -0
  464. package/dist/vendor/node/NodeHttpServer.d.ts +120 -0
  465. package/dist/vendor/node/NodeHttpServer.js +46 -0
  466. package/dist/vendor/node/NodeHttpServerRequest.d.ts +29 -0
  467. package/dist/vendor/node/NodeHttpServerRequest.js +9 -0
  468. package/dist/vendor/node/NodeMultipart.d.ts +46 -0
  469. package/dist/vendor/node/NodeMultipart.js +15 -0
  470. package/dist/vendor/node/NodeMultipartParser.d.ts +97 -0
  471. package/dist/vendor/node/NodeMultipartParser.js +11 -0
  472. package/dist/vendor/node/NodePath.d.ts +27 -0
  473. package/dist/vendor/node/NodePath.js +12 -0
  474. package/dist/vendor/node/NodeRedis.d.ts +74 -0
  475. package/dist/vendor/node/NodeRedis.js +11 -0
  476. package/dist/vendor/node/NodeRuntime.d.ts +85 -0
  477. package/dist/vendor/node/NodeRuntime.js +8 -0
  478. package/dist/vendor/node/NodeServices.d.ts +34 -0
  479. package/dist/vendor/node/NodeServices.js +23 -0
  480. package/dist/vendor/node/NodeSink.d.ts +8 -0
  481. package/dist/vendor/node/NodeSink.js +12 -0
  482. package/dist/vendor/node/NodeSocket.d.ts +38 -0
  483. package/dist/vendor/node/NodeSocket.js +31 -0
  484. package/dist/vendor/node/NodeSocketServer.d.ts +8 -0
  485. package/dist/vendor/node/NodeSocketServer.js +21 -0
  486. package/dist/vendor/node/NodeStdio.d.ts +11 -0
  487. package/dist/vendor/node/NodeStdio.js +10 -0
  488. package/dist/vendor/node/NodeStream.d.ts +8 -0
  489. package/dist/vendor/node/NodeStream.js +27 -0
  490. package/dist/vendor/node/NodeTerminal.d.ts +21 -0
  491. package/dist/vendor/node/NodeTerminal.js +10 -0
  492. package/dist/vendor/node/NodeWorker.d.ts +22 -0
  493. package/dist/vendor/node/NodeWorker.js +9 -0
  494. package/dist/vendor/node/NodeWorkerRunner.d.ts +12 -0
  495. package/dist/vendor/node/NodeWorkerRunner.js +7 -0
  496. package/dist/vendor/node/Undici.d.ts +28 -0
  497. package/dist/vendor/node/Undici.js +9 -0
  498. package/dist/vendor/node/index.d.ts +100 -0
  499. package/dist/vendor/node/index.js +115 -0
  500. package/dist/vendor/node-shared/LICENSE +21 -0
  501. package/dist/vendor/node-shared/NodeChildProcessSpawner.d.ts +57 -0
  502. package/dist/vendor/node-shared/NodeChildProcessSpawner.js +13 -0
  503. package/dist/vendor/node-shared/NodeClusterSocket.d.ts +22 -0
  504. package/dist/vendor/node-shared/NodeClusterSocket.js +11 -0
  505. package/dist/vendor/node-shared/NodeCrypto.d.ts +27 -0
  506. package/dist/vendor/node-shared/NodeCrypto.js +9 -0
  507. package/dist/vendor/node-shared/NodeFileSystem.d.ts +11 -0
  508. package/dist/vendor/node-shared/NodeFileSystem.js +8 -0
  509. package/dist/vendor/node-shared/NodeHttpCompression.d.ts +36 -0
  510. package/dist/vendor/node-shared/NodeHttpCompression.js +13 -0
  511. package/dist/vendor/node-shared/NodePath.d.ts +27 -0
  512. package/dist/vendor/node-shared/NodePath.js +11 -0
  513. package/dist/vendor/node-shared/NodeRuntime.d.ts +47 -0
  514. package/dist/vendor/node-shared/NodeRuntime.js +7 -0
  515. package/dist/vendor/node-shared/NodeSink.d.ts +69 -0
  516. package/dist/vendor/node-shared/NodeSink.js +11 -0
  517. package/dist/vendor/node-shared/NodeSocket.d.ts +134 -0
  518. package/dist/vendor/node-shared/NodeSocket.js +23 -0
  519. package/dist/vendor/node-shared/NodeSocketServer.d.ts +80 -0
  520. package/dist/vendor/node-shared/NodeSocketServer.js +20 -0
  521. package/dist/vendor/node-shared/NodeStdio.d.ts +12 -0
  522. package/dist/vendor/node-shared/NodeStdio.js +9 -0
  523. package/dist/vendor/node-shared/NodeStream.d.ts +179 -0
  524. package/dist/vendor/node-shared/NodeStream.js +26 -0
  525. package/dist/vendor/node-shared/NodeTerminal.d.ts +22 -0
  526. package/dist/vendor/node-shared/NodeTerminal.js +9 -0
  527. package/dist/vendor/node-shared/index.d.ts +56 -0
  528. package/dist/vendor/node-shared/index.js +58 -0
  529. package/dist/vendor/node-shared/internal/nodeChildProcessSpawner.d.ts +4 -0
  530. package/dist/vendor/node-shared/internal/nodeChildProcessSpawner.js +7 -0
  531. package/dist/vendor/node-shared/internal/utils.d.ts +2 -0
  532. package/dist/vendor/node-shared/internal/utils.js +7 -0
  533. package/dist/vendor/platform-bun.d.ts +92 -0
  534. package/dist/vendor/platform-bun.js +107 -0
  535. package/dist/vendor/platform-node-shared.d.ts +56 -0
  536. package/dist/vendor/platform-node-shared.js +58 -0
  537. package/dist/vendor/platform-node.d.ts +100 -0
  538. package/dist/vendor/platform-node.js +115 -0
  539. package/docs/README.md +158 -0
  540. package/docs/api.md +601 -0
  541. package/docs/concepts/projections.md +250 -0
  542. package/docs/concepts/subscriptions.md +204 -0
  543. package/docs/concepts/trust-boundary.md +256 -0
  544. package/docs/guides/diagnose-a-run.md +115 -0
  545. package/docs/guides/follow-a-run.md +150 -0
  546. package/docs/guides/host-the-gateway.md +153 -0
  547. package/docs/guides/read-a-projection.md +138 -0
  548. package/docs/guides/serve-beyond-loopback.md +166 -0
  549. package/docs/guides/submit-an-approval.md +187 -0
  550. package/docs/guides/testing.md +148 -0
  551. package/docs/installation.md +89 -0
  552. package/docs/quickstart.md +200 -0
  553. package/docs/troubleshooting.md +355 -0
  554. package/package.json +264 -32
  555. package/src/Diagnosis.ts +892 -0
  556. package/src/EngineTrace.ts +627 -0
  557. package/src/GatewayError.ts +71 -0
  558. package/src/GatewayProjection.ts +959 -0
  559. package/src/GatewayRpcs.ts +179 -0
  560. package/src/GatewaySchema.ts +516 -0
  561. package/src/GatewayServer.ts +932 -0
  562. package/src/HostBranchPresence.ts +76 -0
  563. package/src/Projections.ts +1735 -0
  564. package/src/RunDevTools.ts +394 -0
  565. package/src/RunHistory.ts +174 -0
  566. package/src/RunTrace.ts +2796 -0
  567. package/src/RuntimeBridge.ts +835 -0
  568. package/src/bun/BunGateway.ts +52 -0
  569. package/src/index.ts +77 -0
  570. package/src/internal/NativeGateway.ts +360 -0
  571. package/src/internal/actionRenderings.ts +212 -0
  572. package/src/internal/callEvents.ts +257 -0
  573. package/src/internal/digestIndex.ts +134 -0
  574. package/src/internal/digestMemory.ts +27 -0
  575. package/src/internal/inspectLabels.ts +18 -0
  576. package/src/internal/nativeResolution.ts +103 -0
  577. package/src/internal/nodeEvents.ts +100 -0
  578. package/src/node/NodeGateway.ts +30 -0
  579. package/openapi.yaml +0 -29945
  580. package/src/README.md +0 -39
  581. package/src/api/README.md +0 -23
  582. package/src/api/apiCollectionNames.js +0 -10
  583. package/src/api/index.d.ts +0 -126
  584. package/src/api/index.js +0 -14
  585. package/src/api/normalizeApiRow.js +0 -36
  586. package/src/api/serializeAccountRow.js +0 -10
  587. package/src/api/serializeApprovalRow.js +0 -53
  588. package/src/api/serializeComparisonScoreRow.js +0 -18
  589. package/src/api/serializeCronRow.js +0 -30
  590. package/src/api/serializeDocRow.js +0 -20
  591. package/src/api/serializeMemoryFactRow.js +0 -21
  592. package/src/api/serializePromptRow.js +0 -10
  593. package/src/api/serializeRunEventRow.js +0 -28
  594. package/src/api/serializeRunRow.js +0 -93
  595. package/src/api/serializeScoreDetailRow.js +0 -25
  596. package/src/api/serializeScoreRow.js +0 -26
  597. package/src/api/serializeTicketRow.js +0 -20
  598. package/src/api/serializeWorkflowRow.js +0 -10
  599. package/src/auth/scopes.d.ts +0 -19
  600. package/src/auth/scopes.js +0 -164
  601. package/src/index.d.ts +0 -316
  602. package/src/index.js +0 -108
  603. package/src/rpc/gatewayRpcTypes.ts +0 -186
  604. package/src/rpc/index.d.ts +0 -223
  605. package/src/rpc/index.js +0 -2527
@@ -0,0 +1,1735 @@
1
+ /**
2
+ * The gateway read path: served projections computed from the control plane.
3
+ *
4
+ * Every projection here is a fold over facts `@smthrs/control` already
5
+ * publishes: `Control.list` for run and flow listings, `Control.watch` for a
6
+ * run's ordered events. The gateway never opens the engine database, so a
7
+ * projection served to a browser through a relay is the same projection a
8
+ * local reader computes, and a projection cannot drift from the control plane
9
+ * by reading a column the control plane does not expose.
10
+ *
11
+ * A snapshot brackets its journal read with run-summary reads and retries
12
+ * when the summary changes. Rows and cursor share the resulting event buffer,
13
+ * so a completion cannot be acknowledged with the preceding run status.
14
+ *
15
+ * A subscription is a snapshot followed by deltas. A delta recomputes the
16
+ * selector's rows from the accumulated events rather than patching them: a
17
+ * projection is a reproducible fold (`@smthrs/journal` `Projection`), and
18
+ * recomputation is the only delta that cannot disagree with a fresh snapshot.
19
+ * `run-events` appends immutable rows. `transcript` normally appends too, but
20
+ * sends an existing snapshot reset when a committed native call fact upgrades
21
+ * previously delivered telemetry. Events accumulate in the stream, so each delta
22
+ * folds without re-reading history. Resuming a historical run summary first
23
+ * rebuilds its compacted health prefix once through durable follow replay.
24
+ *
25
+ * A workspace subscription follows every journal partition without a cursor.
26
+ * The follower stores each retained run's last position and drops replay
27
+ * through that cutoff. Sources and exclusion verdicts share the snapshot's
28
+ * run ceiling; eviction discards a verdict together with its cursor state.
29
+ * Events are gathered for a short window and folded into one frame, and each
30
+ * run's rows are cached so a burst refolds only the runs whose journals grew.
31
+ *
32
+ * @since 1.0.0
33
+ */
34
+
35
+ import type { Service as ControlService } from "@smthrs/control/Control"
36
+ import { Control } from "@smthrs/control/Control"
37
+ import * as ControlSchema from "@smthrs/control/ControlSchema"
38
+ import * as Health from "@smthrs/control/Health"
39
+ import { Context, Effect, Layer, Schema, Stream } from "effect"
40
+ import * as Diagnosis from "./Diagnosis.ts"
41
+ import { GatewayError, settingRefusal } from "./GatewayError.ts"
42
+ import * as GatewayProjection from "./GatewayProjection.ts"
43
+ import * as GatewaySchema from "./GatewaySchema.ts"
44
+ import { callEventKey, nativeCallEvent, nativeStepEvent } from "./internal/callEvents.ts"
45
+ import { retainedDigestBytes } from "./internal/digestMemory.ts"
46
+ import * as NativeResolution from "./internal/nativeResolution.ts"
47
+
48
+ /**
49
+ * How often an idle subscription emits a keepalive frame.
50
+ *
51
+ * The Smithers Cloud relay cuts an idle tunnel at 600 s (the deployed relay behavior), so a
52
+ * quiet run must produce a frame well inside that window or the connection is
53
+ * dropped and every follower reconnects. Thirty seconds leaves twenty
54
+ * heartbeats of margin.
55
+ *
56
+ * @since 1.0.0
57
+ * @category models
58
+ */
59
+ export const heartbeatIntervalMillis = 30_000
60
+
61
+ /**
62
+ * The most runs one workspace projection folds, newest first.
63
+ *
64
+ * A workspace projection reads one full journal per run, so the number of
65
+ * runs it folds is bounded on purpose. This is the gateway's own ceiling, not
66
+ * the control plane's default page size. It is 500 because that equals
67
+ * `ControlSchema.maxPageSize`, letting the control plane satisfy the whole
68
+ * gateway allowance in one page when it can.
69
+ *
70
+ * @since 1.0.0
71
+ * @category models
72
+ */
73
+ export const maxWorkspaceRuns = 500
74
+
75
+ /** Whether `run` was created after `other`, ties broken by run id. */
76
+ const createdAfter = (run: ControlSchema.RunSummary, other: ControlSchema.RunSummary): boolean =>
77
+ run.createdAt !== other.createdAt ? run.createdAt > other.createdAt : run.runId > other.runId
78
+
79
+ /**
80
+ * The most journal events one run projection retains in its window.
81
+ *
82
+ * This is a retention ceiling, not a refusal. A run that journals more keeps
83
+ * its most recent {@link maxEventsPerRun} events and folds everything older
84
+ * into the carried digest that {@link maxProjectionBytes} describes.
85
+ *
86
+ * @since 1.0.0
87
+ * @category models
88
+ */
89
+ export const maxEventsPerRun = 10_000
90
+
91
+ /**
92
+ * The most journal events one run projection reads before it stops reading.
93
+ *
94
+ * The window ceiling bounds memory; this bounds work. A projection folds every
95
+ * event it reads, so a run with a million of them would cost a million decodes
96
+ * on every snapshot however few it kept. Ten times the window is the honest
97
+ * compromise: every real run is far inside it, and a runaway one still answers
98
+ * rather than running the reader out of time.
99
+ *
100
+ * @since 1.0.0
101
+ * @category models
102
+ */
103
+ export const maxEventsScanned = 100_000
104
+
105
+ /**
106
+ * The most events one `run-events` snapshot page carries.
107
+ *
108
+ * `run-events` is the one selector whose rows are the journal itself, so it is
109
+ * the one selector a client must page rather than fold. A snapshot answers at
110
+ * most this many events and a cursor to ask for the next page from, so reading
111
+ * a long run costs one bounded response per page instead of one response the
112
+ * size of the whole run.
113
+ *
114
+ * @since 1.0.0
115
+ * @category models
116
+ */
117
+ export const maxEventsPerPage = 1_000
118
+
119
+ /**
120
+ * The most terminal runs of one flow a duration fold measures.
121
+ *
122
+ * A prediction wants recent history, not all of it: an old run measured a
123
+ * different declaration on a different machine. The newest finished runs are
124
+ * the ones whose durations still describe what the flow does now, and the
125
+ * bound is what keeps a snapshot from reading every journal a flow ever wrote.
126
+ *
127
+ * @since 1.0.0
128
+ * @category models
129
+ */
130
+ export const maxDurationRuns = 20
131
+
132
+ /**
133
+ * The largest encoded event window or projected row set one run admits.
134
+ *
135
+ * The row budget is a refusal: rows go on the wire, and a frame larger than
136
+ * this is one no client asked for. The window budget is retention: events
137
+ * older than the newest {@link maxProjectionBytes} are folded into a carried
138
+ * digest instead of being held. Its compact identity contributions share this
139
+ * budget; if they cannot fit, the read refuses instead of reporting inexact
140
+ * counters or retaining an unbounded identity set.
141
+ *
142
+ * @since 1.0.0
143
+ * @category models
144
+ */
145
+ export const maxProjectionBytes = 4 * 1024 * 1024
146
+
147
+ /**
148
+ * The largest encoded form one retained journal event may take.
149
+ *
150
+ * A model request body, a response transcript, and a tool's captured output
151
+ * are all journaled inline, and any one of them can be megabytes. Nothing a
152
+ * projection reads out of a payload is long: a seat name, a flow name, an
153
+ * outcome, a token count, a first line. A retained event is therefore clipped
154
+ * to this budget rather than held whole, which is what keeps one wiki refresh
155
+ * that retried a model call nine times from costing a run card its whole
156
+ * history. `run-events` pages preserve native engine evidence because clients
157
+ * decode its complete contracts to offer actions. Other large bodies remain
158
+ * clipped; their full content belongs in run artifacts.
159
+ *
160
+ * @since 1.0.0
161
+ * @category models
162
+ */
163
+ export const maxEventBytes = 16 * 1024
164
+
165
+ /**
166
+ * The longest string a clipped payload keeps.
167
+ *
168
+ * Applied only to an event already over {@link maxEventBytes}, so an ordinary
169
+ * event reaches a projection byte for byte.
170
+ */
171
+ const clippedTextPoints = 2_048
172
+
173
+ /**
174
+ * How long a workspace follower gathers events before folding one delta.
175
+ *
176
+ * A workspace delta replaces every row, so its cost is the whole workspace,
177
+ * not the one event that arrived. Gathering a burst into one frame bounds the
178
+ * frame rate at twenty per second however many runs journal at once, and
179
+ * fifty milliseconds is below what a dashboard reader can notice.
180
+ */
181
+ const workspaceDeltaCoalesceMillis = 50
182
+
183
+ /** The most events one workspace batch gathers before folding inside the window. */
184
+ const workspaceDeltaBatchSize = 1_024
185
+
186
+ /**
187
+ * Read-path operations served by the gateway.
188
+ *
189
+ * @since 1.0.0
190
+ * @category models
191
+ */
192
+ export interface Service {
193
+ /**
194
+ * Current rows, or only run-events rows after an issued cursor.
195
+ *
196
+ * A literal selector keeps its own row type, so an approvals reader reads
197
+ * `requestId` and a run-tree reader reads `nodeId` without an assertion. A
198
+ * selector chosen at runtime still answers with the snapshot union.
199
+ */
200
+ readonly snapshot: <S extends GatewaySchema.ProjectionSelector>(
201
+ selector: S,
202
+ after?: GatewaySchema.ProjectionCursor | undefined
203
+ ) => Effect.Effect<GatewaySchema.SnapshotOf<S>, GatewayError>
204
+ /**
205
+ * A snapshot followed by recomputed deltas and keepalive frames, or, when
206
+ * `after` names a cursor this selector issued, the deltas after it alone.
207
+ *
208
+ * The row and delta frames are the selector's own, on the same rule as
209
+ * `snapshot`.
210
+ */
211
+ readonly subscribe: <S extends GatewaySchema.ProjectionSelector>(
212
+ selector: S,
213
+ after?: GatewaySchema.ProjectionCursor | undefined
214
+ ) => Stream.Stream<GatewaySchema.FrameOf<S>, GatewayError>
215
+ }
216
+
217
+ /**
218
+ * The gateway read path.
219
+ *
220
+ * @since 1.0.0
221
+ * @category services
222
+ */
223
+ export class Projections extends Context.Service<Projections, Service>()("@smthrs/gateway/Projections") {}
224
+
225
+ /**
226
+ * The part of a control-plane failure a client is allowed to see: the failure's
227
+ * tag and its stable code, never its message, its nested cause, or the SQL and
228
+ * file paths a `PersistenceError` carries.
229
+ *
230
+ * Server logs carry only an operation identifier and an allowlisted summary.
231
+ * `GatewayError` is the RPC error schema, so anything left on it is serialized
232
+ * to every bearer holder and
233
+ * forwarded to the browser by the product relay, and a cause JSON cannot encode
234
+ * would make the error frame itself fail to encode.
235
+ */
236
+ const summarize = (cause: unknown): { readonly _tag: string; readonly code?: string } | undefined => {
237
+ const record = typeof cause === "object" && cause !== null ? cause as Record<string, unknown> : undefined
238
+ if (record === undefined || typeof record._tag !== "string") return undefined
239
+ return typeof record.code === "string" ? { _tag: record._tag, code: record.code } : { _tag: record._tag }
240
+ }
241
+
242
+ /** Log only known control-plane identifiers, never arbitrary backend text. */
243
+ const logReadFailure = (
244
+ operation: "list-runs" | "read-events" | "follow-run" | "follow-workspace",
245
+ cause: unknown
246
+ ) => {
247
+ const summary = summarize(cause)
248
+ const known = summary?._tag === "/control/PersistenceError" && summary.code === "persistence_failed" ||
249
+ summary?._tag === "/control/Unavailable" && summary.code === "unavailable"
250
+ return Effect.logWarning({ operation, ...(known ? summary : {}) })
251
+ }
252
+
253
+ /** The read failed, with only a public error summary. */
254
+ const unavailable = (message: string, cause: unknown): GatewayError => {
255
+ const summary = summarize(cause)
256
+ return new GatewayError({
257
+ code: "run_unavailable",
258
+ message,
259
+ ...(summary === undefined ? {} : { cause: summary })
260
+ })
261
+ }
262
+
263
+ /** The request named something the control plane cannot serve. */
264
+ const malformed = (message: string): GatewayError => new GatewayError({ code: "malformed_request", message })
265
+
266
+ /** A projection exceeded the bounded read or wire budget. */
267
+ const resourceLimit = (message: string): GatewayError => new GatewayError({ code: "resource_limit", message })
268
+
269
+ /** The run a selector is scoped to, or undefined for a workspace selector. */
270
+ const scopeOf = (selector: GatewaySchema.ProjectionSelector): string | undefined =>
271
+ "runId" in selector ? selector.runId : undefined
272
+
273
+ interface CursorPosition {
274
+ readonly value: number
275
+ readonly offset: number
276
+ }
277
+
278
+ interface EventBuffer {
279
+ readonly compactHealth: boolean
280
+ readonly seen: boolean
281
+ readonly events: Array<ControlSchema.ControlEvent>
282
+ readonly encodedBytes: number
283
+ readonly lastPosition: CursorPosition
284
+ /**
285
+ * The digest of the events this buffer read and dropped to stay bounded.
286
+ *
287
+ * Undefined until something is dropped, so an ordinary run is folded exactly
288
+ * as it was before the window existed. A run card combines it with the
289
+ * digest of the retained window, so turns, calls, edits, tokens, and the run
290
+ * span describe every event read rather than only the ones still held.
291
+ */
292
+ readonly carry: Diagnosis.Digest | undefined
293
+ /** How many events the window dropped, for a reader that reports the cut. */
294
+ readonly dropped: number
295
+ }
296
+
297
+ const emptyEventBuffer = (compactHealth = false): EventBuffer => ({
298
+ compactHealth,
299
+ seen: false,
300
+ events: [],
301
+ encodedBytes: 2,
302
+ lastPosition: { value: 0, offset: 0 },
303
+ carry: undefined,
304
+ dropped: 0
305
+ })
306
+
307
+ const textEncoder = new TextEncoder()
308
+
309
+ const encodedSize = (value: unknown): number => textEncoder.encode(JSON.stringify(value)).byteLength
310
+
311
+ /**
312
+ * One payload with every long string cut to {@link clippedTextPoints}.
313
+ *
314
+ * Structure is preserved so a payload stays decodable: a clipped string is
315
+ * still a string, an object keeps its keys, and an array keeps its length.
316
+ * Only the text shrinks, and `Diagnosis.clip` marks the cut.
317
+ */
318
+ const clipDeep = (value: unknown): unknown => {
319
+ if (typeof value === "string") return Diagnosis.clip(value, clippedTextPoints)
320
+ if (Array.isArray(value)) return value.map(clipDeep)
321
+ if (typeof value === "object" && value !== null) {
322
+ return Object.fromEntries(Object.entries(value).map(([key, member]) => [key, clipDeep(member)]))
323
+ }
324
+ return value
325
+ }
326
+
327
+ const isJsonObject = (json: Schema.Json): json is Schema.JsonObject =>
328
+ typeof json === "object" && json !== null && !Array.isArray(json)
329
+
330
+ /**
331
+ * The retained form of one event: itself when it is small, a clipped copy when
332
+ * it is not.
333
+ *
334
+ * A payload that is still over the budget after clipping is replaced outright.
335
+ * That is a payload whose size is in its shape rather than in its text — tens
336
+ * of thousands of keys — and no projection reads such a shape. A relevance
337
+ * reading is the exception: a large catalog makes its flow and skill verdicts
338
+ * that wide, and the run card reads its memory verdicts
339
+ * (`RunTrace.runMemoryOf`), so those are retained beside the marker.
340
+ */
341
+ const retainedEvent = (event: ControlSchema.ControlEvent): ControlSchema.ControlEvent => {
342
+ if (encodedSize(event) <= maxEventBytes) return event
343
+ const clipped = { ...event, payload: clipDeep(event.payload) as ControlSchema.ControlEvent["payload"] }
344
+ if (encodedSize(clipped) <= maxEventBytes) return clipped
345
+ const marker = { truncated: true, encodedBytes: encodedSize(event.payload) }
346
+ const payload = event.payload
347
+ if (event.kind === "control.agent.relevance-settled" && isJsonObject(payload)) {
348
+ const memory = (verdicts: unknown) =>
349
+ Array.isArray(verdicts)
350
+ ? verdicts.filter((verdict) =>
351
+ typeof verdict === "object" && verdict !== null && (verdict as { readonly kind?: unknown }).kind === "memory"
352
+ )
353
+ : []
354
+ const compact = {
355
+ ...event,
356
+ payload: {
357
+ ...marker,
358
+ kept: clipDeep(memory(payload.kept)),
359
+ withheld: clipDeep(memory(payload.withheld))
360
+ } as ControlSchema.ControlEvent["payload"]
361
+ }
362
+ if (encodedSize(compact) <= maxEventBytes) return compact
363
+ }
364
+ return { ...event, payload: marker }
365
+ }
366
+
367
+ const decodeEvent = Schema.decodeUnknownSync(ControlSchema.ControlEvent)
368
+ const decodeFrame = Schema.decodeUnknownSync(GatewaySchema.GatewayFrame)
369
+ const decodeSnapshot = Schema.decodeUnknownSync(GatewaySchema.ProjectionSnapshot)
370
+
371
+ const decodedEvent = (
372
+ candidate: unknown,
373
+ message: string
374
+ ): Effect.Effect<ControlSchema.ControlEvent, GatewayError> =>
375
+ Effect.suspend(() => {
376
+ try {
377
+ const event = decodeEvent(candidate)
378
+ // Graph topology is a bounded contract. Refuse an oversized legacy page
379
+ // intact; clipping a node id or dependency would silently change the DAG.
380
+ const payload = event.payload
381
+ if (
382
+ event.kind === "control.engine.event" && typeof payload === "object" && payload !== null &&
383
+ "eventType" in payload && (payload.eventType === "flows.engine.plan-recorded" ||
384
+ payload.eventType === "flows.engine.subgraph-appended") &&
385
+ encodedSize(event) > maxEventBytes
386
+ ) {
387
+ return Effect.fail(resourceLimit(`One graph event exceeds ${maxEventBytes} encoded bytes`))
388
+ }
389
+ return Number.isSafeInteger(event.sequence) && event.sequence >= 0
390
+ ? Effect.succeed(event)
391
+ : Effect.fail(unavailable(message, undefined))
392
+ } catch (cause) {
393
+ return Effect.fail(unavailable(message, cause))
394
+ }
395
+ })
396
+
397
+ const appendEvent = (
398
+ state: EventBuffer,
399
+ candidate: unknown,
400
+ message: string,
401
+ run: ControlSchema.RunSummary
402
+ ): Effect.Effect<EventBuffer, GatewayError> =>
403
+ Effect.flatMap(decodedEvent(candidate, message), (decoded) => {
404
+ // Ordering is checked before admission by eventsOf, runDeltaFrames and
405
+ // workspaceDeltaFrames. bufferOf only replays an ordered cursor prefix.
406
+ // A payload larger than one projection reads is clipped before it is
407
+ // measured, so a journaled model body costs the window its own size and
408
+ // not the body's.
409
+ // A bound root's typed answer must remain exact across event clipping.
410
+ // Retain it in the same bounded digest as evicted root decisions; the
411
+ // event window carries only a marker, never a partial typed answer.
412
+ const resolution = encodedSize(decoded) > maxEventBytes ? NativeResolution.fromEvent(decoded) : undefined
413
+ let carry = state.carry
414
+ if (resolution?.result !== undefined) {
415
+ carry = Diagnosis.combine(carry ?? Diagnosis.emptyDigest(), {
416
+ ...Diagnosis.emptyDigest(),
417
+ nativeResolution: resolution
418
+ })
419
+ }
420
+ const event = resolution?.result === undefined ? retainedEvent(decoded) : {
421
+ ...decoded,
422
+ payload: { truncated: true, encodedBytes: encodedSize(decoded.payload) }
423
+ }
424
+ // `decodedEvent` has already rebuilt payload through `Schema.Json`, so it
425
+ // contains neither accessors nor `toJSON` hooks and JSON encoding cannot
426
+ // execute caller code here.
427
+ const bytes = encodedSize(event)
428
+ let removedBytes = 0
429
+ let keep = true
430
+ if (state.compactHealth && event.kind === Health.statusObservedEventType) {
431
+ const decoded = Schema.decodeUnknownOption(Health.HealthObservation)(event.payload)
432
+ // Match the subject before incarnation precedence or eviction, as the
433
+ // authoritative health fold does. Foreign evidence must not displace
434
+ // this run's reading, but its raw journal position still advances below.
435
+ if (
436
+ decoded._tag === "None" || decoded.value.outcome === "discarded" ||
437
+ decoded.value.subjectId !== `run:${run.runId}`
438
+ ) keep = false
439
+ else {
440
+ const same = state.events.findIndex((old) =>
441
+ old.kind === Health.statusObservedEventType &&
442
+ typeof old.payload === "object" && old.payload !== null && !Array.isArray(old.payload) &&
443
+ "incarnation" in old.payload &&
444
+ (old.payload as { readonly incarnation?: unknown }).incarnation === decoded.value.incarnation
445
+ )
446
+ if (same >= 0) {
447
+ const previous = state.events[same]!
448
+ const prior = previous.payload as { readonly evidenceSeq: number }
449
+ if (prior.evidenceSeq > decoded.value.evidenceSeq) keep = false
450
+ else removedBytes += textEncoder.encode(JSON.stringify(state.events.splice(same, 1)[0])).byteLength + 1
451
+ }
452
+ // Pin the authoritative incarnation's strongest reading. Late former
453
+ // owners must not evict it and let weaker evidence replace it later.
454
+ // The other slots retain recent opaque incarnations within the same cap.
455
+ const healthEntries = state.events.filter((old) => old.kind === Health.statusObservedEventType)
456
+ if (keep && healthEntries.length >= 16) {
457
+ const incarnation = Health.runIncarnation(run)
458
+ // There is at most one entry per incarnation, so an unprotected entry exists.
459
+ const oldest = state.events.indexOf(
460
+ healthEntries.find((old) => (old.payload as { readonly incarnation: string }).incarnation !== incarnation)!
461
+ )
462
+ removedBytes += textEncoder.encode(JSON.stringify(state.events.splice(oldest, 1)[0])).byteLength + 1
463
+ }
464
+ }
465
+ } else if (state.compactHealth && event.kind === "control.monitor.beat") keep = false
466
+ let encodedBytes = Math.max(2, state.encodedBytes - removedBytes) +
467
+ retainedDigestBytes(carry) - retainedDigestBytes(state.carry) +
468
+ (keep ? bytes + (state.events.length === 0 ? 0 : 1) : 0)
469
+ // Unreachable today: every term is a byte length, `state.encodedBytes` is
470
+ // bounded by `maxProjectionBytes` by the loop below, and one event is
471
+ // bounded by the string the control plane sent, so the sum is always a
472
+ // safe integer. The refusal stays because a later term need not be, and a
473
+ // window that cannot count its own bytes must refuse rather than report.
474
+ /* v8 ignore next 3 -- unreachable while every term above is a byte length. */
475
+ if (!Number.isSafeInteger(encodedBytes)) {
476
+ return Effect.fail(unavailable(message, undefined))
477
+ }
478
+ const offset = state.seen && event.sequence === state.lastPosition.value
479
+ ? state.lastPosition.offset + 1
480
+ : 0
481
+ if (keep) state.events.push(event)
482
+ // Event bodies may leave the window while compact identity contributions
483
+ // keep the diagnosis exact. Both share the byte budget; if even the scalar
484
+ // state cannot fit, refuse instead of silently forgetting identities.
485
+ let dropped = state.dropped
486
+ while (state.events.length > maxEventsPerRun || encodedBytes > maxProjectionBytes) {
487
+ const evicted = state.events.shift()
488
+ if (evicted === undefined) {
489
+ return Effect.fail(resourceLimit(`Projection identity state exceeds ${maxProjectionBytes} encoded bytes`))
490
+ }
491
+ encodedBytes = Math.max(2, encodedBytes - encodedSize(evicted) - (state.events.length === 0 ? 0 : 1))
492
+ const previousCarryBytes = retainedDigestBytes(carry)
493
+ carry = Diagnosis.combine(carry ?? Diagnosis.emptyDigest(), Diagnosis.digest([evicted], run.runId))
494
+ encodedBytes += retainedDigestBytes(carry) - previousCarryBytes
495
+ dropped += 1
496
+ }
497
+ return Effect.succeed({
498
+ compactHealth: state.compactHealth,
499
+ seen: true,
500
+ events: state.events,
501
+ encodedBytes,
502
+ lastPosition: { value: event.sequence, offset },
503
+ carry,
504
+ dropped
505
+ })
506
+ })
507
+
508
+ const bufferOf = (
509
+ events: ReadonlyArray<ControlSchema.ControlEvent>,
510
+ message: string,
511
+ compactHealth: boolean,
512
+ run: ControlSchema.RunSummary
513
+ ): Effect.Effect<EventBuffer, GatewayError> =>
514
+ events.reduce(
515
+ (state, event) => Effect.flatMap(state, (buffer) => appendEvent(buffer, event, message, run)),
516
+ Effect.succeed(emptyEventBuffer(compactHealth)) as Effect.Effect<EventBuffer, GatewayError>
517
+ )
518
+
519
+ const cursorOf = (
520
+ selector: GatewaySchema.ProjectionSelector,
521
+ position: CursorPosition
522
+ ): GatewaySchema.ProjectionCursor => ({
523
+ selector,
524
+ projection: selector._tag,
525
+ runId: scopeOf(selector) ?? null,
526
+ value: position.value,
527
+ offset: position.offset
528
+ })
529
+
530
+ const comparePosition = (left: CursorPosition, right: CursorPosition): number =>
531
+ left.value === right.value ? left.offset - right.offset : left.value - right.value
532
+
533
+ const positionedEvents = (
534
+ events: ReadonlyArray<ControlSchema.ControlEvent>
535
+ ): ReadonlyArray<{ readonly event: ControlSchema.ControlEvent; readonly position: CursorPosition }> => {
536
+ let sequence = -1
537
+ let offset = 0
538
+ return events.map((event) => {
539
+ offset = event.sequence === sequence ? offset + 1 : 0
540
+ sequence = event.sequence
541
+ return { event, position: { value: sequence, offset } }
542
+ })
543
+ }
544
+
545
+ const sameSelector = (
546
+ left: GatewaySchema.ProjectionSelector,
547
+ right: GatewaySchema.ProjectionSelector
548
+ ): boolean => {
549
+ if (left._tag !== right._tag) return false
550
+ switch (left._tag) {
551
+ case "workspace-runs":
552
+ return left.flowId === (right as GatewaySchema.WorkspaceRunsSelector).flowId
553
+ case "approvals":
554
+ return left.runId === (right as GatewaySchema.ApprovalsSelector).runId
555
+ case "node-output": {
556
+ const candidate = right as GatewaySchema.NodeOutputSelector
557
+ return left.runId === candidate.runId && left.nodeId === candidate.nodeId
558
+ }
559
+ case "flow-durations":
560
+ return left.flowId === (right as GatewaySchema.FlowDurationsSelector).flowId
561
+ default:
562
+ return left.runId === (right as GatewaySchema.RunSummarySelector).runId
563
+ }
564
+ }
565
+
566
+ /** One run and the journal it has committed, read together and read once. */
567
+ interface RunSource extends EventBuffer {
568
+ readonly run: ControlSchema.RunSummary
569
+ }
570
+
571
+ /** Everything a selector folds, read once per snapshot. */
572
+ type Source =
573
+ | { readonly _tag: "run"; readonly run: RunSource }
574
+ | { readonly _tag: "workspace"; readonly runs: ReadonlyArray<RunSource> }
575
+
576
+ /**
577
+ * The rows one run contributes to a selector.
578
+ *
579
+ * Pure and total over facts already read: it never reads the control plane, so
580
+ * a delta folded from accumulated events and a fresh snapshot compute the same
581
+ * rows from the same events. `workspace-runs` appears here because a workspace
582
+ * listing is one summary row per run, folded by this same function.
583
+ */
584
+ const rowsOfRun = (
585
+ selector: GatewaySchema.ProjectionSelector,
586
+ source: {
587
+ readonly run: ControlSchema.RunSummary
588
+ readonly events: ReadonlyArray<ControlSchema.ControlEvent>
589
+ readonly carry?: Diagnosis.Digest | undefined
590
+ },
591
+ now: number
592
+ ): ReadonlyArray<unknown> => {
593
+ switch (selector._tag) {
594
+ case "workspace-runs":
595
+ case "run-summary":
596
+ return [GatewayProjection.runSummary(source.run, source.events, now, source.carry)]
597
+ case "run-tree":
598
+ return GatewayProjection.runTree(source.run, source.events)
599
+ case "run-events":
600
+ return source.events
601
+ case "transcript":
602
+ return GatewayProjection.transcript(source.events)
603
+ case "approvals":
604
+ return GatewayProjection.approvals(source.events, source.run)
605
+ case "node-output":
606
+ return GatewayProjection.nodeOutput(source.events).filter((row) => row.nodeId === selector.nodeId)
607
+ case "flow-durations":
608
+ // One run contributes the executions it measured, never a percentile:
609
+ // a percentile over one run is that run, and the rows this selector
610
+ // serves are folded across every run the flow has finished. The carry
611
+ // travels with the window because the record that names this run's
612
+ // native root is the first one a bounded window drops.
613
+ return GatewayProjection.nodeDurations(source.events, source.carry)
614
+ }
615
+ }
616
+
617
+ /** Turns opened so far, for numbering appended transcript rows without a refold. */
618
+ const turnsOpened = (events: ReadonlyArray<ControlSchema.ControlEvent>): number =>
619
+ GatewayProjection.transcript(events).at(-1)?.turn ?? 0
620
+
621
+ /**
622
+ * The transcript rows one appended event contributes.
623
+ *
624
+ * A transcript row is immutable once folded and only the turn counter carries
625
+ * across events, so folding the one event and offsetting its turn produces
626
+ * exactly the rows a full refold would append.
627
+ */
628
+ const transcriptAppend = (
629
+ event: ControlSchema.ControlEvent,
630
+ turnsBefore: number
631
+ ): ReadonlyArray<GatewayProjection.TranscriptRow> =>
632
+ GatewayProjection.transcript([event]).map((row) => ({ ...row, turn: row.turn + turnsBefore }))
633
+
634
+ /** Snapshot and follow admit exactly the same runs to the workspace inbox. */
635
+ const eligibleForWorkspace = (selector: GatewaySchema.ProjectionSelector, source: RunSource): boolean =>
636
+ selector._tag !== "approvals" ||
637
+ source.run.status === "waiting-approval" &&
638
+ GatewayProjection.approvals(source.events, source.run).some((row) => row.status === "pending")
639
+
640
+ /**
641
+ * Which open human wait a pending approval row is about, if it is about one.
642
+ *
643
+ * A nested wait is rolled onto the root AND onto every execution between, so
644
+ * one question reaches a workspace listing once per ancestor. The inbox is a
645
+ * list of questions, not of the runs that contain them, so it keeps one row
646
+ * for each wait point: the one its outermost owner projects. See
647
+ * {@link ownerRank}.
648
+ */
649
+ const waitIdentity = (row: GatewayProjection.ApprovalRow): string | undefined =>
650
+ row.waitRunId === undefined ? undefined : `${row.waitRunId}:${row.requestId}`
651
+
652
+ /**
653
+ * How far out each listed run sits, as a comparable key: fewer listed
654
+ * ancestors first, then the earlier created, then the run id.
655
+ *
656
+ * Ancestry is the explicit `parentRunId` chain among the runs listed, because
657
+ * the listing order is the control plane's to choose (the SQL listing is
658
+ * newest first) and two runs of one tree can share a creation millisecond.
659
+ * Creation order breaks the ties ancestry leaves: an ancestor exists before
660
+ * the execution it spawned.
661
+ */
662
+ const ownerRank = (
663
+ runs: ReadonlyArray<ControlSchema.RunSummary>
664
+ ): (left: ControlSchema.RunSummary, right: ControlSchema.RunSummary) => number => {
665
+ const listed = new Map(runs.map((run) => [run.runId, run]))
666
+ const depths = new Map<string, number>()
667
+ const depthOf = (run: ControlSchema.RunSummary): number => {
668
+ const known = depths.get(run.runId)
669
+ if (known !== undefined) return known
670
+ // Marked before the walk so a cyclic parent chain ends instead of looping.
671
+ depths.set(run.runId, 0)
672
+ const parent = run.parentRunId === undefined ? undefined : listed.get(run.parentRunId)
673
+ const depth = parent === undefined ? 0 : depthOf(parent) + 1
674
+ depths.set(run.runId, depth)
675
+ return depth
676
+ }
677
+ return (left, right) => depthOf(left) - depthOf(right) || (createdAfter(left, right) ? 1 : -1)
678
+ }
679
+
680
+ /**
681
+ * The workspace approvals inbox over each run's own pending rows.
682
+ *
683
+ * Every read of the inbox, snapshot and delta alike, folds through here, so a
684
+ * nested question is one row owned by its outermost run whichever read
685
+ * produced it. Rows keep the order of the runs given.
686
+ */
687
+ const approvalInbox = (
688
+ perRun: ReadonlyArray<{ readonly run: ControlSchema.RunSummary; readonly rows: ReadonlyArray<unknown> }>
689
+ ): ReadonlyArray<GatewayProjection.ApprovalRow> => {
690
+ const owners = new Map<string, string>()
691
+ const rank = ownerRank(perRun.map(({ run }) => run))
692
+ for (const { run, rows } of [...perRun].sort((left, right) => rank(left.run, right.run))) {
693
+ for (const row of rows as ReadonlyArray<GatewayProjection.ApprovalRow>) {
694
+ const identity = waitIdentity(row)
695
+ if (identity !== undefined && !owners.has(identity)) owners.set(identity, run.runId)
696
+ }
697
+ }
698
+ const inbox: Array<GatewayProjection.ApprovalRow> = []
699
+ for (const { run, rows } of perRun) {
700
+ for (const row of rows as ReadonlyArray<GatewayProjection.ApprovalRow>) {
701
+ const identity = waitIdentity(row)
702
+ if (identity !== undefined) {
703
+ if (owners.get(identity) !== run.runId) continue
704
+ // Taken once: a question listed twice by its owner is still one row.
705
+ owners.delete(identity)
706
+ }
707
+ inbox.push(row)
708
+ }
709
+ }
710
+ return inbox
711
+ }
712
+
713
+ /** One run's rows that are still owed an answer: its share of the inbox. */
714
+ const pendingApprovals = (source: RunSource): ReadonlyArray<GatewayProjection.ApprovalRow> =>
715
+ GatewayProjection.approvals(source.events, source.run).filter((row) => row.status === "pending")
716
+
717
+ /**
718
+ * The rows a workspace selector projects across every run it read.
719
+ *
720
+ * Most of them are one run's rows, concatenated. Two are not. The approvals
721
+ * inbox wants only what a human still owes an answer to, once per question
722
+ * (see {@link approvalInbox}), where a run card wants that run's decided gates
723
+ * too. And `flow-durations` is a cross-run fold: every run contributes the
724
+ * executions it measured and the percentiles are ranked over all of them at
725
+ * once, so a run contributes a sample rather than a row.
726
+ */
727
+ const rowsOfWorkspace = (
728
+ selector: GatewaySchema.ProjectionSelector,
729
+ runs: ReadonlyArray<RunSource>,
730
+ now: number
731
+ ): ReadonlyArray<unknown> => {
732
+ if (selector._tag === "flow-durations") {
733
+ return GatewayProjection.flowDurations(
734
+ selector.flowId,
735
+ runs.flatMap((source) => rowsOfRun(selector, source, now)) as ReadonlyArray<GatewayProjection.NodeDuration>
736
+ )
737
+ }
738
+ if (selector._tag !== "approvals") return runs.flatMap((source) => rowsOfRun(selector, source, now))
739
+ return approvalInbox(runs.map((source) => ({ run: source.run, rows: pendingApprovals(source) })))
740
+ }
741
+
742
+ /** The rows a selector projects from the facts one read produced. */
743
+ const rowsOf = (
744
+ selector: GatewaySchema.ProjectionSelector,
745
+ source: Source,
746
+ now: number
747
+ ): ReadonlyArray<unknown> =>
748
+ source._tag === "run" ? rowsOfRun(selector, source.run, now) : rowsOfWorkspace(selector, source.runs, now)
749
+
750
+ const boundedRows = (
751
+ selector: GatewaySchema.ProjectionSelector,
752
+ candidate: unknown
753
+ ): Effect.Effect<ReadonlyArray<unknown>, GatewayError> =>
754
+ Effect.suspend(() => {
755
+ try {
756
+ const schema = Schema.Array(GatewaySchema.rowSchemaFor(selector))
757
+ const rows = Schema.decodeUnknownSync(schema)(candidate) as ReadonlyArray<unknown>
758
+ const encoded = Schema.encodeUnknownSync(schema)(rows)
759
+ const bytes = textEncoder.encode(JSON.stringify(encoded)).byteLength
760
+ return bytes <= maxProjectionBytes
761
+ ? Effect.succeed(rows)
762
+ : Effect.fail(resourceLimit(`Projection rows exceed ${maxProjectionBytes} encoded bytes`))
763
+ } catch (cause) {
764
+ return Effect.fail(unavailable("Projection produced invalid rows", cause))
765
+ }
766
+ })
767
+
768
+ const frameOf = (candidate: unknown): Effect.Effect<GatewaySchema.GatewayFrame, GatewayError> =>
769
+ Effect.suspend(() => {
770
+ try {
771
+ return Effect.succeed(decodeFrame(candidate))
772
+ /* v8 ignore next -- rows, selectors, cursors, and tags were admitted before this internal assembly step. */
773
+ } catch (cause) {
774
+ /* v8 ignore next -- the assembled frame consists only of already-admitted fields. */
775
+ return Effect.fail(unavailable("Projection produced an invalid frame", cause))
776
+ }
777
+ })
778
+
779
+ const snapshotOf = (candidate: unknown): Effect.Effect<GatewaySchema.ProjectionSnapshot, GatewayError> =>
780
+ Effect.suspend(() => {
781
+ try {
782
+ return Effect.succeed(decodeSnapshot(candidate))
783
+ /* v8 ignore next -- boundedRows and cursorOf admit every field before this internal assembly step. */
784
+ } catch (cause) {
785
+ /* v8 ignore next -- the assembled snapshot consists only of already-admitted fields. */
786
+ return Effect.fail(unavailable("Projection produced an invalid snapshot", cause))
787
+ }
788
+ })
789
+
790
+ /**
791
+ * The cursor the read reached.
792
+ *
793
+ * A run's cursor is the last sequence its journal committed, taken from the
794
+ * same array the rows were folded from. A workspace cursor is `0`: control
795
+ * journal sequences belong to per-run partitions (`@smthrs/control`
796
+ * `ControlLive.streamForRun` and `snapshotHighWater` are keyed by `runId`), so
797
+ * there is no workspace-wide sequence to advertise and no workspace resume.
798
+ */
799
+ const cursorPositionOf = (source: Source): CursorPosition =>
800
+ source._tag === "run" ? source.run.lastPosition : { value: 0, offset: 0 }
801
+
802
+ /** Implements a read path after its construction settings have been admitted. */
803
+ const makeService = (control: ControlService, heartbeatMillis: number, now: () => number): Service => {
804
+ // A bounded, disposable journal read cache. The journal remains authoritative:
805
+ // every read asks it for the suffix, including when the summary is unchanged.
806
+ // Cache eviction or host restart simply rebuilds the same fold from sequence 0.
807
+ const journalBuffers = new Map<string, EventBuffer>()
808
+ const eventsOf = (
809
+ run: ControlSchema.RunSummary,
810
+ compactHealth: boolean
811
+ ): Effect.Effect<EventBuffer, GatewayError> =>
812
+ Effect.suspend(() => {
813
+ const runId = run.runId
814
+ const key = `${runId}:${compactHealth}`
815
+ const cached = journalBuffers.get(key)
816
+ const seed = cached === undefined ? emptyEventBuffer(compactHealth) : { ...cached, events: [...cached.events] }
817
+ const message = `Reading the events of ${runId} failed`
818
+ let position: CursorPosition | undefined
819
+ return Stream.runFoldEffect(
820
+ control.watch({
821
+ runId,
822
+ follow: false,
823
+ ...(cached === undefined || !cached.seen || cached.lastPosition.value === 0
824
+ ? {} :
825
+ { afterSequence: cached.lastPosition.value - 1 })
826
+ }).pipe(
827
+ Stream.tapError((cause) => logReadFailure("read-events", cause)),
828
+ Stream.mapError((cause) => unavailable(message, cause)),
829
+ Stream.take(maxEventsScanned)
830
+ ),
831
+ () => seed,
832
+ (buffer, candidate) =>
833
+ Effect.flatMap(decodedEvent(candidate, message), (event) => {
834
+ if (position !== undefined && event.sequence < position.value) {
835
+ return Effect.fail(unavailable(message, undefined))
836
+ }
837
+ position = { value: event.sequence, offset: position?.value === event.sequence ? position.offset + 1 : 0 }
838
+ if (cached !== undefined && cached.seen && comparePosition(position, cached.lastPosition) <= 0) {
839
+ return Effect.succeed(buffer)
840
+ }
841
+ return appendEvent(buffer, event, message, run)
842
+ })
843
+ ).pipe(Effect.tap((buffer) =>
844
+ Effect.sync(() => {
845
+ const current = journalBuffers.get(key)
846
+ if (current === undefined || comparePosition(buffer.lastPosition, current.lastPosition) >= 0) {
847
+ journalBuffers.delete(key)
848
+ journalBuffers.set(key, { ...buffer, events: [...buffer.events] })
849
+ if (journalBuffers.size > 32) journalBuffers.delete(journalBuffers.keys().next().value!)
850
+ }
851
+ })
852
+ ))
853
+ })
854
+
855
+ /**
856
+ * One bounded page of a run's journal, starting after `after`.
857
+ *
858
+ * This is the `run-events` read, and it is deliberately not the fold every
859
+ * other selector uses. The other selectors answer rows computed from a
860
+ * journal; `run-events` answers the journal, so its response grows with the
861
+ * run unless the read itself is bounded. It reads from the control plane's
862
+ * own cursor, keeps at most {@link maxEventsPerPage} events and
863
+ * {@link maxProjectionBytes}, and reports the position it reached so the
864
+ * caller asks for the next page from there.
865
+ *
866
+ * `afterSequence` replays the cursor's whole journal entry rather than
867
+ * starting inside it, because an entry expands into several events and their
868
+ * offsets are only countable from the entry's start. The replayed prefix is
869
+ * dropped here.
870
+ */
871
+ const runEventsPage = (
872
+ run: ControlSchema.RunSummary,
873
+ after: CursorPosition | undefined
874
+ ): Effect.Effect<
875
+ { readonly events: ReadonlyArray<ControlSchema.ControlEvent>; readonly lastPosition: CursorPosition },
876
+ GatewayError
877
+ > => {
878
+ const runId = run.runId
879
+ const message = `Reading the events of ${runId} failed`
880
+ interface Page {
881
+ readonly events: Array<ControlSchema.ControlEvent>
882
+ readonly bytes: number
883
+ readonly seen: CursorPosition | undefined
884
+ readonly last: CursorPosition | undefined
885
+ readonly full: boolean
886
+ }
887
+ const empty: Page = { events: [], bytes: 2, seen: undefined, last: undefined, full: false }
888
+ // Set when the page is full, so the next pull ends the stream instead of
889
+ // decoding the rest of a long journal into a page that cannot grow.
890
+ let stopped = false
891
+ return Stream.runFoldEffect(
892
+ control.watch({
893
+ runId,
894
+ ...(after === undefined || after.value === 0 ? {} : { afterSequence: after.value - 1 }),
895
+ follow: false
896
+ }).pipe(
897
+ Stream.tapError((cause) => logReadFailure("read-events", cause)),
898
+ Stream.mapError((cause) => unavailable(message, cause)),
899
+ Stream.take(maxEventsScanned),
900
+ Stream.takeWhile(() => !stopped)
901
+ ),
902
+ () => empty,
903
+ (page, candidate): Effect.Effect<Page, GatewayError> =>
904
+ page.full ? Effect.succeed(page) : Effect.flatMap(
905
+ decodedEvent(candidate, `${message}: the control plane returned an invalid event`),
906
+ (decoded) => {
907
+ // A journal that moves backward is a broken control plane, not a
908
+ // short page, and the fold refuses it exactly as the window does.
909
+ if (page.seen !== undefined && decoded.sequence < page.seen.value) {
910
+ return Effect.fail(unavailable(message, undefined))
911
+ }
912
+ const position: CursorPosition = {
913
+ value: decoded.sequence,
914
+ offset: page.seen?.value === decoded.sequence ? page.seen.offset + 1 : 0
915
+ }
916
+ const seen = position
917
+ if (after !== undefined && comparePosition(position, after) <= 0) {
918
+ return Effect.succeed({ ...page, seen })
919
+ }
920
+ // Native results are contracts, not display text. Clipping changes
921
+ // their meaning and can hide validated actions such as Vibe.
922
+ // Match live deltas, which already carry the original event.
923
+ const event = decoded.kind === "control.engine.event" ? decoded : retainedEvent(decoded)
924
+ if (encodedSize(event) + 2 > maxProjectionBytes) {
925
+ return Effect.fail(resourceLimit(`One journal event exceeds ${maxProjectionBytes} encoded bytes`))
926
+ }
927
+ const bytes = page.bytes + encodedSize(event) + (page.events.length === 0 ? 0 : 1)
928
+ if (page.events.length > 0 && bytes > maxProjectionBytes) {
929
+ stopped = true
930
+ return Effect.succeed({ ...page, seen, full: true })
931
+ }
932
+ page.events.push(event)
933
+ stopped = page.events.length >= maxEventsPerPage
934
+ return Effect.succeed({ events: page.events, bytes, seen, last: position, full: stopped })
935
+ }
936
+ )
937
+ ).pipe(
938
+ Effect.map((page) => ({
939
+ events: page.events,
940
+ lastPosition: page.last ?? after ?? { value: 0, offset: 0 }
941
+ }))
942
+ )
943
+ }
944
+
945
+ const runsMatching = (
946
+ filters: {
947
+ readonly runId?: string
948
+ readonly flowId?: string
949
+ readonly status?: ControlSchema.RunStatus
950
+ readonly terminal?: boolean
951
+ },
952
+ newest?: number
953
+ ): Effect.Effect<ReadonlyArray<ControlSchema.RunSummary>, GatewayError> => {
954
+ const singleRun = filters.runId !== undefined
955
+ // Only runOf omits the bound: it reads one identified run.
956
+ const ceiling = newest ?? 1
957
+ const page = (
958
+ accumulated: ReadonlyArray<ControlSchema.RunSummary>,
959
+ seen: Set<string>,
960
+ cursor?: string | undefined
961
+ ): Effect.Effect<ReadonlyArray<ControlSchema.RunSummary>, GatewayError> => {
962
+ const remaining = ceiling - accumulated.length
963
+ return control.list({
964
+ _tag: "runs",
965
+ filters,
966
+ limit: remaining,
967
+ ...(newest === undefined ? {} : { order: "newest" as const }),
968
+ ...(cursor === undefined ? {} : { cursor })
969
+ }).pipe(
970
+ Effect.tapError((cause) => logReadFailure("list-runs", cause)),
971
+ Effect.mapError((cause) => unavailable("Listing runs failed", cause)),
972
+ Effect.flatMap((response) => {
973
+ if (response._tag !== "runs") return Effect.succeed(accumulated)
974
+ const ids = new Set(accumulated.map((run) => run.runId))
975
+ const added = response.items.filter((run) => {
976
+ if (ids.has(run.runId)) return false
977
+ ids.add(run.runId)
978
+ return true
979
+ }).slice(0, remaining)
980
+ const runs = [...accumulated, ...added]
981
+ if (
982
+ singleRun || added.length === 0 || response.nextCursor === undefined ||
983
+ runs.length >= ceiling || seen.has(response.nextCursor)
984
+ ) return Effect.succeed(runs)
985
+ seen.add(response.nextCursor)
986
+ // Each continued page adds at least one run, so the run ceiling
987
+ // also caps the number of pages at maxWorkspaceRuns.
988
+ return page(runs, seen, response.nextCursor)
989
+ })
990
+ )
991
+ }
992
+ return Effect.suspend(() => page([], new Set()))
993
+ }
994
+
995
+ /**
996
+ * The one run a run-scoped selector names, or a typed refusal.
997
+ *
998
+ * Every run-scoped selector goes through here, so an unknown run id is
999
+ * refused the same way for all of them. Reading a run's events alone cannot
1000
+ * tell an unknown run from a run with no events: `@smthrs/control`
1001
+ * `ControlLive.snapshotForRun` answers an empty stream for both.
1002
+ */
1003
+ const runOf = (runId: string): Effect.Effect<ControlSchema.RunSummary, GatewayError> =>
1004
+ runsMatching({ runId }).pipe(
1005
+ Effect.flatMap((runs) => {
1006
+ const run = runs.find((candidate) => candidate.runId === runId)
1007
+ return run === undefined
1008
+ ? Effect.fail(new GatewayError({ code: "run_not_found", message: `No run ${runId}` }))
1009
+ : Effect.succeed(run)
1010
+ })
1011
+ )
1012
+
1013
+ /** Pin the journal between equal summaries; refuse a continuously moving row. */
1014
+ const consistentRunSource = (
1015
+ before: ControlSchema.RunSummary,
1016
+ attempts = 8,
1017
+ compactHealth = true
1018
+ ): Effect.Effect<RunSource, GatewayError> =>
1019
+ Effect.gen(function*() {
1020
+ const buffer = yield* eventsOf(before, compactHealth)
1021
+ const run = yield* runOf(before.runId)
1022
+ if (JSON.stringify(before) === JSON.stringify(run)) return { run, ...buffer }
1023
+ if (attempts === 1) return yield* Effect.fail(unavailable("Run changed throughout the snapshot read", undefined))
1024
+ return yield* consistentRunSource(run, attempts - 1, compactHealth)
1025
+ })
1026
+
1027
+ /** One run and its journal at a reconciled cutoff. */
1028
+ const runSourceOf = (runId: string, compactHealth = true): Effect.Effect<RunSource, GatewayError> =>
1029
+ Effect.flatMap(runOf(runId), (run) => consistentRunSource(run, 8, compactHealth))
1030
+
1031
+ /** Newest terminal runs, filtered and limited by the control query itself. */
1032
+ const durationRunsOf = (flowId: string): Effect.Effect<ReadonlyArray<ControlSchema.RunSummary>, GatewayError> =>
1033
+ runsMatching({ flowId, terminal: true }, maxDurationRuns)
1034
+
1035
+ /** Every run a workspace selector folds, and each one's journal. */
1036
+ const workspaceSourceOf = (
1037
+ selector: GatewaySchema.ProjectionSelector
1038
+ ): Effect.Effect<ReadonlyArray<RunSource>, GatewayError> =>
1039
+ Effect.flatMap(
1040
+ // The inbox asks the control plane which runs are parked rather than
1041
+ // reading every run's journal to find out, and a duration fold asks it
1042
+ // for one flow's runs rather than for the workspace's.
1043
+ selector._tag === "flow-durations"
1044
+ ? durationRunsOf(selector.flowId)
1045
+ : runsMatching(
1046
+ selector._tag === "approvals" ?
1047
+ { status: "waiting-approval" }
1048
+ : selector._tag === "workspace-runs" && selector.flowId !== undefined
1049
+ ? { flowId: selector.flowId }
1050
+ : {},
1051
+ maxWorkspaceRuns
1052
+ ),
1053
+ (runs) =>
1054
+ Effect.map(
1055
+ Effect.forEach(runs, (run) => consistentRunSource(run), { concurrency: 8 }),
1056
+ (sources) => sources.filter((source) => eligibleForWorkspace(selector, source))
1057
+ )
1058
+ )
1059
+
1060
+ /** The reconciled source a snapshot folds. */
1061
+ const sourceOf = (selector: GatewaySchema.ProjectionSelector): Effect.Effect<Source, GatewayError> => {
1062
+ const runId = scopeOf(selector)
1063
+ return runId === undefined
1064
+ ? Effect.map(workspaceSourceOf(selector), (runs) => ({ _tag: "workspace" as const, runs }))
1065
+ : Effect.map(runSourceOf(runId, selector._tag !== "run-events"), (run) => ({ _tag: "run" as const, run }))
1066
+ }
1067
+
1068
+ const snapshot = (
1069
+ selector: GatewaySchema.ProjectionSelector,
1070
+ after?: GatewaySchema.ProjectionCursor | undefined
1071
+ ): Effect.Effect<GatewaySchema.ProjectionSnapshot, GatewayError> =>
1072
+ Effect.gen(function*() {
1073
+ if (after !== undefined) {
1074
+ if (selector._tag !== "run-events") {
1075
+ return yield* malformed("Only run-events snapshots accept an after cursor")
1076
+ }
1077
+ const scope = resumeScope(selector, after)
1078
+ if (typeof scope !== "string") return yield* scope
1079
+ }
1080
+ // `run-events` answers the journal itself, so it pages rather than folds:
1081
+ // one bounded read from the caller's cursor, and the position it reached
1082
+ // to ask for the next page from. Every other selector answers rows whose
1083
+ // size is bounded by the selector, so it folds the reconciled source.
1084
+ if (selector._tag === "run-events") {
1085
+ const run = yield* runOf(selector.runId)
1086
+ const page = yield* runEventsPage(run, after)
1087
+ const rows = yield* boundedRows(selector, page.events)
1088
+ return yield* snapshotOf({ selector, cursor: cursorOf(selector, page.lastPosition), rows })
1089
+ }
1090
+ const source = yield* sourceOf(selector)
1091
+ const rows = yield* boundedRows(selector, rowsOf(selector, source, now()))
1092
+ return yield* snapshotOf({ selector, cursor: cursorOf(selector, cursorPositionOf(source)), rows })
1093
+ })
1094
+
1095
+ const snapshotFrames = (
1096
+ selector: GatewaySchema.ProjectionSelector,
1097
+ source: Source,
1098
+ rows: ReadonlyArray<unknown>
1099
+ ): Effect.Effect<ReadonlyArray<GatewaySchema.GatewayFrame>, GatewayError> => {
1100
+ const cursor = cursorOf(selector, cursorPositionOf(source))
1101
+ return Effect.forEach([
1102
+ { _tag: "snapshot-start", selector, cursor },
1103
+ ...rows.map((row) => ({ _tag: "row", selector, cursor, row })),
1104
+ { _tag: "snapshot-end", selector, cursor }
1105
+ ], frameOf)
1106
+ }
1107
+
1108
+ /**
1109
+ * The rows one delta carries.
1110
+ *
1111
+ * `run-events`' rows are the ordered events themselves, so its delta is the
1112
+ * one event that arrived. `transcript` rows are immutable once folded, so
1113
+ * its delta is the rows that event appended, numbered after the turns the
1114
+ * follower has already counted. Every other selector answers a full
1115
+ * replacement folded from the accumulated events, because recomputation is
1116
+ * the only delta that cannot disagree with a fresh snapshot. The caller
1117
+ * refreshes authoritative rows before compaction so retention and rendering
1118
+ * use the same owner even when a fenced status write journals no event.
1119
+ */
1120
+ const deltaRows = (
1121
+ selector: GatewaySchema.ProjectionSelector,
1122
+ run: ControlSchema.RunSummary,
1123
+ event: ControlSchema.ControlEvent,
1124
+ events: ReadonlyArray<ControlSchema.ControlEvent>,
1125
+ turnsBefore: number,
1126
+ duplicateCall: boolean,
1127
+ carry: Diagnosis.Digest | undefined
1128
+ ): Effect.Effect<ReadonlyArray<unknown>, GatewayError> => {
1129
+ if (selector._tag === "run-events") return Effect.succeed([event])
1130
+ if (selector._tag === "transcript") {
1131
+ return Effect.succeed(duplicateCall ? [] : transcriptAppend(event, turnsBefore))
1132
+ }
1133
+ return Effect.succeed(rowsOfRun(selector, { run, events, carry }, now()))
1134
+ }
1135
+
1136
+ interface RunFollowState {
1137
+ readonly buffer: EventBuffer
1138
+ readonly observed: CursorPosition | undefined
1139
+ /** Turns the transcript has opened so far; zero for every other selector. */
1140
+ readonly turns: number
1141
+ /** Derived once from the cursor prefix, then extended in constant time. */
1142
+ readonly reportedCalls: Map<string, boolean>
1143
+ }
1144
+
1145
+ interface RunDelta {
1146
+ readonly run: ControlSchema.RunSummary
1147
+ readonly event: ControlSchema.ControlEvent
1148
+ readonly events: ReadonlyArray<ControlSchema.ControlEvent>
1149
+ readonly carry: Diagnosis.Digest | undefined
1150
+ readonly position: CursorPosition
1151
+ readonly turnsBefore: number
1152
+ readonly duplicateCall: boolean
1153
+ readonly replaceTranscript: boolean
1154
+ }
1155
+
1156
+ /**
1157
+ * Deltas for one run, following from `from` and folding over the events the
1158
+ * caller already read plus each event that arrives.
1159
+ *
1160
+ * A historical run summary rebuilds its health prefix once from the follow
1161
+ * replay. Later deltas only append: re-reading history for every event would
1162
+ * make a subscription quadratic in run length and resend the whole log on
1163
+ * each frame.
1164
+ */
1165
+ const runDeltaFrames = (
1166
+ selector: GatewaySchema.ProjectionSelector,
1167
+ source: RunSource,
1168
+ from: CursorPosition
1169
+ ): Stream.Stream<GatewaySchema.GatewayFrame, GatewayError> => {
1170
+ const runId = source.run.runId
1171
+ const message = `Following ${runId} failed`
1172
+ // The final compacted buffer cannot seed an earlier health cursor: a
1173
+ // later reading may have replaced the stronger observation held there.
1174
+ // Rebuild that prefix in the same bounded fold from the follow replay.
1175
+ const replayHealth = selector._tag === "run-summary" && comparePosition(from, source.lastPosition) < 0
1176
+ const seedEvents = positionedEvents(source.events)
1177
+ .filter(({ position }) => comparePosition(position, from) <= 0)
1178
+ .map(({ event }) => event)
1179
+ const seed = replayHealth ?
1180
+ Effect.succeed(emptyEventBuffer(true)) :
1181
+ comparePosition(from, source.lastPosition) === 0
1182
+ ? Effect.succeed<EventBuffer>({
1183
+ compactHealth: source.compactHealth,
1184
+ seen: source.seen,
1185
+ events: source.events,
1186
+ encodedBytes: source.encodedBytes,
1187
+ lastPosition: source.lastPosition,
1188
+ carry: source.carry,
1189
+ dropped: source.dropped
1190
+ })
1191
+ : bufferOf(seedEvents, `${message}: the cursor seed is invalid`, source.compactHealth, source.run)
1192
+ return Stream.unwrap(Effect.map(seed, (initial) =>
1193
+ control.watch({
1194
+ runId,
1195
+ // Sequence zero also needs replay for empty seeds and derived offsets.
1196
+ ...(from.value === 0 || replayHealth ? {} : { afterSequence: from.value - 1 }),
1197
+ follow: true
1198
+ }).pipe(
1199
+ Stream.tapError((cause) => logReadFailure("follow-run", cause)),
1200
+ Stream.mapError((cause) => unavailable(message, cause)),
1201
+ Stream.mapAccumEffect(
1202
+ (): RunFollowState => ({
1203
+ buffer: initial,
1204
+ observed: undefined,
1205
+ turns: selector._tag === "transcript" ? turnsOpened(initial.events) : 0,
1206
+ reportedCalls: (() => {
1207
+ const seen = new Map<string, boolean>()
1208
+ if (selector._tag === "transcript") {
1209
+ for (const event of initial.events) {
1210
+ const key = callEventKey(event)
1211
+ if (key !== undefined) {
1212
+ seen.set(
1213
+ key,
1214
+ seen.get(key) === true || (nativeCallEvent(event) ?? nativeStepEvent(event)) !== undefined
1215
+ )
1216
+ }
1217
+ }
1218
+ }
1219
+ return seen
1220
+ })()
1221
+ }),
1222
+ (state, candidate): Effect.Effect<
1223
+ readonly [RunFollowState, ReadonlyArray<RunDelta>],
1224
+ GatewayError
1225
+ > =>
1226
+ Effect.flatMap(
1227
+ decodedEvent(candidate, `${message}: the control plane returned an invalid event`),
1228
+ (event) => {
1229
+ if (state.observed !== undefined && event.sequence < state.observed.value) {
1230
+ return Effect.fail(unavailable(`${message}: event sequences moved backward`, undefined))
1231
+ }
1232
+ const position: CursorPosition = {
1233
+ value: event.sequence,
1234
+ offset: state.observed?.value === event.sequence ? state.observed.offset + 1 : 0
1235
+ }
1236
+ const nextState: RunFollowState = { ...state, observed: position }
1237
+ if (
1238
+ (replayHealth || seedEvents.length > 0 || from.value > 0 || from.offset > 0) &&
1239
+ comparePosition(position, from) <= 0
1240
+ ) {
1241
+ return replayHealth
1242
+ ? Effect.map(
1243
+ appendEvent(state.buffer, event, `${message}: cursor history is invalid`, source.run),
1244
+ (buffer) => [{ ...nextState, buffer }, [] as ReadonlyArray<RunDelta>] as const
1245
+ )
1246
+ : Effect.succeed([nextState, [] as ReadonlyArray<RunDelta>] as const)
1247
+ }
1248
+ const turnsBefore = state.turns
1249
+ // A resumed follower must also suppress duplicates of calls
1250
+ // reported before its cursor, without re-folding that prefix.
1251
+ const callKey = selector._tag === "transcript" ? callEventKey(event) : undefined
1252
+ const duplicateCall = callKey !== undefined && state.reportedCalls.has(callKey)
1253
+ const authoritative = (nativeCallEvent(event) ?? nativeStepEvent(event)) !== undefined
1254
+ const replaceTranscript = duplicateCall && authoritative && state.reportedCalls.get(callKey) === false
1255
+ if (callKey !== undefined) {
1256
+ state.reportedCalls.set(callKey, authoritative || state.reportedCalls.get(callKey) === true)
1257
+ }
1258
+ const turns = selector._tag === "transcript"
1259
+ ? transcriptAppend(event, turnsBefore).at(-1)?.turn ?? turnsBefore
1260
+ : 0
1261
+ return Effect.flatMap(
1262
+ selector._tag === "run-summary" || selector._tag === "run-tree" || selector._tag === "approvals"
1263
+ ? runOf(runId)
1264
+ : Effect.succeed(source.run),
1265
+ (run) =>
1266
+ Effect.map(
1267
+ appendEvent(state.buffer, event, `${message}: event history is invalid`, run),
1268
+ (buffer) =>
1269
+ [
1270
+ {
1271
+ buffer,
1272
+ observed: position,
1273
+ turns,
1274
+ reportedCalls: state.reportedCalls
1275
+ } satisfies RunFollowState,
1276
+ [
1277
+ {
1278
+ run,
1279
+ event,
1280
+ events: buffer.events,
1281
+ carry: buffer.carry,
1282
+ position,
1283
+ turnsBefore,
1284
+ duplicateCall,
1285
+ replaceTranscript
1286
+ }
1287
+ ] satisfies ReadonlyArray<RunDelta>
1288
+ ] as const
1289
+ )
1290
+ )
1291
+ }
1292
+ )
1293
+ ),
1294
+ Stream.mapEffect(({ carry, duplicateCall, event, events, position, replaceTranscript, run, turnsBefore }) =>
1295
+ replaceTranscript
1296
+ ? Effect.flatMap(boundedRows(selector, GatewayProjection.transcript(events)), (rows) =>
1297
+ Effect.forEach([
1298
+ { _tag: "snapshot-start", selector, cursor: cursorOf(selector, position) },
1299
+ ...rows.map((row) => ({ _tag: "row", selector, cursor: cursorOf(selector, position), row })),
1300
+ { _tag: "snapshot-end", selector, cursor: cursorOf(selector, position) }
1301
+ ], frameOf))
1302
+ : Effect.flatMap(deltaRows(selector, run, event, events, turnsBefore, duplicateCall, carry), (rows) =>
1303
+ Effect.flatMap(boundedRows(selector, rows), (delta) =>
1304
+ Effect.map(
1305
+ frameOf({
1306
+ _tag: "delta",
1307
+ selector,
1308
+ cursor: cursorOf(selector, position),
1309
+ delta
1310
+ }),
1311
+ (frame) => [frame]
1312
+ )))
1313
+ ),
1314
+ Stream.flattenIterable
1315
+ )))
1316
+ }
1317
+
1318
+ /**
1319
+ * Workspace deltas from the unscoped control follow.
1320
+ *
1321
+ * Control sequences belong to run partitions, so this stream cannot start
1322
+ * after one workspace cursor. It follows without a cursor instead. That
1323
+ * replays partition history, and the sources seeded from the snapshot drop
1324
+ * events through each run's last folded position. Excluded runs retain only
1325
+ * a verdict and replay positions, sharing the source ceiling. Their replay
1326
+ * costs no further journal reads until the verdict is evicted.
1327
+ *
1328
+ * Events are gathered for `workspaceDeltaCoalesceMillis` and admitted as one
1329
+ * batch. A batch re-reads each changed run's row once, refolds only those
1330
+ * runs' cached rows, and answers one frame, so a burst across many runs
1331
+ * costs one frame and one row read per run that changed.
1332
+ */
1333
+ const workspaceDeltaFrames = (
1334
+ selector: GatewaySchema.ProjectionSelector,
1335
+ runs: ReadonlyArray<RunSource>
1336
+ ): Stream.Stream<GatewaySchema.GatewayFrame, GatewayError> =>
1337
+ Stream.unwrap(Effect.sync(() => {
1338
+ interface FollowedSource {
1339
+ source: RunSource
1340
+ observed: CursorPosition | undefined
1341
+ /** This run's rows, folded when its journal grows rather than on every frame. */
1342
+ rows: ReadonlyArray<unknown>
1343
+ }
1344
+ interface JudgedRun {
1345
+ readonly lastPosition: CursorPosition | undefined
1346
+ observed: CursorPosition
1347
+ }
1348
+ const rowsOfSource = (source: RunSource): ReadonlyArray<unknown> =>
1349
+ selector._tag === "approvals" ? pendingApprovals(source) : rowsOfRun(selector, source, now())
1350
+ const sources = new Map<string, FollowedSource>(runs.map((source) => [
1351
+ source.run.runId,
1352
+ { source, observed: undefined, rows: rowsOfSource(source) }
1353
+ ]))
1354
+ // A verdict retains no journal. Undefined lastPosition means run_not_found.
1355
+ // Sources and verdicts share one ceiling; only verdicts may be evicted.
1356
+ const judged = new Map<string, JudgedRun>()
1357
+ const makeRoom = () => {
1358
+ if (sources.size + judged.size >= maxWorkspaceRuns) {
1359
+ // Callers have room for a source, so a full cache has a verdict.
1360
+ judged.delete(judged.keys().next().value!)
1361
+ }
1362
+ }
1363
+ const remember = (runId: string, lastPosition: CursorPosition | undefined, observed: CursorPosition) => {
1364
+ judged.delete(runId)
1365
+ makeRoom()
1366
+ judged.set(runId, { lastPosition, observed })
1367
+ }
1368
+ /** The followed run created first, the one a newer run displaces at the ceiling. */
1369
+ const oldestFollowed = (): FollowedSource | undefined => {
1370
+ let oldest: FollowedSource | undefined
1371
+ for (const followed of sources.values()) {
1372
+ if (oldest === undefined || createdAfter(oldest.source.run, followed.source.run)) oldest = followed
1373
+ }
1374
+ return oldest
1375
+ }
1376
+ const noFrames: ReadonlyArray<GatewaySchema.GatewayFrame> = []
1377
+ const delta = (): Effect.Effect<ReadonlyArray<GatewaySchema.GatewayFrame>, GatewayError> =>
1378
+ Effect.flatMap(
1379
+ boundedRows(
1380
+ selector,
1381
+ selector._tag === "approvals"
1382
+ // Each run's pending rows are cached apart; the inbox is folded
1383
+ // across all of them, exactly as a snapshot folds it.
1384
+ ? approvalInbox([...sources.values()].map(({ rows, source }) => ({ run: source.run, rows })))
1385
+ : [...sources.values()].flatMap(({ rows }) => rows)
1386
+ ),
1387
+ (rows) =>
1388
+ Effect.map(
1389
+ frameOf({
1390
+ _tag: "delta",
1391
+ selector,
1392
+ cursor: cursorOf(selector, { value: 0, offset: 0 }),
1393
+ delta: rows
1394
+ }),
1395
+ (frame) => [frame]
1396
+ )
1397
+ )
1398
+ const message = "Following the workspace failed"
1399
+ /**
1400
+ * What one admitted event did to the retained state: nothing a frame
1401
+ * must report, a change already folded, or a change to a followed run
1402
+ * whose row must be re-read before its rows are refolded.
1403
+ */
1404
+ type Admission = "unchanged" | "fresh" | "stale"
1405
+ const admit = (
1406
+ event: ControlSchema.ControlEvent,
1407
+ refreshed: Set<string>
1408
+ ): Effect.Effect<Admission, GatewayError> =>
1409
+ Effect.gen(function*() {
1410
+ const runId = event.runId
1411
+ if (runId === undefined) return "unchanged"
1412
+ const current = sources.get(runId)
1413
+ const verdict = judged.get(runId)
1414
+ const previous = current?.observed ?? verdict?.observed
1415
+ if (previous !== undefined && event.sequence < previous.value) {
1416
+ return yield* Effect.fail(unavailable(`${message}: event sequences moved backward`, undefined))
1417
+ }
1418
+ const position: CursorPosition = {
1419
+ value: event.sequence,
1420
+ offset: previous?.value === event.sequence ? previous.offset + 1 : 0
1421
+ }
1422
+ if (current !== undefined) {
1423
+ current.observed = position
1424
+ if (
1425
+ current.source.events.length > 0 && comparePosition(position, current.source.lastPosition) <= 0
1426
+ ) return "unchanged"
1427
+ if (!refreshed.has(runId)) {
1428
+ current.source = { ...current.source, run: yield* runOf(runId) }
1429
+ refreshed.add(runId)
1430
+ }
1431
+ const buffer = yield* appendEvent(
1432
+ current.source,
1433
+ event,
1434
+ `${message}: event history is invalid`,
1435
+ current.source.run
1436
+ )
1437
+ current.source = { run: current.source.run, ...buffer }
1438
+ return "stale"
1439
+ }
1440
+ if (verdict !== undefined) {
1441
+ verdict.observed = position
1442
+ if (verdict.lastPosition === undefined || comparePosition(position, verdict.lastPosition) <= 0) {
1443
+ return "unchanged"
1444
+ }
1445
+ }
1446
+ // At the ceiling a run newer than the oldest followed run displaces
1447
+ // it, so the workspace keeps the newest runs as the snapshot does.
1448
+ const displaced = sources.size >= maxWorkspaceRuns ? oldestFollowed() : undefined
1449
+ if (displaced !== undefined) {
1450
+ const newer = yield* runOf(runId).pipe(
1451
+ Effect.map((run) => createdAfter(run, displaced.source.run)),
1452
+ Effect.catchIf((failure) => failure.code === "run_not_found", () => Effect.succeed(false))
1453
+ )
1454
+ if (!newer) return "unchanged"
1455
+ }
1456
+ return yield* runSourceOf(runId).pipe(
1457
+ Effect.flatMap((source) => {
1458
+ const admitted = source.events.length > 0 && comparePosition(position, source.lastPosition) <= 0
1459
+ ? Effect.succeed(source)
1460
+ : Effect.map(
1461
+ appendEvent(source, event, `${message}: event history is invalid`, source.run),
1462
+ (buffer): RunSource => ({ run: source.run, ...buffer })
1463
+ )
1464
+ return Effect.map(admitted, (complete): Admission => {
1465
+ if (!eligibleForWorkspace(selector, complete)) {
1466
+ remember(runId, complete.lastPosition, position)
1467
+ return "unchanged"
1468
+ }
1469
+ judged.delete(runId)
1470
+ if (displaced !== undefined && sources.size >= maxWorkspaceRuns) {
1471
+ sources.delete(displaced.source.run.runId)
1472
+ }
1473
+ makeRoom()
1474
+ sources.set(runId, { source: complete, observed: position, rows: rowsOfSource(complete) })
1475
+ return "fresh"
1476
+ })
1477
+ }),
1478
+ Effect.catchIf(
1479
+ (failure) => failure.code === "run_not_found",
1480
+ () => {
1481
+ remember(runId, undefined, position)
1482
+ return Effect.succeed<Admission>("unchanged")
1483
+ }
1484
+ )
1485
+ )
1486
+ })
1487
+ /** Refold one grown run using the row refreshed before its batch's compaction, or exclude it. */
1488
+ const refresh = (runId: string): Effect.Effect<void, GatewayError> =>
1489
+ Effect.gen(function*() {
1490
+ const current = sources.get(runId)
1491
+ /* v8 ignore next -- only runs still followed after the batch are refreshed. */
1492
+ if (current === undefined || current.observed === undefined) return
1493
+ const source = current.source
1494
+ if (eligibleForWorkspace(selector, source)) {
1495
+ current.rows = rowsOfSource(source)
1496
+ } else {
1497
+ sources.delete(runId)
1498
+ remember(runId, source.lastPosition, current.observed)
1499
+ }
1500
+ })
1501
+ return control.watch({ follow: true }).pipe(
1502
+ Stream.tapError((cause) => logReadFailure("follow-workspace", cause)),
1503
+ Stream.mapError((cause) => unavailable(message, cause)),
1504
+ Stream.groupedWithin(workspaceDeltaBatchSize, workspaceDeltaCoalesceMillis),
1505
+ Stream.mapEffect((batch) =>
1506
+ Effect.gen(function*() {
1507
+ const stale = new Set<string>()
1508
+ const refreshed = new Set<string>()
1509
+ let changed = false
1510
+ for (const candidate of batch) {
1511
+ const event = yield* decodedEvent(candidate, `${message}: the control plane returned an invalid event`)
1512
+ const admission = yield* admit(event, refreshed)
1513
+ if (admission === "unchanged") continue
1514
+ changed = true
1515
+ if (admission === "stale" && event.runId !== undefined) stale.add(event.runId)
1516
+ }
1517
+ yield* Effect.forEach(stale, refresh, { discard: true })
1518
+ return changed ? yield* delta() : noFrames
1519
+ })
1520
+ ),
1521
+ Stream.flattenIterable
1522
+ )
1523
+ }))
1524
+
1525
+ /**
1526
+ * Deltas for a selector.
1527
+ *
1528
+ * A run selector follows after its sequence cursor. A workspace selector has
1529
+ * no resume cursor because control journal sequences belong to per-run
1530
+ * partitions, but it can follow every partition without one.
1531
+ */
1532
+ const deltaFrames = (
1533
+ selector: GatewaySchema.ProjectionSelector,
1534
+ source: Source,
1535
+ from: CursorPosition
1536
+ ): Stream.Stream<GatewaySchema.GatewayFrame, GatewayError> => {
1537
+ // A duration row is ranked over the runs a flow has ALREADY finished, so
1538
+ // no event of a live run changes one, and the run that would change one
1539
+ // is not in the window until it settles. The honest delta is another
1540
+ // snapshot, which is what a client takes.
1541
+ if (selector._tag === "flow-durations") return Stream.empty
1542
+ return source._tag === "workspace"
1543
+ ? workspaceDeltaFrames(selector, source.runs)
1544
+ : runDeltaFrames(selector, source.run, from)
1545
+ }
1546
+
1547
+ /**
1548
+ * The keepalive channel. `Stream.tick` emits immediately and then on the
1549
+ * interval, and an immediate keepalive would arrive before the snapshot it
1550
+ * is supposed to keep alive; dropping the first tick makes the cadence what
1551
+ * it says it is.
1552
+ */
1553
+ const heartbeats: Stream.Stream<GatewaySchema.GatewayFrame> = Stream.tick(heartbeatMillis).pipe(
1554
+ Stream.drop(1),
1555
+ Stream.mapEffect(() =>
1556
+ Effect.map(Effect.clockWith((clock) => clock.currentTimeMillis), (atMs) => ({
1557
+ _tag: "heartbeat" as const,
1558
+ atMs
1559
+ }))
1560
+ )
1561
+ )
1562
+
1563
+ /** The run a cursor resumes, or the refusal explaining why it resumes none. */
1564
+ const resumeScope = (
1565
+ selector: GatewaySchema.ProjectionSelector,
1566
+ after: GatewaySchema.ProjectionCursor
1567
+ ): string | GatewayError => {
1568
+ const candidate = after as unknown as Record<string, unknown>
1569
+ if (!Number.isSafeInteger(candidate.value) || (candidate.value as number) < 0) {
1570
+ return malformed(
1571
+ `Cursor value ${String(candidate.value)} must be a non-negative safe integer`
1572
+ )
1573
+ }
1574
+ if (!Number.isSafeInteger(candidate.offset) || (candidate.offset as number) < 0) {
1575
+ return malformed(
1576
+ `Cursor offset ${String(candidate.offset)} must be a non-negative safe integer`
1577
+ )
1578
+ }
1579
+ if (!Schema.is(GatewaySchema.ProjectionCursor)(after)) {
1580
+ return malformed("The projection cursor is not a valid gateway cursor")
1581
+ }
1582
+ const runId = scopeOf(selector)
1583
+ if (after.projection !== selector._tag) {
1584
+ return malformed(
1585
+ `A cursor for the ${after.projection} projection cannot resume a ${selector._tag} subscription`
1586
+ )
1587
+ }
1588
+ if (!sameSelector(after.selector, selector)) {
1589
+ return malformed("A cursor can resume only the exact selector that issued it")
1590
+ }
1591
+ if (runId === undefined) {
1592
+ return malformed(
1593
+ `A ${selector._tag} subscription has no resumable cursor, because control journal sequences belong to per-run partitions`
1594
+ )
1595
+ }
1596
+ if (after.runId !== runId) {
1597
+ return malformed(`A cursor for run ${after.runId ?? "none"} cannot resume a subscription to run ${runId}`)
1598
+ }
1599
+ return runId
1600
+ }
1601
+
1602
+ /** Snapshot and follow share the reconciled event buffer. */
1603
+ const fromSnapshot = (
1604
+ selector: GatewaySchema.ProjectionSelector
1605
+ ): Effect.Effect<Stream.Stream<GatewaySchema.GatewayFrame, GatewayError>, GatewayError> =>
1606
+ Effect.flatMap(
1607
+ sourceOf(selector),
1608
+ (source) =>
1609
+ Effect.flatMap(boundedRows(selector, rowsOf(selector, source, now())), (rows) =>
1610
+ Effect.map(snapshotFrames(selector, source, rows), (frames) =>
1611
+ Stream.concat(
1612
+ Stream.fromIterable(frames),
1613
+ deltaFrames(selector, source, cursorPositionOf(source))
1614
+ )))
1615
+ )
1616
+
1617
+ /**
1618
+ * The deltas after a cursor the client already holds, with no snapshot.
1619
+ *
1620
+ * The read still happens: a folded projection cannot be recomputed from the
1621
+ * events after the cursor alone. What the client skips is receiving rows it
1622
+ * already has.
1623
+ */
1624
+ const fromCursor = (
1625
+ selector: GatewaySchema.ProjectionSelector,
1626
+ after: GatewaySchema.ProjectionCursor
1627
+ ): Effect.Effect<Stream.Stream<GatewaySchema.GatewayFrame, GatewayError>, GatewayError> => {
1628
+ const scope = resumeScope(selector, after)
1629
+ return typeof scope === "string"
1630
+ ? Effect.flatMap(runSourceOf(scope, selector._tag !== "run-events"), (source) => {
1631
+ const position = { value: after.value, offset: after.offset }
1632
+ const last = source.lastPosition
1633
+ if (comparePosition(position, last) > 0) {
1634
+ return Effect.fail(malformed(
1635
+ `Cursor ${after.value}:${after.offset} cannot resume run ${scope} past its last position ${last.value}:${last.offset}`
1636
+ ))
1637
+ }
1638
+ const retained = position.value === 0 && position.offset === 0 ||
1639
+ positionedEvents(source.events).some((candidate) => comparePosition(candidate.position, position) === 0)
1640
+ // A valid issued health cursor may have been superseded in the bounded fold.
1641
+ // Verify it against the durable stream rather than accepting arbitrary positions.
1642
+ const issued = retained ? Effect.succeed(true) : source.compactHealth ?
1643
+ control.watch({
1644
+ runId: scope,
1645
+ follow: false,
1646
+ ...(position.value === 0 ? {} : { afterSequence: position.value - 1 })
1647
+ }).pipe(
1648
+ Stream.takeWhile((event) => event.sequence <= position.value),
1649
+ Stream.take(maxEventsPerRun + 1),
1650
+ Stream.runCollect,
1651
+ Effect.map((events) =>
1652
+ events.length <= maxEventsPerRun && events[position.offset]?.sequence === position.value
1653
+ ),
1654
+ Effect.mapError((cause) => unavailable("Checking the projection cursor failed", cause))
1655
+ ) :
1656
+ Effect.succeed(false)
1657
+ return Effect.flatMap(issued, (valid) =>
1658
+ valid
1659
+ ? Effect.succeed(runDeltaFrames(selector, source, position))
1660
+ : Effect.fail(malformed(`Cursor ${after.value}:${after.offset} was not issued by run ${scope}`)))
1661
+ })
1662
+ : Effect.fail(scope)
1663
+ }
1664
+
1665
+ const subscribe = (
1666
+ selector: GatewaySchema.ProjectionSelector,
1667
+ after?: GatewaySchema.ProjectionCursor | undefined
1668
+ ): Stream.Stream<GatewaySchema.GatewayFrame, GatewayError> =>
1669
+ Stream.merge(
1670
+ Stream.unwrap(after === undefined ? fromSnapshot(selector) : fromCursor(selector, after)),
1671
+ heartbeats,
1672
+ { haltStrategy: "left" }
1673
+ )
1674
+
1675
+ /*
1676
+ * The service is generic in its selector; the implementation folds every
1677
+ * selector through one code path and is not. The correlation the signature
1678
+ * promises is enforced at runtime rather than assumed: `boundedRows`
1679
+ * decodes each fold against `GatewaySchema.rowSchemaFor(selector)` and
1680
+ * fails the read when a row does not belong to the selector that asked.
1681
+ */
1682
+ return { snapshot, subscribe } as Service
1683
+ }
1684
+
1685
+ /**
1686
+ * Builds the read path over a control plane.
1687
+ *
1688
+ * Invalid keepalive settings are returned as typed `bind_failed` failures;
1689
+ * construction never throws synchronously.
1690
+ *
1691
+ * @param control the control service to read through
1692
+ * @param options the keepalive cadence to use
1693
+ * @since 1.0.0
1694
+ * @category constructors
1695
+ */
1696
+ export const make = (
1697
+ control: ControlService,
1698
+ options: { readonly heartbeatMillis?: number | undefined } = {}
1699
+ ): Effect.Effect<Service, GatewayError> =>
1700
+ Effect.suspend(() => {
1701
+ const refusal = settingRefusal("The gateway keepalive cadence", options.heartbeatMillis)
1702
+ return refusal === undefined
1703
+ ? Effect.clockWith((clock) =>
1704
+ Effect.succeed(
1705
+ makeService(
1706
+ control,
1707
+ options.heartbeatMillis ?? heartbeatIntervalMillis,
1708
+ () => clock.currentTimeMillisUnsafe()
1709
+ )
1710
+ )
1711
+ )
1712
+ : Effect.fail(refusal)
1713
+ })
1714
+
1715
+ /**
1716
+ * Provides the gateway read path over the ambient control plane.
1717
+ *
1718
+ * @since 1.0.0
1719
+ * @category layers
1720
+ */
1721
+ export const layer: Layer.Layer<Projections, GatewayError, Control> = Layer.effect(Projections)(
1722
+ Effect.flatMap(Control, (control) => make(control))
1723
+ )
1724
+
1725
+ /**
1726
+ * Provides the gateway read path under an explicit keepalive cadence.
1727
+ *
1728
+ * @param options the keepalive cadence to use
1729
+ * @since 1.0.0
1730
+ * @category layers
1731
+ */
1732
+ export const layerWith = (
1733
+ options: { readonly heartbeatMillis?: number | undefined }
1734
+ ): Layer.Layer<Projections, GatewayError, Control> =>
1735
+ Layer.effect(Projections)(Effect.flatMap(Control, (control) => make(control, options)))