agent-dag 1.33.63 → 1.33.64

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>agents-deck</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-DQe_LZzS.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-B5DB7xOD.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-CF2yG9oG.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.33.63",
3
+ "version": "1.33.64",
4
4
  "description": "Live deck of Claude Code and Codex agents — watch parallel subagents fork, call tools, and return on one calm canvas. Also available as npx ccdeck and npx agent-dag.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,6 +4,7 @@
4
4
  import { readdir, open, stat } from "node:fs/promises";
5
5
  import { join } from "node:path";
6
6
  import { homedir } from "node:os";
7
+ import { StringDecoder } from "node:string_decoder";
7
8
 
8
9
  const CODEX_HOME = process.env.CODEX_HOME
9
10
  ? process.env.CODEX_HOME
@@ -18,6 +19,47 @@ const CACHE_MS = 60_000;
18
19
  const WINDOW_5H_MS = 5 * 60 * 60 * 1000;
19
20
  const WINDOW_7D_MS = 7 * 24 * 60 * 60 * 1000;
20
21
 
22
+ // A rollout file grows for as long as its session runs, and a week's worth of
23
+ // them is however much the user happened to type. Reading them all at once made
24
+ // the peak working set a function of that total; these two caps make it a
25
+ // function of the pool instead — at most MAX_PARALLEL_READS files in flight,
26
+ // each holding one chunk plus the line it is still assembling.
27
+ const READ_CHUNK_BYTES = 256 * 1024;
28
+ const MAX_PARALLEL_READS = 4;
29
+
30
+ // Run `worker` over `items` with at most `limit` of them in flight. Workers
31
+ // pull from a shared cursor, so a slow file delays only its own worker instead
32
+ // of stalling a fixed-size batch.
33
+ async function forEachLimited(items, limit, worker) {
34
+ let next = 0;
35
+ const workers = [];
36
+ for (let i = 0; i < Math.min(limit, items.length); i++) {
37
+ workers.push((async () => {
38
+ while (next < items.length) await worker(items[next++]);
39
+ })());
40
+ }
41
+ await Promise.all(workers);
42
+ }
43
+
44
+ // Append one rollout line's token_count event to the series, if it has one.
45
+ function foldTokenLine(series, raw) {
46
+ // Cheap pre-filter before the (relatively) expensive JSON.parse.
47
+ if (!raw.includes("total_token_usage")) return;
48
+ let obj;
49
+ try { obj = JSON.parse(raw); } catch { return; }
50
+ if (obj.type !== "event_msg" || obj.payload?.type !== "token_count") return;
51
+ const u = obj.payload.info?.total_token_usage;
52
+ if (!u) return;
53
+ const ts = obj.timestamp ? Date.parse(obj.timestamp) : NaN;
54
+ series.push({
55
+ ts: isNaN(ts) ? null : ts,
56
+ inp: u.input_tokens ?? 0,
57
+ out: u.output_tokens ?? 0,
58
+ cacheR: u.cached_input_tokens ?? 0,
59
+ total: u.total_tokens ?? ((u.input_tokens ?? 0) + (u.output_tokens ?? 0)),
60
+ });
61
+ }
62
+
21
63
  // Read the full series of cumulative token_count events from a rollout file.
22
64
  // Each token_count event carries `info.total_token_usage` — the running total
23
65
  // for the session at that point. We keep the whole series (with timestamps) so
@@ -34,25 +76,32 @@ async function readTokenSeries(filePath) {
34
76
  fd = await open(filePath, "r");
35
77
  const { size } = await fd.stat();
36
78
  if (size === 0) return null;
37
- const text = (await fd.readFile()).toString("utf8");
38
79
  const series = [];
39
- for (const raw of text.split("\n")) {
40
- // Cheap pre-filter before the (relatively) expensive JSON.parse.
41
- if (!raw.includes("total_token_usage")) continue;
42
- let obj;
43
- try { obj = JSON.parse(raw); } catch { continue; }
44
- if (obj.type !== "event_msg" || obj.payload?.type !== "token_count") continue;
45
- const u = obj.payload.info?.total_token_usage;
46
- if (!u) continue;
47
- const ts = obj.timestamp ? Date.parse(obj.timestamp) : NaN;
48
- series.push({
49
- ts: isNaN(ts) ? null : ts,
50
- inp: u.input_tokens ?? 0,
51
- out: u.output_tokens ?? 0,
52
- cacheR: u.cached_input_tokens ?? 0,
53
- total: u.total_tokens ?? ((u.input_tokens ?? 0) + (u.output_tokens ?? 0)),
54
- });
80
+ // A chunk at a time rather than the whole file: only the token_count
81
+ // numbers survive the pass, so buffering megabytes of prompt text (once as
82
+ // a Buffer, again as a string, a third time as the split array) bought
83
+ // nothing. StringDecoder carries a UTF-8 sequence that straddles a chunk
84
+ // boundary over to the next chunk.
85
+ const buf = Buffer.allocUnsafe(READ_CHUNK_BYTES);
86
+ const decoder = new StringDecoder("utf8");
87
+ let pending = "";
88
+ let pos = 0;
89
+ while (pos < size) {
90
+ const { bytesRead } = await fd.read(buf, 0, READ_CHUNK_BYTES, pos);
91
+ if (bytesRead <= 0) break;
92
+ pos += bytesRead;
93
+ pending += decoder.write(buf.subarray(0, bytesRead));
94
+ let from = 0;
95
+ let nl;
96
+ while ((nl = pending.indexOf("\n", from)) >= 0) {
97
+ foldTokenLine(series, pending.slice(from, nl));
98
+ from = nl + 1;
99
+ }
100
+ if (from > 0) pending = pending.slice(from);
55
101
  }
102
+ // A rollout still being written can end without its final newline.
103
+ pending += decoder.end();
104
+ if (pending) foldTokenLine(series, pending);
56
105
  return series.length ? series : null;
57
106
  } catch { return null; }
58
107
  finally { fd?.close().catch(() => {}); }
@@ -161,13 +210,16 @@ export async function fetchCodexUsage({ force = false } = {}) {
161
210
  // longer drops active-but-old sessions or over-counts the pre-window tail.
162
211
  const files = await listRolloutFiles(WINDOW_7D_MS);
163
212
 
164
- await Promise.all(files.map(async ({ path }) => {
213
+ // Bounded fan-out: a week of rollouts is an open-ended list, and opening
214
+ // every one of them at once also risked EMFILE, which readTokenSeries
215
+ // swallows into a silent undercount.
216
+ await forEachLimited(files, MAX_PARALLEL_READS, async ({ path }) => {
165
217
  const series = await readTokenSeries(path);
166
218
  if (!series) return;
167
219
  // Same series feeds both windows; baseline differs per window start.
168
220
  addTo(w5h, windowDelta(series, start5h));
169
221
  addTo(w7d, windowDelta(series, start7d));
170
- }));
222
+ });
171
223
  } catch (err) {
172
224
  console.error("agents-deck codex-usage: scan failed:", err?.message ?? err);
173
225
  const result = { ok: false, fetchedAt: now };