squadrant 0.13.5 → 0.14.1

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.
@@ -33,7 +33,9 @@ function assembleDaemonSnapshot(input, now) {
33
33
  ageMs: input.lastSweepAt == null ? null : now - input.lastSweepAt,
34
34
  cadenceMs: input.sweepCadenceMs
35
35
  },
36
- log: input.log
36
+ log: input.log,
37
+ telegram: input.telegram,
38
+ lifecycleSources: input.lifecycleSources
37
39
  },
38
40
  tier1: input.health,
39
41
  tier2: {
@@ -45,7 +47,8 @@ function assembleDaemonSnapshot(input, now) {
45
47
  lastAckedSeq: p.lastAckedSeq,
46
48
  behind: Math.max(0, p.mailbox.maxSeq - p.lastAckedSeq)
47
49
  },
48
- store: { byState: p.storeByState, corruptCount: p.corruptCount }
50
+ store: { byState: p.storeByState, corruptCount: p.corruptCount },
51
+ deferral: p.deferral ?? { maxDeferCount: 0, stuck: false }
49
52
  })),
50
53
  results: input.results
51
54
  }
@@ -534,6 +537,8 @@ function reduce(rec, ev, now) {
534
537
  case "task.turn.completed":
535
538
  if (rec.state === "blocked")
536
539
  return { ...rec, lastHeartbeat: now, lastEvent: ev.type };
540
+ if (rec.pendingTool)
541
+ return stampAttempt(base, {}, now);
537
542
  return { ...stampAttempt(base, {}, now), state: "awaiting-input", pendingTool: void 0 };
538
543
  case "task.delta":
539
544
  return stampAttempt(base, {}, now);
@@ -1128,18 +1133,21 @@ async function* readFromCursor(opts) {
1128
1133
  }
1129
1134
  async function mailboxStats(stateRoot, project) {
1130
1135
  const file = logPath(stateRoot, project);
1136
+ const rotated = await listRotatedOldestFirst(stateRoot, project);
1131
1137
  let sizeBytes = 0;
1132
- try {
1133
- sizeBytes = (await fs7.stat(file)).size;
1134
- } catch (e) {
1135
- if (e.code !== "ENOENT")
1136
- throw e;
1138
+ for (const f of [file, ...rotated]) {
1139
+ try {
1140
+ sizeBytes += (await fs7.stat(f)).size;
1141
+ } catch (e) {
1142
+ if (e.code !== "ENOENT")
1143
+ throw e;
1144
+ }
1137
1145
  }
1138
- const rotated = await listRotatedOldestFirst(stateRoot, project);
1146
+ const oldestFile = rotated[0] ?? file;
1139
1147
  return {
1140
1148
  maxSeq: await readMaxSeq(stateRoot, project),
1141
1149
  sizeBytes,
1142
- oldestEntryAgeMs: await oldestEntryAgeMs(file),
1150
+ oldestEntryAgeMs: await oldestEntryAgeMs(oldestFile),
1143
1151
  rotationCount: rotated.length
1144
1152
  };
1145
1153
  }
@@ -1412,13 +1420,14 @@ function projectHealth(input) {
1412
1420
  for (const c of crews) {
1413
1421
  if (TERMINAL.has(c.state))
1414
1422
  continue;
1423
+ const undelivered = c.mode === "interactive" && !c.firstTurnConfirmedAt && c.heartbeatBudgetMs != null && now - c.lastHeartbeat > c.heartbeatBudgetMs;
1415
1424
  out.push({
1416
1425
  kind: "crew",
1417
1426
  project,
1418
1427
  ref: c.name ?? c.id.slice(0, 8),
1419
1428
  state: classifyHealth(c.lastHeartbeat, now, CREW_STALE_MS, CREW_GONE_MS),
1420
1429
  lastSeenMs: c.lastHeartbeat,
1421
- detail: c.state
1430
+ detail: undelivered ? `undelivered (${c.state})` : c.state
1422
1431
  });
1423
1432
  }
1424
1433
  return out;
@@ -1638,6 +1647,7 @@ function buildContext(opts) {
1638
1647
  opencodeBridge: null,
1639
1648
  cmuxEventsBridge: null,
1640
1649
  telegramBridge: void 0,
1650
+ lifecycleSources: opts.lifecycleSources ?? [],
1641
1651
  broadcast: () => {
1642
1652
  },
1643
1653
  schedulePromotion: () => {
@@ -1969,6 +1979,14 @@ var CaptainDelivery = class {
1969
1979
  return { deferred: true };
1970
1980
  }
1971
1981
  }
1982
+ /** Read-only. Never mutates — safe to poll from the snapshot assembler every tick. */
1983
+ stats() {
1984
+ let maxDeferCount = 0;
1985
+ for (const c of this.deferCounts.values())
1986
+ if (c > maxDeferCount)
1987
+ maxDeferCount = c;
1988
+ return { maxDeferCount, stuck: maxDeferCount >= this.opts.maxDefers };
1989
+ }
1972
1990
  };
1973
1991
 
1974
1992
  // packages/core/dist/daemon/delivery-loop.js
@@ -2008,11 +2026,12 @@ function createDelivery(ctx, daemonCmux) {
2008
2026
  }
2009
2027
  };
2010
2028
  if (!daemonCmux) {
2011
- return { defaultNotify, deliveryTick: void 0 };
2029
+ return { defaultNotify, deliveryTick: void 0, deliveryStats: () => void 0 };
2012
2030
  }
2013
2031
  const cmux2 = daemonCmux;
2014
2032
  const cfg = loadConfig();
2015
2033
  const deliveries = /* @__PURE__ */ new Map();
2034
+ const deliveryStats = (project) => deliveries.get(project)?.stats();
2016
2035
  const sessionStartMs = Date.now();
2017
2036
  let delivering = false;
2018
2037
  const deliveryCore = async () => {
@@ -2095,7 +2114,7 @@ function createDelivery(ctx, daemonCmux) {
2095
2114
  delivering = false;
2096
2115
  }
2097
2116
  };
2098
- return { defaultNotify, deliveryTick };
2117
+ return { defaultNotify, deliveryTick, deliveryStats };
2099
2118
  }
2100
2119
 
2101
2120
  // packages/core/dist/daemon/gates.js
@@ -2275,7 +2294,7 @@ function startDaemon(ctx, opts, pkgVersion) {
2275
2294
  const { stateRoot, store, log, isPidAlive, resultsDir, taskTimeoutMs, inFlightHeadlessIds, activeHeadlessKills, broadcast, cancelPromotionsFor } = ctx;
2276
2295
  const { daemonCmux } = ctx;
2277
2296
  const probes = createProbes(ctx);
2278
- const { defaultNotify, deliveryTick: initialDeliveryTick } = createDelivery(ctx, daemonCmux);
2297
+ const { defaultNotify, deliveryTick: initialDeliveryTick, deliveryStats } = createDelivery(ctx, daemonCmux);
2279
2298
  const baseNotify = opts.notify ?? defaultNotify;
2280
2299
  const notify = ctx.telegramBridge ? async (args) => {
2281
2300
  await baseNotify(args);
@@ -2349,7 +2368,8 @@ function startDaemon(ctx, opts, pkgVersion) {
2349
2368
  mailbox: await mailboxStats(stateRoot, project),
2350
2369
  lastAckedSeq: cursor?.lastAckedSeq ?? 0,
2351
2370
  storeByState: storeStats.byState,
2352
- corruptCount: storeStats.corruptCount
2371
+ corruptCount: storeStats.corruptCount,
2372
+ deferral: deliveryStats(project)
2353
2373
  };
2354
2374
  }));
