agent-dag 1.33.49 → 1.33.51

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-DXPYK_Sr.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-O75ELHTf.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.49",
3
+ "version": "1.33.51",
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": {
@@ -898,9 +898,18 @@ async function codexScanOnce(firstRun) {
898
898
  }
899
899
  }
900
900
 
901
- function startCodexWatcher(workspace) {
901
+ export function startCodexWatcher(workspace) {
902
902
  codexWorkspace = workspace ?? "";
903
- if (!existsSync(CODEX_SESSIONS_DIR)) return null;
903
+ // Deliberately not gated on CODEX_SESSIONS_DIR existing. `codex login`
904
+ // creates ~/.codex, but sessions/ only appears when the first session
905
+ // starts — and rollout tailing is the only Codex capture path there is, so
906
+ // bailing out here meant a fresh install had to restart the deck before any
907
+ // Codex agent ever showed up, while the banner claimed to be watching.
908
+ // listRecentCodexRollouts returns [] while the directory is missing, so the
909
+ // poll below is a cheap no-op that doubles as the existence re-check. A
910
+ // filesystem watch is no help: fs.watch on a missing path throws, and
911
+ // watching the parent recursively is macOS/Windows-only.
912
+ //
904
913
  // Initial catalog: create roots for in-progress sessions, skip their
905
914
  // history, then poll for new lines.
906
915
  codexScanOnce(true).catch(() => {});
@@ -909,6 +918,12 @@ function startCodexWatcher(workspace) {
909
918
  return codexWatchTimer;
910
919
  }
911
920
 
921
+ /** Envelopes newer than `seq` from the ring buffer, oldest first. */
922
+ export function eventsSince(seq) {
923
+ const after = Number(seq) || 0;
924
+ return events.filter(e => e.seq > after);
925
+ }
926
+
912
927
  function pushEvent(raw, source, opts = {}) {
913
928
  // Synchronous enrichment: if we already know this session's model, stamp
914
929
  // it on the payload so the client's recursive scanner picks it up.
@@ -1513,8 +1528,7 @@ export async function startServer({ port = 4317, host = "127.0.0.1", persist = n
1513
1528
  if (req.method === "POST" && url.pathname === "/api/cswap-auto") return guard(handleCswapAutoAction(req, res), res);
1514
1529
 
1515
1530
  if (req.method === "GET" && url.pathname === "/api/events") {
1516
- const since = Number(url.searchParams.get("since") ?? 0);
1517
- return send(res, 200, events.filter(e => e.seq > since));
1531
+ return send(res, 200, eventsSince(url.searchParams.get("since") ?? 0));
1518
1532
  }
1519
1533
 
1520
1534
  // POST /api/clear — wipe in-memory buffer + persistence file (UI reset)
@@ -45,12 +45,26 @@ const CHECK_MS = 3600_000;
45
45
  // enough that a full npm outage costs a dozen ~20-byte requests an hour.
46
46
  const RETRY_MS = 300_000;
47
47
  const FETCH_TIMEOUT_MS = 6_000;
48
- // A third marker path: ccusage owns ~/.agents-deck/ccusage/.last-update-check
48
+ // A third marker family: ccusage owns ~/.agents-deck/ccusage/.last-update-check
49
49
  // and cswap owns ~/.agents-deck/.cswap-update-check. Sharing one would make the
50
50
  // three features fight over a single daily slot.
51
- const MARKER = join(homedir(), ".agents-deck", ".self-update-check");
52
-
53
- let _inflight = null; // dedupe concurrent /api/version calls into one fetch
51
+ const MARKER_DIR = join(homedir(), ".agents-deck");
52
+ // The name this deck was published under is part of the marker's identity.
53
+ //
54
+ // A single ~/.agents-deck/.self-update-check was shared by every deck on the
55
+ // machine, so whichever one asked npm first pinned the answer for all of them
56
+ // until the window expired — including a freshly started `npx ccdeck` that had
57
+ // never written it, and including decks running a DIFFERENT package where the
58
+ // cached version means nothing. Reported with four decks alive at once: three
59
+ // served the same `checkedAt` to the millisecond and none of them showed the
60
+ // seven releases that had shipped meanwhile. One file per package name means
61
+ // `agents-deck`, `ccdeck` and `agent-dag` decks stop silencing each other; the
62
+ // old shared path stays here only to be inherited from once.
63
+ const LEGACY_MARKER = join(MARKER_DIR, ".self-update-check");
64
+
65
+ // Dedupe concurrent /api/version calls into one fetch, per package name — two
66
+ // names are two different questions and must not be answered with one answer.
67
+ const _inflight = new Map();
54
68
 
55
69
  // ── version comparison ───────────────────────────────────────────────────────
56
70
 
@@ -186,18 +200,44 @@ async function fetchLatest(name) {
186
200
  }
187
201
  }
188
202
 
203
+ /** Marker file name for a package: `ccdeck` → `.self-update-check-ccdeck`.
204
+ *
205
+ * Pure, and deliberately strict about what reaches the filesystem — a package
206
+ * name may be scoped (`@scope/pkg`), and `/`, `\` and `:` are either a path
207
+ * separator or outright illegal in a Windows file name. Everything outside
208
+ * `[a-z0-9._-]` collapses to `-`, the scope's leading `@` is dropped so the
209
+ * common case reads plainly, and the tail is trimmed so a pathological name
210
+ * cannot produce a path the OS refuses. An unusable name falls back to the
211
+ * default rather than to the shared file this fix exists to get rid of. */
212
+ export function markerFileName(name = "agents-deck") {
213
+ const raw = typeof name === "string" ? name.trim().toLowerCase() : "";
214
+ const safe = raw
215
+ .replace(/^@/, "")
216
+ .replace(/[^a-z0-9._-]+/g, "-")
217
+ .slice(0, 64)
218
+ // Trimmed after the slice, and at both ends: Windows silently drops a
219
+ // trailing dot from a file name, so a name that ends in one would write to
220
+ // a path that is not the path we would later read.
221
+ .replace(/^[-.]+|[-.]+$/g, "");
222
+ return `.self-update-check-${safe || "agents-deck"}`;
223
+ }
224
+
225
+ function markerPath(name) {
226
+ return join(MARKER_DIR, markerFileName(name));
227
+ }
228
+
189
229
  // The marker carries the answer, not just the timestamp. The existing markers
190
- // store only an mtime, which means a restart inside the 24h window forgets what
191
- // npm said and shows nothing until the window expires.
230
+ // store only an mtime, which means a restart inside the window forgets what npm
231
+ // said and shows nothing until the window expires.
192
232
  //
193
233
  // It carries the outcome too. `at` is when npm last ANSWERED and `version` is
194
234
  // what it said; `failedAt` is when the last attempt failed, and is null the
195
235
  // moment one succeeds. Keeping them apart is what lets the deck say "checked
196
236
  // 3m ago" and "could not reach npm" as the different things they are, instead
197
237
  // of reporting a timeout as a fresh, up-to-date check.
198
- function readMarker() {
238
+ function readMarkerFile(path) {
199
239
  try {
200
- const m = JSON.parse(readFileSync(MARKER, "utf8"));
240
+ const m = JSON.parse(readFileSync(path, "utf8"));
201
241
  // Either half is enough to be worth keeping: a marker written by a first
202
242
  // attempt that failed has no `at` yet, and markers written before
203
243
  // `failedAt` existed have no `failedAt` at all.
@@ -206,10 +246,26 @@ function readMarker() {
206
246
  return null;
207
247
  }
208
248
  }
209
- function writeMarker(marker) {
249
+
250
+ // The unsuffixed marker every earlier deck wrote, read at most once per name
251
+ // per process: upgrading to this version should not throw away an answer npm
252
+ // already gave. It is consulted only while the per-name file is still missing,
253
+ // so the shared file never goes back to being in charge.
254
+ const _legacy = new Map();
255
+ function legacyMarker(name) {
256
+ const key = markerFileName(name);
257
+ if (!_legacy.has(key)) _legacy.set(key, readMarkerFile(LEGACY_MARKER));
258
+ return _legacy.get(key);
259
+ }
260
+
261
+ function readMarker(name) {
262
+ return readMarkerFile(markerPath(name)) ?? legacyMarker(name);
263
+ }
264
+ function writeMarker(name, marker) {
265
+ const path = markerPath(name);
210
266
  try {
211
- mkdirSync(dirname(MARKER), { recursive: true });
212
- writeFileSync(MARKER, JSON.stringify(marker));
267
+ mkdirSync(dirname(path), { recursive: true });
268
+ writeFileSync(path, JSON.stringify(marker));
213
269
  } catch { /* a read-only home must not break the deck */ }
214
270
  }
215
271
 
@@ -226,11 +282,12 @@ export function nextMarker({ prev, now, ok, version }) {
226
282
  return { at: prev?.at ?? null, version: prev?.version ?? null, failedAt: now };
227
283
  }
228
284
 
229
- // The marker is shared by every deck on the machine, so one deck's check
230
- // silences the others for the rest of the window. Asking once per PROCESS fixes
231
- // that, and it lands the check exactly where the user expects the truth: a
232
- // freshly started deck — which for `npx ccdeck` is every single run.
233
- let _askedThisProcess = false;
285
+ // Even a per-package marker is shared by every deck running that package, so
286
+ // one deck's check still answers for the others inside the window. Asking once
287
+ // per PROCESS lands the check exactly where the user expects the truth: a
288
+ // freshly started deck — which for `npx ccdeck` is every single run. Keyed by
289
+ // name, so a process that asked about one package has not asked about another.
290
+ const _askedThisProcess = new Set();
234
291
 
235
292
  /**
236
293
  * Whether to ask npm, or reuse the answer on disk. Pure, because "why did no
@@ -258,22 +315,27 @@ export function checkDue({ at, failedAt, now, first = false, force = false, ttlM
258
315
  * `force` skips the window: the first call in this process, and an explicit
259
316
  * "check now" from the UI. */
260
317
  async function latestOnNpm(name, now, force = false) {
261
- const m = readMarker();
262
- const first = !_askedThisProcess;
263
- _askedThisProcess = true;
318
+ const m = readMarker(name);
319
+ const key = markerFileName(name);
320
+ const first = !_askedThisProcess.has(key);
321
+ _askedThisProcess.add(key);
264
322
  if (!checkDue({ at: m?.at, failedAt: m?.failedAt, now, first, force })) return m?.version ?? null;
265
- if (_inflight) return _inflight;
266
- _inflight = fetchLatest(name)
267
- // Record the outcome, not just the moment. Only an answer stamps `at`; a
268
- // failure takes the short retry window instead of the hour, and keeps the
269
- // version we already knew rather than erasing it.
323
+ const pending = _inflight.get(key);
324
+ if (pending) return pending;
325
+ const run = fetchLatest(name)
326
+ // Record the outcome, not just the moment, and record it against THIS
327
+ // package: only an answer stamps `at`; a failure takes the short retry
328
+ // window instead of the hour, keeps the version we already knew rather
329
+ // than erasing it, and lands in this name's marker rather than spending
330
+ // another package's window on a lookup that was never about it.
270
331
  .then(({ ok, version }) => {
271
- writeMarker(nextMarker({ prev: m, now, ok, version }));
332
+ writeMarker(name, nextMarker({ prev: m, now, ok, version }));
272
333
  return (ok ? version : null) ?? m?.version ?? null;
273
334
  })
274
335
  .catch(() => m?.version ?? null)
275
- .finally(() => { _inflight = null; });
276
- return _inflight;
336
+ .finally(() => { _inflight.delete(key); });
337
+ _inflight.set(key, run);
338
+ return run;
277
339
  }
278
340
 
279
341
  // ── the notice ───────────────────────────────────────────────────────────────
@@ -448,7 +510,7 @@ export async function versionReport({ running, pkgRoot, name = "agents-deck", no
448
510
  process.env.AGENTS_DECK_NO_UPDATE_CHECK === "1" ||
449
511
  process.env.AGENTS_DECK_NO_INSTALL === "1";
450
512
  const latest = skipRegistry ? null : await latestOnNpm(name, now, force);
451
- const marker = skipRegistry ? null : readMarker();
513
+ const marker = skipRegistry ? null : readMarker(name);
452
514
  const blocked = upgradeBlock(pkgRoot);
453
515
  return {
454
516
  name,