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