agent-dag 1.33.11 → 1.33.13

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-Cjhxhzjo.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-Cif0aUlE.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-Cpi89XJ8.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.11",
3
+ "version": "1.33.13",
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": {
@@ -199,24 +199,39 @@ export function runInteractive(cmd, args, { timeout = 300_000, maxOutput = 256 <
199
199
  if (i >= tries.length) return finish(-1, { code: "ENOENT" });
200
200
  const raw = tries[i];
201
201
  const { file, args: argv, opts } = isBatch(raw) ? viaCmd(raw, args) : { file: raw, args, opts: {} };
202
+ let proc;
202
203
  try {
203
- child = spawn(file, argv, { stdio: ["pipe", "pipe", "pipe"], shell: false, windowsHide: true, ...opts });
204
+ proc = spawn(file, argv, { stdio: ["pipe", "pipe", "pipe"], shell: false, windowsHide: true, ...opts });
204
205
  } catch (err) {
205
206
  return tryNext(err) ? attempt(i + 1) : finish(-1, err);
206
207
  }
207
- child.on("error", (err) => {
208
+ child = proc;
209
+ // A spelling that fails to spawn emits 'error' AND THEN 'close' — with code
210
+ // -2 after an ENOENT. Once the error handler has moved on to the next
211
+ // candidate, that trailing 'close' is news about a child nobody is waiting
212
+ // for any more, and answering it settled `done` with ok:false while the
213
+ // real child was still running. On Windows that is the normal path, not a
214
+ // corner case: `claude.exe` does not exist, `claude.cmd` does, so the very
215
+ // first login reported "the code was not accepted" while the child it had
216
+ // abandoned went on to complete the OAuth and switch the live account.
217
+ // Every listener below therefore speaks only while its own child is the
218
+ // current one.
219
+ const stale = () => proc !== child;
220
+ proc.on("error", (err) => {
221
+ if (stale()) return;
208
222
  // Only retry another spelling while nothing has run yet; a mid-run error
209
223
  // is this child's failure, not evidence the name was wrong.
210
224
  if (tryNext(err) && !stdout && !stderr) { child = null; return attempt(i + 1); }
211
225
  finish(-1, err);
212
226
  });
213
- child.on("spawn", () => resolved.set(cmd, raw));
227
+ proc.on("spawn", () => resolved.set(cmd, raw));
214
228
  // Capped so a runaway child cannot grow the heap without bound; the tail is
215
229
  // what carries the error, so the head is what gets dropped.
216
230
  const keep = (buf, text) => (buf + text).slice(-maxOutput);
217
- child.stdout?.on("data", (d) => { const t = String(d); stdout = keep(stdout, t); emitLines(t); });
218
- child.stderr?.on("data", (d) => { const t = String(d); stderr = keep(stderr, t); emitLines(t); });
219
- child.on("close", (code) => {
231
+ proc.stdout?.on("data", (d) => { if (stale()) return; const t = String(d); stdout = keep(stdout, t); emitLines(t); });
232
+ proc.stderr?.on("data", (d) => { if (stale()) return; const t = String(d); stderr = keep(stderr, t); emitLines(t); });
233
+ proc.on("close", (code) => {
234
+ if (stale()) return;
220
235
  // Same cmd.exe case as in `run`: exit 1 with "is not recognized" means
221
236
  // this spelling does not exist, not that the tool failed.
222
237
  if (code !== 0 && looksMissing(`${stderr}\n${stdout}`)) {
@@ -1288,6 +1288,22 @@ function randomPort(min, max) {
1288
1288
  return Math.floor(Math.random() * (max - min + 1)) + min;
1289
1289
  }
1290
1290
 
1291
+ // Parse a request target into a URL, or null when it cannot be parsed.
1292
+ //
1293
+ // The base is the constant "http://localhost", never the Host header. Node's
1294
+ // HTTP parser hands the header through verbatim, so a client sending
1295
+ // `Host: bad host` used to produce `new URL("/", "http://bad host")`, which
1296
+ // throws ERR_INVALID_URL synchronously inside the request listener. Nothing
1297
+ // catches that — it becomes an uncaughtException, the worker exits 1 and the
1298
+ // supervisor tears the whole deck down for a single malformed request. Only
1299
+ // the path and the query are ever read from this URL, so the authority half
1300
+ // is free to be a constant — which is what every other new URL call site in
1301
+ // this file already does. Anything else unparseable answers 400 instead.
1302
+ export function requestUrl(rawUrl) {
1303
+ try { return new URL(rawUrl ?? "/", "http://localhost"); }
1304
+ catch { return null; }
1305
+ }
1306
+
1291
1307
  async function tryListen(server, port, host) {
1292
1308
  return new Promise((res, rej) => {
1293
1309
  server.once("error", rej);
@@ -1340,7 +1356,10 @@ export async function startServer({ port = 4317, host = "127.0.0.1", persist = n
1340
1356
  });
1341
1357
 
1342
1358
  const server = createServer((req, res) => {
1343
- const url = new URL(req.url ?? "/", `http://${req.headers.host ?? host}`);
1359
+ const url = requestUrl(req.url);
1360
+ // Unparseable request target. Nothing below can route it, and throwing here
1361
+ // would be an uncaughtException inside the listener — i.e. the whole deck.
1362
+ if (!url) return send(res, 400, { error: "bad request target" });
1344
1363
 
1345
1364
  if (req.method === "POST" && url.pathname === "/api/event") return guard(handleEventIngest(req, res), res);
1346
1365
  if (req.method === "GET" && url.pathname === "/api/health") return handleHealth(req, res);