agent-dag 1.9.0 → 1.9.1

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.
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>agent-dag</title>
7
7
  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='84' font-size='84'%3E%E2%97%89%3C/text%3E%3C/svg%3E" />
8
- <script type="module" crossorigin src="/assets/index-Dp-ez1Ht.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-g_sZY5sg.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-q0qkmkVM.css">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-dag",
3
- "version": "1.9.0",
3
+ "version": "1.9.1",
4
4
  "description": "Live DAG of Claude Code agents — watch parallel subagents fork, call tools, and return on one calm canvas.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -92,6 +92,80 @@ function maybeResolveModel(payload) {
92
92
  .finally(() => pendingTranscriptReads.delete(sid));
93
93
  }
94
94
 
95
+ // ─── Usage enrichment ────────────────────────────────────────────────────
96
+ // Same story as the model: token counts (input/output/cache) are missing
97
+ // from every CC hook payload but present on every assistant message in
98
+ // the transcript JSONL as a `"usage":{…}` block. We sum them across the
99
+ // whole transcript and ship a synthetic UsageObserved event so the
100
+ // session's root agent gets accurate cumulative usage (and therefore the
101
+ // cost columns actually have something to multiply by).
102
+ const lastUsageReadAt = new Map(); // sid -> ms timestamp
103
+ const pendingUsageReads = new Set(); // sid currently being read
104
+ const USAGE_READ_THROTTLE_MS = 2500;
105
+
106
+ async function readUsageFromTranscript(path) {
107
+ try {
108
+ const s = await stat(path);
109
+ if (s.size === 0) return null;
110
+ // Transcripts can grow large (thinking blocks, tool inputs) — read the
111
+ // whole file. Each entry has its own usage object and we sum every
112
+ // occurrence, so missing earlier bytes would undercount. Files are
113
+ // usually < 1MB; tens-of-MB sessions cost a few ms to scan.
114
+ const fh = await open(path, "r");
115
+ let buf;
116
+ try {
117
+ buf = Buffer.alloc(s.size);
118
+ await fh.read(buf, 0, s.size, 0);
119
+ } finally {
120
+ await fh.close();
121
+ }
122
+ const text = buf.toString("utf8");
123
+ const totals = {
124
+ input_tokens: 0, output_tokens: 0,
125
+ cache_read_input_tokens: 0, cache_creation_input_tokens: 0,
126
+ };
127
+ // Match each `"usage":{...}` block and sum the four numeric fields.
128
+ // Regex is good enough — these blocks are flat single-level JSON.
129
+ const re = /"usage"\s*:\s*\{([^}]+)\}/g;
130
+ const grab = (blob, key) => {
131
+ const km = blob.match(new RegExp(`"${key}"\\s*:\\s*(\\d+)`));
132
+ return km ? Number(km[1]) : 0;
133
+ };
134
+ for (const m of text.matchAll(re)) {
135
+ const blob = m[1];
136
+ totals.input_tokens += grab(blob, "input_tokens");
137
+ totals.output_tokens += grab(blob, "output_tokens");
138
+ totals.cache_read_input_tokens += grab(blob, "cache_read_input_tokens");
139
+ totals.cache_creation_input_tokens += grab(blob, "cache_creation_input_tokens");
140
+ }
141
+ if (totals.input_tokens === 0 && totals.output_tokens === 0
142
+ && totals.cache_read_input_tokens === 0 && totals.cache_creation_input_tokens === 0) return null;
143
+ return totals;
144
+ } catch {
145
+ return null;
146
+ }
147
+ }
148
+
149
+ function maybeResolveUsage(payload) {
150
+ if (!payload || typeof payload !== "object") return;
151
+ const sid = payload.session_id;
152
+ const tp = payload.transcript_path;
153
+ if (!sid || !tp) return;
154
+ if (pendingUsageReads.has(sid)) return;
155
+ const now = Date.now();
156
+ const last = lastUsageReadAt.get(sid) ?? 0;
157
+ if (now - last < USAGE_READ_THROTTLE_MS) return;
158
+ lastUsageReadAt.set(sid, now);
159
+ pendingUsageReads.add(sid);
160
+ readUsageFromTranscript(tp)
161
+ .then(usage => {
162
+ if (!usage) return;
163
+ pushEvent({ hook_event_name: "UsageObserved", session_id: sid, usage }, "internal");
164
+ })
165
+ .catch(() => {})
166
+ .finally(() => pendingUsageReads.delete(sid));
167
+ }
168
+
95
169
  function pushEvent(raw, source, opts = {}) {
96
170
  // Synchronous enrichment: if we already know this session's model, stamp
97
171
  // it on the payload so the client's recursive scanner picks it up.
@@ -120,10 +194,14 @@ function pushEvent(raw, source, opts = {}) {
120
194
  appendFile(persistPath, JSON.stringify(evt) + "\n", "utf8").catch(() => {});
121
195
  }
122
196
 
123
- // Kick off async transcript scan for unknown sessions. The result lands
124
- // as a synthetic ModelObserved event a few ms later that backfills any
125
- // agents already on the canvas.
126
- if (source === "hook" && !opts.replay) maybeResolveModel(raw);
197
+ // Kick off async transcript scans. Model arrives as a one-shot
198
+ // ModelObserved; usage is re-read periodically (throttled to 2.5s per
199
+ // session) so the cost columns track running totals as the session
200
+ // progresses. Both result in synthetic events.
201
+ if (source === "hook" && !opts.replay) {
202
+ maybeResolveModel(raw);
203
+ maybeResolveUsage(raw);
204
+ }
127
205
 
128
206
  return evt;
129
207
  }