@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.
- package/README.md +69 -0
- package/install-notice.mjs +29 -0
- package/package.json +58 -5
- package/packages/server/dist/adoptable.d.ts +127 -0
- package/packages/server/dist/adoptable.js +315 -0
- package/packages/server/dist/app.d.ts +67 -0
- package/packages/server/dist/app.js +325 -0
- package/packages/server/dist/archive.d.ts +42 -0
- package/packages/server/dist/archive.js +125 -0
- package/packages/server/dist/attachments-config.d.ts +114 -0
- package/packages/server/dist/attachments-config.js +165 -0
- package/packages/server/dist/attachments-hint.d.ts +53 -0
- package/packages/server/dist/attachments-hint.js +140 -0
- package/packages/server/dist/attachments.d.ts +33 -0
- package/packages/server/dist/attachments.js +117 -0
- package/packages/server/dist/auth.d.ts +84 -0
- package/packages/server/dist/auth.js +247 -0
- package/packages/server/dist/background.d.ts +36 -0
- package/packages/server/dist/background.js +137 -0
- package/packages/server/dist/bind-safety.d.ts +56 -0
- package/packages/server/dist/bind-safety.js +51 -0
- package/packages/server/dist/brand.d.ts +44 -0
- package/packages/server/dist/brand.js +88 -0
- package/packages/server/dist/chat-dto.d.ts +210 -0
- package/packages/server/dist/chat-dto.js +308 -0
- package/packages/server/dist/cli/import-chats.d.ts +21 -0
- package/packages/server/dist/cli/import-chats.js +197 -0
- package/packages/server/dist/cli/paddock.d.ts +2 -0
- package/packages/server/dist/cli/paddock.js +247 -0
- package/packages/server/dist/concurrency.d.ts +57 -0
- package/packages/server/dist/concurrency.js +71 -0
- package/packages/server/dist/config.d.ts +538 -0
- package/packages/server/dist/config.js +625 -0
- package/packages/server/dist/curation-config.d.ts +55 -0
- package/packages/server/dist/curation-config.js +64 -0
- package/packages/server/dist/environment-prompt.d.ts +52 -0
- package/packages/server/dist/environment-prompt.js +61 -0
- package/packages/server/dist/event-bus.d.ts +87 -0
- package/packages/server/dist/event-bus.js +48 -0
- package/packages/server/dist/git.d.ts +128 -0
- package/packages/server/dist/git.js +446 -0
- package/packages/server/dist/github-auth.d.ts +44 -0
- package/packages/server/dist/github-auth.js +169 -0
- package/packages/server/dist/herdctl-agent-config.d.ts +98 -0
- package/packages/server/dist/herdctl-agent-config.js +331 -0
- package/packages/server/dist/herdctl-agent-names.d.ts +172 -0
- package/packages/server/dist/herdctl-agent-names.js +228 -0
- package/packages/server/dist/herdctl-jobs-index.d.ts +65 -0
- package/packages/server/dist/herdctl-jobs-index.js +225 -0
- package/packages/server/dist/herdctl-jobs.d.ts +98 -0
- package/packages/server/dist/herdctl-jobs.js +225 -0
- package/packages/server/dist/herdctl.d.ts +623 -0
- package/packages/server/dist/herdctl.js +1279 -0
- package/packages/server/dist/hook-config.d.ts +217 -0
- package/packages/server/dist/hook-config.js +224 -0
- package/packages/server/dist/http-bytes.d.ts +24 -0
- package/packages/server/dist/http-bytes.js +52 -0
- package/packages/server/dist/index.d.ts +1 -0
- package/packages/server/dist/index.js +14 -0
- package/packages/server/dist/instance-config.d.ts +143 -0
- package/packages/server/dist/instance-config.js +385 -0
- package/packages/server/dist/localcommand.d.ts +23 -0
- package/packages/server/dist/localcommand.js +140 -0
- package/packages/server/dist/management-auth.d.ts +99 -0
- package/packages/server/dist/management-auth.js +165 -0
- package/packages/server/dist/management-config.d.ts +164 -0
- package/packages/server/dist/management-config.js +286 -0
- package/packages/server/dist/management-mcp-server.d.ts +20 -0
- package/packages/server/dist/management-mcp-server.js +95 -0
- package/packages/server/dist/management-metadata.d.ts +67 -0
- package/packages/server/dist/management-metadata.js +56 -0
- package/packages/server/dist/management-ops.d.ts +110 -0
- package/packages/server/dist/management-ops.js +449 -0
- package/packages/server/dist/management-policy.d.ts +172 -0
- package/packages/server/dist/management-policy.js +259 -0
- package/packages/server/dist/message-provenance.d.ts +112 -0
- package/packages/server/dist/message-provenance.js +260 -0
- package/packages/server/dist/models.d.ts +164 -0
- package/packages/server/dist/models.js +208 -0
- package/packages/server/dist/openapi.d.ts +66 -0
- package/packages/server/dist/openapi.js +183 -0
- package/packages/server/dist/parent-detach.d.ts +30 -0
- package/packages/server/dist/parent-detach.js +109 -0
- package/packages/server/dist/preload.d.ts +43 -0
- package/packages/server/dist/preload.js +55 -0
- package/packages/server/dist/project-files.d.ts +76 -0
- package/packages/server/dist/project-files.js +191 -0
- package/packages/server/dist/project-mime.d.ts +35 -0
- package/packages/server/dist/project-mime.js +55 -0
- package/packages/server/dist/project-paths.d.ts +88 -0
- package/packages/server/dist/project-paths.js +126 -0
- package/packages/server/dist/project-types.d.ts +316 -0
- package/packages/server/dist/project-types.js +48 -0
- package/packages/server/dist/projects.d.ts +202 -0
- package/packages/server/dist/projects.js +894 -0
- package/packages/server/dist/queued-message.d.ts +35 -0
- package/packages/server/dist/queued-message.js +116 -0
- package/packages/server/dist/read-state.d.ts +46 -0
- package/packages/server/dist/read-state.js +139 -0
- package/packages/server/dist/recovery-config.d.ts +111 -0
- package/packages/server/dist/recovery-config.js +105 -0
- package/packages/server/dist/recovery.d.ts +262 -0
- package/packages/server/dist/recovery.js +490 -0
- package/packages/server/dist/resume-drain.d.ts +122 -0
- package/packages/server/dist/resume-drain.js +158 -0
- package/packages/server/dist/route-context.d.ts +168 -0
- package/packages/server/dist/route-context.js +39 -0
- package/packages/server/dist/route-errors.d.ts +8 -0
- package/packages/server/dist/route-errors.js +13 -0
- package/packages/server/dist/routes/chats.d.ts +12 -0
- package/packages/server/dist/routes/chats.js +1406 -0
- package/packages/server/dist/routes/git.d.ts +13 -0
- package/packages/server/dist/routes/git.js +236 -0
- package/packages/server/dist/routes/mcp.d.ts +83 -0
- package/packages/server/dist/routes/mcp.js +250 -0
- package/packages/server/dist/routes/meta.d.ts +15 -0
- package/packages/server/dist/routes/meta.js +447 -0
- package/packages/server/dist/routes/projects.d.ts +25 -0
- package/packages/server/dist/routes/projects.js +736 -0
- package/packages/server/dist/routes/triggers.d.ts +21 -0
- package/packages/server/dist/routes/triggers.js +324 -0
- package/packages/server/dist/routes/workspace-mount.d.ts +48 -0
- package/packages/server/dist/routes/workspace-mount.js +32 -0
- package/packages/server/dist/routes.d.ts +26 -0
- package/packages/server/dist/routes.js +29 -0
- package/packages/server/dist/run-provenance.d.ts +109 -0
- package/packages/server/dist/run-provenance.js +206 -0
- package/packages/server/dist/runs.d.ts +92 -0
- package/packages/server/dist/runs.js +72 -0
- package/packages/server/dist/schedule-config.d.ts +68 -0
- package/packages/server/dist/schedule-config.js +75 -0
- package/packages/server/dist/schedule-session.d.ts +34 -0
- package/packages/server/dist/schedule-session.js +120 -0
- package/packages/server/dist/self-mcp-descriptions.d.ts +20 -0
- package/packages/server/dist/self-mcp-descriptions.js +145 -0
- package/packages/server/dist/self-mcp-read.d.ts +15 -0
- package/packages/server/dist/self-mcp-read.js +97 -0
- package/packages/server/dist/self-mcp-triggers.d.ts +30 -0
- package/packages/server/dist/self-mcp-triggers.js +144 -0
- package/packages/server/dist/self-mcp-types.d.ts +248 -0
- package/packages/server/dist/self-mcp-types.js +10 -0
- package/packages/server/dist/self-mcp-util.d.ts +73 -0
- package/packages/server/dist/self-mcp-util.js +157 -0
- package/packages/server/dist/self-mcp-write.d.ts +35 -0
- package/packages/server/dist/self-mcp-write.js +215 -0
- package/packages/server/dist/self-mcp.d.ts +95 -0
- package/packages/server/dist/self-mcp.js +448 -0
- package/packages/server/dist/send-file-mcp.d.ts +46 -0
- package/packages/server/dist/send-file-mcp.js +281 -0
- package/packages/server/dist/session-hub.d.ts +186 -0
- package/packages/server/dist/session-hub.js +291 -0
- package/packages/server/dist/spawn-capability.d.ts +99 -0
- package/packages/server/dist/spawn-capability.js +106 -0
- package/packages/server/dist/star.d.ts +26 -0
- package/packages/server/dist/star.js +94 -0
- package/packages/server/dist/start.d.ts +1 -0
- package/packages/server/dist/start.js +21 -0
- package/packages/server/dist/subagents.d.ts +337 -0
- package/packages/server/dist/subagents.js +626 -0
- package/packages/server/dist/sweep.d.ts +122 -0
- package/packages/server/dist/sweep.js +696 -0
- package/packages/server/dist/tooldetails.d.ts +33 -0
- package/packages/server/dist/tooldetails.js +466 -0
- package/packages/server/dist/transcribe.d.ts +69 -0
- package/packages/server/dist/transcribe.js +119 -0
- package/packages/server/dist/transcripts.d.ts +42 -0
- package/packages/server/dist/transcripts.js +164 -0
- package/packages/server/dist/trigger-config.d.ts +278 -0
- package/packages/server/dist/trigger-config.js +417 -0
- package/packages/server/dist/trigger-runtime.d.ts +94 -0
- package/packages/server/dist/trigger-runtime.js +82 -0
- package/packages/server/dist/trigger-session.d.ts +34 -0
- package/packages/server/dist/trigger-session.js +122 -0
- package/packages/server/dist/triggers.d.ts +83 -0
- package/packages/server/dist/triggers.js +124 -0
- package/packages/server/dist/trusted-proxy.d.ts +57 -0
- package/packages/server/dist/trusted-proxy.js +157 -0
- package/packages/server/dist/turn-notice.d.ts +154 -0
- package/packages/server/dist/turn-notice.js +271 -0
- package/packages/server/dist/unread.d.ts +36 -0
- package/packages/server/dist/unread.js +123 -0
- package/packages/server/dist/usage.d.ts +66 -0
- package/packages/server/dist/usage.js +243 -0
- package/packages/server/dist/wake-injection.d.ts +156 -0
- package/packages/server/dist/wake-injection.js +136 -0
- package/packages/server/dist/whisper-local.d.ts +9 -0
- package/packages/server/dist/whisper-local.js +122 -0
- package/packages/server/dist/ws-context.d.ts +158 -0
- package/packages/server/dist/ws-context.js +1 -0
- package/packages/server/dist/ws-protocol.d.ts +423 -0
- package/packages/server/dist/ws-protocol.js +251 -0
- package/packages/server/dist/ws-self-mcp.d.ts +52 -0
- package/packages/server/dist/ws-self-mcp.js +28 -0
- package/packages/server/dist/ws-triggers.d.ts +29 -0
- package/packages/server/dist/ws-triggers.js +261 -0
- package/packages/server/dist/ws-turn.d.ts +86 -0
- package/packages/server/dist/ws-turn.js +751 -0
- package/packages/server/dist/ws.d.ts +62 -0
- package/packages/server/dist/ws.js +1024 -0
- package/packages/server/package.json +6 -0
- package/packages/web/dist/assets/InstanceConfigPage-Bypd_quL.js +2 -0
- package/packages/web/dist/assets/MarkdownRenderer-Co8w9iuX.js +30 -0
- package/packages/web/dist/assets/ProjectRedirect-DXrA24Cy.js +2 -0
- package/packages/web/dist/assets/ProjectView-B9opP7Sv.js +13 -0
- package/packages/web/dist/assets/ProjectsGrid-D2cwb2jY.js +2 -0
- package/packages/web/dist/assets/arc-B--FIRB8.js +2 -0
- package/packages/web/dist/assets/architectureDiagram-3BPJPVTR-B6dRWZZ6.js +37 -0
- package/packages/web/dist/assets/bash-I8pq0VWm.js +2 -0
- package/packages/web/dist/assets/blockDiagram-GPEHLZMM-DLxFAvvf.js +133 -0
- package/packages/web/dist/assets/c-D2F1sQUL.js +2 -0
- package/packages/web/dist/assets/c4Diagram-AAUBKEIU-CNee5ks4.js +11 -0
- package/packages/web/dist/assets/channel-Bw0A52CB.js +2 -0
- package/packages/web/dist/assets/chunk-2J33WTMH-DObw03Xl.js +2 -0
- package/packages/web/dist/assets/chunk-4BX2VUAB-CH2SR6nN.js +2 -0
- package/packages/web/dist/assets/chunk-55IACEB6-WlVpegT3.js +2 -0
- package/packages/web/dist/assets/chunk-727SXJPM-g8GT9Rgd.js +207 -0
- package/packages/web/dist/assets/chunk-AQP2D5EJ-DbN4PsDF.js +232 -0
- package/packages/web/dist/assets/chunk-FMBD7UC4-DSN3BjFG.js +16 -0
- package/packages/web/dist/assets/chunk-ND2GUHAM-D9R4le0z.js +2 -0
- package/packages/web/dist/assets/chunk-QZHKN3VN-D4YgRaKH.js +2 -0
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-itf_kt-q.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-itf_kt-q.js +2 -0
- package/packages/web/dist/assets/core-CysJ2gLj.js +4 -0
- package/packages/web/dist/assets/cose-bilkent-S5V4N54A-D1PB0KNu.js +2 -0
- package/packages/web/dist/assets/cpp-Bm3ii6kY.js +2 -0
- package/packages/web/dist/assets/csharp-BNpuYZMG.js +2 -0
- package/packages/web/dist/assets/css-DazXZka4.js +2 -0
- package/packages/web/dist/assets/cytoscape.esm-CUqq0XTU.js +332 -0
- package/packages/web/dist/assets/dagre-BM42HDAG-C6Uuq-dq.js +5 -0
- package/packages/web/dist/assets/defaultLocale-DX6XiGOO.js +2 -0
- package/packages/web/dist/assets/diagram-2AECGRRQ-C9JvQ425.js +44 -0
- package/packages/web/dist/assets/diagram-5GNKFQAL-Cu6pdh2d.js +11 -0
- package/packages/web/dist/assets/diagram-KO2AKTUF-PmXcUQhY.js +4 -0
- package/packages/web/dist/assets/diagram-LMA3HP47-9ww6_9Iq.js +25 -0
- package/packages/web/dist/assets/diagram-OG6HWLK6-DWQVjv_n.js +25 -0
- package/packages/web/dist/assets/erDiagram-TEJ5UH35-6F7xTuff.js +86 -0
- package/packages/web/dist/assets/flowDiagram-I6XJVG4X-SCiGu7m6.js +163 -0
- package/packages/web/dist/assets/format-yf7Vf9kR.js +2 -0
- package/packages/web/dist/assets/ganttDiagram-6RSMTGT7-BNpPwxkL.js +293 -0
- package/packages/web/dist/assets/gitGraphDiagram-PVQCEYII-C0gyAJjW.js +107 -0
- package/packages/web/dist/assets/go-BPx_y7j8.js +2 -0
- package/packages/web/dist/assets/graph-BlwPajkw.js +2 -0
- package/packages/web/dist/assets/index-B-YuzkF6.js +71 -0
- package/packages/web/dist/assets/index-CvGiQddO.css +1 -0
- package/packages/web/dist/assets/infoDiagram-5YYISTIA-CJ2CFPfy.js +3 -0
- package/packages/web/dist/assets/ini-B3BVgEsG.js +2 -0
- package/packages/web/dist/assets/init-Gi6I4Gst.js +2 -0
- package/packages/web/dist/assets/ishikawaDiagram-YF4QCWOH-Cs0D32Ry.js +71 -0
- package/packages/web/dist/assets/java-B5VKqtH2.js +2 -0
- package/packages/web/dist/assets/javascript-BKRaQes9.js +2 -0
- package/packages/web/dist/assets/journeyDiagram-JHISSGLW-Bc9fuI2i.js +140 -0
- package/packages/web/dist/assets/json-DIYVocXf.js +2 -0
- package/packages/web/dist/assets/kanban-definition-UN3LZRKU-D_tC7QfC.js +90 -0
- package/packages/web/dist/assets/katex-C5jXJg4s.js +258 -0
- package/packages/web/dist/assets/kotlin-Cw-eF_bm.js +3 -0
- package/packages/web/dist/assets/lastTab-CU1zsF0j.js +2 -0
- package/packages/web/dist/assets/layout-Boy8vhSP.js +2 -0
- package/packages/web/dist/assets/linear-BzGHkF9K.js +2 -0
- package/packages/web/dist/assets/mermaid.core-DlbsYzE-.js +310 -0
- package/packages/web/dist/assets/mindmap-definition-RKZ34NQL-Bxeh-ToD.js +97 -0
- package/packages/web/dist/assets/ordinal-Cboi1Yqb.js +2 -0
- package/packages/web/dist/assets/php-CRdd4dfS.js +3 -0
- package/packages/web/dist/assets/pieDiagram-4H26LBE5-CDmilo8S.js +31 -0
- package/packages/web/dist/assets/python-kSlZHhJb.js +2 -0
- package/packages/web/dist/assets/quadrantDiagram-W4KKPZXB-D14-JyE9.js +8 -0
- package/packages/web/dist/assets/queued-LamEU0GH.js +2 -0
- package/packages/web/dist/assets/requirementDiagram-4Y6WPE33-CJ1PUGu-.js +85 -0
- package/packages/web/dist/assets/ruby-7T4ktxyv.js +2 -0
- package/packages/web/dist/assets/rust-HC3MBUhY.js +2 -0
- package/packages/web/dist/assets/sankeyDiagram-5OEKKPKP-BkUK73W0.js +41 -0
- package/packages/web/dist/assets/scss-DZLA7YXz.js +2 -0
- package/packages/web/dist/assets/sequenceDiagram-3UESZ5HK-sbcvsuLp.js +163 -0
- package/packages/web/dist/assets/sql-BC88IN-V.js +2 -0
- package/packages/web/dist/assets/stateDiagram-AJRCARHV-DV-7BdHK.js +2 -0
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BlkGdXKW.js +2 -0
- package/packages/web/dist/assets/swift-Dn8kKO_1.js +2 -0
- package/packages/web/dist/assets/timeline-definition-PNZ67QCA-C5su21G_.js +121 -0
- package/packages/web/dist/assets/typescript-C2FFdlUC.js +2 -0
- package/packages/web/dist/assets/vennDiagram-CIIHVFJN-JNkIJAaQ.js +35 -0
- package/packages/web/dist/assets/wardley-L42UT6IY-qcz6t2vR.js +174 -0
- package/packages/web/dist/assets/wardleyDiagram-YWT4CUSO-CMrOIZ-c.js +79 -0
- package/packages/web/dist/assets/xml-BXBhIUeX.js +2 -0
- package/packages/web/dist/assets/xychartDiagram-2RQKCTM6-C9q6Oy9u.js +8 -0
- package/packages/web/dist/assets/yaml-drsDnIKT.js +2 -0
- package/packages/web/dist/favicon.ico +0 -0
- package/packages/web/dist/fonts/inter-latin.woff2 +0 -0
- package/packages/web/dist/fonts/jetbrains-mono-latin.woff2 +0 -0
- package/packages/web/dist/icons/apple-touch-icon.png +0 -0
- package/packages/web/dist/icons/favicon-16.png +0 -0
- package/packages/web/dist/icons/favicon-32.png +0 -0
- package/packages/web/dist/icons/icon-192.png +0 -0
- package/packages/web/dist/icons/icon-512.png +0 -0
- package/packages/web/dist/icons/maskable-512.png +0 -0
- package/packages/web/dist/index.html +56 -0
- package/packages/web/dist/manifest.webmanifest +32 -0
- 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
|
+
}
|