agent-dag 1.35.36 → 1.35.38

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.
@@ -40,7 +40,7 @@
40
40
  document.documentElement.setAttribute("data-theme", stored === "light" ? "light" : "dark");
41
41
  })();
42
42
  </script>
43
- <script type="module" crossorigin src="/assets/index-CLOiw52E.js"></script>
43
+ <script type="module" crossorigin src="/assets/index-_Jvl7GLr.js"></script>
44
44
  <link rel="stylesheet" crossorigin href="/assets/index-C2ruFt14.css">
45
45
  </head>
46
46
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-dag",
3
- "version": "1.35.36",
3
+ "version": "1.35.38",
4
4
  "description": "Live deck of Claude Code and Codex agents — watch tool calls, token spend and every Claude Code subagent on one calm canvas. Also available as npx ccdeck and npx agent-dag.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,7 +11,7 @@
11
11
  // the current call always serves from the already-installed copy. If the
12
12
  // managed install is missing/broken we fall back to the old npx path so the
13
13
  // feature still works on a fresh machine.
14
- import { spawn, spawnSync } from "node:child_process";
14
+ import { spawn } from "node:child_process";
15
15
  import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
16
16
  import path from "node:path";
17
17
  import os from "node:os";
@@ -107,7 +107,7 @@ let _checkedThisRun = false; // only kick the daily check once per process boot
107
107
 
108
108
  // The last `npm install` that failed, in one line of its own words.
109
109
  //
110
- // Module scope rather than a value thrown out of installSync, because the two
110
+ // Module scope rather than a value thrown out of the install, because the two
111
111
  // callers that matter never see that throw. primeCcusage runs the install at
112
112
  // boot and only logs the rejection; a second modal open that arrives while an
113
113
  // install is in flight awaits the SHARED promise, whose rejection has already
@@ -125,12 +125,26 @@ const INSTALL_ERROR_ROOM = 240;
125
125
  * Start the one shared install, remembering how it ended.
126
126
  *
127
127
  * Deduped through `_installing` the way it always was, so concurrent callers
128
- * cost one `npm install` between them; the only new thing is that the outcome
129
- * survives the promise.
128
+ * cost one `npm install` between them, and the outcome survives the promise.
129
+ *
130
+ * This used to read `_installing = (async () => { installSync(spec); })()`, and
131
+ * that wrapper was the whole of #476: an async function body runs synchronously
132
+ * up to its first `await`, and there was none, so the IIFE turned a throw into
133
+ * a rejection and bought no asynchrony at all. `installSync` was a `spawnSync`
134
+ * of `npm install` with a two-minute deadline, and it ran on the caller's
135
+ * stack — which at boot is bin/deck.js's, beside jobs that really are async.
136
+ * primeCcusage answered `{ state: "installing" }`, a sentence that says the
137
+ * wait was deferred, while the process could not accept an HTTP connection,
138
+ * write an SSE frame, ingest a hook or repaint the pulse line until npm exited.
139
+ * On a first run, with nothing cached, that is the one boot a new user judges
140
+ * the tool by.
141
+ *
142
+ * `install` below is the fix, and there is now exactly one install mechanism in
143
+ * this module rather than a synchronous one and a background one.
130
144
  */
