agent-dag 1.33.97 → 1.33.99

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-mMOusrOQ.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-CZeYjQ5o.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-dwRx-2y0.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.97",
3
+ "version": "1.33.99",
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": {
@@ -555,6 +555,11 @@ export function failureText(r, what = "cswap") {
555
555
  ? "the claude CLI could not be run: not on PATH. Set AGENTS_DECK_CLAUDE to its full path."
556
556
  : "cswap could not be run: not on PATH, and not in the places uv and pipx install to. Set AGENTS_DECK_CSWAP to its full path.";
557
557
  }
558
+ // A run its deadline stopped has no exit status worth quoting and no last
559
+ // line worth quoting either — whatever it had printed, it had not finished.
560
+ // Asked for one anyway, this said "cswap export exited 0", a success code for
561
+ // a command that never completed.
562
+ if (r?.timedOut || r?.code === "ETIMEDOUT") return `${what} took too long and was stopped`;
558
563
  return firstUseful(out) || `${what} exited ${r?.code}`;
559
564
  }
560
565
 
@@ -189,34 +189,54 @@ export function looksMissing(text, name = "") {
189
189
  return true;
190
190
  }
191
191
 
192
+ // How much of a hung child's output the deadline keeps. The full buffers belong
193
+ // to execFile's callback, which a timed-out run never waits for, and the tail is
194
+ // where a tool puts the line that explains itself.
195
+ const TIMEOUT_TAIL = 8 << 10;
196
+
192
197
  /**
193
198
  * Run a command and collect its output. Never rejects, and never throws —
194
199
  * failures come back as `{ ok: false }`, because every caller here is a poll or
195
200
  * a UI action where a missing tool is an expected state rather than an
196
201
  * exception. execFile can throw synchronously on Windows, so the call itself is
197
202
  * guarded as well as its callback.
203
+ *
204
+ * A run stopped by its deadline answers `{ ok: false, code: "ETIMEDOUT",
205
+ * killed: true, timedOut: true }` — never ok, whatever the child said on its
206
+ * way out.
198
207
  */
199
208
  export function run(cmd, args, { timeout = 20_000, maxBuffer = 4 << 20 } = {}) {
200
209
  const tries = candidates(cmd);
201
210
  return new Promise((resolve) => {
202
211
  const attempt = (i) => {
203
212
  if (i >= tries.length) {
204
- return resolve({ ok: false, code: "ENOENT", killed: false, stdout: "", stderr: "" });
213
+ return resolve({ ok: false, code: "ENOENT", killed: false, timedOut: false, stdout: "", stderr: "" });
205
214
  }
206
215
  const raw = tries[i];
207
216
  const { file, args: argv, opts } = isBatch(raw)
208
217
  ? viaCmd(raw, args)
209
218
  : { file: raw, args, opts: {} };
210
219
 
211
- // execFile's own `timeout` ends with one signal to the process it
212
- // started, which for a batch candidate is the cmd.exe wrapper: the
213
- // deadline was reported as enforced while the tool underneath went on
214
- // running. A batch candidate is timed here instead, on the whole tree.
215
220
  const tree = isBatch(raw);
216
221
  let timer = null, timedOut = false;
222
+ // A tail of what the child managed to say. The deadline below answers
223
+ // before execFile's callback does, and the callback owns the full
224
+ // buffers, so without this copy a timed-out run reports nothing at all —
225
+ // and the last line a hung tool printed is usually the only clue why it
226
+ // hung.
227
+ let sawOut = "", sawErr = "";
217
228
 
218
229
  const done = (err, stdout, stderr) => {
219
230
  clearTimeout(timer);
231
+ // The deadline already gave this attempt its verdict, and killed the
232
+ // child to make it stop. What the corpse reports is not news, and
233
+ // believing it is what put "cswap export exited 0" in front of the
234
+ // user: execFile calls a signalled exit `code: null`, which `?? 0`
235
+ // turns into a success code, and a tool that handles SIGTERM by
236
+ // exiting 0 — the well-behaved kind — arrives here with no error at
237
+ // all, so the run we cut short came back `ok: true` and got its
238
+ // spelling remembered as one that works.
239
+ if (timedOut) return;
220
240
  // cmd.exe's "is not recognized" counts as "not this spelling" too, and
221
241
  // it arrives as a normal non-zero exit rather than a spawn error. Only
222
242
  // a batch candidate goes through a shell, so only there can the output
@@ -231,9 +251,8 @@ export function run(cmd, args, { timeout = 20_000, maxBuffer = 4 << 20 } = {}) {
231
251
  // A tool cmd.exe could not find is missing, not "exited 1" — callers
232
252
  // key their message off this.
233
253
  code: missing ? "ENOENT" : (err?.code ?? 0),
234
- // Our own tree kill is not `killed` as far as execFile can tell, so
235
- // a timeout reports the same either way.
236
- killed: Boolean(err?.killed) || timedOut,
254
+ killed: Boolean(err?.killed),
255
+ timedOut: false,
237
256
  stdout: String(stdout ?? ""),
238
257
  stderr: String(stderr ?? ""),
239
258
  });
@@ -241,11 +260,27 @@ export function run(cmd, args, { timeout = 20_000, maxBuffer = 4 << 20 } = {}) {
241
260
 
242
261
  try {
243
262
  const cp = execFile(file, argv,
244
- { timeout: tree ? 0 : timeout, shell: false, windowsHide: true, maxBuffer, ...opts }, done);
245
- if (tree) {
246
- timer = setTimeout(() => { timedOut = true; killTree(cp); }, timeout);
247
- timer.unref?.();
248
- }
263
+ { timeout: 0, shell: false, windowsHide: true, maxBuffer, ...opts }, done);
264
+ cp.stdout?.on("data", (d) => { sawOut = (sawOut + d).slice(-TIMEOUT_TAIL); });
265
+ cp.stderr?.on("data", (d) => { sawErr = (sawErr + d).slice(-TIMEOUT_TAIL); });
266
+ // The deadline states the outcome itself and only then kills, which is
267
+ // the order startUpgrade needs for the same reason: the answer must not
268
+ // depend on the killed child cooperating.
269
+ //
270
+ // execFile's own `timeout` is not used at all. It ends with one signal
271
+ // to the process it started, which for a batch candidate is the cmd.exe
272
+ // wrapper — the deadline was reported as enforced while the tool
273
+ // underneath went on running — and it reports through the callback,
274
+ // which waits for the stdio pipes. On Windows those are held by the
275
+ // grandchild under the wrapper, so when the tree kill could not reach
276
+ // it the callback never came and the run never settled at all: the
277
+ // accounts panel sat on a request that had already timed out.
278
+ timer = setTimeout(() => {
279
+ timedOut = true;
280
+ resolve({ ok: false, code: "ETIMEDOUT", killed: true, timedOut: true, stdout: sawOut, stderr: sawErr });
281
+ killTree(cp);
282
+ }, timeout);
283
+ timer.unref?.();
249
284
  } catch (err) {
250
285
  // Synchronous throw — the EINVAL case. Same handling as a callback
251
286
  // error; letting it propagate here is what crashed the server, because