agent-dag 1.33.71 → 1.33.73

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-BlsUOLDL.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-BNuS947v.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.71",
3
+ "version": "1.33.73",
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": {
@@ -167,12 +167,34 @@ export function npxRestartSpec(pkgRoot, name = "agents-deck") {
167
167
  return npxSpecFromMeta(meta, name);
168
168
  }
169
169
 
170
+ /** The package an upgrade would actually install here — the only package worth
171
+ * asking npm about.
172
+ *
173
+ * The check used to ask about `agents-deck` no matter what the upgrade
174
+ * command installed, so a deck started with `npx ccdeck` compared its version
175
+ * against `agents-deck`'s dist-tag and then handed back `npx -y ccdeck@latest`.
176
+ * Nothing tied the two together. CI publishes the three names one after
177
+ * another, so between the first and the last publish they genuinely disagree,
178
+ * and inside that window the deck offered a version the command could not
179
+ * install — the ETARGET below, with a window measured in publishes rather than
180
+ * in seconds of propagation.
181
+ *
182
+ * Deriving the name from the resolved upgrade spec keeps the two halves
183
+ * consistent by construction: whatever the command will install is what gets
184
+ * asked about, and the per-name marker follows the same name. A global install
185
+ * installs `name` and a checkout installs nothing, so both keep asking about
186
+ * the name this build was published under. */
187
+ export function upgradeName(pkgRoot, name = "agents-deck") {
188
+ if (!isNpxInstall(pkgRoot) || isGitCheckout(pkgRoot)) return name;
189
+ return bareSpecName(npxRestartSpec(pkgRoot, name)) ?? name;
190
+ }
191
+
170
192
  /** The exact line the user can paste, for the way THIS copy was installed. */
171
193
  export function upgradeCommand(pkgRoot, name = "agents-deck") {
172
194
  // A checkout is updated by pulling, and the bundle is built, not shipped —
173
195
  // so `npm run build` is part of the answer rather than an afterthought.
174
196
  if (isGitCheckout(pkgRoot)) return "git pull && npm run build";
175
- if (isNpxInstall(pkgRoot)) return `npx -y ${npxRestartSpec(pkgRoot, name) ?? `${name}@latest`}`;
197
+ if (isNpxInstall(pkgRoot)) return `npx -y ${upgradeName(pkgRoot, name)}@latest`;
176
198
  return `npm i -g ${name}@latest`;
177
199
  }
178
200
 
@@ -200,6 +222,41 @@ async function fetchLatest(name) {
200
222
  }
201
223
  }
202
224
 