131
145
  function startInstall(spec = "latest") {
132
146
  if (!_installing) {
133
- _installing = (async () => { installSync(spec); })()
147
+ _installing = install(spec)
134
148
  .then(() => { _lastInstallError = null; })
135
149
  .catch(e => {
136
150
  _lastInstallError = oneLine(e?.message ?? e, INSTALL_ERROR_ROOM);
@@ -238,17 +252,21 @@ export function resolveEntry() {
238
252
  }
239
253
 
240
254
  /**
241
- * Everything spawnSync knows about a failed install, in the order the answer is
255
+ * Everything a failed install is known to have said, in the order the answer is
242
256
  * usually in.
243
257
  *
244
258
  * The old line read `(r.stderr || "").trim() || r.status`, which threw away the
245
- * two things most likely to be the whole story. `r.error` is where spawnSync
246
- * reports a failure to LAUNCH — ENOENT for a comspec that is not there, EINVAL
247
- * for a .cmd Node refuses to spawn directly, ETIMEDOUT when the deadline above
248
- * fired and it comes with `status: null`, so what reached the terminal in
249
- * every one of those cases was the word "null". And npm has never confined
250
- * itself to stderr: a shim that dies before npm starts writes wherever Node
251
- * chose, and `npm ERR!` blocks have landed on stdout across majors.
259
+ * two things most likely to be the whole story. `error` is where a failure to
260
+ * LAUNCH lands — ENOENT for a comspec that is not there, EINVAL for a .cmd Node
261
+ * refuses to spawn directly, and the expired deadline — and it comes with no
262
+ * status at all, so what reached the terminal in every one of those cases was
263
+ * the word "null". And npm has never confined itself to stderr: a shim that
264
+ * dies before npm starts writes wherever Node chose, and `npm ERR!` blocks have
265
+ * landed on stdout across majors.
266
+ *
267
+ * The four fields are spawnSync's, because that is where they were first read
268
+ * off; `install` above now fills the same shape from the 'error' event, the
269
+ * exit status and the two collected streams.
252
270
  */
253
271
  function installFailureText(r) {
254
272
  const parts = [];
@@ -290,46 +308,95 @@ function installTreeReport() {
290
308
  return "the package is there but its bin entry could not be read or does not point inside it";
291
309
  }
292
310
 
293
- // Run `npm install ccusage@<spec> --prefix CACHE_DIR`. Synchronous variant for
294
- // the first-run cold path (we must have a binary before we can answer).
295
- function installSync(spec = "latest") {
296
- mkdirSync(CACHE_DIR, { recursive: true });
297
- const { file, args, opts } = installSpec(spec);
298
- const r = spawnSync(
299
- file,
300
- args,
301
- { windowsHide: true, timeout: INSTALL_TIMEOUT_MS, encoding: "utf8", ...opts },
302
- );
303
- if (r.status !== 0) {
304
- throw new Error(`npm install ccusage failed: ${installFailureText(r)}`);
305
- }
306
- // A zero exit is npm's opinion, not a fact about the disk, and the caller
307
- // treats "installSync returned" as "there is something to run". Checking here
308
- // is what stops a silent success: getRunner used to call resolveEntry(), get
309
- // null, and fall through to the npx fallback with NOTHING recorded anywhere
310
- // about why so the modal explained the fallback's stderr and the install's
311
- // half of the story was never written down at all.
312
- if (!resolveEntry()) {
313
- throw new Error(
314
- `npm install ccusage exited 0 but left nothing runnable under ${CACHE_DIR}: `
315
- + `${installTreeReport()}`,
316
- );
317
- }
318
- }
319
-
320
- // Background, non-blocking install (used by the daily update path).
321
- function installAsync(spec = "latest") {
322
- try {
311
+ /**
312
+ * Run `npm install ccusage@<spec> --prefix CACHE_DIR`, off this stack.
313
+ *
314
+ * The ONE install in this module, awaited by startInstall and dropped on the
315
+ * floor by the daily update path below. It was two — a `spawnSync` for the cold
316
+ * path and a fire-and-forget `spawn` for the background one — and the sync half
317
+ * is what #476 removes: nothing here needs an answer before the next tick, and
318
+ * a two-minute `spawnSync` at boot is two minutes of a dead process.
319
+ *
320
+ * Everything the sync path was careful about is carried over unchanged, because
321
+ * every one of those cares was bought with a bug report:
322
+ *
323
+ * - the vector is `installSpec`'s, spread into spawn exactly as spawnSync got
324
+ * it, which is what keeps #456's absolute `npm.cmd` and #362's per-argument
325
+ * quoting on Windows. `opts` is spread LAST for the same reason it was
326
+ * before: it carries windowsVerbatimArguments, and nothing above it may win.
327
+ * - `windowsHide`, so no console window flashes up.
328
+ * - INSTALL_TIMEOUT_MS, as a deadline this module enforces itself rather than
329
+ * spawn's own `timeout` option. That option sends one signal to the process
330
+ * it started, and on Windows a `.cmd` runs THROUGH cmd.exe — so the signal
331
+ * would land on the wrapper and leave npm downloading, which is the same
332
+ * distinction exec.mjs's `run` states and the reason killTree exists. Same
333
+ * shape as runOnce below, so this file has one deadline pattern rather than
334
+ * two.
335
+ *
336
+ * Diagnosis is unchanged too: `installFailureText` is handed the same four
337
+ * fields spawnSync used to hand it — a failure to LAUNCH in `error`, the exit
338
+ * status, and both output streams, because npm has never confined itself to
339
+ * stderr.
340
+ */
341
+ function install(spec = "latest") {
342
+ return new Promise((resolve, reject) => {
323
343
  mkdirSync(CACHE_DIR, { recursive: true });
324
344
  const { file, args, opts } = installSpec(spec);
325
- const child = spawn(
326
- file,
327
- args,
328
- { windowsHide: true, detached: false, stdio: "ignore", ...opts },
329
- );
330
- child.on("error", () => {});
331
- child.unref?.();
332
- } catch { /* best-effort */ }
345
+ const child = spawn(file, args, { windowsHide: true, ...opts });
346
+ let out = "", err = "", settled = false;
347
+ let timer = null;
348
+ const finish = (fn, value) => {
349
+ if (settled) return;
350
+ settled = true;
351
+ clearTimeout(timer);
352
+ fn(value);
353
+ };
354
+ // A child that dies emits 'error' AND THEN 'close', and a deadline that
355
+ // fires kills the child and so provokes both — hence `settled`. Whichever
356
+ // arrives first is the account that travels.
357
+ const failed = (r) => finish(reject, new Error(`npm install ccusage failed: ${installFailureText(r)}`));
358
+ timer = setTimeout(() => {
359
+ // The verdict is stated before the kill, the order exec.mjs's `run` uses:
360
+ // the answer must not depend on the killed child cooperating.
361
+ failed({
362
+ error: { message: `npm install timed out after ${INSTALL_TIMEOUT_MS}ms` },
363
+ stdout: out,
364
+ stderr: err,
365
+ });
366
+ killTree(child);
367
+ }, INSTALL_TIMEOUT_MS);
368
+ timer.unref?.();
369
+ child.stdout?.on("data", d => { out += d; });
370
+ child.stderr?.on("data", d => { err += d; });
371
+ child.on("error", e => failed({ error: e, stdout: out, stderr: err }));
372
+ child.on("close", status => {
373
+ if (status !== 0) return failed({ status, stdout: out, stderr: err });
374
+ // A zero exit is npm's opinion, not a fact about the disk, and the caller
375
+ // treats "the install resolved" as "there is something to run". Checking
376
+ // here is what stops a silent success: getRunner used to call
377
+ // resolveEntry(), get null, and fall through to the npx fallback with
378
+ // NOTHING recorded anywhere about why — so the modal explained the
379
+ // fallback's stderr and the install's half of the story was never written
380
+ // down at all.
381
+ if (!resolveEntry()) {
382
+ return finish(reject, new Error(
383
+ `npm install ccusage exited 0 but left nothing runnable under ${CACHE_DIR}: `
384
+ + `${installTreeReport()}`,
385
+ ));
386
+ }
387
+ finish(resolve, undefined);
388
+ });
389
+ });
390
+ }
391
+
392
+ // The daily update path's install: the same one above, with nobody listening.
393
+ //
394
+ // It has no shared promise and no `_lastInstallError` on purpose. This runs
395
+ // behind a ccusage that already works, so a failed upgrade is not news the
396
+ // modal should lead with — the copy on disk still answers, and the check comes
397
+ // round again tomorrow.
398
+ function installInBackground(spec = "latest") {
399
+ install(spec).catch(() => { /* best-effort */ });
333
400
  }
334
401
 
335
402
  // True at most once per UPDATE_CHECK_MS, gated by the marker file's mtime so the
@@ -363,7 +430,7 @@ function maybeBackgroundUpdate(installedVersion) {
363
430
  child.on("error", () => {});
364
431
  child.on("close", () => {
365
432
  const latest = out.trim();
366
- if (latest && latest !== installedVersion) installAsync(latest);
433
+ if (latest && latest !== installedVersion) installInBackground(latest);
367
434
  });
368
435
  } catch { /* ignore */ }
369
436
  }
@@ -831,7 +898,10 @@ export async function fetchCcusageDaily({ since, until, force = false } = {}) {
831
898
  *
832
899
  * Returns { state: "present" | "user" | "installing" | "updating" |
833
900
  * "unavailable" }. Never throws and never blocks on the install itself — a slow
834
- * registry must not hold up the server.
901
+ * registry must not hold up the server. That second half was a claim rather
902
+ * than a fact until #476: `startInstall` wrapped a `spawnSync` in an async IIFE
903
+ * with nothing to await in it, so `{ state: "installing" }` was returned only
904
+ * after the install had already happened, on this stack.
835
905
  *
836
906
  * The order here is getRunner's order, and it has to be: a boot that installs a
837
907
  * managed copy while the user already has one on PATH would make getRunner's
@@ -152,7 +152,18 @@ const transcriptScans = new Map(); // path -> scan state
152
152
  const transcriptScanInFlight = new Map(); // path -> in-progress scan promise
153
153
  const MAX_TRANSCRIPT_SCANS = 256; // bound the per-path state
154
154
 
155
- const MODEL_ID_RE = /^claude[-_]/i;
155
+ // The transcript's `message.model`, and the only filter standing between it and
156
+ // every model the deck shows. Bedrock and Mantle put a provider namespace in
157
+ // front of the id — `us.anthropic.claude-opus-5`, `anthropic.claude-opus-5` —
158
+ // so a bare `^claude` dropped every line a Bedrock session writes and the deck
159
+ // showed those users no model, no context window and no cost at all (#475).
160
+ //
161
+ // The prefix list is `VENDOR_PREFIX_RE` in src/web/model-id.ts, written out a
162
+ // second time here rather than imported: this file is plain .mjs that node runs
163
+ // straight off disk with no build step, so it cannot reach a `.ts` module.
164
+ // bedrock-model-ids.test.ts sweeps both against one list of ids so the copies
165
+ // cannot drift apart.
166
+ const MODEL_ID_RE = /^(?:(?:us-gov|global|apac|us|eu|jp|au)\.anthropic\.|anthropic\.)?claude[-_]/i;
156
167
  const USAGE_BLOCK_RE = /"usage"\s*:\s*\{([^}]+)\}/g;
157
168
  // CC `/clear` and `/compact` write a marker into the transcript and reset the
158
169
  // context window to ~0 while the JSONL keeps growing — everything before the