@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,2796 @@
1
+ /**
2
+ * The run trace model (factory spec 06, "the run trace": one card shows every
3
+ * run).
4
+ *
5
+ * A run is a recursive program: the agent writes REPL cells in a code
6
+ * container, every `ctx.call` is a durable span, and each turn is a frame. The
7
+ * control journal already records exactly that (`@smthrs/agent` AgentSession
8
+ * journals turn-opened, model-settled, cell-produced, cell-call-started,
9
+ * cell-call-settled, cell-printed, cell-settled, resolved), and the gateway
10
+ * serves it as the `run-events` projection the run card already reads. This
11
+ * module folds those records into a span tree, run → frame → cell → call, and
12
+ * the geometry of one bar per span on a shared time axis. No second data
13
+ * source: what the journal does not carry (realm variables, child runs' own
14
+ * frames) the model does not invent.
15
+ *
16
+ * Pure: the card renders the model, the tests read it from a fixture.
17
+ * @since 1.0.0
18
+ */
19
+
20
+ import { CallFact } from "@smthrs/journal"
21
+ import { CallPresentation, FlowActivity, type FlowDescriptor } from "@smthrs/registry/Descriptor"
22
+ import { Schema } from "effect"
23
+ import { callScope, openCallIndex } from "./Diagnosis.ts"
24
+ import { engineExecutionEvidence, engineTraceFromJournal } from "./EngineTrace.ts"
25
+ import { ACTION_RENDERINGS } from "./internal/actionRenderings.ts"
26
+ import { type CallEventFilter, callEventFilter, nativeStepEvent, uniqueCallEvents } from "./internal/callEvents.ts"
27
+ import { inspectLabel } from "./internal/inspectLabels.ts"
28
+
29
+ export { inspectLabel } from "./internal/inspectLabels.ts"
30
+
31
+ /** One control journal record, as the run card stores it (the run-events projection's row shape).
32
+ *
33
+ * @category models
34
+ * @since 1.0.0
35
+ */
36
+ export interface JournalRecord {
37
+ readonly runId?: string | undefined
38
+ readonly sequence?: number | undefined
39
+ readonly kind?: string | undefined
40
+ readonly occurredAt?: number | undefined
41
+ readonly payload?: unknown | undefined
42
+ }
43
+
44
+ /**
45
+ * What kind of node a span is in the tree. `fork` is the row 0 of a forked run
46
+ * (spec 06 §1); no journal record folds into it yet, so no fold produces one.
47
+ *
48
+ * @category models
49
+ * @since 1.0.0
50
+ */
51
+ export type SpanKind =
52
+ | "run"
53
+ | "frame"
54
+ | "model"
55
+ | "cell"
56
+ | "call"
57
+ | "approval"
58
+ | "resolved"
59
+ | "event"
60
+ | "fork"
61
+ | "execution"
62
+ | "attempt"
63
+
64
+ /** What the journal said about the span; the run root wears the run's own status word.
65
+ *
66
+ * @category models
67
+ * @since 1.0.0
68
+ */
69
+ export type SpanStatus = "running" | "completed" | "failed" | "waiting" | "approved" | "denied" | string
70
+
71
+ /** The facts the details pane shows for one span: only what its journal records carry.
72
+ *
73
+ * @category models
74
+ * @since 1.0.0
75
+ */
76
+ export interface SpanDetail {
77
+ /** The journal sequence that opened the span. */
78
+ readonly sequence?: number | undefined
79
+ /** The journal kind that opened the span. */
80
+ readonly event?: string | undefined
81
+ readonly seat?: string | undefined
82
+ /** A cell's source text. */
83
+ readonly source?: string | undefined
84
+ /** What a cell printed for the next model turn. */
85
+ readonly printed?: string | undefined
86
+ /**
87
+ * A call's flow name exactly as journaled. The span's `label` is its
88
+ * display form; read call semantics from this field, never from the label.
89
+ *
90
+ * @since 1.0.0
91
+ */
92
+ readonly flowName?: string | undefined
93
+ /** A call's input, as journaled. */
94
+ readonly input?: unknown | undefined
95
+ /** A call's settled value, a model's text, or a resolved text. */
96
+ readonly output?: string | undefined
97
+ /** A detached child's recorded execution id; only the agent/spawn result establishes this edge. */
98
+ readonly childRunId?: string | undefined
99
+ /** A failure's message. */
100
+ readonly message?: string | undefined
101
+ readonly usage?: { readonly inputTokens?: number | undefined; readonly outputTokens?: number | undefined } | undefined
102
+ /** Every other payload field the opening record carried, as the journal bound it. */
103
+ readonly fields?: Readonly<Record<string, unknown>> | undefined
104
+ }
105
+
106
+ /**
107
+ * One nested execution span with its original journal evidence.
108
+ *
109
+ * @category models
110
+ * @since 1.0.0
111
+ */
112
+ export interface TraceSpan {
113
+ readonly id: string
114
+ readonly kind: SpanKind
115
+ readonly label: string
116
+ readonly status: SpanStatus
117
+ readonly startedAt: number
118
+ /** Absent while the span is open. */
119
+ readonly endedAt?: number | undefined
120
+ readonly depth: number
121
+ readonly children: ReadonlyArray<TraceSpan>
122
+ readonly detail: SpanDetail
123
+ }
124
+
125
+ /** The run the trace belongs to, as the run card knows it.
126
+ *
127
+ * @category models
128
+ * @since 1.0.0
129
+ */
130
+ export interface TraceRun {
131
+ readonly runId: string
132
+ readonly flowId: string
133
+ /** The run card's phase word (running, completed, failed, cancelled, waiting-approval, ...). */
134
+ readonly status: string
135
+ readonly kind?: string | undefined
136
+ }
137
+
138
+ /**
139
+ * The recorded start and end of a trace in milliseconds.
140
+ *
141
+ * @category models
142
+ * @since 1.0.0
143
+ */
144
+ export interface TraceExtent {
145
+ readonly start: number
146
+ readonly end: number
147
+ }
148
+
149
+ /**
150
+ * What a frame was doing, derived from what it did — never declared.
151
+ *
152
+ * `unrecorded` is the honest reading of a frame whose journal carries nothing
153
+ * this fold can name: every other word here is a claim about the frame's work,
154
+ * and a claim needs a record behind it.
155
+ *
156
+ * @category models
157
+ * @since 1.0.0
158
+ */
159
+ export type PhaseId = "researching" | "implementing" | "testing" | "stuck" | "blocked" | "unrecorded"
160
+
161
+ /** One contiguous run of frames in one phase.
162
+ *
163
+ * @category models
164
+ * @since 1.0.0
165
+ */
166
+ export interface PhaseBand {
167
+ readonly phase: PhaseId
168
+ readonly startedAt: number
169
+ readonly endedAt: number
170
+ /** Span ids of the frames in this band, in order. */
171
+ readonly frames: ReadonlyArray<string>
172
+ /** Journal sequence to scrub to when this band is clicked. */
173
+ readonly seq: number
174
+ }
175
+
176
+ /** A moment a person would scrub to.
177
+ *
178
+ * @category models
179
+ * @since 1.0.0
180
+ */
181
+ export interface Milestone {
182
+ readonly seq: number
183
+ readonly at: number
184
+ readonly label: string
185
+ /** `muted` is a person's stop: neither a success nor a failure. */
186
+ readonly tone: "warn" | "bad" | "good" | "brand" | "muted"
187
+ /**
188
+ * The frame this moment was recorded under, carried through the scoped
189
+ * merge. Two steps run at once in one journal, so the frame a milestone
190
+ * belongs to is the one its OWN step had open, never whichever frame opened
191
+ * last.
192
+ */
193
+ readonly spanId: string
194
+ }
195
+
196
+ /**
197
+ * The frame one journal record was recorded under.
198
+ *
199
+ * The strip scrubs to sequences no milestone names, so the ownership rule has
200
+ * to hold for every record, not only for the ones that mint a pin.
201
+ *
202
+ * @category models
203
+ * @since 1.0.0
204
+ */
205
+ export interface TraceOwner {
206
+ readonly seq: number
207
+ readonly spanId: string
208
+ }
209
+
210
+ /** What one frame did, in plain English, derived from its calls.
211
+ *
212
+ * @category models
213
+ * @since 1.0.0
214
+ */
215
+ export interface FrameLine {
216
+ readonly spanId: string
217
+ /** Frame ordinal as shown to a person, 1-based. */
218
+ readonly frame: number
219
+ readonly verb: string
220
+ /** Empty when the call's input named nothing this fold reads as a subject; the card then prints the verb alone. */
221
+ readonly subject: string
222
+ readonly result: string
223
+ readonly failed: boolean
224
+ readonly wrote: boolean
225
+ /** Frame ordinal this repeats, set ONLY inside a stall streak. */
226
+ readonly repeatOf?: number | undefined
227
+ }
228
+
229
+ /** A discipline event, rendered where it happened.
230
+ *
231
+ * @category models
232
+ * @since 1.0.0
233
+ */
234
+ export interface TraceNote {
235
+ readonly seq: number
236
+ /** Span id of the frame it attaches under. */
237
+ readonly spanId: string
238
+ readonly tone: "warn" | "bad" | "good"
239
+ readonly title: string
240
+ readonly body: string
241
+ /** Quoted evidence lines, already clipped. */
242
+ readonly evidence?: ReadonlyArray<string> | undefined
243
+ }
244
+
245
+ /** Extra options the fold takes; every field optional, absent = today's behaviour.
246
+ *
247
+ * @category models
248
+ * @since 1.0.0
249
+ */
250
+ export interface TraceOptions {
251
+ /** Plan targets are coverage requirements, never evidence that a call ran tests. */
252
+ readonly checkTargets?: ReadonlyArray<string> | undefined
253
+ }
254
+
255
+ /**
256
+ * The shared projection rendered by app and terminal monitors.
257
+ *
258
+ * @category models
259
+ * @since 1.0.0
260
+ */
261
+ export interface TraceModel {
262
+ /** Source records for status and check receipts, including their settlement sequences. */
263
+ readonly journal: ReadonlyArray<JournalRecord>
264
+ readonly root: TraceSpan
265
+ /** Every span in tree order, with its depth: the tree's rows and the waterfall's rows. */
266
+ readonly rows: ReadonlyArray<TraceSpan>
267
+ readonly extent: TraceExtent
268
+ readonly counts: { readonly spans: number; readonly running: number; readonly failed: number }
269
+ /** The run's frames merged into what they were doing; empty when the journal opened no frame. */
270
+ readonly bands: ReadonlyArray<PhaseBand>
271
+ /** The moments a person scrubs to, in journal order; empty when the journal recorded none. */
272
+ readonly milestones: ReadonlyArray<Milestone>
273
+ /** One line per frame that called something; a frame that called nothing has none. */
274
+ readonly lines: ReadonlyArray<FrameLine>
275
+ /** The discipline records that carry fields, attached where they happened. */
276
+ readonly notes: ReadonlyArray<TraceNote>
277
+ /** Every recorded sequence with the frame it was recorded under, in journal order. */
278
+ readonly owners: ReadonlyArray<TraceOwner>
279
+ }
280
+
281
+ /** Mutable draft shared by the control and native journal folds; never persisted.
282
+ *
283
+ * @category models
284
+ * @since 1.0.0
285
+ */
286
+ export interface TraceBuilder {
287
+ readonly id: string
288
+ readonly kind: SpanKind
289
+ label: string
290
+ status: SpanStatus
291
+ startedAt: number
292
+ endedAt?: number | undefined
293
+ readonly children: Array<TraceBuilder>
294
+ detail: SpanDetail
295
+ }
296
+ type Builder = TraceBuilder
297
+
298
+ const TERMINAL_RUN: ReadonlySet<string> = new Set(["completed", "failed", "cancelled", "no-capacity"])
299
+
300
+ const asRecord = (value: unknown): Record<string, unknown> =>
301
+ typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {}
302
+ const asString = (value: unknown): string | undefined => typeof value === "string" ? value : undefined
303
+ const asNumber = (value: unknown): number | undefined =>
304
+ typeof value === "number" && Number.isFinite(value) ? value : undefined
305
+
306
+ /** The record's time: the agent's own `at` stamp when it carries one, the journal's otherwise (the gateway's rule). */
307
+ const timeOf = (record: JournalRecord, payload: Record<string, unknown>): number =>
308
+ asNumber(payload.at) ?? asNumber(record.occurredAt) ?? 0
309
+
310
+ /** One line of text for a value the journal traced, or nothing. */
311
+ const textOf = (value: unknown): string | undefined => {
312
+ if (value === undefined || value === null) return undefined
313
+ if (typeof value === "string") return value
314
+ try {
315
+ return JSON.stringify(value)
316
+ } catch {
317
+ return String(value)
318
+ }
319
+ }
320
+
321
+ /** The payload fields the pane does not already show by name. */
322
+ const restOf = (
323
+ payload: Record<string, unknown>,
324
+ shown: ReadonlyArray<string>
325
+ ): Record<string, unknown> | undefined => {
326
+ const rest: Record<string, unknown> = {}
327
+ for (const [key, value] of Object.entries(payload)) {
328
+ if (shown.includes(key) || key === "at" || key === "journalVersion") continue
329
+ rest[key] = value
330
+ }
331
+ return Object.keys(rest).length === 0 ? undefined : rest
332
+ }
333
+
334
+ const builder = (
335
+ id: string,
336
+ kind: SpanKind,
337
+ label: string,
338
+ status: SpanStatus,
339
+ startedAt: number,
340
+ detail: SpanDetail
341
+ ): Builder => ({ id, kind, label, status, startedAt, children: [], detail })
342
+
343
+ /*
344
+ * What the frame was DOING.
345
+ *
346
+ * Same discipline as the tree above, held one level closer to the reader:
347
+ * every word below is read off a journal record, never off what the model said
348
+ * it would do. A frame's line comes from the calls it made; its phase comes
349
+ * from those calls and from the controller's own observations of the tree; a
350
+ * note is a discipline record's own fields, quoted. Nothing here counts what
351
+ * the journal may be missing, and a frame the journal says nothing about gets
352
+ * nothing said about it.
353
+ */
354
+
355
+ /**
356
+ * What a recorded call means: what it did, and how it reads.
357
+ *
358
+ * The rows, the phase bands and the run header all read a call through this
359
+ * one answer, so they cannot tell three different stories about one record.
360
+ *
361
+ * @category models
362
+ * @since 1.0.0
363
+ */
364
+ export type CallMetadata = Pick<FlowDescriptor, "activity" | "presentation">
365
+
366
+ /** Compatibility for journals whose descriptors predate presentation metadata. */
367
+ const legacy = (
368
+ activity: FlowActivity | undefined,
369
+ pending: string,
370
+ success: string,
371
+ failure: string,
372
+ subject: CallPresentation["subject"],
373
+ result: CallPresentation["result"]
374
+ ): CallMetadata => ({
375
+ ...(activity === undefined ? {} : { activity }),
376
+ presentation: { verb: { pending, success, failure }, subject, result }
377
+ })
378
+
379
+ const LEGACY_PRESENTATION: ReadonlyMap<string, CallMetadata> = new Map([
380
+ // `bash` has no compatibility activity for the same reason `@smthrs/std`
381
+ // declares none: what a shell call does is in the command, which the rule
382
+ // below reads off the record.
383
+ ["bash", legacy(undefined, "running", "ran", "failed to run", "command", "command")],
384
+ ["test", legacy("tests", "running", "ran", "failed to run", "selection", "tests")],
385
+ ["edit", legacy("writes", "editing", "edited", "failed to edit", "path", "edit")],
386
+ ["write", legacy("writes", "writing", "wrote", "failed to write", "path", "write")],
387
+ ["apply_patch", legacy("writes", "patching", "patched", "failed to patch", "patch", "patch")],
388
+ ["read", legacy("reads", "reading", "read", "failed to read", "path", "read")],
389
+ ["grep", legacy("reads", "searching", "searched", "failed to search", "pattern", "matches")],
390
+ ["glob", legacy("reads", "listing", "listed", "failed to list", "pattern", "paths")],
391
+ ["ls", legacy("reads", "listing", "listed", "failed to list", "path", "entries")]
392
+ ])
393
+
394
+ /** Read only the descriptor's validated display fields; a call input is never metadata. */
395
+ const recordedMetadata = (value: unknown, flowName: string): CallMetadata | undefined => {
396
+ const descriptor = asRecord(value)
397
+ if (descriptor.name !== flowName) return undefined
398
+ if (descriptor.activity === undefined && descriptor.presentation === undefined) return undefined
399
+ const activity = Schema.is(FlowActivity)(descriptor.activity) ? descriptor.activity : undefined
400
+ const presentation = Schema.is(CallPresentation)(descriptor.presentation) ? descriptor.presentation : undefined
401
+ return { activity, presentation }
402
+ }
403
+
404
+ /**
405
+ * The name a queued checkpoint mint carries (`QuickJSSandbox` `checkpointFlow`).
406
+ * A mint journals `control.agent.checkpoint-minted` and never a call, so no
407
+ * call in a journal wears this name today; the dominant-call rule skips it
408
+ * anyway, so a host that binds a flow by that name cannot make a frame's
409
+ * headline read as bookkeeping.
410
+ */
411
+ const CHECKPOINT_FLOW = "checkpoint"
412
+
413
+ /** The command line a call ran, for the flows that run one. */
414
+ const commandOf = (input: unknown): string | undefined => {
415
+ const fields = asRecord(input)
416
+ return asString(fields.command) ?? asString(fields.script)
417
+ }
418
+
419
+ /** The last segment of a path: the name a person calls the file. */
420
+ const basename = (path: string): string => path.split("/").filter((part) => part !== "").pop() ?? path
421
+
422
+ /**
423
+ * The files a V4A patch names (`@smthrs/std` ApplyPatch's `*** Add File: `,
424
+ * `*** Delete File: ` and `*** Update File: ` markers).
425
+ *
426
+ * `apply_patch` carries its entire patch in one `input` string, and the patch
427
+ * is not the subject: what it patches is. A string that is not a patch names no
428
+ * files and yields none, so a flow that happens to call its field `input` is
429
+ * not mistaken for one.
430
+ */
431
+ const patchedPaths = (patch: string): ReadonlyArray<string> => {
432
+ const paths: Array<string> = []
433
+ for (const line of patch.split("\n")) {
434
+ const named = /^\*\*\* (?:Add|Delete|Update) File:\s*(\S.*)$/.exec(line.trim())?.[1]
435
+ if (named !== undefined) paths.push(named.trim())
436
+ }
437
+ return paths
438
+ }
439
+
440
+ /**
441
+ * Several files in the room one name has: the first named, the rest counted.
442
+ *
443
+ * A multi-file patch's line and a frame's write pin both say it through here,
444
+ * so `views.py +1` means one thing wherever the card prints it. The count is a
445
+ * fact the journal carried, never an estimate. No file, no words.
446
+ */
447
+ const filesLabel = (paths: ReadonlyArray<string>): string => {
448
+ const first = paths[0]
449
+ if (first === undefined) return ""
450
+ return paths.length === 1 ? basename(first) : `${basename(first)} +${paths.length - 1}`
451
+ }
452
+
453
+ /** The files an edit-like call names: its `path`, or every file its patch names. */
454
+ const writtenPaths = (input: unknown): ReadonlyArray<string> => {
455
+ const fields = asRecord(input)
456
+ const path = asString(fields.path)
457
+ if (path !== undefined) return [path]
458
+ const patch = asString(fields.input)
459
+ return patch === undefined ? [] : patchedPaths(patch)
460
+ }
461
+
462
+ /**
463
+ * What one recorded call was about, in the shape its declaration chose.
464
+ *
465
+ * Exported because the run header names the same subject as the row beneath
466
+ * it: two readings of one input is two answers to one question.
467
+ *
468
+ * @param input the call input, as journaled
469
+ * @param format the declared subject field, when the declaration named one
470
+ *
471
+ * @category utilities
472
+ * @since 1.0.0
473
+ */
474
+ export const callSubject = (input: unknown, format?: CallPresentation["subject"]): string => {
475
+ const fields = asRecord(input)
476
+ if (format === "none") return ""
477
+ if (format === "path") return asString(fields.path) === undefined ? "" : basename(fields.path as string)
478
+ if (format === "command") return commandOf(input) ?? ""
479
+ if (format === "patch") return filesLabel(patchedPaths(asString(fields.input) ?? ""))
480
+ if (format === "pattern") return asString(fields.pattern) ?? ""
481
+ if (format === "selection") return stringList(fields.selection)?.join(" ") ?? ""
482
+ const path = asString(fields.path)
483
+ if (path !== undefined) return basename(path)
484
+ const command = commandOf(input)
485
+ if (command !== undefined) return command
486
+ const patch = asString(fields.input)
487
+ if (patch !== undefined) {
488
+ // The patch is not the subject; the files it patches are.
489
+ const files = filesLabel(patchedPaths(patch))
490
+ if (files !== "") return files
491
+ }
492
+ // `test` names what it selected (`@smthrs/std` TestRun `Input.selection`).
493
+ // An omitted selection means the record named nothing; that the flow would
494
+ // then run everything is the flow's default, not a fact this record carries.
495
+ const selection = Array.isArray(fields.selection)
496
+ ? fields.selection.filter((one): one is string => typeof one === "string")
497
+ : undefined
498
+ if (selection !== undefined && selection.length > 0) return selection.join(" ")
499
+ return asString(fields.pattern) ?? ""
500
+ }
501
+
502
+ /**
503
+ * Legacy shell journals have no structured activity. Recognize only direct
504
+ * invocations of known runners. Shell composition, expansion and quoted
505
+ * programs remain unknown. Arguments can narrow a check without changing
506
+ * its activity; no command here establishes a plan's required coverage.
507
+ */
508
+ const shellActivity = (command: string): FlowActivity => {
509
+ if (/[;&|<>`$\n\r]/.test(command)) return "other"
510
+ const words = command.trim().split(/\s+/)
511
+ const first = words[0]!
512
+ if (["pytest", "vitest", "jest"].includes(first)) return "tests"
513
+ if (/^python[23]?$/.test(first) && words[1] === "-m" && ["pytest", "unittest"].includes(words[2] ?? "")) {
514
+ return "tests"
515
+ }
516
+ if (first === "bun" && words[1] === "test") return "tests"
517
+ if (["pnpm", "npm", "bun"].includes(first)) {
518
+ if (words[1] === "test" || (words[1] === "run" && words[2] === "test")) return "tests"
519
+ if (words[1] === "exec" && ["vitest", "jest"].includes(words[2] ?? "")) return "tests"
520
+ if (words[1] === "run" && ["check", "typecheck", "lint"].includes(words[2] ?? "")) return "checks"
521
+ }
522
+ if (first === "go" && words[1] === "test") return "tests"
523
+ if (first === "cargo" && words[1] === "test") return "tests"
524
+ if (first === "tsc") return "checks"
525
+ return "other"
526
+ }
527
+
528
+ /**
529
+ * The one reading of a recorded call's semantics, shared by the rows, the
530
+ * phase bands and the run header.
531
+ *
532
+ * Precedence, and why: the record's own `descriptor` wins, because it is what
533
+ * the declaration said when the call was made, and only a record that carries
534
+ * none falls back to {@link LEGACY_PRESENTATION}, which is a guess about the
535
+ * flows that were journaled before a declaration could say anything. A flow the
536
+ * table does not know stays unknown, and unknown is said as unknown.
537
+ *
538
+ * `bash` is the one activity read off the call rather than the declaration.
539
+ * `@smthrs/std` declares none for it on purpose — the same flow runs a test
540
+ * suite, a type check and `cat` — so a recognised runner in the recorded
541
+ * command is the only thing that names the activity, and an explicit
542
+ * declaration still outranks it.
543
+ *
544
+ * @param flowName the flow the record names
545
+ * @param payload the `cell-call-started` payload, as journaled
546
+ *
547
+ * @category utilities
548
+ * @since 1.0.0
549
+ */
550
+ export const callSemantics = (
551
+ flowName: string,
552
+ payload: Record<string, unknown>
553
+ ): CallMetadata => {
554
+ const recorded = recordedMetadata(payload.descriptor, flowName)
555
+ const metadata = recorded ?? LEGACY_PRESENTATION.get(flowName)
556
+ const activity = metadata?.activity
557
+ ?? (flowName === "bash" ? shellActivity(commandOf(payload.input) ?? "") : undefined)
558
+ return {
559
+ ...(activity === undefined ? {} : { activity }),
560
+ ...(metadata?.presentation === undefined ? {} : { presentation: metadata.presentation })
561
+ }
562
+ }
563
+
564
+ const stringList = (value: unknown): ReadonlyArray<string> | undefined =>
565
+ Array.isArray(value) && value.every((entry) => typeof entry === "string") ? value : undefined
566
+
567
+ const countOf = (value: unknown): number | undefined =>
568
+ typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined
569
+
570
+ const counted = (value: number, noun: string, plural = `${noun}s`): string => `${value} ${value === 1 ? noun : plural}`
571
+
572
+ /** Known output fields only. Unknown objects remain in the call's raw output. */
573
+ const resultOf = (value: unknown, format?: CallPresentation["result"]): string => {
574
+ if (format === "none") return ""
575
+ if (typeof value === "string") return clip(value) ?? ""
576
+ const fields = asRecord(value)
577
+ if (fields.truncated === true && typeof fields.digest === "string") return ""
578
+ switch (format) {
579
+ case "read": {
580
+ const start = countOf(fields.startLine)
581
+ const end = countOf(fields.endLine)
582
+ return start !== undefined && start > 0 && end !== undefined && end >= start - 1
583
+ ? counted(end - start + 1, "line") :
584
+ ""
585
+ }
586
+ case "write":
587
+ return countOf(fields.bytesWritten) === undefined ? "" : counted(fields.bytesWritten as number, "byte")
588
+ case "edit":
589
+ return countOf(fields.replacements) === undefined ? "" : counted(fields.replacements as number, "replacement")
590
+ case "patch": {
591
+ const groups = [stringList(fields.added), stringList(fields.modified), stringList(fields.deleted)]
592
+ return groups.every((group) => group !== undefined) ? counted(new Set(groups.flat()).size, "file") : ""
593
+ }
594
+ case "tests": {
595
+ const passed = countOf(fields.passed)
596
+ const failed = stringList(fields.failed)
597
+ if (fields.parsed === true && passed !== undefined && failed !== undefined && fields.invalidProbe === undefined) {
598
+ const exit = countOf(fields.exitCode)
599
+ return `${passed} passed${
600
+ failed.length > 0 ? ` · ${failed.length} failed` : exit !== undefined && exit !== 0 ? ` · exit ${exit}` : ""
601
+ }`
602
+ }
603
+ return countOf(fields.exitCode) === undefined ? "" : `exit ${fields.exitCode}`
604
+ }
605
+ case "command":
606
+ return countOf(fields.exitCode) === undefined ? "" : `exit ${fields.exitCode}`
607
+ case "matches":
608
+ return Array.isArray(fields.matches) ? counted(fields.matches.length, "match", "matches") : ""
609
+ case "paths":
610
+ return stringList(fields.paths) === undefined
611
+ ? ""
612
+ : counted((fields.paths as ReadonlyArray<string>).length, "file")
613
+ case "entries":
614
+ return Array.isArray(fields.entries) ? counted(fields.entries.length, "entry", "entries") : ""
615
+ default:
616
+ return ""
617
+ }
618
+ }
619
+
620
+ /** A note body from the sentences the payload carried; a field the record omitted contributes nothing. */
621
+ const bodyOf = (...sentences: ReadonlyArray<string | undefined>): string =>
622
+ sentences.filter((sentence) => sentence !== undefined).join(" ")
623
+
624
+ /** One quoted line of evidence, bounded the way the turn list bounds prose. */
625
+ const clip = (value: unknown): string | undefined => {
626
+ const line = asString(value)
627
+ if (line === undefined || line === "") return undefined
628
+ return line.length <= 160 ? line : `${line.slice(0, 159).trimEnd()}…`
629
+ }
630
+
631
+ /** One call a frame made, as the journal opened and settled it. */
632
+ interface CallFacts {
633
+ readonly flowName: string
634
+ readonly callId?: string | undefined
635
+ readonly input: unknown
636
+ readonly activity?: FlowActivity | undefined
637
+ readonly presentation?: CallPresentation | undefined
638
+ /**
639
+ * What a repeat is judged by: the flow and the input exactly as journaled. A
640
+ * field the trail truncated still canonicalizes to its own digest, so a
641
+ * repeat of a huge input is still recognised as one.
642
+ */
643
+ readonly signature: string
644
+ settled?:
645
+ | { readonly failed: boolean; readonly result: string; readonly denied: boolean; readonly value: unknown }
646
+ | undefined
647
+ /** What a repeat is matched on: the signature and the settlement. */
648
+ reading?: string | undefined
649
+ }
650
+
651
+ /** What one frame did, as its own records said it. */
652
+ interface FrameFacts {
653
+ readonly id: string
654
+ readonly frame: number
655
+ readonly seq: number
656
+ readonly startedAt: number
657
+ endedAt: number
658
+ blocked: boolean
659
+ /** `control.agent.mutation-observed`'s answer; absent when the frame closed on none. */
660
+ mutated?: boolean | undefined
661
+ /** A measured tree comparison outranks declared writes and successful no-op calls. */
662
+ observedMutation?: boolean | undefined
663
+ /** The write pin this frame minted: where it sits in the milestones, and every file it counts. */
664
+ wrote?: { readonly milestone: number; readonly paths: Array<string> } | undefined
665
+ readonly calls: Array<CallFacts>
666
+ }
667
+
668
+ /** Whether the frame made an edit-like call the journal recorded settling successfully. */
669
+ const madeAWrite = (entry: FrameFacts): boolean =>
670
+ entry.calls.some((call) => call.activity === "writes" && call.settled?.failed === false && !call.settled.denied)
671
+
672
+ /**
673
+ * The call a frame's line is about: a change it made, else the check it ran,
674
+ * else the first thing it asked for.
675
+ *
676
+ * The phase rules and the lines read the same answer, so the two can never
677
+ * disagree about whether the journal said anything about a frame at all.
678
+ */
679
+ const dominantCall = (entry: FrameFacts): CallFacts | undefined =>
680
+ entry.calls.find((one) => one.activity === "writes")
681
+ ?? entry.calls.find((one) => one.activity === "checks" || one.activity === "tests")
682
+ ?? entry.calls.find((one) => one.flowName !== CHECKPOINT_FLOW)
683
+
684
+ /**
685
+ * Folds the journal a second time, into what a person would say the run was
686
+ * doing: its phase bands, its milestones, one line per frame, and the
687
+ * discipline notes.
688
+ *
689
+ * Read off the RECORDS rather than off the spans the fold above builds, for
690
+ * three reasons. The spans keep a settled value as text (`textOf`), so an
691
+ * `ask` denial would have to be parsed back out of JSON. An unrecognised
692
+ * kind's span hangs off `cell ?? frame ?? root`, so the frame a note belongs
693
+ * to would have to be recovered from tree position. And that span shape is the
694
+ * fold's `default` arm: the day a kind gains a case of its own, notes read off
695
+ * `detail.fields` would vanish without a word, while notes read off the
696
+ * records keep working.
697
+ *
698
+ * @param runId the run, for the notes that land before any frame opened
699
+ * @param ordered the journal, in sequence order, call events already deduplicated
700
+ */
701
+ interface DisciplineState {
702
+ readonly runId: string
703
+ readonly frames: Array<FrameFacts>
704
+ readonly notes: Array<TraceNote>
705
+ readonly milestones: Array<Milestone>
706
+ readonly owners: Array<TraceOwner>
707
+ readonly open: Array<CallFacts>
708
+ frame: FrameFacts | undefined
709
+ lastAt: number
710
+ /** Each frame's line as last built, reused while its inputs are the same objects. */
711
+ readonly lineOf: WeakMap<FrameFacts, {
712
+ readonly call: CallFacts
713
+ readonly settled: CallFacts["settled"]
714
+ readonly repeatOf: number | undefined
715
+ readonly line: FrameLine
716
+ }>
717
+ }
718
+
719
+ const disciplineInit = (runId: string): DisciplineState => ({
720
+ runId,
721
+ frames: [],
722
+ notes: [],
723
+ milestones: [],
724
+ owners: [],
725
+ open: [],
726
+ frame: undefined,
727
+ lastAt: 0,
728
+ lineOf: new WeakMap()
729
+ })
730
+
731
+ /** One record of the discipline fold; the state is advanced in place. */
732
+ const disciplineStep = (state: DisciplineState, record: JournalRecord): void => {
733
+ const { runId, frames, notes, milestones, owners, open } = state
734
+ let frame = state.frame
735
+ /** Where a discipline record attaches: the open frame, else the run itself. */
736
+ const here = (): string => frame?.id ?? `run:${runId}`
737
+ const note = (
738
+ seq: number,
739
+ tone: TraceNote["tone"],
740
+ title: string,
741
+ body: string,
742
+ evidence: ReadonlyArray<string | undefined>
743
+ ): void => {
744
+ const quoted = evidence.filter((line): line is string => line !== undefined)
745
+ notes.push({ seq, spanId: here(), tone, title, body, ...(quoted.length === 0 ? {} : { evidence: quoted }) })
746
+ }
747
+ /** A moment, owned by the frame that recorded it, exactly the way a note is. */
748
+ const pin = (seq: number, at: number, label: string, tone: Milestone["tone"]): void => {
749
+ milestones.push({ seq, at, label, tone, spanId: here() })
750
+ }
751
+
752
+ {
753
+ const kind = record.kind ?? ""
754
+ const payload = asRecord(record.payload)
755
+ const at = timeOf(record, payload)
756
+ const seq = record.sequence ?? 0
757
+ state.lastAt = Math.max(state.lastAt, at)
758
+ if (frame !== undefined) frame.endedAt = Math.max(frame.endedAt, at)
759
+ switch (kind) {
760
+ case "control.agent.turn-opened": {
761
+ frame = {
762
+ id: `frame-${frames.length + 1}`,
763
+ frame: frames.length + 1,
764
+ seq,
765
+ startedAt: at,
766
+ endedAt: at,
767
+ blocked: false,
768
+ calls: []
769
+ }
770
+ frames.push(frame)
771
+ break
772
+ }
773
+ case "control.agent.cell-call-started": {
774
+ const flowName = asString(payload.flowName) ?? ""
775
+ const metadata = callSemantics(flowName, payload)
776
+ const call: CallFacts = {
777
+ flowName,
778
+ callId: asString(payload.callId),
779
+ input: payload.input,
780
+ activity: metadata.activity,
781
+ presentation: metadata.presentation,
782
+ signature: `${flowName} ${textOf(payload.input) ?? ""}`
783
+ }
784
+ open.push(call)
785
+ // A call journaled outside a frame belongs to no frame, and the tree
786
+ // above puts it on the run. It is still open work, so it still pairs.
787
+ frame?.calls.push(call)
788
+ break
789
+ }
790
+ case "control.agent.cell-call-settled": {
791
+ if (payload.outcome !== "success" && payload.outcome !== "failure") break
792
+ const index = openCallIndex(open, asString(payload.callId), asString(payload.flowName))
793
+ const call = index < 0 ? undefined : open.splice(index, 1)[0]
794
+ if (call === undefined) break
795
+ const failed = asString(payload.outcome) === "failure"
796
+ // `ask` settles SUCCESSFULLY with the person's answer (`AgentSession`
797
+ // answers `{ answer, approved }`), so a refusal is a denial and not a
798
+ // failure. That is the call that settles with a denial.
799
+ const denied = !failed && asRecord(payload.value).approved === false
800
+ call.settled = {
801
+ failed,
802
+ result: (failed ? textOf(payload.message) : textOf(payload.value)) ?? "",
803
+ denied,
804
+ value: payload.value
805
+ }
806
+ if (denied && frame !== undefined) frame.blocked = true
807
+ // A write is the fastest thing a run does, so its band is always too
808
+ // narrow to carry a label, and its own marker is the only way to reach
809
+ // the moment that matters most.
810
+ if (!failed && !denied && call.activity === "writes") {
811
+ // A pin is its label, so a pin with none is a marker nobody can read
812
+ // and nobody can tell from the next one. The journal gave this call
813
+ // no file; no file, no pin.
814
+ const paths = [...new Set(writtenPaths(call.input))]
815
+ const minted = frame?.wrote
816
+ if (minted !== undefined) {
817
+ // A frame's writes are ONE moment. A frame that edits fifteen files
818
+ // does it inside a second, so a pin per file is fifteen names
819
+ // stacked over a point on the axis nobody can aim between. The pin
820
+ // stays where the frame started writing, on the file it names, and
821
+ // counts the rest; a file written twice is still one file.
822
+ for (const path of paths) if (!minted.paths.includes(path)) minted.paths.push(path)
823
+ milestones[minted.milestone] = { ...milestones[minted.milestone]!, label: filesLabel(minted.paths) }
824
+ } else if (paths.length > 0) {
825
+ // A write journaled outside a frame has no frame to be counted
826
+ // into, so it stays a moment of its own.
827
+ if (frame !== undefined) frame.wrote = { milestone: milestones.length, paths }
828
+ pin(seq, at, filesLabel(paths), "brand")
829
+ }
830
+ }
831
+ break
832
+ }
833
+ case "control.agent.mutation-observed": {
834
+ const mutated = payload.mutated === true
835
+ if (frame !== undefined) frame.mutated = mutated
836
+ if (frame !== undefined && payload.basis === "observed" && typeof payload.mutated === "boolean") {
837
+ frame.observedMutation = payload.mutated
838
+ }
839
+ // Written for every frame, so only a frame that changed something is
840
+ // worth a note. `basis` is the body because a `declared` answer is
841
+ // paperwork and an `observed` one is a fact about the tree, and a
842
+ // reader must not have to guess which one they are holding. `paths` is
843
+ // a COUNT of what the measurement covered, not the paths themselves,
844
+ // so it is not quotable evidence and is not quoted.
845
+ if (mutated) {
846
+ note(
847
+ seq,
848
+ asString(payload.basis) === "observed" ? "good" : "warn",
849
+ "changed",
850
+ asString(payload.basis) ?? "",
851
+ []
852
+ )
853
+ }
854
+ break
855
+ }
856
+ case "control.agent.permission-required": {
857
+ if (frame !== undefined) frame.blocked = true
858
+ pin(seq, at, "permission", "warn")
859
+ break
860
+ }
861
+ case "control.agent.suspended": {
862
+ if (frame !== undefined) frame.blocked = true
863
+ break
864
+ }
865
+ case "control.agent.checkpoint-minted": {
866
+ note(seq, "good", "checkpoint", asString(payload.ref) ?? "", [])
867
+ break
868
+ }
869
+ case "control.agent.read-only-demanded": {
870
+ // The caps are this run's own armed numbers, read off the payload: a
871
+ // host overrides any of them, so a constant here would print a number
872
+ // the run never used.
873
+ const streak = asNumber(payload.streak)
874
+ const cap = asNumber(payload.cap)
875
+ const nextFrame = asNumber(payload.nextFrame)
876
+ const nextAction = asString(payload.nextAction)
877
+ note(
878
+ seq,
879
+ "warn",
880
+ "read-only",
881
+ bodyOf(
882
+ streak === undefined || cap === undefined ? undefined : `${streak} of ${cap} frames changed nothing.`,
883
+ nextFrame === undefined || nextAction === undefined ? undefined : `Frame ${nextFrame}: ${nextAction}.`
884
+ ),
885
+ []
886
+ )
887
+ pin(seq, at, "read-only", "warn")
888
+ break
889
+ }
890
+ case "control.agent.repeat-demanded": {
891
+ const spent = asNumber(payload.frames)
892
+ const cap = asNumber(payload.cap)
893
+ const nextFrame = asNumber(payload.nextFrame)
894
+ note(
895
+ seq,
896
+ "warn",
897
+ "repeat",
898
+ bodyOf(
899
+ spent === undefined || cap === undefined ? undefined : `${spent} of ${cap} frames repeated calls.`,
900
+ nextFrame === undefined ? undefined : `Frame ${nextFrame}.`
901
+ ),
902
+ []
903
+ )
904
+ pin(seq, at, "repeat", "warn")
905
+ break
906
+ }
907
+ case "control.agent.narrowed-demanded": {
908
+ const flow = asString(payload.flow)
909
+ const nextFrame = asNumber(payload.nextFrame)
910
+ note(
911
+ seq,
912
+ "bad",
913
+ "narrowed",
914
+ bodyOf(
915
+ flow === undefined ? undefined : `${flow} ran narrower than the reading it stands in for.`,
916
+ nextFrame === undefined ? undefined : `Frame ${nextFrame}.`
917
+ ),
918
+ [clip(payload.broader), clip(payload.narrower)]
919
+ )
920
+ pin(seq, at, "narrowed", "bad")
921
+ break
922
+ }
923
+ case "control.agent.unmoved-demanded": {
924
+ const nextFrame = asNumber(payload.nextFrame)
925
+ // Both digests or neither: one alone cannot tell an unmoved tree from a
926
+ // measurement that never happened, and either may be the empty string.
927
+ const opened = clip(payload.openedDigest)
928
+ const current = clip(payload.currentDigest)
929
+ note(
930
+ seq,
931
+ "bad",
932
+ "unmoved",
933
+ bodyOf(
934
+ "The tree the run opened on is the tree it closed on.",
935
+ nextFrame === undefined ? undefined : `Frame ${nextFrame}.`
936
+ ),
937
+ opened === undefined || current === undefined ? [] : [opened, current]
938
+ )
939
+ pin(seq, at, "unmoved", "bad")
940
+ break
941
+ }
942
+ case "control.agent.unresolved-demanded": {
943
+ const flow = asString(payload.flow)
944
+ const nextFrame = asNumber(payload.nextFrame)
945
+ note(
946
+ seq,
947
+ "bad",
948
+ "unresolved",
949
+ bodyOf(
950
+ flow === undefined ? undefined : `${flow} failed and was not answered.`,
951
+ nextFrame === undefined ? undefined : `Frame ${nextFrame}.`
952
+ ),
953
+ [clip(payload.failed), clip(payload.instead)]
954
+ )
955
+ pin(seq, at, "unresolved", "bad")
956
+ break
957
+ }
958
+ case "control.agent.claim-demanded": {
959
+ // Written on EVERY evaluation, and a reading that neither handed the
960
+ // completion back nor refused it cost the run nothing and changed
961
+ // nothing, so only those two are moments anyone would scrub to. A
962
+ // refusal is the larger of them: it ends the run and takes its answer.
963
+ if (payload.demanded !== true && payload.refused !== true) break
964
+ const complete = asNumber(payload.complete)
965
+ const overclaims = asNumber(payload.overclaims)
966
+ const nextFrame = asNumber(payload.nextFrame)
967
+ note(
968
+ seq,
969
+ "bad",
970
+ payload.refused === true ? "claim refused" : "claim",
971
+ bodyOf(
972
+ complete === undefined || overclaims === undefined
973
+ ? undefined
974
+ : `complete ${complete}, overclaims ${overclaims}.`,
975
+ nextFrame === undefined ? undefined : `Frame ${nextFrame}.`
976
+ ),
977
+ []
978
+ )
979
+ pin(seq, at, payload.refused === true ? "claim refused" : "claim", "bad")
980
+ break
981
+ }
982
+ /*
983
+ * The five kinds `AgentSession`'s `default` arm once journaled with
984
+ * `payload: {}`, however rich their `AgentEvent` schema was:
985
+ * cell-rejected-in-frame, read-only-demand-issued,
986
+ * narrow-only-demanded, steering-drained and sufficiency-observed.
987
+ * Their whole payload is late (`AgentSession` `lateFields`), so one
988
+ * record carries every field or none, and each arm below reads what its
989
+ * record has: a legacy record still folds, and still says nothing the
990
+ * record does not carry. A note with no body and no evidence is a title
991
+ * on its own, so a fieldless record writes none.
992
+ */
993
+ case "control.agent.cell-rejected-in-frame": {
994
+ // A refused reply is a model call the frame paid for. The code is why
995
+ // the parse refused it; the message is what the frame said back.
996
+ const attempt = asNumber(payload.attempt)
997
+ const code = asString(payload.code)
998
+ const body = bodyOf(
999
+ code === undefined ? undefined : `${code}.`,
1000
+ attempt === undefined ? undefined : `Attempt ${attempt}.`
1001
+ )
1002
+ if (body !== "") note(seq, "warn", "rejected", body, [clip(payload.message)])
1003
+ break
1004
+ }
1005
+ case "control.agent.read-only-demand-issued": {
1006
+ // The issuance's own numbers. `read-only-demanded` above carries the
1007
+ // same demand's later answer, and a run that crashes between the two
1008
+ // boundaries still has this one.
1009
+ const streak = asNumber(payload.streak)
1010
+ const cap = asNumber(payload.cap)
1011
+ const nextFrame = asNumber(payload.nextFrame)
1012
+ const body = bodyOf(
1013
+ streak === undefined || cap === undefined ? undefined : `${streak} of ${cap} frames changed nothing.`,
1014
+ nextFrame === undefined ? undefined : `Frame ${nextFrame}.`
1015
+ )
1016
+ if (body !== "") note(seq, "warn", "read-only", body, [])
1017
+ pin(seq, at, "read-only", "warn")
1018
+ break
1019
+ }
1020
+ case "control.agent.narrow-only-demanded": {
1021
+ // The sibling of `narrowed-demanded`: that one fires when a broader
1022
+ // reading exists and was not re-run, this one when none was ever
1023
+ // taken. There is no broader input to quote, so the subjects the
1024
+ // demand is about stand in its place.
1025
+ const flow = asString(payload.flow)
1026
+ const targets = Array.isArray(payload.targets)
1027
+ ? payload.targets.filter((one): one is string => typeof one === "string")
1028
+ : []
1029
+ const nextFrame = asNumber(payload.nextFrame)
1030
+ const body = bodyOf(
1031
+ flow === undefined || targets.length === 0
1032
+ ? undefined
1033
+ : `${flow} ran on ${targets.join(", ")} and nothing broader.`,
1034
+ nextFrame === undefined ? undefined : `Frame ${nextFrame}.`
1035
+ )
1036
+ if (body !== "") note(seq, "warn", "narrow-only", body, [clip(payload.check)])
1037
+ pin(seq, at, "narrow-only", "warn")
1038
+ break
1039
+ }
1040
+ case "control.agent.steering-drained": {
1041
+ // A drain is written at every frame boundary, and the queue is empty
1042
+ // at almost all of them. Only messages ON the record establish that a
1043
+ // person reached the run, so an empty drain is not a moment and a
1044
+ // legacy record — which carries no messages either way — cannot be
1045
+ // read as one. A steer too large to trace was still delivered; the
1046
+ // record says so even where its words are gone.
1047
+ const messages = Array.isArray(payload.messages) ? payload.messages : []
1048
+ if (messages.length === 0) break
1049
+ note(
1050
+ seq,
1051
+ "warn",
1052
+ "steering",
1053
+ messages.length === 1 ? "1 steer." : `${messages.length} steers.`,
1054
+ messages.map((message) => clip(asRecord(message).text))
1055
+ )
1056
+ pin(seq, at, "steering", "warn")
1057
+ break
1058
+ }
1059
+ case "control.agent.sufficiency-observed": {
1060
+ // The one observation that rewards a run rather than braking it: a
1061
+ // failing check answered by a passing one. The two inputs are the
1062
+ // whole of it, so they are quoted.
1063
+ const flow = asString(payload.flow)
1064
+ const nextFrame = asNumber(payload.nextFrame)
1065
+ const body = bodyOf(
1066
+ flow === undefined ? undefined : `${flow} failed before the change and passed after it.`,
1067
+ nextFrame === undefined ? undefined : `Frame ${nextFrame}.`
1068
+ )
1069
+ if (body !== "") note(seq, "good", "sufficiency", body, [clip(payload.failed), clip(payload.passed)])
1070
+ pin(seq, at, "sufficiency", "good")
1071
+ break
1072
+ }
1073
+ case "control.run.completed":
1074
+ case "control.run.failed":
1075
+ case "control.run.cancelled": {
1076
+ const verdict = kind.slice("control.run.".length)
1077
+ // A person's stop reads as the product word every surface uses, and
1078
+ // is never drawn as a failure.
1079
+ if (verdict === "cancelled") pin(seq, at, "stopped", "muted")
1080
+ else pin(seq, at, verdict, verdict === "completed" ? "good" : "bad")
1081
+ break
1082
+ }
1083
+ default:
1084
+ break
1085
+ }
1086
+ // After the record's own arm, so a turn-opened belongs to the frame it
1087
+ // opened: the rule `here()` already gives a note and a pin.
1088
+ owners.push({ seq, spanId: here() })
1089
+ }
1090
+ state.frame = frame
1091
+ }
1092
+
1093
+ /**
1094
+ * What {@link disciplineStep} has read so far, as the model's arrays.
1095
+ *
1096
+ * The derived rows are rebuilt from the frames; the recorded ones are copied,
1097
+ * so a model taken now never sees a record stepped after it.
1098
+ */
1099
+ const disciplineModel = (
1100
+ state: DisciplineState
1101
+ ): Pick<TraceModel, "bands" | "milestones" | "lines" | "notes" | "owners"> => {
1102
+ const { frames, lastAt } = state
1103
+ const notes = state.notes.slice()
1104
+ const milestones = state.milestones.slice()
1105
+ const owners = state.owners.slice()
1106
+
1107
+ // A measured unchanged tree is authoritative even when a write succeeded.
1108
+ const changed = frames.map((entry) => entry.observedMutation ?? (entry.mutated === true || madeAWrite(entry)))
1109
+
1110
+ // A repeated call is only evidence of a stall inside a STREAK, and only
1111
+ // across a span the workspace never moved in.
1112
+ //
1113
+ // Re-running one check after an edit is the OPPOSITE of a stall: it is the
1114
+ // answer `control.agent.sufficiency-observed` rewards. A rule that flagged
1115
+ // any second occurrence of a call would mark that good frame and a looping
1116
+ // one identically, and a rule that only checked THIS frame for a change
1117
+ // would still flag it, because the edit it answers happened in the frame
1118
+ // before. So a change resets what a first occurrence is: a call is judged
1119
+ // against the last time it ran on THIS tree, never against a run from before
1120
+ // an edit landed. On top of that a repeat counts only where the frames beside
1121
+ // it repeat too — each of them recording no change of its own — and
1122
+ // `repeatOf` is set only inside a streak of two or more such frames. Every
1123
+ // substantive call must repeat a settled reading; a fresh call, new result,
1124
+ // or pending result means this frame has not been shown to be redundant.
1125
+ const firstSeen = new Map<string, number>()
1126
+ const repeatedFrom = new Map<CallFacts, number>()
1127
+ const redundant: Array<boolean> = []
1128
+ frames.forEach((entry, index) => {
1129
+ const calls = entry.calls.filter((call) => call.flowName !== CHECKPOINT_FLOW)
1130
+ for (const call of calls) {
1131
+ if (call.settled === undefined) continue
1132
+ // A call settles once, so its reading is built once, not per model taken.
1133
+ call.reading ??= `${call.signature} ${JSON.stringify(call.settled)}`
1134
+ const reading = call.reading
1135
+ const seen = firstSeen.get(reading)
1136
+ if (seen === undefined) firstSeen.set(reading, entry.frame)
1137
+ else if (seen < entry.frame) repeatedFrom.set(call, seen)
1138
+ }
1139
+ redundant.push(calls.length > 0 && calls.every((call) => repeatedFrom.has(call)))
1140
+ // The tree moved in this frame, so every call recorded before it ran
1141
+ // against a tree that no longer exists and none of them can be repeated.
1142
+ if (changed[index] === true) firstSeen.clear()
1143
+ })
1144
+ const stalling = frames.map((_frame, index) => redundant[index] === true && changed[index] !== true)
1145
+ const stuck = stalling.map((flag, index) => flag && (stalling[index - 1] === true || stalling[index + 1] === true))
1146
+
1147
+ // Phase, most specific rule first. The last rule is the honesty one: every
1148
+ // other word is a claim about what the frame was doing, and `researching` is
1149
+ // as much of a claim as the rest. A frame this fold cannot name one call for
1150
+ // is a frame it declines to write a line about, so it declines to name a
1151
+ // phase for it too.
1152
+ const phases = frames.map((entry, index): PhaseId =>
1153
+ entry.blocked
1154
+ ? "blocked"
1155
+ : stuck[index] === true
1156
+ ? "stuck"
1157
+ : changed[index] === true || entry.calls.some((call) => call.activity === "writes")
1158
+ ? "implementing"
1159
+ : entry.calls.some((call) => call.activity === "checks" || call.activity === "tests")
1160
+ ? "testing"
1161
+ : entry.calls.some((call) => call.activity === "reads")
1162
+ ? "researching"
1163
+ : "unrecorded"
1164
+ )
1165
+
1166
+ const bands: Array<PhaseBand> = []
1167
+ frames.forEach((entry, index) => {
1168
+ const phase = phases[index]!
1169
+ // A frame's band ends where the next frame opens, and the last one ends
1170
+ // where the journal does: the rule the tree's own frames close by.
1171
+ const endedAt = frames[index + 1]?.startedAt ?? Math.max(entry.endedAt, lastAt)
1172
+ const last = bands[bands.length - 1]
1173
+ if (last !== undefined && last.phase === phase) {
1174
+ bands[bands.length - 1] = { ...last, endedAt, frames: [...last.frames, entry.id] }
1175
+ return
1176
+ }
1177
+ bands.push({ phase, startedAt: entry.startedAt, endedAt, frames: [entry.id], seq: entry.seq })
1178
+ })
1179
+
1180
+ const lines = frames.flatMap((entry, index): ReadonlyArray<FrameLine> => {
1181
+ const call = dominantCall(entry)
1182
+ // A frame that called nothing has no line. Absence is absence.
1183
+ if (call === undefined) return []
1184
+ const repeatOf = stuck[index] === true ? repeatedFrom.get(call) : undefined
1185
+ const held = state.lineOf.get(entry)
1186
+ if (held !== undefined && held.call === call && held.settled === call.settled && held.repeatOf === repeatOf) {
1187
+ return [held.line]
1188
+ }
1189
+ const outcome = call.settled === undefined
1190
+ ? "pending"
1191
+ : call.settled.failed || call.settled.denied
1192
+ ? "failure"
1193
+ : "success"
1194
+ const line: FrameLine = {
1195
+ spanId: entry.id,
1196
+ frame: entry.frame,
1197
+ verb: call.presentation?.verb[outcome] ??
1198
+ `${call.flowName}${outcome === "pending" ? " pending" : outcome === "failure" ? " failed" : ""}`,
1199
+ subject: callSubject(call.input, call.presentation?.subject),
1200
+ // Read supported output fields through the descriptor's presentation.
1201
+ // The selected call retains the raw value, including unsupported shapes.
1202
+ result: call.settled?.failed === true
1203
+ ? clip(call.settled.result) ?? ""
1204
+ : resultOf(call.settled?.value, call.presentation?.result),
1205
+ failed: call.settled?.failed === true,
1206
+ wrote: call.activity === "writes" && call.settled?.failed === false && !call.settled.denied,
1207
+ ...(repeatOf === undefined ? {} : { repeatOf })
1208
+ }
1209
+ state.lineOf.set(entry, { call, settled: call.settled, repeatOf, line })
1210
+ return [line]
1211
+ })
1212
+
1213
+ return { bands, milestones, lines, notes, owners }
1214
+ }
1215
+
1216
+ /** The run a fold belongs to; its status is read when a model is taken. */
1217
+ type TraceIdentity = Omit<TraceRun, "status">
1218
+
1219
+ /**
1220
+ * One dispatch's span fold, advanced one record at a time.
1221
+ *
1222
+ * Every field is what {@link foldStep} has read so far. The builders are live:
1223
+ * a model is taken by freezing them, never by handing them out.
1224
+ */
1225
+ interface FoldState {
1226
+ readonly run: TraceIdentity
1227
+ readonly root: Builder
1228
+ frame: Builder | undefined
1229
+ cell: Builder | undefined
1230
+ frames: number
1231
+ calls: number
1232
+ seat: string | undefined
1233
+ lastAt: number
1234
+ /** Records stepped after deduplication: `ordered.length`. */
1235
+ count: number
1236
+ /** The journal's own verdict, from a `control.run.*` record. */
1237
+ verdict: string | undefined
1238
+ readonly openCalls: Array<{ readonly flowName: string; readonly callId?: string | undefined; readonly span: Builder }>
1239
+ readonly approvals: Map<string, Builder>
1240
+ readonly discipline: DisciplineState
1241
+ /** The native rows of this fold's own input, in order. */
1242
+ readonly engine: Array<JournalRecord>
1243
+ /** {@link engineTraceFromJournal} of `engine`, until another row arrives. */
1244
+ engineTrace: ReadonlyArray<Builder> | undefined
1245
+ /** The span each builder last froze to. */
1246
+ readonly frozen: WeakMap<Builder, TraceSpan>
1247
+ /**
1248
+ * The run's direct children a step may have changed since the last model.
1249
+ * Every other child is a closed subtree, and its frozen span is reused as is.
1250
+ */
1251
+ readonly dirty: Set<Builder>
1252
+ /** The run's direct child a call or approval span sits under, for {@link dirty}. */
1253
+ readonly topOf: WeakMap<Builder, Builder>
1254
+ }
1255
+
1256
+ const isEngineRow = (record: JournalRecord): boolean =>
1257
+ record.kind === "control.engine.event" || record.kind === "control.engine.projection-gap"
1258
+
1259
+ const foldInit = (run: TraceIdentity): FoldState => ({
1260
+ run,
1261
+ root: builder(`run:${run.runId}`, "run", `run ${run.runId} · ${run.flowId}`, "running", 0, {
1262
+ ...(run.kind === undefined ? {} : { fields: { kind: run.kind } })
1263
+ }),
1264
+ frame: undefined,
1265
+ cell: undefined,
1266
+ frames: 0,
1267
+ calls: 0,
1268
+ seat: undefined,
1269
+ lastAt: 0,
1270
+ count: 0,
1271
+ verdict: undefined,
1272
+ openCalls: [],
1273
+ approvals: new Map(),
1274
+ discipline: disciplineInit(run.runId),
1275
+ engine: [],
1276
+ engineTrace: undefined,
1277
+ frozen: new WeakMap(),
1278
+ dirty: new Set(),
1279
+ topOf: new WeakMap()
1280
+ })
1281
+
1282
+ /**
1283
+ * Folds one record, in sequence order, into a dispatch's trace.
1284
+ *
1285
+ * Frames open on `turn-opened` and close when the next opens or the run ends;
1286
+ * cells open on `cell-produced` and close on `cell-settled`; calls open on
1287
+ * `cell-call-started` under the open cell (or the frame, when a call is
1288
+ * journaled outside a cell) and settle by stable call identity the way the gateway's own
1289
+ * `run-tree` pairs them, so a call's `call-N` id is the node id the
1290
+ * `node-output` projection knows it by. A journal with no records yields the
1291
+ * run root alone, wearing the run's status: the honest empty trace.
1292
+ *
1293
+ * The same records fold a second time, into what a person would say the run
1294
+ * was doing: see {@link disciplineStep}.
1295
+ */
1296
+ const foldStep = (state: FoldState, record: JournalRecord): void => {
1297
+ if (isEngineRow(record)) {
1298
+ state.engine.push(record)
1299
+ state.engineTrace = undefined
1300
+ }
1301
+ // route receives only the records kept by the shared call-event filter.
1302
+ const { root, openCalls, approvals, dirty, topOf } = state
1303
+ let { frame, cell, frames, calls, seat } = state
1304
+ const top = frame ?? cell
1305
+ if (top !== undefined) dirty.add(top)
1306
+ /** Marks the run's child that `span` sits under as changed. */
1307
+ const touch = (span: Builder): void => {
1308
+ // Both callers retrieve spans registered in topOf when they were opened.
1309
+ dirty.add(topOf.get(span)!)
1310
+ }
1311
+ /** Where a new span attaches: the open cell, else the open frame, else the run. */
1312
+ const parent = (): Builder => cell ?? frame ?? root
1313
+ const closeCell = (at: number, status: SpanStatus): void => {
1314
+ if (cell === undefined) return
1315
+ cell.endedAt = at
1316
+ cell.status = status
1317
+ cell = undefined
1318
+ }
1319
+ const closeFrame = (at: number): void => {
1320
+ if (frame === undefined) return
1321
+ closeCell(at, cell?.children.some((child) => child.status === "failed") === true ? "failed" : "completed")
1322
+ frame.endedAt = at
1323
+ frame.status = frame.children.some((child) => child.status === "failed") ? "failed" : "completed"
1324
+ frame = undefined
1325
+ }
1326
+
1327
+ {
1328
+ const kind = record.kind ?? ""
1329
+ const payload = asRecord(record.payload)
1330
+ const at = timeOf(record, payload)
1331
+ if (state.count === 0) {
1332
+ root.startedAt = at
1333
+ state.lastAt = at
1334
+ } else state.lastAt = Math.max(state.lastAt, at)
1335
+ state.count += 1
1336
+ const opened = { sequence: record.sequence, event: kind }
1337
+ switch (kind) {
1338
+ case "control.engine.event":
1339
+ case "control.engine.projection-gap":
1340
+ case "control.engine.projection-started":
1341
+ case "control.engine.projection-settled":
1342
+ // Native records have their own identities and lifecycle. Never put
1343
+ // them under whichever agent frame happened to be open at ingestion.
1344
+ break
1345
+ case "control.agent.turn-opened": {
1346
+ closeFrame(at)
1347
+ frames += 1
1348
+ seat = asString(payload.seat) ?? seat
1349
+ frame = builder(
1350
+ `frame-${frames}`,
1351
+ "frame",
1352
+ `frame ${frames}${seat === undefined ? "" : ` · ${seat}`}`,
1353
+ "running",
1354
+ at,
1355
+ {
1356
+ ...opened,
1357
+ ...(seat === undefined ? {} : { seat }),
1358
+ fields: restOf(payload, ["seat"])
1359
+ }
1360
+ )
1361
+ root.children.push(frame)
1362
+ break
1363
+ }
1364
+ case "control.agent.model-settled": {
1365
+ const usage = asRecord(payload.usage)
1366
+ const duration = asNumber(payload.durationMillis)
1367
+ const span = builder(
1368
+ `model-${record.sequence ?? at}`,
1369
+ "model",
1370
+ "model",
1371
+ "completed",
1372
+ duration === undefined ? at : at - duration,
1373
+ {
1374
+ ...opened,
1375
+ ...(seat === undefined ? {} : { seat }),
1376
+ output: textOf(payload.text),
1377
+ usage: { inputTokens: asNumber(usage.inputTokens), outputTokens: asNumber(usage.outputTokens) },
1378
+ fields: restOf(payload, ["text", "usage", "durationMillis"])
1379
+ }
1380
+ )
1381
+ span.endedAt = at
1382
+ ;(frame ?? root).children.push(span)
1383
+ break
1384
+ }
1385
+ case "control.agent.cell-produced": {
1386
+ closeCell(at, "completed")
1387
+ const language = asString(payload.language)
1388
+ cell = builder(
1389
+ `cell-${record.sequence ?? at}`,
1390
+ "cell",
1391
+ `cell${language === undefined ? "" : ` · ${language}`}`,
1392
+ "running",
1393
+ at,
1394
+ {
1395
+ ...opened,
1396
+ source: asString(payload.text),
1397
+ fields: restOf(payload, ["text", "language"])
1398
+ }
1399
+ )
1400
+ ;(frame ?? root).children.push(cell)
1401
+ break
1402
+ }
1403
+ case "control.agent.cell-call-started": {
1404
+ calls += 1
1405
+ const recordedName = asString(payload.flowName)
1406
+ const flowName = recordedName ?? `call-${calls}`
1407
+ const span = builder(`call-${calls}`, "call", inspectLabel(flowName), "running", at, {
1408
+ ...opened,
1409
+ ...(recordedName === undefined ? {} : { flowName: recordedName }),
1410
+ input: payload.input,
1411
+ fields: restOf(payload, ["flowName", "input"])
1412
+ })
1413
+ openCalls.push({ flowName, callId: asString(payload.callId), span })
1414
+ topOf.set(span, frame ?? cell ?? span)
1415
+ parent().children.push(span)
1416
+ break
1417
+ }
1418
+ case "control.agent.cell-call-settled": {
1419
+ const index = openCallIndex(openCalls, asString(payload.callId), asString(payload.flowName))
1420
+ const settled = index < 0 ? undefined : openCalls.splice(index, 1)[0]
1421
+ if (settled === undefined) break
1422
+ const failed = asString(payload.outcome) === "failure"
1423
+ touch(settled.span)
1424
+ settled.span.endedAt = at
1425
+ settled.span.status = failed ? "failed" : "completed"
1426
+ settled.span.detail = {
1427
+ ...settled.span.detail,
1428
+ ...(failed ? { message: textOf(payload.message) } : { output: textOf(payload.value) }),
1429
+ ...(!failed && settled.flowName === "agent/spawn" && asString(asRecord(payload.value).child) !== undefined
1430
+ ? { childRunId: asString(asRecord(payload.value).child) }
1431
+ : {})
1432
+ }
1433
+ break
1434
+ }
1435
+ case "control.agent.cell-printed": {
1436
+ if (cell === undefined) {
1437
+ parent().children.push(
1438
+ builder(`printed-${record.sequence ?? at}`, "event", "printed", "completed", at, {
1439
+ ...opened,
1440
+ printed: textOf(payload.text)
1441
+ })
1442
+ )
1443
+ break
1444
+ }
1445
+ cell.detail = {
1446
+ ...cell.detail,
1447
+ printed: [cell.detail.printed, textOf(payload.text)].filter((text) => text !== undefined).join("\n")
1448
+ }
1449
+ break
1450
+ }
1451
+ case "control.agent.cell-settled": {
1452
+ closeCell(at, asString(payload.outcome) === "failure" ? "failed" : "completed")
1453
+ break
1454
+ }
1455
+ case "control.agent.resolved": {
1456
+ const span = builder(`resolved-${record.sequence ?? at}`, "resolved", "resolved", "completed", at, {
1457
+ ...opened,
1458
+ output: textOf(payload.text)
1459
+ })
1460
+ span.endedAt = at
1461
+ ;(frame ?? root).children.push(span)
1462
+ break
1463
+ }
1464
+ case "control.approval.requested": {
1465
+ const requestId = asString(payload.requestId) ?? `approval-${record.sequence ?? at}`
1466
+ const question = asString(payload.question)
1467
+ const span = builder(
1468
+ `approval-${requestId}`,
1469
+ "approval",
1470
+ `approval${question === undefined ? "" : ` · ${question}`}`,
1471
+ "waiting",
1472
+ at,
1473
+ {
1474
+ ...opened,
1475
+ fields: restOf(payload, ["question", "requestId", "payload", "runId"])
1476
+ }
1477
+ )
1478
+ approvals.set(requestId, span)
1479
+ topOf.set(span, frame ?? span)
1480
+ ;(frame ?? root).children.push(span)
1481
+ break
1482
+ }
1483
+ case "control.approval.approved":
1484
+ case "control.approval.denied": {
1485
+ const decided = kind === "control.approval.approved" ? "approved" : "denied"
1486
+ const key = asString(payload.tokenId) ?? asString(payload.requestId)
1487
+ const span = key === undefined
1488
+ ? [...approvals.values()].find((entry) => entry.status === "waiting")
1489
+ : approvals.get(key)
1490
+ if (span === undefined) break
1491
+ touch(span)
1492
+ span.status = decided
1493
+ span.endedAt = at
1494
+ break
1495
+ }
1496
+ case "control.run.completed":
1497
+ case "control.run.failed":
1498
+ case "control.run.cancelled": {
1499
+ closeFrame(at)
1500
+ root.endedAt = at
1501
+ // The journal's own verdict outranks the card's phase word: a scrub to this record shows the run settled.
1502
+ state.verdict = kind.slice("control.run.".length)
1503
+ break
1504
+ }
1505
+ default: {
1506
+ if (!kind.startsWith("control.")) break
1507
+ const span = builder(
1508
+ `event-${record.sequence ?? at}`,
1509
+ "event",
1510
+ kind.slice("control.".length),
1511
+ "completed",
1512
+ at,
1513
+ {
1514
+ ...opened,
1515
+ fields: restOf(payload, [])
1516
+ }
1517
+ )
1518
+ span.endedAt = at
1519
+ parent().children.push(span)
1520
+ if (kind === "control.agent.turn-closed" && payload.step !== undefined) {
1521
+ const closed = frame
1522
+ closeFrame(at)
1523
+ if (closed !== undefined && payload.outcome === "suspended") closed.status = "waiting"
1524
+ if (closed !== undefined && payload.outcome === "aborted") closed.status = "cancelled"
1525
+ }
1526
+ }
1527
+ }
1528
+ }
1529
+ Object.assign(state, { frame, cell, frames, calls, seat })
1530
+ const after = frame ?? cell
1531
+ if (after !== undefined) dirty.add(after)
1532
+ disciplineStep(state.discipline, record)
1533
+ }
1534
+
1535
+ /**
1536
+ * The trace {@link foldStep} has read so far, as the run with `status` would
1537
+ * show it.
1538
+ *
1539
+ * Nothing live is mutated. A settled run closes its open frame where the
1540
+ * journal ends, so that close is applied to the frozen copy only.
1541
+ */
1542
+ const foldModel = (
1543
+ state: FoldState,
1544
+ status: string,
1545
+ journal: ReadonlyArray<JournalRecord>,
1546
+ engine?: ReadonlyArray<Builder>
1547
+ ): TraceModel => {
1548
+ const { root, frame, cell, lastAt } = state
1549
+ const closed = new Map<Builder, { readonly status: SpanStatus; readonly endedAt: number }>()
1550
+ const statusOf = (node: Builder): SpanStatus => closed.get(node)?.status ?? node.status
1551
+ // A settled run leaves no frame open: the last frame ends where the journal does.
1552
+ const terminal = TERMINAL_RUN.has(status)
1553
+ if (terminal && frame !== undefined) {
1554
+ if (cell !== undefined) {
1555
+ closed.set(cell, {
1556
+ status: cell.children.some((child) => child.status === "failed") ? "failed" : "completed",
1557
+ endedAt: lastAt
1558
+ })
1559
+ }
1560
+ closed.set(frame, {
1561
+ status: frame.children.some((child) => statusOf(child) === "failed") ? "failed" : "completed",
1562
+ endedAt: lastAt
1563
+ })
1564
+ }
1565
+ if (engine === undefined && state.engineTrace === undefined) state.engineTrace = engineTraceFromJournal(state.engine)
1566
+ const children = [...root.children, ...(engine ?? state.engineTrace!)]
1567
+ closed.set(root, {
1568
+ status: state.verdict ?? status,
1569
+ endedAt: root.endedAt ?? (terminal && state.count > 0 ? lastAt : Number.NaN)
1570
+ })
1571
+
1572
+ const freeze = (node: Builder, depth: number, members: ReadonlyArray<Builder>): TraceSpan =>
1573
+ freezeWith(node, depth, members.map((child) => freeze(child, depth + 1, child.children)))
1574
+ const freezeWith = (node: Builder, depth: number, frozenChildren: ReadonlyArray<TraceSpan>): TraceSpan => {
1575
+ const override = closed.get(node)
1576
+ const nodeStatus = override?.status ?? node.status
1577
+ const endedAt = override === undefined || Number.isNaN(override.endedAt) ? node.endedAt : override.endedAt
1578
+ const held = state.frozen.get(node)
1579
+ if (
1580
+ held !== undefined && held.depth === depth && held.label === node.label && held.status === nodeStatus &&
1581
+ held.startedAt === node.startedAt && held.endedAt === endedAt && held.detail === node.detail &&
1582
+ held.children.length === frozenChildren.length &&
1583
+ held.children.every((child, index) => child === frozenChildren[index])
1584
+ ) return held
1585
+ const span: TraceSpan = {
1586
+ id: node.id,
1587
+ kind: node.kind,
1588
+ label: node.label,
1589
+ status: nodeStatus,
1590
+ startedAt: node.startedAt,
1591
+ ...(endedAt === undefined ? {} : { endedAt }),
1592
+ depth,
1593
+ children: frozenChildren,
1594
+ detail: node.detail
1595
+ }
1596
+ state.frozen.set(node, span)
1597
+ return span
1598
+ }
1599
+ // A child no step touched since the last model is a closed subtree: its
1600
+ // frozen span stands, and nothing under it is walked again.
1601
+ const settledChild = (child: Builder): TraceSpan | undefined => {
1602
+ if (state.dirty.has(child) || closed.has(child)) return undefined
1603
+ const held = state.frozen.get(child)
1604
+ return held?.depth === 1 ? held : undefined
1605
+ }
1606
+ const frozenChildren = children.map((child) => settledChild(child) ?? freeze(child, 1, child.children))
1607
+ state.dirty.clear()
1608
+ const frozenRoot = freezeWith(root, 0, frozenChildren)
1609
+ const rows: Array<TraceSpan> = []
1610
+ const walk = (span: TraceSpan): void => {
1611
+ rows.push(span)
1612
+ for (const child of span.children) walk(child)
1613
+ }
1614
+ walk(frozenRoot)
1615
+ let start = Number.POSITIVE_INFINITY
1616
+ let end = 0
1617
+ let running = 0
1618
+ let failed = 0
1619
+ for (const span of rows) {
1620
+ start = Math.min(start, span.startedAt)
1621
+ end = Math.max(end, span.endedAt ?? span.startedAt)
1622
+ if (span.kind !== "run" && span.status === "running") running += 1
1623
+ if (span.kind !== "run" && span.status === "failed") failed += 1
1624
+ }
1625
+ const extent = state.count === 0 || !Number.isFinite(start)
1626
+ ? { start: 0, end: 0 }
1627
+ : { start, end: Math.max(end, start) }
1628
+ return {
1629
+ journal,
1630
+ root: frozenRoot,
1631
+ rows,
1632
+ extent,
1633
+ counts: { spans: rows.length - 1, running, failed },
1634
+ ...disciplineModel(state.discipline)
1635
+ }
1636
+ }
1637
+
1638
+ /**
1639
+ * A run's trace fold, advanced one record at a time.
1640
+ *
1641
+ * {@link traceFromJournal} is {@link traceFold} over the whole journal and then
1642
+ * {@link traceFoldModel}; a live reader keeps the fold and hands it each new
1643
+ * record with {@link traceFoldStep}, so a record costs its own work rather than
1644
+ * a refold of the journal before it. The fold is mutable: stepping it consumes
1645
+ * the value it was, and a reader that needs an older trace takes a model first.
1646
+ *
1647
+ * @category models
1648
+ * @since 1.0.0
1649
+ */
1650
+ export interface TraceFold {
1651
+ readonly run: Omit<TraceRun, "status">
1652
+ /** Every record stepped, in sequence order: the model's `journal`. */
1653
+ readonly journal: ReadonlyArray<JournalRecord>
1654
+ }
1655
+
1656
+ interface FoldTop extends TraceFold {
1657
+ /** Records as they arrived, so a refold sorts exactly what the batch sorts. */
1658
+ arrived: Array<JournalRecord>
1659
+ journal: Array<JournalRecord>
1660
+ filter: CallEventFilter<JournalRecord>
1661
+ base: FoldState
1662
+ groups: Map<string, FoldState>
1663
+ /** The native rows of the raw journal, read while no record names a step. */
1664
+ engine: Array<JournalRecord>
1665
+ engineTrace: ReadonlyArray<Builder> | undefined
1666
+ firstAt: number
1667
+ terminal: JournalRecord | undefined
1668
+ /**
1669
+ * The records {@link traceFoldSync} last consumed, copied so a caller that
1670
+ * grows or rewrites its own array in place is still compared to what was read.
1671
+ */
1672
+ source: ReadonlyArray<JournalRecord> | undefined
1673
+ memo: { readonly status: string; readonly model: TraceModel } | undefined
1674
+ }
1675
+
1676
+ const sequenceOf = (record: JournalRecord): number => record.sequence ?? 0
1677
+
1678
+ const TERMINAL_KINDS: ReadonlySet<string> = new Set([
1679
+ "control.run.completed",
1680
+ "control.run.failed",
1681
+ "control.run.cancelled"
1682
+ ])
1683
+
1684
+ /** One deduplicated record to the fold of the dispatch it was recorded under. */
1685
+ const route = (state: FoldTop, record: JournalRecord): void => {
1686
+ // A run verdict stamped with another run's id (a child's, riding in this
1687
+ // journal) must not settle this run's trace or pin its milestone.
1688
+ if (
1689
+ record.kind?.startsWith("control.run.") === true && record.runId !== undefined && record.runId !== state.run.runId
1690
+ ) {
1691
+ return
1692
+ }
1693
+ state.firstAt = Math.min(state.firstAt, timeOf(record, asRecord(record.payload)))
1694
+ if (TERMINAL_KINDS.has(record.kind ?? "")) state.terminal = record
1695
+ const scope = callScope(record)
1696
+ if (scope === undefined) return foldStep(state.base, record)
1697
+ let group = state.groups.get(scope)
1698
+ if (group === undefined) {
1699
+ group = foldInit(state.run)
1700
+ state.groups.set(scope, group)
1701
+ }
1702
+ foldStep(group, record)
1703
+ }
1704
+
1705
+ /** Refolds `arrived` from nothing: the batch rules, exactly. */
1706
+ const reseed = (state: FoldTop, arrived: Array<JournalRecord>): void => {
1707
+ const journal = [...arrived].sort((left, right) => sequenceOf(left) - sequenceOf(right))
1708
+ const filter = callEventFilter<JournalRecord>()
1709
+ for (const record of journal) filter.push(record)
1710
+ Object.assign(state, {
1711
+ arrived,
1712
+ journal,
1713
+ filter,
1714
+ base: foldInit(state.run),
1715
+ groups: new Map(),
1716
+ engine: journal.filter(isEngineRow),
1717
+ engineTrace: undefined,
1718
+ firstAt: Number.POSITIVE_INFINITY,
1719
+ terminal: undefined,
1720
+ memo: undefined
1721
+ })
1722
+ for (const record of uniqueCallEvents(journal)) route(state, record)
1723
+ }
1724
+
1725
+ /**
1726
+ * Folds `records` into a fold that later records can be stepped onto.
1727
+ *
1728
+ * @param run the run the journal belongs to
1729
+ * @param records its journal so far, in any order
1730
+ *
1731
+ * @category constructors
1732
+ * @since 1.0.0
1733
+ */
1734
+ export const traceFold = (run: Omit<TraceRun, "status">, records: ReadonlyArray<JournalRecord> = []): TraceFold => {
1735
+ const state = {
1736
+ run: { runId: run.runId, flowId: run.flowId, ...(run.kind === undefined ? {} : { kind: run.kind }) }
1737
+ } as FoldTop
1738
+ state.source = undefined
1739
+ reseed(state, [...records])
1740
+ return state
1741
+ }
1742
+
1743
+ /**
1744
+ * Folds one more record onto `fold`, which it returns.
1745
+ *
1746
+ * A record in sequence order costs its own work. One that arrives before a
1747
+ * sequence already stepped, or a native fact that supersedes telemetry the
1748
+ * fold already placed, refolds the journal, because the batch rules move an
1749
+ * earlier record for both.
1750
+ *
1751
+ * @param fold the fold; consumed
1752
+ * @param record the next journal record
1753
+ *
1754
+ * @category combinators
1755
+ * @since 1.0.0
1756
+ */
1757
+ export const traceFoldStep = (fold: TraceFold, record: JournalRecord): TraceFold => {
1758
+ const state = fold as FoldTop
1759
+ state.memo = undefined
1760
+ state.arrived.push(record)
1761
+ const last = state.journal.at(-1)
1762
+ if (last !== undefined && !(sequenceOf(record) >= sequenceOf(last))) {
1763
+ reseed(state, state.arrived)
1764
+ return fold
1765
+ }
1766
+ state.journal.push(record)
1767
+ if (isEngineRow(record)) {
1768
+ state.engine.push(record)
1769
+ state.engineTrace = undefined
1770
+ }
1771
+ const verdict = state.filter.push(record)
1772
+ if (verdict._tag === "rewrite") reseed(state, state.arrived)
1773
+ else if (verdict._tag === "keep") route(state, verdict.event)
1774
+ return fold
1775
+ }
1776
+
1777
+ /**
1778
+ * Brings `fold` up to `records`, stepping only what it has not read.
1779
+ *
1780
+ * `records` extends the array the fold last read when every earlier element is
1781
+ * the same object; anything else, or a different run, starts a new fold. The
1782
+ * comparison is against the records the fold consumed, not the caller's array,
1783
+ * so growing or rewriting that same array in place is seen. A reader that
1784
+ * re-renders one growing journal therefore pays per new record.
1785
+ *
1786
+ * @param fold the fold the reader holds, if any; consumed
1787
+ * @param run the run the journal belongs to
1788
+ * @param records the whole journal as the reader now holds it
1789
+ *
1790
+ * @category combinators
1791
+ * @since 1.0.0
1792
+ */
1793
+ export const traceFoldSync = (
1794
+ fold: TraceFold | undefined,
1795
+ run: Omit<TraceRun, "status">,
1796
+ records: ReadonlyArray<JournalRecord>
1797
+ ): TraceFold => {
1798
+ const held = fold as FoldTop | undefined
1799
+ const source = held?.source
1800
+ const extends_ = held !== undefined && source !== undefined && held.run.runId === run.runId &&
1801
+ held.run.flowId === run.flowId && held.run.kind === run.kind && records.length >= source.length &&
1802
+ source.every((record, index) => records[index] === record)
1803
+ const next = extends_ ? held : traceFold(run) as FoldTop
1804
+ for (let index = extends_ ? source.length : 0; index < records.length; index++) traceFoldStep(next, records[index]!)
1805
+ next.source = records.slice()
1806
+ return next
1807
+ }
1808
+
1809
+ /**
1810
+ * The trace a fold has read, as the run with `status` shows it.
1811
+ *
1812
+ * Taken again without a step between, it is the same object.
1813
+ *
1814
+ * @param fold the fold
1815
+ * @param status the run card's phase word
1816
+ *
1817
+ * @category utilities
1818
+ * @since 1.0.0
1819
+ */
1820
+ export const traceFoldModel = (fold: TraceFold, status: string): TraceModel => {
1821
+ const state = fold as FoldTop
1822
+ if (state.memo?.status === status) return state.memo.model
1823
+ const model = mergeModel(state, status)
1824
+ state.memo = { status, model }
1825
+ return model
1826
+ }
1827
+
1828
+ const mergeModel = (state: FoldTop, status: string): TraceModel => {
1829
+ const journal = state.journal.slice()
1830
+ if (state.groups.size === 0) {
1831
+ if (state.engineTrace === undefined) state.engineTrace = engineTraceFromJournal(state.engine)
1832
+ return foldModel(state.base, status, journal, state.engineTrace)
1833
+ }
1834
+ const base = foldModel(state.base, status, journal)
1835
+ const children = [...base.root.children]
1836
+ const bands = [...base.bands]
1837
+ const milestones = [...base.milestones]
1838
+ const lines = [...base.lines]
1839
+ const notes = [...base.notes]
1840
+ const owners = [...base.owners]
1841
+ const terminal = state.terminal
1842
+ const terminalAt = terminal === undefined ? undefined : timeOf(terminal, asRecord(terminal.payload))
1843
+ for (const [scope, group] of state.groups) {
1844
+ const prefix = `step:${encodeURIComponent(scope)}/`
1845
+ const scoped = foldModel(group, "running", journal)
1846
+ const rename = (span: TraceSpan): TraceSpan => ({
1847
+ ...span,
1848
+ id: `${prefix}${span.id}`,
1849
+ children: span.children.map(rename),
1850
+ ...(span.status === "running" && TERMINAL_RUN.has(base.root.status) ?
1851
+ {
1852
+ // A terminal run proves work stopped. It does not prove which open
1853
+ // step succeeded or caused the run to fail.
1854
+ status: "stopped",
1855
+ ...(terminalAt === undefined ? {} : { endedAt: terminalAt })
1856
+ } :
1857
+ {})
1858
+ })
1859
+ children.push(...scoped.root.children.map(rename))
1860
+ bands.push(...scoped.bands.map((band) => ({ ...band, frames: band.frames.map((id) => `${prefix}${id}`) })))
1861
+ // A moment, a line, a note and a position all name the frame they were
1862
+ // recorded under, so the merge renames each of them the same way: the
1863
+ // step's own frame, or this run's root for what the step recorded outside
1864
+ // any frame.
1865
+ const owned = (spanId: string): string => spanId === scoped.root.id ? base.root.id : `${prefix}${spanId}`
1866
+ milestones.push(...scoped.milestones.map((one) => ({ ...one, spanId: owned(one.spanId) })))
1867
+ lines.push(...scoped.lines.map((line) => ({ ...line, spanId: `${prefix}${line.spanId}` })))
1868
+ notes.push(...scoped.notes.map((note) => ({ ...note, spanId: owned(note.spanId) })))
1869
+ owners.push(...scoped.owners.map((one) => ({ ...one, spanId: owned(one.spanId) })))
1870
+ }
1871
+ children.sort((left, right) =>
1872
+ left.startedAt - right.startedAt || (left.detail.sequence ?? 0) - (right.detail.sequence ?? 0)
1873
+ )
1874
+ const root = { ...base.root, startedAt: state.firstAt, children }
1875
+ const rows: Array<TraceSpan> = []
1876
+ const walk = (span: TraceSpan): void => {
1877
+ rows.push(span)
1878
+ span.children.forEach(walk)
1879
+ }
1880
+ walk(root)
1881
+ const positions = new Map(rows.map((span, index) => [span.id, index]))
1882
+ // Each step folds its own frames from 1, so a merged run would print two
1883
+ // rows numbered 1 and a `same as 1` that names either. The merged list
1884
+ // numbers the rows a person reads, and a repeat names the renumbered row of
1885
+ // the step that recorded it.
1886
+ const merged = lines.sort((left, right) => positions.get(left.spanId)! - positions.get(right.spanId)!)
1887
+ const stepOf = (spanId: string): string => spanId.slice(0, spanId.lastIndexOf("/") + 1)
1888
+ const renumbered = new Map(merged.map((line, index) => [`${stepOf(line.spanId)}${line.frame}`, index + 1]))
1889
+ let start = Number.POSITIVE_INFINITY
1890
+ let end = Number.NEGATIVE_INFINITY
1891
+ for (const span of rows) {
1892
+ start = Math.min(start, span.startedAt)
1893
+ end = Math.max(end, span.endedAt ?? span.startedAt)
1894
+ }
1895
+ return {
1896
+ journal,
1897
+ root,
1898
+ rows,
1899
+ extent: { start, end },
1900
+ counts: {
1901
+ spans: rows.length - 1,
1902
+ running: rows.filter((span) => span.kind !== "run" && span.status === "running").length,
1903
+ failed: rows.filter((span) => span.kind !== "run" && span.status === "failed").length
1904
+ },
1905
+ bands: bands.sort((left, right) => left.startedAt - right.startedAt || left.seq - right.seq),
1906
+ milestones: milestones.sort((left, right) => left.seq - right.seq),
1907
+ lines: merged.map((line, index) => ({
1908
+ ...line,
1909
+ frame: index + 1,
1910
+ ...(line.repeatOf === undefined
1911
+ ? {}
1912
+ : { repeatOf: renumbered.get(`${stepOf(line.spanId)}${line.repeatOf}`)! })
1913
+ })),
1914
+ notes: notes.sort((left, right) => left.seq - right.seq),
1915
+ owners: owners.sort((left, right) => left.seq - right.seq)
1916
+ }
1917
+ }
1918
+
1919
+ /**
1920
+ * Fold each recorded dispatch independently, keeping prompt-run addresses unchanged.
1921
+ *
1922
+ * The same fold a live reader advances with {@link traceFoldStep}, taken over
1923
+ * the whole journal at once.
1924
+ *
1925
+ * The options the card carries are accepted and read by nothing: a plan's
1926
+ * check targets are a coverage requirement, and a requirement is not evidence
1927
+ * that any call ran it. Every word this fold says is read off a record.
1928
+ *
1929
+ * @category utilities
1930
+ * @since 1.0.0
1931
+ */
1932
+ export const traceFromJournal = (
1933
+ run: TraceRun,
1934
+ records: ReadonlyArray<JournalRecord>,
1935
+ _options: TraceOptions = {}
1936
+ ): TraceModel => traceFoldModel(traceFold(run, records), run.status)
1937
+
1938
+ /**
1939
+ * One span's bar on the shared axis, as percentages. An open span runs to the
1940
+ * axis end; an instant span is zero width and renders as a marker.
1941
+ *
1942
+ * @param span the span
1943
+ * @param extent the axis
1944
+ *
1945
+ * @category utilities
1946
+ * @since 1.0.0
1947
+ */
1948
+ export const waterfallGeometry = (
1949
+ span: TraceSpan,
1950
+ extent: TraceExtent
1951
+ ): { readonly left: number; readonly width: number } => {
1952
+ const width = Math.max(extent.end - extent.start, 1)
1953
+ const from = span.startedAt
1954
+ const to = span.endedAt ?? extent.end
1955
+ const left = ((from - extent.start) / width) * 100
1956
+ const bar = Math.max(((to - from) / width) * 100, 0)
1957
+ return { left: Math.round(left * 100) / 100, width: Math.round(bar * 100) / 100 }
1958
+ }
1959
+
1960
+ /**
1961
+ * The axis the phase strip measures on: the bands' own span when the journal
1962
+ * opened frames, the run's whole extent when it opened none.
1963
+ *
1964
+ * A milestone is a journal record, and the records that write one do not need a
1965
+ * frame: a journal of `read-only-demanded` and `sufficiency-observed` with no
1966
+ * `turn-opened` carries two moments and opens no band. Milestones therefore
1967
+ * survive without bands, and the axis that places them cannot be read off the
1968
+ * bands alone. The other choice — dropping a moment because no band was drawn
1969
+ * beside it — would be this fold deciding a record the journal made did not
1970
+ * happen, which is the one thing it may never do.
1971
+ *
1972
+ * @param model the trace
1973
+ *
1974
+ * @category utilities
1975
+ * @since 1.0.0
1976
+ */
1977
+ export const phaseExtent = (model: TraceModel): TraceExtent => {
1978
+ return model.bands.length === 0 ? model.extent : {
1979
+ start: Math.min(...model.bands.map((band) => band.startedAt)),
1980
+ end: Math.max(...model.bands.map((band) => band.endedAt))
1981
+ }
1982
+ }
1983
+
1984
+ /**
1985
+ * One band's share of that axis, as percentages, the way the waterfall measures
1986
+ * a span.
1987
+ *
1988
+ * A run whose frames all land in one millisecond recorded no elapsed time. Read
1989
+ * against a floored axis every such band is zero wide at left 0, so the whole
1990
+ * strip collapses onto the left edge and says nothing at all. The bands' ORDER
1991
+ * is a journal fact even where their durations are not, so with no duration to
1992
+ * measure the ordinal IS the axis: equal slices, left to right.
1993
+ *
1994
+ * @param band the band
1995
+ * @param extent the axis, from {@link phaseExtent}
1996
+ * @param index the band's place in the strip
1997
+ * @param count how many bands the strip holds
1998
+ *
1999
+ * @category utilities
2000
+ * @since 1.0.0
2001
+ */
2002
+ export const phaseBandGeometry = (
2003
+ band: PhaseBand,
2004
+ extent: TraceExtent,
2005
+ index: number,
2006
+ count: number
2007
+ ): { readonly left: number; readonly width: number } => {
2008
+ const round = (value: number): number => Math.round(value * 100) / 100
2009
+ const axis = extent.end - extent.start
2010
+ if (axis <= 0) {
2011
+ const share = 100 / Math.max(count, 1)
2012
+ return { left: round(index * share), width: round(share) }
2013
+ }
2014
+ return {
2015
+ left: round(((band.startedAt - extent.start) / axis) * 100),
2016
+ width: round((Math.max(band.endedAt - band.startedAt, 0) / axis) * 100)
2017
+ }
2018
+ }
2019
+
2020
+ /** The flows whose spans are messages between a coordinator and its workers (spec 06 §3). */
2021
+ const MESSAGE_FLOWS: ReadonlySet<string> = new Set(["agent/send", "agent/await"])
2022
+
2023
+ /** Whether a span, or any span under it, passes the filter.
2024
+ *
2025
+ * @category utilities
2026
+ * @since 1.0.0
2027
+ */
2028
+ export const spanMatches = (span: TraceSpan, filter: TraceFilter): boolean => {
2029
+ const own = filter === "all"
2030
+ ? true
2031
+ : filter === "running"
2032
+ ? span.status === "running" || span.status === "waiting"
2033
+ : filter === "failed"
2034
+ ? span.status === "failed"
2035
+ : filter === "model"
2036
+ ? span.kind === "model"
2037
+ : filter === "flow"
2038
+ ? span.kind === "call" || span.kind === "execution" || span.kind === "attempt"
2039
+ : span.kind === "call" && MESSAGE_FLOWS.has(span.label)
2040
+ return own || span.children.some((child) => spanMatches(child, filter))
2041
+ }
2042
+
2043
+ /** The tree's filters, in the slash grammar's words (spec 06 §6: `runs.trace.filter <runId> <filter>`).
2044
+ *
2045
+ * @category models
2046
+ * @since 1.0.0
2047
+ */
2048
+ export type TraceFilter = "all" | "running" | "failed" | "model" | "flow" | "messages"
2049
+
2050
+ /**
2051
+ * Stable filter identifiers accepted by run trace commands.
2052
+ *
2053
+ * @category utilities
2054
+ * @since 1.0.0
2055
+ */
2056
+ export const TRACE_FILTER_IDS: ReadonlyArray<TraceFilter> = [
2057
+ "all",
2058
+ "running",
2059
+ "failed",
2060
+ "model",
2061
+ "flow",
2062
+ "messages"
2063
+ ]
2064
+
2065
+ const FILTER_LABELS: Readonly<Record<TraceFilter, string>> = {
2066
+ all: "all",
2067
+ running: "running",
2068
+ failed: "failed",
2069
+ model: "model calls",
2070
+ flow: "flow calls",
2071
+ messages: "messages"
2072
+ }
2073
+
2074
+ /**
2075
+ * The filter chips a run of this kind shows (spec 06 §2 and §3): a prototype
2076
+ * has `all | messages | failed` and nothing else; every other run has the
2077
+ * shared set. `messages` is a prototype's conversation across workers, so it
2078
+ * is not offered elsewhere.
2079
+ *
2080
+ * @param kind the run's kind
2081
+ *
2082
+ * @category utilities
2083
+ * @since 1.0.0
2084
+ */
2085
+ export const traceFiltersFor = (kind: string | undefined): ReadonlyArray<readonly [TraceFilter, string]> =>
2086
+ (kind === "prototype"
2087
+ ? (["all", "messages", "failed"] as const)
2088
+ : (["all", "running", "failed", "model", "flow"] as const)).map((id) => [id, FILTER_LABELS[id]] as const)
2089
+
2090
+ /** Whether a filter word is one the trace knows.
2091
+ *
2092
+ * @category utilities
2093
+ * @since 1.0.0
2094
+ */
2095
+ export const isTraceFilter = (value: string): value is TraceFilter =>
2096
+ (TRACE_FILTER_IDS as ReadonlyArray<string>).includes(value)
2097
+
2098
+ /** The two presentations of the same persisted run card.
2099
+ *
2100
+ * @category models
2101
+ * @since 1.0.0
2102
+ */
2103
+ export type TraceView = "turns" | "timeline"
2104
+
2105
+ /** A concise projection of one recorded turn, never a generated claim about intent or correctness.
2106
+ *
2107
+ * @category models
2108
+ * @since 1.0.0
2109
+ */
2110
+ export interface TurnNarrative {
2111
+ readonly frame: TraceSpan
2112
+ readonly number: number
2113
+ readonly text: string
2114
+ readonly source: "model" | "calls" | "journal"
2115
+ }
2116
+
2117
+ /** The first prose line outside fenced code, bounded for the cheap turn list. Full text stays in the model span. */
2118
+ const proseLine = (text: string | undefined): string | undefined => {
2119
+ if (text === undefined) return undefined
2120
+ let fence: string | undefined
2121
+ for (const line of text.split("\n")) {
2122
+ const trimmed = line.trim()
2123
+ const marker = /^(?:`{3,}|~{3,})/.exec(trimmed)?.[0]
2124
+ if (marker !== undefined) {
2125
+ if (fence === undefined) fence = marker
2126
+ else if (marker[0] === fence[0] && marker.length >= fence.length) fence = undefined
2127
+ continue
2128
+ }
2129
+ if (fence !== undefined || trimmed === "") continue
2130
+ // A code-only response or the journal's truncated-field object is not explanatory prose.
2131
+ if (/^(?:[[{]|(?:const|let|var|await|import|export|function|return)\b)/.test(trimmed)) return undefined
2132
+ const words = trimmed.replace(/^#{1,6}\s+|^[-*+]\s+|^>\s+/, "").replace(/\s+/g, " ")
2133
+ const sentence = words.match(/^.*?[.!?](?=\s|$)/)?.[0] ?? `${words}.`
2134
+ return sentence.length <= 180 ? sentence : `${sentence.slice(0, 179).trimEnd()}…`
2135
+ }
2136
+ return undefined
2137
+ }
2138
+
2139
+ /** One cheap line per turn; falls back to actual call names when the model recorded only code.
2140
+ *
2141
+ * @category utilities
2142
+ * @since 1.0.0
2143
+ */
2144
+ export const turnNarratives = (model: TraceModel): ReadonlyArray<TurnNarrative> =>
2145
+ model.root.children.filter((span) => span.kind === "frame").map((frame, index) => {
2146
+ const text = frame.children.filter((span) => span.kind === "model").map((span) => proseLine(span.detail.output))
2147
+ .find((value) => value !== undefined)
2148
+ if (text !== undefined) return { frame, number: index + 1, text, source: "model" }
2149
+ const names: Array<string> = []
2150
+ const walk = (span: TraceSpan): void => {
2151
+ if (span.kind === "call" && !names.includes(span.label)) names.push(span.label)
2152
+ for (const child of span.children) walk(child)
2153
+ }
2154
+ walk(frame)
2155
+ return names.length > 0
2156
+ ? {
2157
+ frame,
2158
+ number: index + 1,
2159
+ text: `The agent called ${names.slice(0, 3).join(", ")}${
2160
+ names.length > 3 ? ` and ${names.length - 3} other flows` : ""
2161
+ }.`,
2162
+ source: "calls"
2163
+ }
2164
+ : {
2165
+ frame,
2166
+ number: index + 1,
2167
+ text: frame.children.some((span) => span.kind === "cell")
2168
+ ? "The agent produced a script; no flow calls were recorded in this turn."
2169
+ : frame.children.some((span) => span.kind === "model")
2170
+ ? "The model responded; no script or flow calls were recorded in this turn."
2171
+ : "The turn started; no model response or flow calls have been recorded.",
2172
+ source: "journal"
2173
+ }
2174
+ })
2175
+
2176
+ /** The recorded ancestry of a selection, for breadcrumbs and the selected turn's scoped call tree.
2177
+ *
2178
+ * @category utilities
2179
+ * @since 1.0.0
2180
+ */
2181
+ export const spanPath = (model: TraceModel, id: string): ReadonlyArray<TraceSpan> => {
2182
+ const visit = (span: TraceSpan): ReadonlyArray<TraceSpan> | undefined => {
2183
+ if (span.id === id) return [span]
2184
+ for (const child of span.children) {
2185
+ const path = visit(child)
2186
+ if (path !== undefined) return [span, ...path]
2187
+ }
2188
+ return undefined
2189
+ }
2190
+ return visit(model.root) ?? [model.root]
2191
+ }
2192
+
2193
+ /** A duration in the trace's units: milliseconds under a second, seconds under a minute, minutes and seconds under an hour, hours and minutes after.
2194
+ *
2195
+ * @category utilities
2196
+ * @since 1.0.0
2197
+ */
2198
+ export const durationWords = (ms: number): string => {
2199
+ if (ms < 1000) return `${Math.round(ms)}ms`
2200
+ // Round before splitting, so 59.96s reads 1m00s rather than 60.0s, and 51m59.6s reads 52m00s rather than 51m60s.
2201
+ if (ms < 59_950) return `${(ms / 1000).toFixed(1)}s`
2202
+ const total = Math.round(ms / 1000)
2203
+ if (total < 3600) {
2204
+ const minutes = Math.floor(total / 60)
2205
+ return `${minutes}m${String(total - minutes * 60).padStart(2, "0")}s`
2206
+ }
2207
+ const minutes = Math.round(ms / 60_000)
2208
+ const hours = Math.floor(minutes / 60)
2209
+ return `${hours}h${String(minutes - hours * 60).padStart(2, "0")}m`
2210
+ }
2211
+
2212
+ /** One memory item a run's relevance readings weighed.
2213
+ *
2214
+ * @category models
2215
+ * @since 1.0.0
2216
+ */
2217
+ export interface MemoryItem {
2218
+ readonly id: string
2219
+ readonly kind: string
2220
+ /** How likely the item is needed: 1 − Jev's probability that it is not. */
2221
+ readonly relevance: number
2222
+ }
2223
+
2224
+ /** The memory a run was shown and the memory Jev withheld from it.
2225
+ *
2226
+ * @category models
2227
+ * @since 1.0.0
2228
+ */
2229
+ export interface RunMemory {
2230
+ readonly kept: ReadonlyArray<MemoryItem>
2231
+ readonly withheld: ReadonlyArray<MemoryItem>
2232
+ }
2233
+
2234
+ const memoryItems = (value: unknown): ReadonlyArray<MemoryItem> =>
2235
+ Array.isArray(value) ?
2236
+ value.flatMap((raw: unknown) => {
2237
+ const item = typeof raw === "object" && raw !== null ? raw as Record<string, unknown> : undefined
2238
+ return item?.kind === "memory" && typeof item.id === "string" && typeof item.p === "number"
2239
+ ? [{ id: item.id, kind: "memory", relevance: 1 - item.p }]
2240
+ : []
2241
+ }) :
2242
+ []
2243
+
2244
+ /**
2245
+ * What memory a run was brought, from the `control.agent.relevance-settled`
2246
+ * rows of its `run-events` projection: ids and probabilities, never text. A
2247
+ * reading also weighs flows, skills and instructions; only memory counts here.
2248
+ * An item any reading kept is in; one only ever withheld is withheld; both
2249
+ * most relevant first. A row whose verdicts were not retained is no reading.
2250
+ * Undefined when Jev never read for the run.
2251
+ *
2252
+ * @category folds
2253
+ * @since 1.0.0
2254
+ */
2255
+ export const runMemoryOf = (records: ReadonlyArray<JournalRecord>): RunMemory | undefined => {
2256
+ const kept = new Map<string, MemoryItem>()
2257
+ const withheld = new Map<string, MemoryItem>()
2258
+ let read = false
2259
+ for (const record of records) {
2260
+ if (record.kind !== "control.agent.relevance-settled") continue
2261
+ const payload = typeof record.payload === "object" && record.payload !== null
2262
+ ? record.payload as Record<string, unknown>
2263
+ : {}
2264
+ if (!Array.isArray(payload.kept) && !Array.isArray(payload.withheld)) continue
2265
+ read = true
2266
+ for (const item of memoryItems(payload.kept)) kept.set(item.id, item)
2267
+ for (const item of memoryItems(payload.withheld)) withheld.set(item.id, item)
2268
+ }
2269
+ if (!read) return undefined
2270
+ const byRelevance = (left: MemoryItem, right: MemoryItem) =>
2271
+ right.relevance - left.relevance || left.id.localeCompare(right.id)
2272
+ return {
2273
+ kept: [...kept.values()].sort(byRelevance),
2274
+ withheld: [...withheld.values()].filter((item) => !kept.has(item.id)).sort(byRelevance)
2275
+ }
2276
+ }
2277
+
2278
+ /** One recorded agent action inside an engine step, as an Inspect cell.
2279
+ *
2280
+ * @category models
2281
+ * @since 1.0.0
2282
+ */
2283
+ export interface TranscriptCell {
2284
+ readonly id: string
2285
+ readonly kind: "read" | "edit" | "run" | "think" | "ask"
2286
+ label: string
2287
+ code?: string
2288
+ output?: string
2289
+ quote?: string
2290
+ tokens?: number
2291
+ took_s?: number
2292
+ tone?: "ok" | "fail" | "live"
2293
+ }
2294
+
2295
+ /** What the agent did inside one engine dispatch, keyed `<execution id>:<step id>`. */
2296
+ interface StepTranscript {
2297
+ readonly cells: Array<TranscriptCell>
2298
+ tokens: number
2299
+ models: number
2300
+ /** The task the agent was given: its first request's task system part. */
2301
+ input?: unknown
2302
+ }
2303
+
2304
+ /** The system part an agent step's task travels in (`Agent.ts`). */
2305
+ const TASK_PREFIX = "The task for this run:\n\n"
2306
+
2307
+ /** The agent's task from a traced request: its own `task` field, or the task
2308
+ * part of its system text when that fit the journal. Parsed when it is JSON. */
2309
+ const taskOf = (request: Record<string, unknown>): unknown => {
2310
+ const parts = Array.isArray(request.system) ? request.system : []
2311
+ const part = parts.find((text): text is string => typeof text === "string" && text.startsWith(TASK_PREFIX))
2312
+ const task = typeof request.task === "string" ? request.task : part?.slice(TASK_PREFIX.length)
2313
+ if (task === undefined) return undefined
2314
+ try {
2315
+ return JSON.parse(task)
2316
+ } catch {
2317
+ return task
2318
+ }
2319
+ }
2320
+
2321
+ const hexDigest = /^[0-9a-f]{64}$/
2322
+
2323
+ /** A transcript cell shows an excerpt; the journal keeps the full record. */
2324
+ const cellText = (value: unknown): string | undefined => {
2325
+ const text = textOf(value)
2326
+ return text === undefined || text.length <= 2000 ? text : `${text.slice(0, 2000)}…`
2327
+ }
2328
+
2329
+ /** The recorded presentation of a tool call: "Read t5.md", "Failed to write t5.md". */
2330
+ const callCellLabel = (started: Extract<CallFact.Fact, { phase: "invoked" }>, failed: boolean): string => {
2331
+ const descriptor = asRecord(started.descriptor)
2332
+ const presentation = asRecord(descriptor.presentation)
2333
+ const verbs = asRecord(presentation.verb)
2334
+ const name = started.flowName
2335
+ const verb = asString(failed ? verbs.failure : verbs.success) ?? (failed ? `failed ${name}` : name)
2336
+ const subjectKey = asString(presentation.subject)
2337
+ const subject = subjectKey === undefined ? undefined : asString(asRecord(started.input)[subjectKey])
2338
+ const text = subject === undefined ? verb : `${verb} ${subject}`
2339
+ return text.charAt(0).toUpperCase() + text.slice(1)
2340
+ }
2341
+
2342
+ const callCellKind = (started: Extract<CallFact.Fact, { phase: "invoked" }>): TranscriptCell["kind"] => {
2343
+ const activity = asString(asRecord(started.descriptor).activity)
2344
+ const name = started.flowName
2345
+ return name === "ask" ? "ask" : activity === "reads" ? "read" : activity === "writes" ? "edit" : "run"
2346
+ }
2347
+
2348
+ const decodeCallFact = Schema.decodeUnknownOption(CallFact.Fact)
2349
+
2350
+ /** A native tool-call fact and the dispatch digest its scope names. */
2351
+ const callFactOf = (record: JournalRecord) => {
2352
+ if (record.kind !== "control.engine.event") return undefined
2353
+ const envelope = asRecord(record.payload)
2354
+ if (envelope.version !== 1 || envelope.eventType !== CallFact.eventType || envelope.sourceSequence !== 0) {
2355
+ return undefined
2356
+ }
2357
+ const fact = decodeCallFact(envelope.payload)
2358
+ if (fact._tag === "None" || envelope.sourceId !== `call-fact-v1:${fact.value.callId}:${fact.value.phase}`) {
2359
+ return undefined
2360
+ }
2361
+ const stepId = /@([0-9a-f]{64}):\d+#\d+$/.exec(fact.value.identity.runId)?.[1]
2362
+ return stepId === undefined ? undefined : { stepId, fact: fact.value }
2363
+ }
2364
+
2365
+ /**
2366
+ * The agent transcript of every native dispatch in `journal`: model replies,
2367
+ * cells with what they printed, and tool calls, in journal order. Keyed by the
2368
+ * dispatch's step key digest, which the engine's node settlement names in
2369
+ * `stepKeyDigests`. Pure over the records; no model call.
2370
+ */
2371
+ const stepTranscripts = (journal: ReadonlyArray<JournalRecord>): ReadonlyMap<string, StepTranscript> => {
2372
+ const transcripts = new Map<string, StepTranscript>()
2373
+ const entry = (key: string) => {
2374
+ let found = transcripts.get(key)
2375
+ if (found === undefined) transcripts.set(key, found = { cells: [], tokens: 0, models: 0 })
2376
+ return found
2377
+ }
2378
+ const lastCell = new Map<string, TranscriptCell>()
2379
+ const calls = new Map<
2380
+ string,
2381
+ { readonly cell: TranscriptCell; readonly started: Extract<CallFact.Fact, { phase: "invoked" }> }
2382
+ >()
2383
+ for (const record of journal) {
2384
+ const sequence = record.sequence ?? 0
2385
+ const step = nativeStepEvent(record)
2386
+ if (step !== undefined) {
2387
+ const payload = asRecord(step.payload)
2388
+ // nativeStepEvent has decoded StepFact.Scope, whose stepId is a 64-digit hex string.
2389
+ const stepId = asString(asRecord(payload.step).stepId)!
2390
+ const transcript = entry(stepId)
2391
+ if (step.kind === "control.agent.model-requested" && transcript.input === undefined) {
2392
+ const task = taskOf(payload)
2393
+ if (task !== undefined) transcript.input = task
2394
+ } else if (step.kind === "control.agent.model-settled") {
2395
+ const usage = asRecord(payload.usage)
2396
+ const tokens = (asNumber(usage.inputTokens) ?? 0) + (asNumber(usage.outputTokens) ?? 0)
2397
+ const duration = asNumber(payload.durationMillis)
2398
+ const text = cellText(payload.text)
2399
+ transcript.tokens += tokens
2400
+ transcript.models += 1
2401
+ transcript.cells.push({
2402
+ id: `cell:${sequence}`,
2403
+ kind: "think",
2404
+ label: "Model reply",
2405
+ ...(text === undefined ? {} : { quote: text }),
2406
+ tokens,
2407
+ ...(duration === undefined ? {} : { took_s: Math.max(0, duration) / 1000 })
2408
+ })
2409
+ } else if (step.kind === "control.agent.cell-produced") {
2410
+ const code = cellText(payload.text)
2411
+ const cell: TranscriptCell = {
2412
+ id: `cell:${sequence}`,
2413
+ kind: "run",
2414
+ label: "Ran a cell",
2415
+ ...(code === undefined ? {} : { code })
2416
+ }
2417
+ transcript.cells.push(cell)
2418
+ lastCell.set(stepId, cell)
2419
+ } else if (step.kind === "control.agent.cell-printed") {
2420
+ const cell = lastCell.get(stepId)
2421
+ const printed = cellText(payload.text)
2422
+ if (cell !== undefined && printed !== undefined && printed !== "") {
2423
+ cell.output = cell.output === undefined ? printed : `${cell.output}\n${printed}`
2424
+ }
2425
+ } else if (step.kind === "control.agent.cell-settled") {
2426
+ const cell = lastCell.get(stepId)
2427
+ if (cell !== undefined) cell.tone = asString(payload.outcome) === "failure" ? "fail" : "ok"
2428
+ }
2429
+ continue
2430
+ }
2431
+ const call = callFactOf(record)
2432
+ if (call === undefined) continue
2433
+ const handle = `${call.stepId}:${call.fact.callId}`
2434
+ if (call.fact.phase === "invoked") {
2435
+ const input = cellText(call.fact.input)
2436
+ const cell: TranscriptCell = {
2437
+ id: `cell:${sequence}`,
2438
+ kind: callCellKind(call.fact),
2439
+ label: callCellLabel(call.fact, false),
2440
+ ...(input === undefined ? {} : { code: input }),
2441
+ tone: "live"
2442
+ }
2443
+ entry(call.stepId).cells.push(cell)
2444
+ calls.set(handle, { cell, started: call.fact })
2445
+ continue
2446
+ }
2447
+ const open = calls.get(handle)
2448
+ if (open === undefined) continue
2449
+ const failed = call.fact.outcome === "failure"
2450
+ open.cell.label = callCellLabel(open.started, failed)
2451
+ open.cell.tone = failed ? "fail" : "ok"
2452
+ const result = failed ? cellText(call.fact.message) : cellText(call.fact.value)
2453
+ if (result !== undefined) open.cell.output = result
2454
+ }
2455
+ return transcripts
2456
+ }
2457
+
2458
+ /** The dispatches a settled engine node drove, as `<execution id>:<step key digest>`. */
2459
+ const nodeDispatches = (row: TraceSpan): ReadonlyArray<string> => {
2460
+ if (!row.id.startsWith("engine-node:")) return []
2461
+ const fields = asRecord(row.detail.fields)
2462
+ const execution = asString(fields.executionId)
2463
+ const digests = asRecord(fields.payload).stepKeyDigests
2464
+ if (execution === undefined || !Array.isArray(digests)) return []
2465
+ return [
2466
+ ...new Set(digests.filter((digest): digest is string => typeof digest === "string" && hexDigest.test(digest)))
2467
+ ]
2468
+ .map((digest) => `${execution}:${digest}`)
2469
+ }
2470
+
2471
+ /** Plan node kinds that only shape the plan (@smthrs/plan); a child flow's own nodes are its steps. */
2472
+ const PLAN_STRUCTURE: ReadonlySet<string> = new Set([
2473
+ "Succeed",
2474
+ "Fail",
2475
+ "All",
2476
+ "Race",
2477
+ "Map",
2478
+ "AndThen",
2479
+ "Branch",
2480
+ "Catch",
2481
+ "FlowCall",
2482
+ "FunctionIdentity",
2483
+ "PlannedReference"
2484
+ ])
2485
+
2486
+ /** A check receipt's leading scalar fields, read from a preview the engine
2487
+ * may have cut short (its evidence comes last): `{"checkId":…,"status":…`. */
2488
+ const RECEIPT_HEAD = /^\{"checkId":"[^"\\]*"(?:,"[A-Za-z]+":(?:"[^"\\]*"|-?\d+))*,"status":"(passed|failed|superseded)"/
2489
+
2490
+ /** How many checks a step's recorded output reports failed: a check receipt
2491
+ * (`{checkId, status, …}`, possibly a cut-short preview) or a list of them;
2492
+ * zero for any other output. */
2493
+ const checkFailures = (output: string | undefined): number => {
2494
+ if (output === undefined) return 0
2495
+ const head = RECEIPT_HEAD.exec(output)
2496
+ if (head !== null) return head[1] === "failed" ? 1 : 0
2497
+ let value: unknown
2498
+ try {
2499
+ value = JSON.parse(output)
2500
+ } catch {
2501
+ return 0
2502
+ }
2503
+ return (Array.isArray(value) ? value : []).filter((receipt) => {
2504
+ const record = asRecord(receipt)
2505
+ return typeof record.checkId === "string" && record.status === "failed"
2506
+ }).length
2507
+ }
2508
+
2509
+ /** Each recorded engine node's kind and action, by its trace span id (without instance). */
2510
+ const engineNodeShapes = (journal: ReadonlyArray<JournalRecord>) => {
2511
+ const shapes = new Map<string, { kind?: string | undefined; action?: string | undefined }>()
2512
+ for (const record of journal) {
2513
+ if (record.kind !== "control.engine.event") continue
2514
+ const envelope = asRecord(record.payload)
2515
+ const executionId = asString(envelope.executionId)
2516
+ const generation = asNumber(envelope.generation)
2517
+ const payload = asRecord(envelope.payload)
2518
+ if (executionId === undefined || generation === undefined) continue
2519
+ const prefix = `engine-node:${encodeURIComponent(`${encodeURIComponent(executionId)}:${generation}`)}:`
2520
+ if (
2521
+ envelope.eventType === "flows.engine.plan-recorded" || envelope.eventType === "flows.engine.subgraph-appended"
2522
+ ) {
2523
+ const nodes = asRecord(payload.graph).nodes
2524
+ for (const node of Array.isArray(nodes) ? nodes : []) {
2525
+ const shape = asRecord(node)
2526
+ const id = asString(shape.id)
2527
+ if (id !== undefined) {
2528
+ shapes.set(prefix + encodeURIComponent(id), { kind: asString(shape.kind), action: asString(shape.action) })
2529
+ }
2530
+ }
2531
+ } else if (envelope.eventType === "flows.engine.node-scheduled") {
2532
+ const id = asString(payload.nodeId)
2533
+ if (id === undefined) continue
2534
+ const known = shapes.get(prefix + encodeURIComponent(id))
2535
+ shapes.set(prefix + encodeURIComponent(id), {
2536
+ kind: asString(payload.kind) ?? known?.kind,
2537
+ action: asString(payload.action) ?? known?.action
2538
+ })
2539
+ }
2540
+ }
2541
+ return shapes
2542
+ }
2543
+
2544
+ /** Adapt the canonical trace to the install monitor. IDs and timestamps come
2545
+ * from recorded spans; inspection never evaluates flow bodies or presentations.
2546
+ * @since 1.0.0
2547
+ * @category constructors
2548
+ */
2549
+ export const monitorFromJournal = (run: TraceRun, records: ReadonlyArray<JournalRecord>, at?: number) => {
2550
+ const last = records.reduce((last, row) => Math.max(last, row.sequence ?? 0), 0)
2551
+ const journal = at === undefined ? records : records.filter((row) => (row.sequence ?? 0) <= at)
2552
+ // A historical frame must not inherit the current run's terminal status.
2553
+ const lifecycleRow = [...journal].reverse().find((row) =>
2554
+ /^control\.run\.(running|waiting-approval|paused|suspended|held|completed|failed|cancelled|interrupted)$/.test(
2555
+ row.kind ?? ""
2556
+ ) &&
2557
+ (row.runId === undefined || row.runId === run.runId)
2558
+ )
2559
+ const status = at === undefined ? run.status : lifecycleRow?.kind?.split(".").at(-1) ?? "running"
2560
+ const state = (value: string): "running" | "waiting" | "held" | "failed" | "done" | "interrupted" =>
2561
+ value === "completed" || value === "done" ? "done" : value === "failed" ?
2562
+ "failed"
2563
+ : value === "cancelled" || value === "interrupted" ?
2564
+ "interrupted" :
2565
+ value === "held" ?
2566
+ "held"
2567
+ : value === "waiting" || value === "waiting-approval" || value === "paused" || value === "suspended"
2568
+ ? "waiting"
2569
+ : "running"
2570
+ const trace = traceFromJournal({ ...run, status }, journal)
2571
+ const engineLabel = (label: string) =>
2572
+ ACTION_RENDERINGS[label] === null || /^<(?:boundary:|quota-park>|structured-output-count>)/.test(label)
2573
+ const bookkeeping = (row: TraceSpan) => engineLabel(row.label)
2574
+ const transcripts = stepTranscripts(journal)
2575
+ // Every call below is a member of trace.rows, so both lookups are total.
2576
+ const dispatchesOf = new Map(trace.rows.map((row) => [row.id, nodeDispatches(row)] as const))
2577
+ // A step is a node that dispatched an action. Plan structure (sequences,
2578
+ // branches, maps) and child-flow calls, whose own nodes are the steps, are
2579
+ // never steps of their own.
2580
+ const structural = (kind: string | undefined) => kind !== undefined && PLAN_STRUCTURE.has(kind)
2581
+ const nodeShapes = engineNodeShapes(journal)
2582
+ const calls = trace.rows.filter((row) => {
2583
+ // A tool call an agent made inside a step is a cell of that step's
2584
+ // transcript (step:<scope>/…), never a step of its own.
2585
+ if (row.kind !== "call" || bookkeeping(row) || row.id.startsWith("step:")) return false
2586
+ const shape = nodeShapes.get(row.id.replace(/#\d+$/, ""))
2587
+ return shape === undefined || !structural(shape.kind)
2588
+ })
2589
+ const transcriptOf = (row: TraceSpan) => {
2590
+ const parts = dispatchesOf.get(row.id)!.flatMap((dispatch) => {
2591
+ const found = transcripts.get(dispatch.slice(dispatch.lastIndexOf(":") + 1))
2592
+ return found === undefined ? [] : [found]
2593
+ })
2594
+ return {
2595
+ cells: parts.flatMap((part) => part.cells).sort((left, right) =>
2596
+ Number(left.id.slice(5)) - Number(right.id.slice(5))
2597
+ ),
2598
+ tokens: parts.reduce((total, part) => total + part.tokens, 0),
2599
+ models: parts.reduce((total, part) => total + part.models, 0),
2600
+ input: parts.find((part) => part.input !== undefined)?.input
2601
+ }
2602
+ }
2603
+ const steps = calls.map((row) => {
2604
+ const meter = dispatchesOf.get(row.id)!
2605
+ const transcript = transcriptOf(row)
2606
+ return {
2607
+ ...(() => {
2608
+ const matched = /^(engine-node:.*)#(\d+)$/.exec(row.id)
2609
+ const id = matched?.[1] ?? row.id
2610
+ const k = Number(matched?.[2] ?? 1)
2611
+ return { key: `${id}#${k}`, id, k }
2612
+ })(),
2613
+ label: row.label,
2614
+ state: row.status,
2615
+ started_at: new Date(row.startedAt).toISOString(),
2616
+ ...(row.endedAt === undefined
2617
+ ? {}
2618
+ : { ended_at: new Date(row.endedAt).toISOString(), took_s: Math.max(0, row.endedAt - row.startedAt) / 1000 }),
2619
+ ...(row.detail.input !== undefined
2620
+ ? { input: row.detail.input }
2621
+ : transcript.input !== undefined
2622
+ ? { input: transcript.input }
2623
+ : {}),
2624
+ ...(row.detail.output === undefined ? {} : { output: row.detail.output }),
2625
+ // The install prices these dispatches from its metered proxy rows and
2626
+ // replaces them with `usage`; the journal's own token count rides along.
2627
+ ...(meter.length === 0 ? {} : { meter }),
2628
+ ...(transcript.models === 0 ? {} : { model_calls: transcript.models, tokens: transcript.tokens })
2629
+ }
2630
+ })
2631
+ const tone = (value: string): "live" | "ok" | "fail" | "wait" =>
2632
+ value === "failed" ?
2633
+ "fail"
2634
+ : value === "completed"
2635
+ ? "ok"
2636
+ : value === "waiting"
2637
+ ? "wait"
2638
+ : "live"
2639
+ const phases = calls.map((row, index) => {
2640
+ const transcript = transcriptOf(row).cells
2641
+ const failedChecks = checkFailures(row.detail.output)
2642
+ const cells: Array<Omit<TranscriptCell, "tone"> & { tone?: ReturnType<typeof tone> }> = transcript.length > 0
2643
+ ? transcript
2644
+ : [{
2645
+ id: `cell:${row.id}`,
2646
+ kind: /Edited/.test(row.label) ? "edit" : /Read/.test(row.label) ? "read" : "run",
2647
+ label: row.label,
2648
+ ...(row.detail.output === undefined ? {} : { output: row.detail.output }),
2649
+ tone: tone(row.status)
2650
+ }]
2651
+ return {
2652
+ id: `phase:${row.id}`,
2653
+ step: steps[index]!.key,
2654
+ // The title is the Appendix C label plus the outcome its recorded
2655
+ // output reports: "Ran checks · 1 failed".
2656
+ title: failedChecks === 0 ? row.label : `${row.label} · ${failedChecks} failed`,
2657
+ took_s: Math.max(0, (row.endedAt ?? row.startedAt) - row.startedAt) / 1000,
2658
+ tone: failedChecks === 0 ? tone(row.status) : "fail" as const,
2659
+ cells
2660
+ }
2661
+ })
2662
+ const declaredGraph = engineExecutionEvidence(journal).filter((execution) => execution.coherent).flatMap(
2663
+ (execution) => {
2664
+ const prefix = `engine-node:${
2665
+ encodeURIComponent(`${encodeURIComponent(execution.executionId)}:${execution.generation}`)
2666
+ }:`
2667
+ const nodes = execution.graph?.nodes ?? []
2668
+ const hidden = new Map(
2669
+ nodes.filter((node) => engineLabel(node.action ?? node.id) || structural(node.kind))
2670
+ .map((node) => [node.id, node])
2671
+ )
2672
+ // Engine bookkeeping stays in the Engine row. Its dependency edges still
2673
+ // order the visible steps, including across several hidden boundaries.
2674
+ const visibleDeps = (id: string, seen: ReadonlySet<string>): Array<string> => {
2675
+ const node = hidden.get(id)
2676
+ if (node === undefined) return [prefix + encodeURIComponent(id)]
2677
+ if (seen.has(id)) return []
2678
+ return node.dependsOn.flatMap((dependency) => visibleDeps(dependency, new Set([...seen, id])))
2679
+ }
2680
+ return nodes.filter((node) => !hidden.has(node.id)).map((node) => {
2681
+ const id = prefix + encodeURIComponent(node.id)
2682
+ const instances = calls.filter((row) => row.id.startsWith(`${id}#`))
2683
+ const current = instances.at(-1)
2684
+ return {
2685
+ id,
2686
+ label: inspectLabel(node.action ?? node.id),
2687
+ // A branch not taken never ran: it stays next, even in a finished run.
2688
+ state: current === undefined || current.status === "skipped" ?
2689
+ "next" as const :
2690
+ current.status === "completed" ?
2691
+ "done" as const
2692
+ : current.status === "failed"
2693
+ ? "failed" as const
2694
+ : current.status === "waiting"
2695
+ ? "waiting" as const
2696
+ : "current" as const,
2697
+ deps: [...new Set(node.dependsOn.flatMap((dependency) => visibleDeps(dependency, new Set([node.id]))))]
2698
+ }
2699
+ })
2700
+ }
2701
+ )
2702
+ const usageOf = (row: TraceSpan) => (row.detail.usage?.inputTokens ?? 0) + (row.detail.usage?.outputTokens ?? 0)
2703
+ const tokens = trace.rows.reduce((total, row) => total + usageOf(row), 0)
2704
+ // Model calls no native dispatch recorded (a legacy agent journal) can
2705
+ // never be joined to metered rows; the install refuses to price them.
2706
+ const unmetered = trace.rows.filter((row) => row.kind === "model" && asRecord(row.detail.fields).step === undefined)
2707
+ .reduce((total, row) => total + usageOf(row), 0)
2708
+ const approvalWaits = trace.rows.filter((row) => row.kind === "approval").map((row) => ({
2709
+ id: row.id,
2710
+ kind: "approval" as const,
2711
+ label: row.label,
2712
+ since: new Date(row.startedAt).toISOString(),
2713
+ ...(row.endedAt === undefined
2714
+ ? {}
2715
+ : {
2716
+ settled: { by: { kind: "system" as const, color_index: 7 as const }, at: new Date(row.endedAt).toISOString() }
2717
+ })
2718
+ }))
2719
+ // The machine records its declared custom view as data. Only the shipped
2720
+ // text renderer is supported; module paths, HTML and executable data never
2721
+ // cross this seam. Replay selects the last declaration in its own prefix.
2722
+ const custom = [...journal].reverse().find((row) =>
2723
+ (row.runId === undefined || row.runId === run.runId) &&
2724
+ row.kind === "control.engine.event" && asRecord(row.payload).eventType === "flows.run.presentation"
2725
+ )
2726
+ const presentation = asRecord(asRecord(custom?.payload).payload)
2727
+ const text = asString(presentation.text)
2728
+ const customView = presentation.kind === "text" && text !== undefined && text.length <= 65536 &&
2729
+ Object.keys(presentation).every((key) => key === "kind" || key === "text")
2730
+ ? { presentation: { kind: "text" as const, text } } :
2731
+ {}
2732
+ return {
2733
+ ...customView,
2734
+ id: run.runId,
2735
+ flow: run.flowId,
2736
+ version: "",
2737
+ title: run.flowId,
2738
+ state: state(status),
2739
+ attempts: [{
2740
+ n: 1,
2741
+ run_id: run.runId,
2742
+ state: state(status),
2743
+ steps,
2744
+ phases,
2745
+ graph: declaredGraph.length > 0 ?
2746
+ declaredGraph :
2747
+ calls.map((row) => ({
2748
+ id: row.id,
2749
+ label: row.label,
2750
+ state: row.status === "skipped" ? "next" as const : row.status === "completed" ?
2751
+ "done" as const
2752
+ : row.status === "failed"
2753
+ ? "failed" as const
2754
+ : row.status === "waiting"
2755
+ ? "waiting" as const
2756
+ : "current" as const,
2757
+ deps: [] as Array<string>
2758
+ }))
2759
+ }],
2760
+ waits: [
2761
+ ...approvalWaits,
2762
+ ...engineExecutionEvidence(journal).filter((execution) => execution.coherent).flatMap((execution) =>
2763
+ execution.waits.map((wait) => ({
2764
+ id: wait.id,
2765
+ kind: wait.kind,
2766
+ label: inspectLabel("wait"),
2767
+ since: new Date(wait.since).toISOString(),
2768
+ ...(wait.settledAt === undefined
2769
+ ? {}
2770
+ : {
2771
+ settled: {
2772
+ by: { kind: "system" as const, color_index: 7 as const },
2773
+ at: new Date(wait.settledAt).toISOString()
2774
+ }
2775
+ })
2776
+ }))
2777
+ )
2778
+ ],
2779
+ tokens,
2780
+ // Spend is priced where model calls are metered. The host never publishes
2781
+ // an unknown total as zero: a run with model tokens leaves cost to the install.
2782
+ time_s: Math.max(0, trace.extent.end - trace.extent.start) / 1000,
2783
+ ...(tokens === 0 ? { cost_usd: 0 } : {}),
2784
+ ...(unmetered === 0 ? {} : { unmetered_tokens: unmetered }),
2785
+ engine: trace.rows.filter((row) =>
2786
+ row.kind === "event" || row.kind === "execution" || row.kind === "attempt" || bookkeeping(row)
2787
+ ).map((row) => ({ label: row.label, detail: row.detail.output ?? row.detail.message ?? "" })),
2788
+ journal: journal.map((row) => ({
2789
+ seq: row.sequence ?? 0,
2790
+ at: new Date(row.occurredAt ?? 0).toISOString(),
2791
+ type: row.kind ?? "event",
2792
+ text: JSON.stringify(row.payload) ?? "null"
2793
+ })),
2794
+ ...(at === undefined ? {} : { replay: { at: Math.min(at, last), last } })
2795
+ }
2796
+ }