squadrant 0.13.4 → 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); }
@@ -4395,6 +4431,27 @@ function parseDraftFromScreen(screen) {
4395
4431
  }
4396
4432
  return "";
4397
4433
  }
4434
+ function hasModalOptionList(screen) {
4435
+ if (!screen)
4436
+ return false;
4437
+ const lines = screen.split(/\r?\n/);
4438
+ const HR_RE = /^\s*─{10,}\s*$/;
4439
+ let bottomHR = -1;
4440
+ let topHR = -1;
4441
+ for (let i = lines.length - 1; i >= 0; i--) {
4442
+ if (HR_RE.test(lines[i])) {
4443
+ if (bottomHR === -1)
4444
+ bottomHR = i;
4445
+ else {
4446
+ topHR = i;
4447
+ break;
4448
+ }
4449
+ }
4450
+ }
4451
+ if (topHR === -1)
4452
+ return false;
4453
+ return lines.slice(topHR + 1, bottomHR).some((l) => /^\s*\d+\.\s/.test(l));
4454
+ }
4398
4455
  function readInputBoxRaw(screen, opts) {
4399
4456
  if (!screen)
4400
4457
  return null;
@@ -4639,6 +4696,8 @@ function createCmuxDriver() {
4639
4696
  const draft = parseDraftFromScreen(screen);
4640
4697
  if (draft === null)
4641
4698
  throw new DeferDelivery(null);
4699
+ if (hasModalOptionList(screen))
4700
+ throw new DeferDelivery(null);
4642
4701
  if (draft === "") {
4643
4702
  await deliver();
4644
4703
  return;
@@ -4927,6 +4986,8 @@ var CmuxStoreSource = class {
4927
4986
  debounceTimer;
4928
4987
  /** taskId → last reported snapshot (for snapshot() liveness floor). */
4929
4988
  cache = /* @__PURE__ */ new Map();
4989
+ active = false;
4990
+ lastError = null;
4930
4991
  constructor(opts = {}) {
4931
4992
  this.stateDir = opts.stateDir ?? process.env.CMUX_AGENT_HOOK_STATE_DIR ?? join15(homedir10(), ".cmuxterm");
4932
4993
  this.debounceMs = opts.debounceMs ?? 50;
@@ -4942,10 +5003,13 @@ var CmuxStoreSource = class {
4942
5003
  }
4943
5004
  start(deps) {
4944
5005
  this.deps = deps;
5006
+ this.active = true;
5007
+ this.lastError = null;
4945
5008
  this.scan();
4946
5009
  try {
4947
5010
  this.stopWatcher = this.watchDir(this.stateDir, () => this.scheduleDebounced());
4948
5011
  } catch (e) {
5012
+ this.lastError = e.message;
4949
5013
  this.log(`cmux-store: failed to watch ${this.stateDir}: ${e.message}`);
4950
5014
  }
4951
5015
  }
@@ -4958,11 +5022,16 @@ var CmuxStoreSource = class {
4958
5022
  this.stopWatcher = void 0;
4959
5023
  this.deps = void 0;
4960
5024
  this.cache.clear();
5025
+ this.active = false;
4961
5026
  }
4962
5027
  /** Returns the last-reported snapshot for a known crew (liveness floor). */
4963
5028
  snapshot(taskId) {
4964
5029
  return this.cache.get(taskId);
4965
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
+ }
4966
5035
  // ── private ─────────────────────────────────────────────────────────────────
4967
5036
  scheduleDebounced() {
4968
5037
  if (this.debounceTimer !== void 0) {
@@ -5149,6 +5218,7 @@ var NativeHookSource = class {
5149
5218
  deps;
5150
5219
  /** taskId → last-reported snapshot, for snapshot() liveness floor. */
5151
5220
  cache = /* @__PURE__ */ new Map();
5221
+ active = false;
5152
5222
  constructor(opts = {}) {
5153
5223
  this.hookInstall = opts.hookInstall ?? {};
5154
5224
  this.log = opts.log ?? (() => {
@@ -5156,15 +5226,21 @@ var NativeHookSource = class {
5156
5226
  }
5157
5227
  start(deps) {
5158
5228
  this.deps = deps;
5229
+ this.active = true;
5159
5230
  }
5160
5231
  stop() {
5161
5232
  this.deps = void 0;
5162
5233
  this.cache.clear();
5234
+ this.active = false;
5163
5235
  }
5164
5236
  /** Returns the last-reported snapshot for a known crew (liveness floor poll). */
5165
5237
  snapshot(taskId) {
5166
5238
  return this.cache.get(taskId);
5167
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
+ }
5168
5244
  /**
5169
5245
  * Install squadrant-owned hooks into ~/.claude/settings.json.
5170
5246
  * Idempotent — safe to call on every project init or crew spawn.
@@ -5336,6 +5412,7 @@ function startSquadrantd(opts = {}) {
5336
5412
  ctx.codexDriver = codexDriver;
5337
5413
  ctx.opencodeBridge = opencodeBridge;
5338
5414
  ctx.cmuxEventsBridge = cmuxEventsBridge;
5415
+ ctx.lifecycleSources = [cmuxStoreSource, nativeHookSource, codexAppServerSource];
5339
5416
  const tgCfg = loadConfig().telegram;
5340
5417
  ctx.telegramBridge = opts.telegramBridge ?? (tgCfg && !process.env.VITEST ? buildTelegramBridge(tgCfg, stateRoot, log) : void 0);
5341
5418
  ctx.daemonCmux = opts.daemonCmux ?? (opts.makeDaemonCmux ?? (() => new DaemonCmux(createCmuxDriver())))();