@edspencer/paddock 0.0.1 → 0.57.0

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 (296) hide show
  1. package/README.md +69 -0
  2. package/install-notice.mjs +29 -0
  3. package/package.json +58 -5
  4. package/packages/server/dist/adoptable.d.ts +127 -0
  5. package/packages/server/dist/adoptable.js +315 -0
  6. package/packages/server/dist/app.d.ts +67 -0
  7. package/packages/server/dist/app.js +325 -0
  8. package/packages/server/dist/archive.d.ts +42 -0
  9. package/packages/server/dist/archive.js +125 -0
  10. package/packages/server/dist/attachments-config.d.ts +114 -0
  11. package/packages/server/dist/attachments-config.js +165 -0
  12. package/packages/server/dist/attachments-hint.d.ts +53 -0
  13. package/packages/server/dist/attachments-hint.js +140 -0
  14. package/packages/server/dist/attachments.d.ts +33 -0
  15. package/packages/server/dist/attachments.js +117 -0
  16. package/packages/server/dist/auth.d.ts +84 -0
  17. package/packages/server/dist/auth.js +247 -0
  18. package/packages/server/dist/background.d.ts +36 -0
  19. package/packages/server/dist/background.js +137 -0
  20. package/packages/server/dist/bind-safety.d.ts +56 -0
  21. package/packages/server/dist/bind-safety.js +51 -0
  22. package/packages/server/dist/brand.d.ts +44 -0
  23. package/packages/server/dist/brand.js +88 -0
  24. package/packages/server/dist/chat-dto.d.ts +210 -0
  25. package/packages/server/dist/chat-dto.js +308 -0
  26. package/packages/server/dist/cli/import-chats.d.ts +21 -0
  27. package/packages/server/dist/cli/import-chats.js +197 -0
  28. package/packages/server/dist/cli/paddock.d.ts +2 -0
  29. package/packages/server/dist/cli/paddock.js +247 -0
  30. package/packages/server/dist/concurrency.d.ts +57 -0
  31. package/packages/server/dist/concurrency.js +71 -0
  32. package/packages/server/dist/config.d.ts +538 -0
  33. package/packages/server/dist/config.js +625 -0
  34. package/packages/server/dist/curation-config.d.ts +55 -0
  35. package/packages/server/dist/curation-config.js +64 -0
  36. package/packages/server/dist/environment-prompt.d.ts +52 -0
  37. package/packages/server/dist/environment-prompt.js +61 -0
  38. package/packages/server/dist/event-bus.d.ts +87 -0
  39. package/packages/server/dist/event-bus.js +48 -0
  40. package/packages/server/dist/git.d.ts +128 -0
  41. package/packages/server/dist/git.js +446 -0
  42. package/packages/server/dist/github-auth.d.ts +44 -0
  43. package/packages/server/dist/github-auth.js +169 -0
  44. package/packages/server/dist/herdctl-agent-config.d.ts +98 -0
  45. package/packages/server/dist/herdctl-agent-config.js +331 -0
  46. package/packages/server/dist/herdctl-agent-names.d.ts +172 -0
  47. package/packages/server/dist/herdctl-agent-names.js +228 -0
  48. package/packages/server/dist/herdctl-jobs-index.d.ts +65 -0
  49. package/packages/server/dist/herdctl-jobs-index.js +225 -0
  50. package/packages/server/dist/herdctl-jobs.d.ts +98 -0
  51. package/packages/server/dist/herdctl-jobs.js +225 -0
  52. package/packages/server/dist/herdctl.d.ts +623 -0
  53. package/packages/server/dist/herdctl.js +1279 -0
  54. package/packages/server/dist/hook-config.d.ts +217 -0
  55. package/packages/server/dist/hook-config.js +224 -0
  56. package/packages/server/dist/http-bytes.d.ts +24 -0
  57. package/packages/server/dist/http-bytes.js +52 -0
  58. package/packages/server/dist/index.d.ts +1 -0
  59. package/packages/server/dist/index.js +14 -0
  60. package/packages/server/dist/instance-config.d.ts +143 -0
  61. package/packages/server/dist/instance-config.js +385 -0
  62. package/packages/server/dist/localcommand.d.ts +23 -0
  63. package/packages/server/dist/localcommand.js +140 -0
  64. package/packages/server/dist/management-auth.d.ts +99 -0
  65. package/packages/server/dist/management-auth.js +165 -0
  66. package/packages/server/dist/management-config.d.ts +164 -0
  67. package/packages/server/dist/management-config.js +286 -0
  68. package/packages/server/dist/management-mcp-server.d.ts +20 -0
  69. package/packages/server/dist/management-mcp-server.js +95 -0
  70. package/packages/server/dist/management-metadata.d.ts +67 -0
  71. package/packages/server/dist/management-metadata.js +56 -0
  72. package/packages/server/dist/management-ops.d.ts +110 -0
  73. package/packages/server/dist/management-ops.js +449 -0
  74. package/packages/server/dist/management-policy.d.ts +172 -0
  75. package/packages/server/dist/management-policy.js +259 -0
  76. package/packages/server/dist/message-provenance.d.ts +112 -0
  77. package/packages/server/dist/message-provenance.js +260 -0
  78. package/packages/server/dist/models.d.ts +164 -0
  79. package/packages/server/dist/models.js +208 -0
  80. package/packages/server/dist/openapi.d.ts +66 -0
  81. package/packages/server/dist/openapi.js +183 -0
  82. package/packages/server/dist/parent-detach.d.ts +30 -0
  83. package/packages/server/dist/parent-detach.js +109 -0
  84. package/packages/server/dist/preload.d.ts +43 -0
  85. package/packages/server/dist/preload.js +55 -0
  86. package/packages/server/dist/project-files.d.ts +76 -0
  87. package/packages/server/dist/project-files.js +191 -0
  88. package/packages/server/dist/project-mime.d.ts +35 -0
  89. package/packages/server/dist/project-mime.js +55 -0
  90. package/packages/server/dist/project-paths.d.ts +88 -0
  91. package/packages/server/dist/project-paths.js +126 -0
  92. package/packages/server/dist/project-types.d.ts +316 -0
  93. package/packages/server/dist/project-types.js +48 -0
  94. package/packages/server/dist/projects.d.ts +202 -0
  95. package/packages/server/dist/projects.js +894 -0
  96. package/packages/server/dist/queued-message.d.ts +35 -0
  97. package/packages/server/dist/queued-message.js +116 -0
  98. package/packages/server/dist/read-state.d.ts +46 -0
  99. package/packages/server/dist/read-state.js +139 -0
  100. package/packages/server/dist/recovery-config.d.ts +111 -0
  101. package/packages/server/dist/recovery-config.js +105 -0
  102. package/packages/server/dist/recovery.d.ts +262 -0
  103. package/packages/server/dist/recovery.js +490 -0
  104. package/packages/server/dist/resume-drain.d.ts +122 -0
  105. package/packages/server/dist/resume-drain.js +158 -0
  106. package/packages/server/dist/route-context.d.ts +168 -0
  107. package/packages/server/dist/route-context.js +39 -0
  108. package/packages/server/dist/route-errors.d.ts +8 -0
  109. package/packages/server/dist/route-errors.js +13 -0
  110. package/packages/server/dist/routes/chats.d.ts +12 -0
  111. package/packages/server/dist/routes/chats.js +1406 -0
  112. package/packages/server/dist/routes/git.d.ts +13 -0
  113. package/packages/server/dist/routes/git.js +236 -0
  114. package/packages/server/dist/routes/mcp.d.ts +83 -0
  115. package/packages/server/dist/routes/mcp.js +250 -0
  116. package/packages/server/dist/routes/meta.d.ts +15 -0
  117. package/packages/server/dist/routes/meta.js +447 -0
  118. package/packages/server/dist/routes/projects.d.ts +25 -0
  119. package/packages/server/dist/routes/projects.js +736 -0
  120. package/packages/server/dist/routes/triggers.d.ts +21 -0
  121. package/packages/server/dist/routes/triggers.js +324 -0
  122. package/packages/server/dist/routes/workspace-mount.d.ts +48 -0
  123. package/packages/server/dist/routes/workspace-mount.js +32 -0
  124. package/packages/server/dist/routes.d.ts +26 -0
  125. package/packages/server/dist/routes.js +29 -0
  126. package/packages/server/dist/run-provenance.d.ts +109 -0
  127. package/packages/server/dist/run-provenance.js +206 -0
  128. package/packages/server/dist/runs.d.ts +92 -0
  129. package/packages/server/dist/runs.js +72 -0
  130. package/packages/server/dist/schedule-config.d.ts +68 -0
  131. package/packages/server/dist/schedule-config.js +75 -0
  132. package/packages/server/dist/schedule-session.d.ts +34 -0
  133. package/packages/server/dist/schedule-session.js +120 -0
  134. package/packages/server/dist/self-mcp-descriptions.d.ts +20 -0
  135. package/packages/server/dist/self-mcp-descriptions.js +145 -0
  136. package/packages/server/dist/self-mcp-read.d.ts +15 -0
  137. package/packages/server/dist/self-mcp-read.js +97 -0
  138. package/packages/server/dist/self-mcp-triggers.d.ts +30 -0
  139. package/packages/server/dist/self-mcp-triggers.js +144 -0
  140. package/packages/server/dist/self-mcp-types.d.ts +248 -0
  141. package/packages/server/dist/self-mcp-types.js +10 -0
  142. package/packages/server/dist/self-mcp-util.d.ts +73 -0
  143. package/packages/server/dist/self-mcp-util.js +157 -0
  144. package/packages/server/dist/self-mcp-write.d.ts +35 -0
  145. package/packages/server/dist/self-mcp-write.js +215 -0
  146. package/packages/server/dist/self-mcp.d.ts +95 -0
  147. package/packages/server/dist/self-mcp.js +448 -0
  148. package/packages/server/dist/send-file-mcp.d.ts +46 -0
  149. package/packages/server/dist/send-file-mcp.js +281 -0
  150. package/packages/server/dist/session-hub.d.ts +186 -0
  151. package/packages/server/dist/session-hub.js +291 -0
  152. package/packages/server/dist/spawn-capability.d.ts +99 -0
  153. package/packages/server/dist/spawn-capability.js +106 -0
  154. package/packages/server/dist/star.d.ts +26 -0
  155. package/packages/server/dist/star.js +94 -0
  156. package/packages/server/dist/start.d.ts +1 -0
  157. package/packages/server/dist/start.js +21 -0
  158. package/packages/server/dist/subagents.d.ts +337 -0
  159. package/packages/server/dist/subagents.js +626 -0
  160. package/packages/server/dist/sweep.d.ts +122 -0
  161. package/packages/server/dist/sweep.js +696 -0
  162. package/packages/server/dist/tooldetails.d.ts +33 -0
  163. package/packages/server/dist/tooldetails.js +466 -0
  164. package/packages/server/dist/transcribe.d.ts +69 -0
  165. package/packages/server/dist/transcribe.js +119 -0
  166. package/packages/server/dist/transcripts.d.ts +42 -0
  167. package/packages/server/dist/transcripts.js +164 -0
  168. package/packages/server/dist/trigger-config.d.ts +278 -0
  169. package/packages/server/dist/trigger-config.js +417 -0
  170. package/packages/server/dist/trigger-runtime.d.ts +94 -0
  171. package/packages/server/dist/trigger-runtime.js +82 -0
  172. package/packages/server/dist/trigger-session.d.ts +34 -0
  173. package/packages/server/dist/trigger-session.js +122 -0
  174. package/packages/server/dist/triggers.d.ts +83 -0
  175. package/packages/server/dist/triggers.js +124 -0
  176. package/packages/server/dist/trusted-proxy.d.ts +57 -0
  177. package/packages/server/dist/trusted-proxy.js +157 -0
  178. package/packages/server/dist/turn-notice.d.ts +154 -0
  179. package/packages/server/dist/turn-notice.js +271 -0
  180. package/packages/server/dist/unread.d.ts +36 -0
  181. package/packages/server/dist/unread.js +123 -0
  182. package/packages/server/dist/usage.d.ts +66 -0
  183. package/packages/server/dist/usage.js +243 -0
  184. package/packages/server/dist/wake-injection.d.ts +156 -0
  185. package/packages/server/dist/wake-injection.js +136 -0
  186. package/packages/server/dist/whisper-local.d.ts +9 -0
  187. package/packages/server/dist/whisper-local.js +122 -0
  188. package/packages/server/dist/ws-context.d.ts +158 -0
  189. package/packages/server/dist/ws-context.js +1 -0
  190. package/packages/server/dist/ws-protocol.d.ts +423 -0
  191. package/packages/server/dist/ws-protocol.js +251 -0
  192. package/packages/server/dist/ws-self-mcp.d.ts +52 -0
  193. package/packages/server/dist/ws-self-mcp.js +28 -0
  194. package/packages/server/dist/ws-triggers.d.ts +29 -0
  195. package/packages/server/dist/ws-triggers.js +261 -0
  196. package/packages/server/dist/ws-turn.d.ts +86 -0
  197. package/packages/server/dist/ws-turn.js +751 -0
  198. package/packages/server/dist/ws.d.ts +62 -0
  199. package/packages/server/dist/ws.js +1024 -0
  200. package/packages/server/package.json +6 -0
  201. package/packages/web/dist/assets/InstanceConfigPage-Bypd_quL.js +2 -0
  202. package/packages/web/dist/assets/MarkdownRenderer-Co8w9iuX.js +30 -0
  203. package/packages/web/dist/assets/ProjectRedirect-DXrA24Cy.js +2 -0
  204. package/packages/web/dist/assets/ProjectView-B9opP7Sv.js +13 -0
  205. package/packages/web/dist/assets/ProjectsGrid-D2cwb2jY.js +2 -0
  206. package/packages/web/dist/assets/arc-B--FIRB8.js +2 -0
  207. package/packages/web/dist/assets/architectureDiagram-3BPJPVTR-B6dRWZZ6.js +37 -0
  208. package/packages/web/dist/assets/bash-I8pq0VWm.js +2 -0
  209. package/packages/web/dist/assets/blockDiagram-GPEHLZMM-DLxFAvvf.js +133 -0
  210. package/packages/web/dist/assets/c-D2F1sQUL.js +2 -0
  211. package/packages/web/dist/assets/c4Diagram-AAUBKEIU-CNee5ks4.js +11 -0
  212. package/packages/web/dist/assets/channel-Bw0A52CB.js +2 -0
  213. package/packages/web/dist/assets/chunk-2J33WTMH-DObw03Xl.js +2 -0
  214. package/packages/web/dist/assets/chunk-4BX2VUAB-CH2SR6nN.js +2 -0
  215. package/packages/web/dist/assets/chunk-55IACEB6-WlVpegT3.js +2 -0
  216. package/packages/web/dist/assets/chunk-727SXJPM-g8GT9Rgd.js +207 -0
  217. package/packages/web/dist/assets/chunk-AQP2D5EJ-DbN4PsDF.js +232 -0
  218. package/packages/web/dist/assets/chunk-FMBD7UC4-DSN3BjFG.js +16 -0
  219. package/packages/web/dist/assets/chunk-ND2GUHAM-D9R4le0z.js +2 -0
  220. package/packages/web/dist/assets/chunk-QZHKN3VN-D4YgRaKH.js +2 -0
  221. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-itf_kt-q.js +2 -0
  222. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-itf_kt-q.js +2 -0
  223. package/packages/web/dist/assets/core-CysJ2gLj.js +4 -0
  224. package/packages/web/dist/assets/cose-bilkent-S5V4N54A-D1PB0KNu.js +2 -0
  225. package/packages/web/dist/assets/cpp-Bm3ii6kY.js +2 -0
  226. package/packages/web/dist/assets/csharp-BNpuYZMG.js +2 -0
  227. package/packages/web/dist/assets/css-DazXZka4.js +2 -0
  228. package/packages/web/dist/assets/cytoscape.esm-CUqq0XTU.js +332 -0
  229. package/packages/web/dist/assets/dagre-BM42HDAG-C6Uuq-dq.js +5 -0
  230. package/packages/web/dist/assets/defaultLocale-DX6XiGOO.js +2 -0
  231. package/packages/web/dist/assets/diagram-2AECGRRQ-C9JvQ425.js +44 -0
  232. package/packages/web/dist/assets/diagram-5GNKFQAL-Cu6pdh2d.js +11 -0
  233. package/packages/web/dist/assets/diagram-KO2AKTUF-PmXcUQhY.js +4 -0
  234. package/packages/web/dist/assets/diagram-LMA3HP47-9ww6_9Iq.js +25 -0
  235. package/packages/web/dist/assets/diagram-OG6HWLK6-DWQVjv_n.js +25 -0
  236. package/packages/web/dist/assets/erDiagram-TEJ5UH35-6F7xTuff.js +86 -0
  237. package/packages/web/dist/assets/flowDiagram-I6XJVG4X-SCiGu7m6.js +163 -0
  238. package/packages/web/dist/assets/format-yf7Vf9kR.js +2 -0
  239. package/packages/web/dist/assets/ganttDiagram-6RSMTGT7-BNpPwxkL.js +293 -0
  240. package/packages/web/dist/assets/gitGraphDiagram-PVQCEYII-C0gyAJjW.js +107 -0
  241. package/packages/web/dist/assets/go-BPx_y7j8.js +2 -0
  242. package/packages/web/dist/assets/graph-BlwPajkw.js +2 -0
  243. package/packages/web/dist/assets/index-B-YuzkF6.js +71 -0
  244. package/packages/web/dist/assets/index-CvGiQddO.css +1 -0
  245. package/packages/web/dist/assets/infoDiagram-5YYISTIA-CJ2CFPfy.js +3 -0
  246. package/packages/web/dist/assets/ini-B3BVgEsG.js +2 -0
  247. package/packages/web/dist/assets/init-Gi6I4Gst.js +2 -0
  248. package/packages/web/dist/assets/ishikawaDiagram-YF4QCWOH-Cs0D32Ry.js +71 -0
  249. package/packages/web/dist/assets/java-B5VKqtH2.js +2 -0
  250. package/packages/web/dist/assets/javascript-BKRaQes9.js +2 -0
  251. package/packages/web/dist/assets/journeyDiagram-JHISSGLW-Bc9fuI2i.js +140 -0
  252. package/packages/web/dist/assets/json-DIYVocXf.js +2 -0
  253. package/packages/web/dist/assets/kanban-definition-UN3LZRKU-D_tC7QfC.js +90 -0
  254. package/packages/web/dist/assets/katex-C5jXJg4s.js +258 -0
  255. package/packages/web/dist/assets/kotlin-Cw-eF_bm.js +3 -0
  256. package/packages/web/dist/assets/lastTab-CU1zsF0j.js +2 -0
  257. package/packages/web/dist/assets/layout-Boy8vhSP.js +2 -0
  258. package/packages/web/dist/assets/linear-BzGHkF9K.js +2 -0
  259. package/packages/web/dist/assets/mermaid.core-DlbsYzE-.js +310 -0
  260. package/packages/web/dist/assets/mindmap-definition-RKZ34NQL-Bxeh-ToD.js +97 -0
  261. package/packages/web/dist/assets/ordinal-Cboi1Yqb.js +2 -0
  262. package/packages/web/dist/assets/php-CRdd4dfS.js +3 -0
  263. package/packages/web/dist/assets/pieDiagram-4H26LBE5-CDmilo8S.js +31 -0
  264. package/packages/web/dist/assets/python-kSlZHhJb.js +2 -0
  265. package/packages/web/dist/assets/quadrantDiagram-W4KKPZXB-D14-JyE9.js +8 -0
  266. package/packages/web/dist/assets/queued-LamEU0GH.js +2 -0
  267. package/packages/web/dist/assets/requirementDiagram-4Y6WPE33-CJ1PUGu-.js +85 -0
  268. package/packages/web/dist/assets/ruby-7T4ktxyv.js +2 -0
  269. package/packages/web/dist/assets/rust-HC3MBUhY.js +2 -0
  270. package/packages/web/dist/assets/sankeyDiagram-5OEKKPKP-BkUK73W0.js +41 -0
  271. package/packages/web/dist/assets/scss-DZLA7YXz.js +2 -0
  272. package/packages/web/dist/assets/sequenceDiagram-3UESZ5HK-sbcvsuLp.js +163 -0
  273. package/packages/web/dist/assets/sql-BC88IN-V.js +2 -0
  274. package/packages/web/dist/assets/stateDiagram-AJRCARHV-DV-7BdHK.js +2 -0
  275. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BlkGdXKW.js +2 -0
  276. package/packages/web/dist/assets/swift-Dn8kKO_1.js +2 -0
  277. package/packages/web/dist/assets/timeline-definition-PNZ67QCA-C5su21G_.js +121 -0
  278. package/packages/web/dist/assets/typescript-C2FFdlUC.js +2 -0
  279. package/packages/web/dist/assets/vennDiagram-CIIHVFJN-JNkIJAaQ.js +35 -0
  280. package/packages/web/dist/assets/wardley-L42UT6IY-qcz6t2vR.js +174 -0
  281. package/packages/web/dist/assets/wardleyDiagram-YWT4CUSO-CMrOIZ-c.js +79 -0
  282. package/packages/web/dist/assets/xml-BXBhIUeX.js +2 -0
  283. package/packages/web/dist/assets/xychartDiagram-2RQKCTM6-C9q6Oy9u.js +8 -0
  284. package/packages/web/dist/assets/yaml-drsDnIKT.js +2 -0
  285. package/packages/web/dist/favicon.ico +0 -0
  286. package/packages/web/dist/fonts/inter-latin.woff2 +0 -0
  287. package/packages/web/dist/fonts/jetbrains-mono-latin.woff2 +0 -0
  288. package/packages/web/dist/icons/apple-touch-icon.png +0 -0
  289. package/packages/web/dist/icons/favicon-16.png +0 -0
  290. package/packages/web/dist/icons/favicon-32.png +0 -0
  291. package/packages/web/dist/icons/icon-192.png +0 -0
  292. package/packages/web/dist/icons/icon-512.png +0 -0
  293. package/packages/web/dist/icons/maskable-512.png +0 -0
  294. package/packages/web/dist/index.html +56 -0
  295. package/packages/web/dist/manifest.webmanifest +32 -0
  296. package/packages/web/dist/sw.js +139 -0
