squadrant 0.16.3 → 0.16.4

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.
@@ -1457,13 +1457,14 @@ function projectHealth(input) {
1457
1457
  const { project, now, captainName, captainStopped, commandPresent, crews } = input;
1458
1458
  const out = [];
1459
1459
  const captainState = input.captainState ?? (captainStopped === true ? "stopped" : captainStopped === false ? "alive" : "unknown");
1460
+ const deferral = input.captainDeferral;
1460
1461
  out.push({
1461
1462
  kind: "captain",
1462
1463
  project,
1463
1464
  ref: captainName,
1464
1465
  state: captainState,
1465
1466
  lastSeenMs: null,
1466
- detail: captainState === "stopped" ? "captain workspace closed \u2014 crews reaped; delivery paused" : captainState === "gone" ? "captain process died (crash) \u2014 crews reaped" : void 0
1467
+ detail: captainState === "stopped" ? "captain workspace closed \u2014 crews reaped; delivery paused" : captainState === "gone" ? "captain process died (crash) \u2014 crews reaped" : deferral?.stuck ? `\u26A0\uFE0F delivery stuck (${deferral.maxDeferCount}+ retries) \u2014 draft/ghost text blocking captain pane; input never touched, delivers automatically once cleared` : void 0
1467
1468
  });
1468
1469
  if (commandPresent !== null) {
1469
1470
  out.push({
@@ -1797,6 +1798,8 @@ function buildContext(opts) {
1797
1798
  opencodeBridge: null,
1798
1799
  cmuxEventsBridge: null,
1799
1800
  telegramBridge: void 0,
1801
+ notifyFault: opts.notifyFault ?? (() => {
1802
+ }),
1800
1803
  lifecycleSources: opts.lifecycleSources ?? [],
1801
1804
  broadcast: () => {
1802
1805
  },
@@ -2107,7 +2110,7 @@ var CaptainDelivery = class {
2107
2110
  const seq = entry.seq;
2108
2111
  const deferCount = this.deferCounts.get(seq) ?? 0;
2109
2112
  const stable = (this.stableCounts.get(seq) ?? 0) >= this.opts.stableProbePolls;
2110
- const probe = stable || deferCount >= this.opts.maxDefers;
2113
+ const probe = stable;
2111
2114
  try {
2112
2115
  await send(msg, probe ? { probe: true } : void 0);
2113
2116
  this.deferCounts.delete(seq);
@@ -2231,7 +2234,9 @@ async function runLivenessTick(deps) {
2231
2234
  }
2232
2235
  }
2233
2236
  function createDelivery(ctx, daemonCmux) {
2234
- const { stateRoot, store, log, livenessRegistry, isPidAlive, opts } = ctx;
2237
+ const { stateRoot, store, log, livenessRegistry, isPidAlive, opts, telegramBridge } = ctx;
2238
+ const notifyFault = ctx.notifyFault ?? (() => {
2239
+ });
2235
2240
  const defaultNotify = async (args) => {
2236
2241
  try {
2237
2242
  await appendToMailbox({
@@ -2254,6 +2259,7 @@ function createDelivery(ctx, daemonCmux) {
2254
2259
  const cfg = loadConfig();
2255
2260
  const deliveries = /* @__PURE__ */ new Map();
2256
2261
  const deliveryStats = (project) => deliveries.get(project)?.stats();
2262
+ const stuckNotified = /* @__PURE__ */ new Set();
2257
2263
  const sessionStartMs = Date.now();
2258
2264
  let delivering = false;
2259
2265
  const deliveryCore = async () => {
@@ -2325,6 +2331,18 @@ function createDelivery(ctx, daemonCmux) {
2325
2331
  break;
2326
2332
  }
2327
2333
  }
2334
+ const stuck = d.stats().stuck;
2335
+ if (stuck && !stuckNotified.has(project)) {
2336
+ stuckNotified.add(project);
2337
+ const { maxDeferCount } = d.stats();
2338
+ log(`delivery stuck project=${project} deferCount=${maxDeferCount}`);
2339
+ const text = `\u26A0\uFE0F DELIVERY STUCK: an in-progress draft (or ghost text) in your input box has blocked pending notification(s) for ${maxDeferCount}+ retries. Your input is never touched \u2014 this keeps retrying safely and will deliver automatically once you submit or clear it.`;
2340
+ appendCaptainMessage({ stateRoot, project, text, source: "daemon" }).catch((e) => log(`delivery stuck alert failed project=${project}: ${e.message}`));
2341
+ Promise.resolve(notifyFault(project, text)).catch((e) => log(`delivery stuck fault-notify failed project=${project}: ${e.message}`));
2342
+ telegramBridge?.pushRaw(project, text);
2343
+ } else if (!stuck && stuckNotified.has(project)) {
2344
+ stuckNotified.delete(project);
2345
+ }
2328
2346
  }
2329
2347
  };
2330
2348
  const deliveryTick = async () => {
@@ -2575,7 +2593,12 @@ function startDaemon(ctx, opts, pkgVersion) {
2575
2593
  captainStopped: null,
2576
2594
  captainState: deriveCaptainState(capEntry),
2577
2595
  commandPresent: null,
2578
- crews: store.list(project)
2596
+ crews: store.list(project),
2597
+ // #579/#484 Gap 3: surface the same deferral stats already exposed to
2598
+ // the snapshot (line ~135 below) on the health row too, so `squadrant
2599
+ // doctor` / `squadrant status --detailed` show a stuck delivery with
2600
+ // zero configuration.
2601
+ captainDeferral: deliveryStats(project)
2579
2602
  }));
2580
2603
  }
2581
2604
  return out;
@@ -3259,6 +3282,14 @@ function createTelegramBridge(opts) {
3259
3282
  s.offset = next;
3260
3283
  saveState(stateRoot, s);
3261
3284
  }
3285
+ async function sendToTopic(project, text) {
3286
+ let threadId = loadState(stateRoot).topics[topicKey(project)];
3287
+ if (threadId === void 0) {
3288
+ threadId = await client.createForumTopic(cfg.supergroupId, topicName(project));
3289
+ setTopic(stateRoot, project, threadId);
3290
+ }
3291
+ await client.sendMessage(cfg.supergroupId, threadId, text);
3292
+ }
3262
3293
  async function deliverOutbound(project, ev) {
3263
3294
  const resolved = resolveNotify(cfg.notify, loadProjectOverride(project, configRoot));
3264
3295
  const live = loadState(stateRoot).notify[project];
@@ -3267,12 +3298,10 @@ function createTelegramBridge(opts) {
3267
3298
  return;
3268
3299
  if (!tierIncludes(resolved.crew, ev.type))
3269
3300
  return;
3270
- let threadId = loadState(stateRoot).topics[topicKey(project)];
3271
- if (threadId === void 0) {
3272
- threadId = await client.createForumTopic(cfg.supergroupId, topicName(project));
3273
- setTopic(stateRoot, project, threadId);
3274
- }
3275
- await client.sendMessage(cfg.supergroupId, threadId, formatLifecycle(project, ev));
3301
+ await sendToTopic(project, formatLifecycle(project, ev));
3302
+ }
3303
+ async function deliverRawOutbound(project, text) {
3304
+ await sendToTopic(project, text);
3276
3305
  }
3277
3306
  function resolveLiveNotify(project) {
3278
3307
  const resolved = resolveNotify(cfg.notify, loadProjectOverride(project, configRoot));
@@ -3580,6 +3609,11 @@ function createTelegramBridge(opts) {
3580
3609
  log(`telegram outbound failed project=${project}: ${e.message}`);
3581
3610
  });
3582
3611
  },
3612
+ pushRaw(project, text) {
3613
+ void deliverRawOutbound(project, text).catch((e) => {
3614
+ log(`telegram raw push failed project=${project}: ${e.message}`);
3615
+ });
3616
+ },
3583
3617
  health() {
3584
3618
  return { polling: running, lastSuccessfulPollAt, lastError, lastErrorAt };
3585
3619
  }
@@ -5090,7 +5124,60 @@ var RuntimeRegistry = class {
5090
5124
  };
5091
5125
 
5092
5126
  // packages/workspaces/dist/notifiers/cmux.js
5093
- import { execFileSync as execFileSync6, execSync as execSync7 } from "child_process";
5127
+ import { execFile as execFileCb, execSync as execSync7 } from "child_process";
5128
+ import { promisify as promisify2 } from "util";
5129
+ var execFile3 = promisify2(execFileCb);
5130
+ function createCmuxNotifier(_scope) {
5131
+ return {
5132
+ name: "cmux",
5133
+ async probe() {
5134
+ try {
5135
+ execSync7("squadrant runtime status --command", { encoding: "utf-8", stdio: "pipe" });
5136
+ return { installed: true, reachable: true };
5137
+ } catch (err) {
5138
+ const code = err.code;
5139
+ if (code === "ENOENT") {
5140
+ return { installed: false, reachable: false };
5141
+ }
5142
+ return { installed: true, reachable: false };
5143
+ }
5144
+ },
5145
+ async notify(message) {
5146
+ await execFile3("squadrant", ["runtime", "send", "--command", message], { encoding: "utf-8", timeout: CMUX_TIMEOUT });
5147
+ }
5148
+ };
5149
+ }
5150
+
5151
+ // packages/workspaces/dist/notifiers/registry.js
5152
+ var DEFAULT_NOTIFIER = "cmux";
5153
+ var NotifierRegistry = class {
5154
+ factories;
5155
+ constructor(factories) {
5156
+ this.factories = factories;
5157
+ }
5158
+ get(config) {
5159
+ const name = config.notifier ?? DEFAULT_NOTIFIER;
5160
+ return this.getFactory(name)({});
5161
+ }
5162
+ getFactory(name) {
5163
+ const factory = this.factories[name];
5164
+ if (!factory) {
5165
+ throw new Error(`Unknown notifier provider '${name}' \u2014 no factory registered`);
5166
+ }
5167
+ return factory;
5168
+ }
5169
+ async probeAll() {
5170
+ const results = {};
5171
+ for (const [name, factory] of Object.entries(this.factories)) {
5172
+ try {
5173
+ results[name] = await factory({}).probe();
5174
+ } catch {
5175
+ results[name] = { installed: false, reachable: false };
5176
+ }
5177
+ }
5178
+ return results;
5179
+ }
5180
+ };
5094
5181
 
5095
5182
  // packages/workspaces/dist/workspaces/obsidian.js
5096
5183
  import fs16 from "fs/promises";
@@ -5868,6 +5955,16 @@ function buildTelegramBridge(cfg, stateRoot, log) {
5868
5955
  sendReply
5869
5956
  });
5870
5957
  }
5958
+ function buildNotifyFault(log) {
5959
+ const registry = new NotifierRegistry({ cmux: createCmuxNotifier });
5960
+ return async (project, text) => {
5961
+ try {
5962
+ await registry.get(loadConfig()).notify(`[${project}] ${text}`);
5963
+ } catch (e) {
5964
+ log(`fault notify failed project=${project}: ${e.message}`);
5965
+ }
5966
+ };
5967
+ }
5871
5968
  function startSquadrantd(opts = {}) {
5872
5969
  const ctx = buildContext(opts);
5873
5970
  const { stateRoot, store, log, spawn: spawn2, writeResult, inFlightHeadlessIds, activeHeadlessKills } = ctx;
@@ -5931,6 +6028,8 @@ function startSquadrantd(opts = {}) {
5931
6028
  ctx.lifecycleSources = [cmuxStoreSource, nativeHookSource, codexAppServerSource];
5932
6029
  const tgCfg = loadConfig().telegram;
5933
6030
  ctx.telegramBridge = opts.telegramBridge ?? (tgCfg && !process.env.VITEST ? buildTelegramBridge(tgCfg, stateRoot, log) : void 0);
6031
+ if (opts.notifyFault) ctx.notifyFault = opts.notifyFault;
6032
+ else if (!process.env.VITEST) ctx.notifyFault = buildNotifyFault(log);
5934
6033
  ctx.daemonCmux = opts.daemonCmux ?? (opts.makeDaemonCmux ?? (() => new DaemonCmux(createCmuxDriver())))();
5935
6034
  const resendRuntime = createCmuxDriver();
5936
6035
  ctx.resendFirstTurn = opts.resendFirstTurn ?? (async (rec) => {