agent-dag 1.30.8 → 1.31.0

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,8 +5,8 @@
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-1i-Vckvo.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-CEAcuttN.css">
8
+ <script type="module" crossorigin src="/assets/index-CULN68BP.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-BrkDn1XS.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-dag",
3
- "version": "1.30.8",
3
+ "version": "1.31.0",
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": {
@@ -1015,11 +1015,32 @@ function handleSse(req, res) {
1015
1015
  });
1016
1016
  }
1017
1017
 
1018
+ // True only when a supervisor is listening AND the event log is being written.
1019
+ // Without persistence a restart wipes the canvas irrecoverably — replayLog has
1020
+ // no file to read — so the deck must not offer to do it.
1021
+ let _canRestart = false;
1022
+
1018
1023
  async function handleVersion(_req, res) {
1019
1024
  const { versionReport } = await import(
1020
1025
  pathToFileURL(join(PKG_ROOT, "src/server/self-update.mjs")).href
1021
1026
  );
1022
- send(res, 200, await versionReport({ running: RUNNING_VERSION, pkgRoot: PKG_ROOT }));
1027
+ const report = await versionReport({ running: RUNNING_VERSION, pkgRoot: PKG_ROOT });
1028
+ send(res, 200, { ...report, canRestart: _canRestart });
1029
+ }
1030
+
1031
+ // Restart is a two-party act: this half answers before it stops listening, so
1032
+ // the caller learns it was accepted rather than losing the socket mid-reply.
1033
+ function handleRestart(_req, res) {
1034
+ if (!_onRestart) return send(res, 501, { ok: false, reason: "unsupervised" });
1035
+ // Enforced here and not only in the UI. Under --no-persist a restart destroys
1036
+ // the whole canvas — replayLog has no file to read — and a destructive act
1037
+ // must not be prevented by a hidden button alone.
1038
+ if (!_canRestart) return send(res, 409, { ok: false, reason: "no_persist" });
1039
+ if (_restarting) return send(res, 202, { ok: true, already: true });
1040
+ _restarting = true;
1041
+ send(res, 200, { ok: true });
1042
+ // Let the response flush before the listener goes away.
1043
+ setTimeout(() => { try { _onRestart(); } catch { _restarting = false; } }, 120).unref();
1023
1044
  }
1024
1045
 
1025
1046
  async function handleQuota(req, res) {
@@ -1194,7 +1215,20 @@ async function tryListen(server, port, host) {
1194
1215
  });
1195
1216
  }
1196
1217
 
1197
- export async function startServer({ port = 4317, host = "127.0.0.1", persist = null, portRange = [4318, 4400], workspace = "", codex = true } = {}) {
1218
+ // Set from startServer's options. The server cannot restart itself — the
1219
+ // process lifecycle belongs to the supervisor in bin/agent-dag.js, which is the
1220
+ // only thing that can bring a replacement up on the same port without racing
1221
+ // the dying listener. Absent (running the module directly, or under an older
1222
+ // launcher), /api/restart answers 501 and the UI hides the control rather than
1223
+ // offering a button that does nothing.
1224
+ let _onRestart = null;
1225
+ // A restart is in flight. Several browser tabs watching the same deck will each
1226
+ // ask; the second ask must not re-enter the shutdown.
1227
+ let _restarting = false;
1228
+
1229
+ export async function startServer({ port = 4317, host = "127.0.0.1", persist = null, portRange = [4318, 4400], workspace = "", codex = true, onRestart = null } = {}) {
1230
+ _onRestart = typeof onRestart === "function" ? onRestart : null;
1231
+ _canRestart = _onRestart != null && persist != null;
1198
1232
  const removed = await sweepStaleDiscovery();
1199
1233
  if (removed > 0) console.log(` swept ${removed} stale discovery file(s)`);
1200
1234
  if (persist) {
@@ -1223,6 +1257,7 @@ export async function startServer({ port = 4317, host = "127.0.0.1", persist = n
1223
1257
  if (req.method === "GET" && url.pathname === "/api/health") return handleHealth(req, res);
1224
1258
  if (req.method === "GET" && url.pathname === "/events") return handleSse(req, res);
1225
1259
  if (req.method === "GET" && url.pathname === "/api/version") return guard(handleVersion(req, res), res);
1260
+ if (req.method === "POST" && url.pathname === "/api/restart") return handleRestart(req, res);
1226
1261
  if (req.method === "GET" && url.pathname === "/api/quota") return guard(handleQuota(req, res), res);
1227
1262
  if (req.method === "GET" && url.pathname === "/api/codex-usage") return guard(handleCodexUsage(req, res), res);
1228
1263
  if (req.method === "GET" && url.pathname === "/api/codex-quota") return guard(handleCodexQuota(req, res), res);
@@ -357,6 +357,20 @@ async function storeQuota() {
357
357
  const REREAD_TRIES = 3;
358
358
  const REREAD_GAP_MS = 800;
359
359
 
360
+ /**
361
+ * Whichever of two readings was collected later, regardless of source.
362
+ *
363
+ * Quota numbers only ever move forward in time, so "newer" is the only ranking
364
+ * that makes sense between a store row and something we fetched ourselves. A
365
+ * five-hour window can also reset between the two, which makes an older reading
366
+ * not merely stale but wrong — 23% from before the reset, 3% after it.
367
+ */
368
+ export function freshest(a, b) {
369
+ if (!a) return b ?? null;
370
+ if (!b) return a;
371
+ return (b.fetchedAt ?? 0) > (a.fetchedAt ?? 0) ? b : a;
372
+ }
373
+
360
374
  /** Ask for a collection, then watch the store for the result. */
361
375
  async function nudgeAndReread(previous) {
362
376
  let asked = false;
@@ -426,8 +440,13 @@ async function _doFetch(now, force = false) {
426
440
  // Nothing usable in the store. Everything below spends the user's budget, so
427
441
  // it happens on a floor, and not at all while a 429 cooldown is running.
428
442
  if (!maySelfPoll({ now, force, lastSelfPollAt: _lastSelfPollAt, rateLimitedUntil: _rateLimitedUntil })) {
429
- // A stale row still beats an empty panel, and says how stale it is.
430
- const held = store ?? _lastGood;
443
+ // A stale row still beats an empty panel, and says how stale it is — but
444
+ // it must be the freshest thing we hold, not just the store. Preferring
445
+ // the store here threw away readings we had already paid for: after a boot
446
+ // that fell through to the CLI, the panel showed 3% (fetched seconds ago)
447
+ // and then reverted to 23% (from a 48-minute-old store row) on the very
448
+ // next poll, because the store had not moved.
449
+ const held = freshest(store, _lastGood);
431
450
  if (held) {
432
451
  const result = { ...held, stale: true };
433
452
  _cache = result; _cacheAt = now;