2355
2375
  return {
@@ -2360,6 +2380,8 @@ function startDaemon(ctx, opts, pkgVersion) {
2360
2380
  lastSweepAt: ctx.lastSweepAt.value,
2361
2381
  sweepCadenceMs: opts.sweepMs ?? 3e4,
2362
2382
  log: gatherLogStats(logPath2, now, SNAPSHOT_LOG_WINDOW_MS),
2383
+ telegram: ctx.telegramBridge ? { configured: true, ...ctx.telegramBridge.health() } : { configured: false, polling: false, lastSuccessfulPollAt: null, lastError: null, lastErrorAt: null },
2384
+ lifecycleSources: ctx.lifecycleSources.map((s) => ({ name: s.name, ...s.health?.() ?? { active: true, error: null } })),
2363
2385
  health: buildHealth(),
2364
2386
  projects,
2365
2387
  results: gatherResults(resultsDir)
@@ -2978,6 +3000,9 @@ function createTelegramBridge(opts) {
2978
3000
  const configRoot = opts.configRoot ?? path9.join(os3.homedir(), ".config", "squadrant");
2979
3001
  const pollMs = cfg.pollMs ?? 1e3;
2980
3002
  let running = false;
3003
+ let lastSuccessfulPollAt = null;
3004
+ let lastError = null;
3005
+ let lastErrorAt = null;
2981
3006
  function persistOffset(next) {
2982
3007
  const s = loadState(stateRoot);
2983
3008
  s.offset = next;
@@ -3276,7 +3301,10 @@ function createTelegramBridge(opts) {
3276
3301
  await handleUpdate(u);
3277
3302
  persistOffset(u.update_id + 1);
3278
3303
  }
3304
+ lastSuccessfulPollAt = Date.now();
3279
3305
  } catch (e) {
3306
+ lastError = e.message;
3307
+ lastErrorAt = Date.now();
3280
3308
  log(`telegram inbound poll failed: ${e.message}`);
3281
3309
  }
3282
3310
  if (running)
@@ -3297,6 +3325,9 @@ function createTelegramBridge(opts) {
3297
3325
  void deliverOutbound(project, ev).catch((e) => {
3298
3326
  log(`telegram outbound failed project=${project}: ${e.message}`);
3299
3327
  });
3328
+ },
3329
+ health() {
3330
+ return { polling: running, lastSuccessfulPollAt, lastError, lastErrorAt };
3300
3331
  }
3301
3332
  };
3302
3333
  }
@@ -3568,17 +3599,24 @@ var CodexAppServerSource = class {
3568
3599
  deps;
3569
3600
  /** taskId → last reported snapshot (for snapshot() liveness floor). */
3570
3601
  cache = /* @__PURE__ */ new Map();
3602
+ active = false;
3571
3603
  start(deps) {
3572
3604
  this.deps = deps;
3605
+ this.active = true;
3573
3606
  }
3574
3607
  stop() {
3575
3608
  this.deps = void 0;
3576
3609
  this.cache.clear();
3610
+ this.active = false;
3577
3611
  }
3578
3612
  /** Returns the last-reported snapshot for a known crew (liveness floor). */
3579
3613
  snapshot(taskId) {
3580
3614
  return this.cache.get(taskId);
3581
3615
  }
3616
+ /** Read-only source health (B4). Purely push-driven — never errors on its own. */
3617
+ health() {
3618
+ return { active: this.active, error: null };
3619
+ }
3582
3620
  /**
3583
3621
  * Feed a ControlEvent from CodexInteractiveDriver into this source.
3584
3622
  * The daemon wires: emit = (ev) => { source.observe(ev); handle(ev); }
@@ -4950,6 +4988,8 @@ var CmuxStoreSource = class {
4950
4988
  debounceTimer;
4951
4989
  /** taskId → last reported snapshot (for snapshot() liveness floor). */
4952
4990
  cache = /* @__PURE__ */ new Map();
4991
+ active = false;
4992
+ lastError = null;
4953
4993
  constructor(opts = {}) {
4954
4994
  this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join15(homedir10(), ".cmuxterm");
4955
4995
  this.debounceMs = opts.debounceMs ?? 50;
@@ -4965,10 +5005,13 @@ var CmuxStoreSource = class {
4965
5005
  }
4966
5006
  start(deps) {
4967
5007
  this.deps = deps;
5008
+ this.active = true;
5009
+ this.lastError = null;
4968
5010
  this.scan();
4969
5011
  try {
4970
5012
  this.stopWatcher = this.watchDir(this.stateDir, () => this.scheduleDebounced());
4971
5013
  } catch (e) {
5014
+ this.lastError = e.message;
4972
5015
  this.log(`cmux-store: failed to watch ${this.stateDir}: ${e.message}`);
4973
5016
  }
4974
5017
  }
@@ -4981,11 +5024,16 @@ var CmuxStoreSource = class {
4981
5024
  this.stopWatcher = void 0;
4982
5025
  this.deps = void 0;
4983
5026
  this.cache.clear();
5027
+ this.active = false;
4984
5028
  }
4985
5029
  /** Returns the last-reported snapshot for a known crew (liveness floor). */
4986
5030
  snapshot(taskId) {
4987
5031
  return this.cache.get(taskId);
4988
5032
  }
5033
+ /** Read-only source health (B4 — dashboard visibility into which sources are up). */
5034
+ health() {
5035
+ return { active: this.active, error: this.lastError };
5036
+ }
4989
5037
  // ── private ─────────────────────────────────────────────────────────────────
4990
5038
  scheduleDebounced() {
4991
5039
  if (this.debounceTimer !== void 0) {
@@ -5172,6 +5220,7 @@ var NativeHookSource = class {
5172
5220
  deps;
5173
5221
  /** taskId → last-reported snapshot, for snapshot() liveness floor. */
5174
5222
  cache = /* @__PURE__ */ new Map();
5223
+ active = false;
5175
5224
  constructor(opts = {}) {
5176
5225
  this.hookInstall = opts.hookInstall ?? {};
5177
5226
  this.log = opts.log ?? (() => {
@@ -5179,15 +5228,21 @@ var NativeHookSource = class {
5179
5228
  }
5180
5229
  start(deps) {
5181
5230
  this.deps = deps;
5231
+ this.active = true;
5182
5232
  }
5183
5233
  stop() {
5184
5234
  this.deps = void 0;
5185
5235
  this.cache.clear();
5236
+ this.active = false;
5186
5237
  }
5187
5238
  /** Returns the last-reported snapshot for a known crew (liveness floor poll). */
5188
5239
  snapshot(taskId) {
5189
5240
  return this.cache.get(taskId);
5190
5241
  }
5242
+ /** Read-only source health (B4). Purely push-driven — never errors on its own. */
5243
+ health() {
5244
+ return { active: this.active, error: null };
5245
+ }
5191
5246
  /**
5192
5247
  * Install squadrant-owned hooks into ~/.claude/settings.json.
5193
5248
  * Idempotent — safe to call on every project init or crew spawn.
@@ -5359,6 +5414,7 @@ function startSquadrantd(opts = {}) {
5359
5414
  ctx.codexDriver = codexDriver;
5360
5415
  ctx.opencodeBridge = opencodeBridge;
5361
5416
  ctx.cmuxEventsBridge = cmuxEventsBridge;
5417
+ ctx.lifecycleSources = [cmuxStoreSource, nativeHookSource, codexAppServerSource];
5362
5418
  const tgCfg = loadConfig().telegram;
5363
5419
  ctx.telegramBridge = opts.telegramBridge ?? (tgCfg && !process.env.VITEST ? buildTelegramBridge(tgCfg, stateRoot, log) : void 0);
5364
5420
  ctx.daemonCmux = opts.daemonCmux ?? (opts.makeDaemonCmux ?? (() => new DaemonCmux(createCmuxDriver())))();