trantor 0.18.50 → 0.18.52

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.
package/bin/duty.mjs CHANGED
@@ -245,6 +245,14 @@ if (cmd === "up") {
245
245
  const env = (() => {
246
246
  const e = { RELAY_URL: hub, RUNNER_RULES: RULES, CREW_KICKOFF: KICKOFF,
247
247
  RUNNER_DUTY_NUDGES: "1",
248
+ // A parked seat waits for `trantor up`. That is right for a CODE seat — burning a
249
+ // plan re-sending the same contract helps nobody (#6270). It is wrong for DUTY,
250
+ // which is the seat every other seat's liveness runs through: on 2026-09-09 it
251
+ // parked on a quota read and sat holding 48 messages for 21.9 hours while the
252
+ // crew finished a night's work nobody gated. Duty is the one seat under a launchd
253
+ // keepalive, so it does not need to sit there — it can exit and let the supervisor
254
+ // bring it back clean, which also re-reads auth. 15 minutes, then hand over.
255
+ RUNNER_PARK_MAX_MS: "900000",
248
256
  RUNNER_TITLE: "Trantor Duty Agent", RUNNER_ABOUT: ABOUT,
249
257
  // launchd starts jobs with a MINIMAL Path — the resurrected seat could not find
250
258
  // `claude` and every turn died exit 127 "missing-cli" (found live 2026-08-31,
@@ -360,6 +368,31 @@ if (cmd === "down") {
360
368
  process.exit(0);
361
369
  }
362
370
 
371
+ /**
372
+ * Is duty actually moving mail? Read from the runner's own on-disk queue rather than trusting the
373
+ * process table. `pending-<agent>-<project>.json` is written on every failed delivery and deleted
374
+ * when the queue drains, so its existence means messages are held and its oldest entry says for how
375
+ * long. STALL_HOURS is deliberately well past the runner's own 15-minute retry ceiling: anything
376
+ * older than that is not a retry in progress, it is a parked seat nobody restarted.
377
+ */
378
+ const STALL_HOURS = 1;
379
+ function dutyHealth() {
380
+ const f = join(BUS, `pending-${AGENT}-trantor-duty.json`);
381
+ try {
382
+ const j = JSON.parse(readFileSync(f, "utf8"));
383
+ const wake = Array.isArray(j.wake) ? j.wake : [];
384
+ const bcast = Array.isArray(j.bcast) ? j.bcast : [];
385
+ const held = wake.length + bcast.length;
386
+ if (!held) return { held: 0, stalled: false, oldestHours: "0.0" };
387
+ const stamps = [...wake, ...bcast].map(m => m?.ts).filter(Number.isFinite);
388
+ const oldest = stamps.length ? Math.min(...stamps) : j.ts;
389
+ const hours = (Date.now() - oldest) / 3.6e6;
390
+ return { held, stalled: hours >= STALL_HOURS, oldestHours: hours.toFixed(1) };
391
+ } catch {
392
+ return { held: 0, stalled: false, oldestHours: "0.0" }; // no queue file = nothing held
393
+ }
394
+ }
395
+
363
396
  // status
364
397
  {
365
398
  const pid = alivePid();
@@ -370,7 +403,41 @@ if (cmd === "down") {
370
403
  console.log(" whether a seat is still alive, and clears away dead runners. It never writes code");
371
404
  console.log(" and never edits your project files. Stop it with: trantor duty down");
372
405
  console.log("");
373
- console.log(pid ? `RUNNING (pid ${pid}) as ${SESSION}` : "NOT running");
406
+ // A pid is not health. This line used to read `pid ? "RUNNING" : "NOT running"`, and on 2026-09-09
407
+ // it said RUNNING for a seat whose last successful turn was 21.8 hours earlier and which was
408
+ // holding 48 undelivered messages, the oldest 21.9 hours old — the runner had parked itself
409
+ // waiting on a quota reset and only `trantor up` un-parks it. Nothing noticed, because the one
410
+ // command whose job is to answer "is duty working" was answering "does a process exist".
411
+ //
412
+ // The irony worth keeping: ten lines below, this same function already refuses to assume the hub
413
+ // feed is wired, on the grounds that "a running seat the hub isn't feeding looks identical to a
414
+ // working one from the outside". That rigour was applied to the hub and not to the seat itself.
415
+ //
416
+ // The queue is the honest signal because it survives the process: the runner persists it to
417
+ // pending-<agent>-<project>.json on every failed delivery, so a backlog with an old head means
418
+ // messages are not moving no matter what the process table says.
419
+ const health = dutyHealth();
420
+ if (!pid) {
421
+ console.log("NOT running");
422
+ // A backlog matters MORE when the seat is down, not less: those messages have nobody to deliver
423
+ // them and no retry ladder running. Reporting it only in the pid branch was the first version
424
+ // of this fix, and it hid exactly the case that needs saying out loud.
425
+ if (health.held) {
426
+ console.log(` \x1b[31mand ${health.held} undelivered message(s) are held on disk, oldest ${health.oldestHours}h old\x1b[0m`);
427
+ console.log(` → nothing will deliver them until the seat is back: trantor duty up`);
428
+ } else {
429
+ console.log(" queue empty — nothing is waiting");
430
+ }
431
+ }
432
+ else if (health.stalled) {
433
+ console.log(`\x1b[31mSTALLED\x1b[0m (pid ${pid}) as ${SESSION} — the process is up but mail is not moving`);
434
+ console.log(` holding ${health.held} undelivered message(s), oldest ${health.oldestHours}h old`);
435
+ console.log(` → the runner parks on quota/api failure and only a restart un-parks it: trantor duty up`);
436
+ } else if (health.held) {
437
+ console.log(`RUNNING (pid ${pid}) as ${SESSION} — draining ${health.held} queued message(s), oldest ${health.oldestHours}h old`);
438
+ } else {
439
+ console.log(`RUNNING (pid ${pid}) as ${SESSION} — queue empty`);
440
+ }
374
441
  console.log(existsSync(DUTY_PLIST)
375
442
  ? `keepalive: installed (${DUTY_LABEL}${process.platform === "darwin" ? (dutyLoaded() ? ", loaded" : ", not loaded in this session") : ""}) — launchd relaunches the seat after a crash or reboot`
376
443
  : "keepalive: NOT installed — a crash or reboot leaves the seat down (trantor duty up installs it)");