@@ -0,0 +1,626 @@
1
+ /**
2
+ * Sub-agent (Task/Agent tool) transcript reader — issue #37.
3
+ *
4
+ * When a chat agent spawns a sub-agent via the `Task` tool (classic Claude Code)
5
+ * or the `Agent` tool (Agent SDK), the sub-agent runs its own turn whose full
6
+ * step-by-step transcript is written to disk alongside the parent session:
7
+ *
8
+ * <projectDir>/.chats/<sessionId>/subagents/agent-<hex>.jsonl // the steps
9
+ * <projectDir>/.chats/<sessionId>/subagents/agent-<hex>.meta.json // linking sidecar
10
+ *
11
+ * The `.meta.json` sidecar is the key linking artifact:
12
+ * { "agentType", "description", "toolUseId", "spawnDepth" }
13
+ * Its `toolUseId` ties the sub-agent transcript back to the parent turn's
14
+ * `Task`/`Agent` tool_use block, so we can render each sub-agent's activity
15
+ * underneath the tool block that launched it.
16
+ *
17
+ * herdctl deliberately filters these `isSidechain` files out of session
18
+ * discovery (they'd clutter the top-level chat list), and its parsed
19
+ * `ChatToolCall` carries neither the tool input nor the `toolUseId`. Rather than
20
+ * change upstream, we read the raw transcript here (paddock already owns the
21
+ * `.chats/` layout via `transcripts.ts`) and reuse core's exported
22
+ * `parseSessionMessages` — the expensive jsonl→messages parsing — on each
23
+ * sub-agent file. Discovery lives here; parsing stays in core.
24
+ */
25
+ import { promises as fs, createReadStream } from "node:fs";
26
+ import { createInterface } from "node:readline";
27
+ import path from "node:path";
28
+ import { parseSessionMessages } from "@herdctl/core";
29
+ import { projectChatsDir } from "./transcripts.js";
30
+ import { readSessionTokenUsage, readSessionTokenUsageFile, } from "./usage.js";
31
+ import { estimateCostUsdByModel } from "./models.js";
32
+ /** Tool names that launch a sub-agent. `Task` = classic Claude Code, `Agent` = Agent SDK. */
33
+ export const SUBAGENT_TOOL_NAMES = new Set(["Task", "Agent"]);
34
+ /**
35
+ * Extract every `Task`/`Agent` `tool_use` block from a single raw SDK message,
36
+ * with `subagent_type` + `description` read directly from the tool input. Returns
37
+ * [] for any message that carries no sub-agent launch (the common case). This is
38
+ * the live counterpart to {@link readTaskToolUses} (which recovers the same fields
39
+ * from the transcript on disk): it lets ws-turn enrich the live `chat:tool_start`
40
+ * / `chat:tool_call` frames as the launch streams, closing the "generic launch-ack
41
+ * live, only enriches on refresh" gap (#429). The message shape is loosely typed
42
+ * in core, so every field access is guarded.
43
+ */
44
+ export function extractSubagentLaunches(m) {
45
+ const content = m?.message?.content;
46
+ if (!Array.isArray(content))
47
+ return [];
48
+ const out = [];
49
+ for (const block of content) {
50
+ const b = block;
51
+ if (b?.type === "tool_use" && b.name && SUBAGENT_TOOL_NAMES.has(b.name) && typeof b.id === "string") {
52
+ out.push({
53
+ toolUseId: b.id,
54
+ subagentType: str(b.input?.subagent_type),
55
+ description: str(b.input?.description),
56
+ });
57
+ }
58
+ }
59
+ return out;
60
+ }
61
+ /** Both sessionId and toolUseId are path segments — keep them inside `.chats/`. */
62
+ export const SAFE_SEGMENT = /^[A-Za-z0-9._-]+$/;
63
+ // ---------------------------------------------------------------------------
64
+ // mtime-keyed memo (issue #147)
65
+ //
66
+ // Opening a sub-agent-bearing chat used to re-stream the whole main transcript a
67
+ // second time (to recover Task/Agent tool_use ids that core's parser drops) and
68
+ // read every sub-agent .jsonl in full — on *every* open, including a plain
69
+ // refresh of an unchanged chat. Transcripts are immutable except when a new turn
70
+ // appends (which bumps mtime), so memoize these per-file reads keyed on the
71
+ // file's mtime: a refresh of an unchanged chat now skips both the second parse
72
+ // and the sub-agent reads. Mirrors core's mtime caches (herdctl #351).
73
+ // ---------------------------------------------------------------------------
74
+ /**
75
+ * Max files retained per mtime cache.
76
+ *
77
+ * Was 64, which a real project blows straight through: one live-scale project
78
+ * holds 514 transcript files across 234 chats, so a single sweep of
79
+ * `/chats/usage` evicted every entry before the next sweep could reuse one — the
80
+ * cache was pure overhead. Measured on that corpus (issue #537), raising the cap
81
+ * took the warm full-project usage call from ~440ms to ~29ms.
82
+ *
83
+ * The entries are token tallies and tool-use records, not transcript text, so
84
+ * this is cheap: forcing every one of the 1,515 sessions in that corpus through
85
+ * both the usage and sub-agent-listing paths and then running GC retained
86
+ * **1.4 MB** at this cap versus **1.3 MB** at 64 — a ~0.1 MB difference for a
87
+ * 15× speedup.
88
+ */
89
+ export const MTIME_CACHE_MAX = 1024;
90
+ const taskUsesCache = new Map();
91
+ const durationCache = new Map();
92
+ const usageCache = new Map();
93
+ /** The file's mtime in epoch ms, or undefined if it can't be stat'd. */
94
+ export async function statMtimeMs(file) {
95
+ try {
96
+ return (await fs.stat(file)).mtimeMs;
97
+ }
98
+ catch {
99
+ return undefined;
100
+ }
101
+ }
102
+ /** Return the cached value if the recorded mtime still matches (LRU-touch on hit). */
103
+ export function mtimeCacheGet(cache, file, mtimeMs) {
104
+ const entry = cache.get(file);
105
+ if (entry && entry.mtimeMs === mtimeMs) {
106
+ cache.delete(file);
107
+ cache.set(file, entry); // re-insert to mark most-recently-used
108
+ return { hit: true, value: entry.value };
109
+ }
110
+ return { hit: false };
111
+ }
112
+ /** Store a value against the file's mtime, evicting the least-recently-used past the cap. */
113
+ export function mtimeCacheSet(cache, file, mtimeMs, value) {
114
+ cache.set(file, { mtimeMs, value });
115
+ while (cache.size > MTIME_CACHE_MAX) {
116
+ const oldest = cache.keys().next().value;
117
+ if (oldest === undefined)
118
+ break;
119
+ cache.delete(oldest);
120
+ }
121
+ }
122
+ /**
123
+ * Stream the main session transcript and recover, in file order, every
124
+ * `Task`/`Agent` tool_use that was **paired** with a tool_result. Pairing is
125
+ * tracked so the returned list aligns 1:1 with herdctl's parsed tool messages
126
+ * for completed calls.
127
+ *
128
+ * Paired-only by design — {@link readTaskUsesFromFile} exposes the in-flight
129
+ * launches separately, and mixing them here would break that alignment.
130
+ */
131
+ export async function readTaskToolUses(projectDir, sessionId) {
132
+ return (await readTaskUsesForSession(projectDir, sessionId)).paired;
133
+ }
134
+ /**
135
+ * Both halves of a session's Task/Agent launches. `sessionId` is a path segment,
136
+ * so it is validated here — every caller reaches the transcript through this.
137
+ */
138
+ async function readTaskUsesForSession(projectDir, sessionId) {
139
+ if (!SAFE_SEGMENT.test(sessionId))
140
+ return { paired: [], pending: [] };
141
+ return readTaskUsesFromFile(path.join(projectChatsDir(projectDir), `${sessionId}.jsonl`));
142
+ }
143
+ /**
144
+ * Recover Task/Agent tool_uses (in order, split paired vs in-flight) from an
145
+ * arbitrary transcript file — the main session file, or a sub-agent's own
146
+ * transcript (which lets a sub-agent's nested launches be enriched for recursive
147
+ * expansion).
148
+ */
149
+ async function readTaskUsesFromFile(file) {
150
+ const mtimeMs = await statMtimeMs(file);
151
+ if (mtimeMs !== undefined) {
152
+ const cached = mtimeCacheGet(taskUsesCache, file, mtimeMs);
153
+ if (cached.hit)
154
+ return cached.value;
155
+ }
156
+ const value = await readTaskUsesFromFileUncached(file);
157
+ if (mtimeMs !== undefined)
158
+ mtimeCacheSet(taskUsesCache, file, mtimeMs, value);
159
+ return value;
160
+ }
161
+ async function readTaskUsesFromFileUncached(file) {
162
+ const byId = new Map();
163
+ const order = [];
164
+ const resultIds = new Set();
165
+ const stream = createReadStream(file, { encoding: "utf8" });
166
+ stream.on("error", () => undefined);
167
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
168
+ try {
169
+ for await (const line of rl) {
170
+ const trimmed = line.trim();
171
+ if (!trimmed)
172
+ continue;
173
+ let parsed;
174
+ try {
175
+ parsed = JSON.parse(trimmed);
176
+ }
177
+ catch {
178
+ continue;
179
+ }
180
+ const content = parsed.message?.content;
181
+ if (!Array.isArray(content))
182
+ continue;
183
+ for (const block of content) {
184
+ const b = block;
185
+ if (b?.type === "tool_use" && b.name && SUBAGENT_TOOL_NAMES.has(b.name) && b.id) {
186
+ if (!byId.has(b.id)) {
187
+ order.push(b.id);
188
+ byId.set(b.id, {
189
+ toolUseId: b.id,
190
+ subagentType: str(b.input?.subagent_type),
191
+ description: str(b.input?.description),
192
+ prompt: str(b.input?.prompt),
193
+ });
194
+ }
195
+ }
196
+ else if (b?.type === "tool_result" && typeof b.tool_use_id === "string") {
197
+ resultIds.add(b.tool_use_id);
198
+ }
199
+ }
200
+ }
201
+ }
202
+ catch {
203
+ return { paired: [], pending: [] };
204
+ }
205
+ finally {
206
+ rl.close();
207
+ stream.destroy();
208
+ }
209
+ return {
210
+ paired: order.filter((id) => resultIds.has(id)).map((id) => byId.get(id)),
211
+ pending: order.filter((id) => !resultIds.has(id)).map((id) => byId.get(id)),
212
+ };
213
+ }
214
+ /**
215
+ * Live sub-agent enrichment for a tool frame (issue #429). When the tool is a
216
+ * `Task`/`Agent` launch this turn already recovered (via {@link extractSubagentLaunches}),
217
+ * return the real sub-agent type + title plus `hasSubagent: true`, so the client
218
+ * renders the enriched, expandable card the moment the launch streams — instead of
219
+ * the generic "Agent · <ms>" launch-ack that only filled in on refresh. A safe
220
+ * spread for any non-sub-agent tool (returns an empty object). `hasSubagent` is set
221
+ * optimistically: a `Task`/`Agent` launch is expected to write a sub-agent
222
+ * transcript, and the client's expand path degrades gracefully (a "waiting…"
223
+ * placeholder) until the sidecar appears on disk.
224
+ */
225
+ export function subagentLaunchFields(launches, toolName, toolUseId) {
226
+ if (!toolUseId || !SUBAGENT_TOOL_NAMES.has(toolName))
227
+ return {};
228
+ const l = launches.get(toolUseId);
229
+ return { subagentType: l?.subagentType, description: l?.description, hasSubagent: true };
230
+ }
231
+ /**
232
+ * Read the `subagents/*.meta.json` sidecars for a session, keyed by `toolUseId`.
233
+ * Returns an empty map when the session has no sub-agents (the common case) or
234
+ * the directory is missing/unreadable.
235
+ */
236
+ export async function listSubagents(projectDir, sessionId) {
237
+ const out = new Map();
238
+ if (!SAFE_SEGMENT.test(sessionId))
239
+ return out;
240
+ const dir = path.join(projectChatsDir(projectDir), sessionId, "subagents");
241
+ const entries = await fs.readdir(dir).catch(() => []);
242
+ for (const entry of entries) {
243
+ if (!entry.endsWith(".meta.json"))
244
+ continue;
245
+ const raw = await fs.readFile(path.join(dir, entry), "utf8").catch(() => null);
246
+ if (raw === null)
247
+ continue;
248
+ let meta;
249
+ try {
250
+ meta = JSON.parse(raw);
251
+ }
252
+ catch {
253
+ continue;
254
+ }
255
+ if (typeof meta.toolUseId !== "string")
256
+ continue;
257
+ // This sub-agent's own id is the `<hex>` of its `agent-<hex>.meta.json` filename
258
+ // (the sidecar records only its PARENT's id, not its own); `parentAgentId` links
259
+ // it to the sub-agent that spawned it. Together they build the tree for the cost
260
+ // rollup. An empty id (a malformed name) is dropped to undefined.
261
+ const agentId = entry.replace(/^agent-/, "").replace(/\.meta\.json$/, "") || undefined;
262
+ out.set(meta.toolUseId, {
263
+ toolUseId: meta.toolUseId,
264
+ agentType: str(meta.agentType),
265
+ description: str(meta.description),
266
+ spawnDepth: typeof meta.spawnDepth === "number" ? meta.spawnDepth : undefined,
267
+ agentId,
268
+ parentAgentId: str(meta.parentAgentId),
269
+ // The transcript is the sidecar's sibling: agent-<hex>.meta.json → agent-<hex>.jsonl
270
+ transcriptPath: path.join(dir, entry.replace(/\.meta\.json$/, ".jsonl")),
271
+ });
272
+ }
273
+ return out;
274
+ }
275
+ /**
276
+ * Parse a single sub-agent's transcript into messages, reusing core's parser.
277
+ * Returns [] when the `toolUseId` doesn't correspond to a known sub-agent or the
278
+ * transcript is missing. Both segments are validated to stay inside `.chats/`.
279
+ */
280
+ export async function readSubagentMessages(projectDir, sessionId, toolUseId) {
281
+ if (!SAFE_SEGMENT.test(sessionId) || !SAFE_SEGMENT.test(toolUseId))
282
+ return [];
283
+ const subagents = await listSubagents(projectDir, sessionId);
284
+ const meta = subagents.get(toolUseId);
285
+ if (!meta)
286
+ return [];
287
+ const messages = await parseSessionMessages(meta.transcriptPath).catch(() => []);
288
+ if (!hasSubagentTool(messages))
289
+ return messages;
290
+ // A sub-agent may itself spawn sub-agents (spawnDepth > 1). Their sidecars are
291
+ // flat in the SAME session's subagents/ dir, so enriching this transcript's own
292
+ // Task/Agent blocks against `subagents` lets the UI expand them recursively.
293
+ const taskUses = await readTaskUsesFromFile(meta.transcriptPath);
294
+ const [durations, costs] = await Promise.all([
295
+ subagentDurations(subagents),
296
+ subagentCosts(subagents),
297
+ ]);
298
+ return attachSubagentFields(messages, taskUses, subagents, durations, costs);
299
+ }
300
+ /**
301
+ * Enrich a session's parsed messages: attach the recovered sub-agent fields
302
+ * (`toolUseId`, `subagentType`, `description`, `prompt`, `hasSubagent`) to every
303
+ * `Task`/`Agent` tool message, joining by file order. Non-Task tool calls and
304
+ * sessions without sub-agents pass through unchanged (a cheap early return).
305
+ */
306
+ export async function enrichWithSubagents(projectDir, sessionId, messages) {
307
+ if (!hasSubagentTool(messages))
308
+ return messages;
309
+ const [taskUses, subagents] = await Promise.all([
310
+ readTaskUsesForSession(projectDir, sessionId),
311
+ listSubagents(projectDir, sessionId),
312
+ ]);
313
+ const [durations, costs] = await Promise.all([
314
+ subagentDurations(subagents),
315
+ subagentCosts(subagents),
316
+ ]);
317
+ return attachSubagentFields(messages, taskUses, subagents, durations, costs);
318
+ }
319
+ /**
320
+ * Compute each sub-agent's run time (ms) from the first→last timestamp in its
321
+ * own transcript. Keyed by toolUseId. Runs once per session on chat open (only
322
+ * over that session's sub-agent files), so the cost is bounded to the open chat.
323
+ */
324
+ async function subagentDurations(subagents) {
325
+ const out = new Map();
326
+ await Promise.all([...subagents.values()].map(async (m) => {
327
+ const ms = await readSubagentDurationMs(m.transcriptPath);
328
+ if (ms !== undefined)
329
+ out.set(m.toolUseId, ms);
330
+ }));
331
+ return out;
332
+ }
333
+ /**
334
+ * The elapsed wall-clock of a sub-agent transcript: the delta between its first
335
+ * and last `timestamp`. Scans for timestamps without a full JSON parse; returns
336
+ * undefined if the file is missing or has < 2 timestamps.
337
+ */
338
+ async function readSubagentDurationMs(file) {
339
+ const mtimeMs = await statMtimeMs(file);
340
+ if (mtimeMs !== undefined) {
341
+ const cached = mtimeCacheGet(durationCache, file, mtimeMs);
342
+ if (cached.hit)
343
+ return cached.value;
344
+ }
345
+ const value = await readSubagentDurationMsUncached(file);
346
+ // Cache even undefined (missing/malformed) so a repeat open of an unchanged,
347
+ // duration-less transcript doesn't re-read the whole file every time.
348
+ if (mtimeMs !== undefined)
349
+ mtimeCacheSet(durationCache, file, mtimeMs, value);
350
+ return value;
351
+ }
352
+ async function readSubagentDurationMsUncached(file) {
353
+ const raw = await fs.readFile(file, "utf8").catch(() => null);
354
+ if (raw === null)
355
+ return undefined;
356
+ const matches = [...raw.matchAll(/"timestamp":"([^"]+)"/g)];
357
+ if (matches.length < 2)
358
+ return undefined;
359
+ const first = Date.parse(matches[0][1]);
360
+ const last = Date.parse(matches[matches.length - 1][1]);
361
+ if (Number.isNaN(first) || Number.isNaN(last) || last < first)
362
+ return undefined;
363
+ return last - first;
364
+ }
365
+ /**
366
+ * Compute each sub-agent's estimated API-rate cost (USD), keyed by toolUseId, as
367
+ * the RECURSIVE total of the sub-agent PLUS every sub-agent it (transitively)
368
+ * spawned. Runs once per session on chat open, over that session's sub-agent files.
369
+ *
370
+ * The sub-agent tree comes from the `.meta.json` sidecars: each carries its
371
+ * `parentAgentId`, and its own agent id is its `agent-<hex>` transcript name (see
372
+ * {@link listSubagents}). For each node we merge the per-model token usage across
373
+ * its whole subtree, then price once ({@link estimateCostUsdByModel}). Merging
374
+ * TOKEN USAGE (rather than summing already-priced dollars) is what keeps an
375
+ * unpriced-model descendant from zeroing out its priced neighbours: the result is
376
+ * `null` only when the entire subtree has no priceable usage (e.g. a leaf that ran
377
+ * on a model with no known pricing and spawned nothing), and a number otherwise.
378
+ *
379
+ * COST ONLY. Duration is deliberately NOT rolled up ({@link subagentDurations}
380
+ * stays per-agent): nested children run in PARALLEL, so their wall-clock overlaps
381
+ * the parent's and each other's — summing durations would badly overstate elapsed
382
+ * time. Dollar cost, by contrast, is additive regardless of concurrency, so a
383
+ * subtree total is meaningful. The chat grand-total
384
+ * ({@link readSessionTokenUsageWithSubagents}) is a SEPARATE value that already
385
+ * flat-sums every sub-agent once (issue #242) — untouched here, and no
386
+ * double-count, since these per-card subtree totals are never summed together.
387
+ */
388
+ async function subagentCosts(subagents) {
389
+ const metas = [...subagents.values()];
390
+ // 1. Each sub-agent's OWN per-model token usage (parallel, mtime-memoized reads).
391
+ const ownByModel = new Map();
392
+ await Promise.all(metas.map(async (m) => {
393
+ const usage = await readSubagentUsageFile(m.transcriptPath);
394
+ ownByModel.set(m.toolUseId, usage.byModel);
395
+ }));
396
+ // 2. Build the tree: agentId → meta, and parentAgentId → child agentIds. A
397
+ // depth-1 sub-agent has no parentAgentId (its parent is the main session), so
398
+ // it's a root; only links to a KNOWN sub-agent id form an edge.
399
+ const metaByAgentId = new Map();
400
+ for (const m of metas)
401
+ if (m.agentId)
402
+ metaByAgentId.set(m.agentId, m);
403
+ const childrenOf = new Map();
404
+ for (const m of metas) {
405
+ if (m.agentId && m.parentAgentId && metaByAgentId.has(m.parentAgentId)) {
406
+ const kids = childrenOf.get(m.parentAgentId) ?? [];
407
+ kids.push(m.agentId);
408
+ childrenOf.set(m.parentAgentId, kids);
409
+ }
410
+ }
411
+ // 3. For each node, DFS its subtree (self + all transitive descendants), merging
412
+ // per-model usage, then price once. `seen` guards against a malformed cyclic link.
413
+ const out = new Map();
414
+ for (const m of metas) {
415
+ const merged = {};
416
+ const seen = new Set();
417
+ const visit = (toolUseId, agentId) => {
418
+ mergeByModelInto(merged, ownByModel.get(toolUseId) ?? {});
419
+ if (!agentId)
420
+ return;
421
+ for (const childId of childrenOf.get(agentId) ?? []) {
422
+ if (seen.has(childId))
423
+ continue;
424
+ seen.add(childId);
425
+ const child = metaByAgentId.get(childId);
426
+ if (child)
427
+ visit(child.toolUseId, child.agentId);
428
+ }
429
+ };
430
+ if (m.agentId)
431
+ seen.add(m.agentId);
432
+ visit(m.toolUseId, m.agentId);
433
+ out.set(m.toolUseId, estimateCostUsdByModel(merged));
434
+ }
435
+ return out;
436
+ }
437
+ /** Add one per-model usage map into a running accumulator (mirrors the fold in
438
+ * {@link mergeSubagentUsage}). Used by the recursive cost rollup. */
439
+ function mergeByModelInto(target, src) {
440
+ for (const [model, t] of Object.entries(src)) {
441
+ const b = (target[model] ??= {
442
+ inputTokens: 0,
443
+ outputTokens: 0,
444
+ cacheReadTokens: 0,
445
+ cacheCreationTokens: 0,
446
+ });
447
+ b.inputTokens += t.inputTokens;
448
+ b.outputTokens += t.outputTokens;
449
+ b.cacheReadTokens += t.cacheReadTokens;
450
+ b.cacheCreationTokens += t.cacheCreationTokens;
451
+ }
452
+ }
453
+ /**
454
+ * Cumulative token usage of a single sub-agent transcript, memoized on its mtime.
455
+ * The shared primitive behind both the per-card cost rollup ({@link subagentCosts})
456
+ * and the chat-total roll-up ({@link readSessionTokenUsageWithSubagents}), so a
457
+ * chat open parses each sub-agent file at most once.
458
+ */
459
+ async function readSubagentUsageFile(file) {
460
+ const mtimeMs = await statMtimeMs(file);
461
+ if (mtimeMs !== undefined) {
462
+ const cached = mtimeCacheGet(usageCache, file, mtimeMs);
463
+ if (cached.hit)
464
+ return cached.value;
465
+ }
466
+ const usage = await readSessionTokenUsageFile(file);
467
+ if (mtimeMs !== undefined)
468
+ mtimeCacheSet(usageCache, file, mtimeMs, usage);
469
+ return usage;
470
+ }
471
+ /**
472
+ * A chat's cumulative token usage *including every sub-agent it spawned* (issue
473
+ * #242).
474
+ *
475
+ * The per-chat cost/token headline (issue #152) originally priced only the main
476
+ * transcript, but a chat's `Task`/`Agent` sub-agents run in their own sibling
477
+ * transcripts (`.chats/<sessionId>/subagents/agent-*.jsonl`) — so a chat that
478
+ * fanned work out to sub-agents under-reported its true spend, sometimes by ~90%.
479
+ * This folds those transcripts back in: the four token totals and the per-model
480
+ * buckets gain every sub-agent's usage, so both the token figure and the
481
+ * per-model-priced dollar estimate reflect the whole chat.
482
+ *
483
+ * `contextTokens` and `turnCount` stay main-only: those describe the parent's
484
+ * last-turn context-window fill (what the ContextMeter shows), a distinct concept
485
+ * from lifetime spend — each sub-agent has its own, separate context window.
486
+ *
487
+ * No double-counting: herdctl writes sub-agent (`isSidechain`) turns to the
488
+ * sibling files, never into the main transcript, so `main + sub-agents` is a
489
+ * clean sum. Nested sub-agents (spawnDepth > 1) sit flat in the same `subagents/`
490
+ * dir, so their usage is included too. Returns the main usage unchanged when the
491
+ * chat spawned no sub-agents (the common case — a cheap early return).
492
+ */
493
+ export async function readSessionTokenUsageWithSubagents(projectDir, sessionId) {
494
+ const main = await readSessionTokenUsage(projectDir, sessionId);
495
+ if (!SAFE_SEGMENT.test(sessionId))
496
+ return main;
497
+ const dir = path.join(projectChatsDir(projectDir), sessionId, "subagents");
498
+ const entries = await fs.readdir(dir).catch(() => []);
499
+ const files = entries.filter((e) => e.endsWith(".jsonl"));
500
+ if (files.length === 0)
501
+ return main;
502
+ const subUsages = await Promise.all(files.map((f) => readSubagentUsageFile(path.join(dir, f))));
503
+ return mergeSubagentUsage(main, subUsages);
504
+ }
505
+ /**
506
+ * Fold sub-agent usages into a *copy* of the main usage. Builds fresh token
507
+ * buckets rather than mutating `main.byModel` / the sub-agents' cached objects —
508
+ * both come straight out of mtime caches and must stay pristine. Pricing stays
509
+ * per-model (buckets keyed by `message.model`), so a sub-agent that ran on a
510
+ * different, pricier model than the parent is costed at its own rate.
511
+ */
512
+ function mergeSubagentUsage(main, subs) {
513
+ const byModel = {};
514
+ const add = (model, t) => {
515
+ const b = (byModel[model] ??= {
516
+ inputTokens: 0,
517
+ outputTokens: 0,
518
+ cacheReadTokens: 0,
519
+ cacheCreationTokens: 0,
520
+ });
521
+ b.inputTokens += t.inputTokens;
522
+ b.outputTokens += t.outputTokens;
523
+ b.cacheReadTokens += t.cacheReadTokens;
524
+ b.cacheCreationTokens += t.cacheCreationTokens;
525
+ };
526
+ for (const [model, t] of Object.entries(main.byModel))
527
+ add(model, t);
528
+ let { hasData, inputTotal, outputTotal, cacheReadTotal, cacheCreationTotal } = main;
529
+ for (const s of subs) {
530
+ if (!s.hasData)
531
+ continue;
532
+ hasData = true;
533
+ inputTotal += s.inputTotal;
534
+ outputTotal += s.outputTotal;
535
+ cacheReadTotal += s.cacheReadTotal;
536
+ cacheCreationTotal += s.cacheCreationTotal;
537
+ for (const [model, t] of Object.entries(s.byModel))
538
+ add(model, t);
539
+ }
540
+ return {
541
+ hasData,
542
+ turnCount: main.turnCount,
543
+ contextTokens: main.contextTokens,
544
+ inputTotal,
545
+ outputTotal,
546
+ cacheReadTotal,
547
+ cacheCreationTotal,
548
+ byModel,
549
+ };
550
+ }
551
+ /** True when any message is a Task/Agent tool call worth enriching. */
552
+ function hasSubagentTool(messages) {
553
+ return messages.some((m) => m.toolCall && SUBAGENT_TOOL_NAMES.has(m.toolCall.toolName));
554
+ }
555
+ /**
556
+ * Attach recovered sub-agent fields to each Task/Agent tool message by joining
557
+ * against `taskUses` in file order (both are file-ordered; only paired uses are
558
+ * present, so the join stays aligned). Other messages pass through untouched.
559
+ */
560
+ function attachSubagentFields(messages, taskUses, subagents, durations, costs) {
561
+ let i = 0;
562
+ let p = 0;
563
+ return messages.map((m) => {
564
+ if (!m.toolCall || !SUBAGENT_TOOL_NAMES.has(m.toolCall.toolName))
565
+ return m;
566
+ // An in-flight (`pending`) tool_use has no matching tool_result yet, and
567
+ // core@5.24.0 injects it into the parsed stream so it stays visible on
568
+ // rehydration (herdctl#399). It is joined off its OWN cursor against the
569
+ // in-flight launches, never off the paired one — consuming a paired slot here
570
+ // would shift every following completed sub-agent's enrichment onto the wrong
571
+ // call (the parallel case: a running Agent inheriting a finished sibling's
572
+ // `toolUseId`/`hasSubagent` and wrongly rendering as expandable).
573
+ //
574
+ // Enriching it at all is what keeps the running-sub-agents bar alive across a
575
+ // re-mount: the bar's candidates need `toolUseId` + `hasSubagent`, which the
576
+ // live `chat:tool_start` frame supplies but rehydration used to drop — so
577
+ // navigating away from a running chat and back emptied the bar for the rest of
578
+ // the run, with no way to recover short of the sub-agent finishing (#622).
579
+ if (m.toolCall.pending) {
580
+ const use = taskUses.pending[p++];
581
+ if (!use)
582
+ return m;
583
+ return {
584
+ ...m,
585
+ toolCall: {
586
+ ...m.toolCall,
587
+ toolUseId: use.toolUseId,
588
+ subagentType: use.subagentType,
589
+ description: use.description,
590
+ prompt: use.prompt,
591
+ // Optimistic, exactly as the live path does (see `subagentLaunchFields`):
592
+ // the sidecar may not be on disk for the first moment of the run, and the
593
+ // client's expand/poll paths already degrade to "starting…" until it is.
594
+ hasSubagent: true,
595
+ // Deliberately NO `subagentDurationMs`: it is derived from the first→last
596
+ // timestamp of a transcript that is still growing, so setting it here
597
+ // would both freeze a bogus part-way duration onto the card AND mark the
598
+ // sub-agent finished — `useRunningSubagents` drops any card that has one.
599
+ },
600
+ };
601
+ }
602
+ const use = taskUses.paired[i++];
603
+ if (!use)
604
+ return m;
605
+ return {
606
+ ...m,
607
+ toolCall: {
608
+ ...m.toolCall,
609
+ toolUseId: use.toolUseId,
610
+ subagentType: use.subagentType,
611
+ description: use.description,
612
+ prompt: use.prompt,
613
+ hasSubagent: subagents.has(use.toolUseId),
614
+ subagentDurationMs: durations.get(use.toolUseId),
615
+ subagentCostUsd: costs.get(use.toolUseId),
616
+ },
617
+ };
618
+ });
619
+ }
620
+ /** Coerce a JSON value to a trimmed non-empty string, else undefined. */
621
+ function str(v) {
622
+ if (typeof v !== "string")
623
+ return undefined;
624
+ const t = v.trim();
625
+ return t.length ? t : undefined;
626
+ }