225
+ // A dist-tag is a pointer, and being pointed at is not the same as being
226
+ // installable.
227
+ //
228
+ // npm makes the moved tag visible before the version document has propagated to
229
+ // the replica the installer reads, so for a window `{"latest":"1.33.28"}` and
230
+ // `No matching version found for ccdeck@1.33.28` are both true at the same
231
+ // moment. Reported live: the banner offered v1.33.28, the restart ran
232
+ // `npx -y ccdeck@latest`, npm answered ETARGET, and the deck came back on the
233
+ // version it started with after tearing itself down — under npx a restart is
234
+ // not free, since the worker exits and hands the port over before anything is
235
+ // fetched.
236
+ //
237
+ // So the tag is checked against the version document, which is the same
238
+ // question `npm view <name>@<version> version` asks and the same document the
239
+ // installer resolves against. 404 is the answer this exists for: published
240
+ // tag, unpublished version, try again in five minutes. `ok` is false only when
241
+ // the registry gave no usable answer at all — that is not a licence to
242
+ // announce either, but it is not evidence of an unpublished version.
243
+ async function isPublished(name, version) {
244
+ try {
245
+ const res = await fetch(`https://registry.npmjs.org/${name}/${version}`, {
246
+ headers: { accept: "application/json", "user-agent": "agents-deck" },
247
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
248
+ });
249
+ if (res.status === 404) return { ok: true, published: false };
250
+ if (!res.ok) return { ok: false, published: false };
251
+ // The document has to be the one asked for: a registry that answers 200
252
+ // with something else has not shown that this version is resolvable.
253
+ const body = await res.json().catch(() => null);
254
+ return { ok: true, published: body?.version === version };
255
+ } catch {
256
+ return { ok: false, published: false };
257
+ }
258
+ }
259
+
203
260
  /** Marker file name for a package: `ccdeck` → `.self-update-check-ccdeck`.
204
261
  *
205
262
  * Pure, and deliberately strict about what reaches the filesystem — a package
@@ -276,10 +333,27 @@ function writeMarker(name, marker) {
276
333
  * A success stamps the hour and clears the failure. A failure records only
277
334
  * itself, leaving the last real answer and the time it arrived untouched —
278
335
  * the deck keeps showing what it knew, and stops claiming it just confirmed
279
- * it. */
280
- export function nextMarker({ prev, now, ok, version }) {
281
- if (ok) return { at: now, version: version ?? null, failedAt: null };
282
- return { at: prev?.at ?? null, version: prev?.version ?? null, failedAt: now };
336
+ * it.
337
+ *
338
+ * `installable` is the third outcome: npm answered, and what it named is not
339
+ * yet a version anything can install. That is a real answer — `at` moves, the
340
+ * registry was reached — but the version is held in `pending` instead of
341
+ * `version`, so `latest` stays a number the upgrade command can resolve, and
342
+ * `pendingAt` puts the next look on the short window rather than the hour. */
343
+ export function nextMarker({ prev, now, ok, version, installable = true }) {
344
+ if (!ok) {
345
+ return {
346
+ at: prev?.at ?? null,
347
+ version: prev?.version ?? null,
348
+ failedAt: now,
349
+ pending: prev?.pending ?? null,
350
+ pendingAt: prev?.pendingAt ?? null,
351
+ };
352
+ }
353
+ if (!installable) {
354
+ return { at: now, version: prev?.version ?? null, failedAt: null, pending: version ?? null, pendingAt: now };
355
+ }
356
+ return { at: now, version: version ?? null, failedAt: null, pending: null, pendingAt: null };
283
357
  }
284
358
 
285
359
  // Even a per-package marker is shared by every deck running that package, so
@@ -294,15 +368,20 @@ const _askedThisProcess = new Set();
294
368
  * banner appear" is the question this feature gets asked, and the rule behind
295
369
  * it should be readable in one place.
296
370
  */
297
- export function checkDue({ at, failedAt, now, first = false, force = false, ttlMs = CHECK_MS, retryMs = RETRY_MS }) {
371
+ export function checkDue({ at, failedAt, pendingAt, now, first = false, force = false, ttlMs = CHECK_MS, retryMs = RETRY_MS }) {
298
372
  if (force || first) return true; // explicit ask, or this process's first
299
- // The last attempt failed, so there is nothing to reuse and the long window
300
- // does not apply — but the short one does, or an unreachable registry turns
301
- // every poll into another request. Answered first: `at` here is the older,
302
- // successful check, and letting it decide would ask again immediately.
303
- if (typeof failedAt === "number") {
304
- if (failedAt > now) return true; // clock moved; do not wait it out
305
- return now - failedAt >= retryMs;
373
+ // Two ways of not having an answer yet, and neither may spend the hour a real
374
+ // answer buys: the last attempt failed, or npm named a version that cannot be
375
+ // installed yet. Both take the short window instead — an unreachable registry
376
+ // must not turn every poll into another request, and a release mid-publish is
377
+ // resolvable minutes later, not an hour later. Answered before `at`, which
378
+ // here is the older, settled check and would otherwise ask again immediately
379
+ // (or, for a pending version, not for another hour).
380
+ const unsettled = [failedAt, pendingAt].filter(t => typeof t === "number");
381
+ if (unsettled.length) {
382
+ const last = Math.max(...unsettled);
383
+ if (last > now) return true; // clock moved; do not wait it out
384
+ return now - last >= retryMs;
306
385
  }
307
386
  if (typeof at !== "number") return true; // never checked
308
387
  if (at > now) return true; // marker from the future: a moved clock
@@ -319,18 +398,20 @@ async function latestOnNpm(name, now, force = false) {
319
398
  const key = markerFileName(name);
320
399
  const first = !_askedThisProcess.has(key);
321
400
  _askedThisProcess.add(key);
322
- if (!checkDue({ at: m?.at, failedAt: m?.failedAt, now, first, force })) return m?.version ?? null;
323
- const pending = _inflight.get(key);
324
- if (pending) return pending;
325
- const run = fetchLatest(name)
401
+ if (!checkDue({ at: m?.at, failedAt: m?.failedAt, pendingAt: m?.pendingAt, now, first, force })) {
402
+ return m?.version ?? null;
403
+ }
404
+ const inflight = _inflight.get(key);
405
+ if (inflight) return inflight;
406
+ const run = runCheck(name, m, now)
326
407
  // Record the outcome, not just the moment, and record it against THIS
327
408
  // package: only an answer stamps `at`; a failure takes the short retry
328
409
  // window instead of the hour, keeps the version we already knew rather
329
410
  // than erasing it, and lands in this name's marker rather than spending
330
411
  // another package's window on a lookup that was never about it.
331
- .then(({ ok, version }) => {
332
- writeMarker(name, nextMarker({ prev: m, now, ok, version }));
333
- return (ok ? version : null) ?? m?.version ?? null;
412
+ .then((marker) => {
413
+ writeMarker(name, marker);
414
+ return marker.version ?? null;
334
415
  })
335
416
  .catch(() => m?.version ?? null)
336
417
  .finally(() => { _inflight.delete(key); });
@@ -338,6 +419,20 @@ async function latestOnNpm(name, now, force = false) {
338
419
  return run;
339
420
  }
340
421
 
422
+ /** One check, as a marker: what npm's dist-tag says, and — only when that is a
423
+ * version this deck has not already confirmed — whether it can be installed.
424
+ *
425
+ * The second request is what keeps the banner honest, and it is skipped in the
426
+ * case that runs all day: a tag that has not moved was confirmed the first
427
+ * time it was seen, so a deck sitting on the current release still costs one
428
+ * ~20-byte GET per check. Confirming costs one more, once per release. */
429
+ async function runCheck(name, prev, now) {
430
+ const { ok, version } = await fetchLatest(name);
431
+ if (!ok || version === prev?.version) return nextMarker({ prev, now, ok, version });
432
+ const probe = await isPublished(name, version);
433
+ return nextMarker({ prev, now, ok, version, installable: probe.ok && probe.published });
434
+ }
435
+
341
436
  // ── the notice ───────────────────────────────────────────────────────────────
342
437
 
343
438
  /** Pure: turns three version strings into at most one thing worth saying.
@@ -526,6 +621,11 @@ export function lastMeaningfulLine(text) {
526
621
  * registry is unreachable. */
527
622
  export async function versionReport({ running, pkgRoot, name = "agents-deck", now = Date.now(), force = false }) {
528
623
  const installed = installedVersion(pkgRoot);
624
+ // Asked about the package the command installs, not about the one this build
625
+ // happens to be named after — see upgradeName. Everything registry-shaped in
626
+ // this report is about `target`: the version, the marker it is cached in, and
627
+ // the name the report gives for it.
628
+ const target = upgradeName(pkgRoot, name);
529
629
  // Only an explicit opt-out silences the registry.
530
630
  //
531
631
  // A checkout used to be excluded here too, on the reasoning that its version
@@ -539,11 +639,11 @@ export async function versionReport({ running, pkgRoot, name = "agents-deck", no
539
639
  const skipRegistry =
540
640
  process.env.AGENTS_DECK_NO_UPDATE_CHECK === "1" ||
541
641
  process.env.AGENTS_DECK_NO_INSTALL === "1";
542
- const latest = skipRegistry ? null : await latestOnNpm(name, now, force);
543
- const marker = skipRegistry ? null : readMarker(name);
642
+ const latest = skipRegistry ? null : await latestOnNpm(target, now, force);
643
+ const marker = skipRegistry ? null : readMarker(target);
544
644
  const blocked = upgradeBlock(pkgRoot);
545
645
  return {
546
- name,
646
+ name: target,
547
647
  running: running ?? null,
548
648
  installed,
549
649
  latest,
@@ -556,6 +656,11 @@ export async function versionReport({ running, pkgRoot, name = "agents-deck", no
556
656
  // most common reason for a missing update button — a proxy, a flaky line,
557
657
  // an offline machine — is indistinguishable from being up to date.
558
658
  checkFailedAt: marker?.failedAt ?? null,
659
+ // A version npm's dist-tag names that the registry cannot serve yet — the
660
+ // one thing `latest` deliberately will not say, since saying it is what
661
+ // sent a deck into a restart that ended in ETARGET. Reported so the state
662
+ // is visible rather than looking like nothing was published at all.
663
+ latestPending: marker?.pending ?? null,
559
664
  checkDisabled: skipRegistry,
560
665
  notice: pickNotice({ running, installed, latest }),
561
666
  command: upgradeCommand(pkgRoot, name),