squadrant 0.16.5 → 0.16.6

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.
@@ -500,6 +500,11 @@ function nextPendingTool(current, ev, now) {
500
500
  return void 0;
501
501
  return current;
502
502
  }
503
+ function nextPendingMonitor(current, ev, now) {
504
+ if (ev.note === "agent.hook.PreToolUse" && ev.tool === "Monitor")
505
+ return { since: now };
506
+ return current;
507
+ }
503
508
  function reduce(rec, ev, now) {
504
509
  if (ev.type === "task.reopened") {
505
510
  return { ...rec, state: "working", question: void 0, error: void 0, lastHeartbeat: now, lastEvent: ev.type };
@@ -516,14 +521,17 @@ function reduce(rec, ev, now) {
516
521
  sessionId: ev.sessionId ?? rec.sessionId,
517
522
  question: void 0,
518
523
  // resuming after a blocked→reply clears the question
519
- pendingTool: void 0
524
+ pendingTool: void 0,
520
525
  // #354: a new turn closes any prior tool window
526
+ pendingMonitor: void 0
527
+ // #594a: same reset — a new turn moots any prior watch
521
528
  };
522
529
  case "task.progress": {
523
530
  const pendingTool = nextPendingTool(rec.pendingTool, ev, now);
531
+ const pendingMonitor = nextPendingMonitor(rec.pendingMonitor, ev, now);
524
532
  if (isStickyAttention(rec.state))
525
- return { ...rec, lastHeartbeat: now, lastEvent: ev.type, pendingTool };
526
- const b = { ...base, pendingTool };
533
+ return { ...rec, lastHeartbeat: now, lastEvent: ev.type, pendingTool, pendingMonitor };
534
+ const b = { ...base, pendingTool, pendingMonitor };
527
535
  if (rec.state === "awaiting-input" || rec.state === "stalled")
528
536
  return { ...stampAttempt(b, {}, now), state: "working" };
529
537
  return stampAttempt(b, {}, now);
@@ -537,9 +545,9 @@ function reduce(rec, ev, now) {
537
545
  case "task.blocked":
538
546
  if (rec.state === "blocked")
539
547
  return { ...rec, lastHeartbeat: now, lastEvent: ev.type };
540
- return { ...base, state: "blocked", question: ev.question, pendingTool: void 0 };
548
+ return { ...base, state: "blocked", question: ev.question, pendingTool: void 0, pendingMonitor: void 0 };
541
549
  case "task.review":
542
- return { ...base, state: "review", reviewNote: ev.message, pendingTool: void 0 };
550
+ return { ...base, state: "review", reviewNote: ev.message, pendingTool: void 0, pendingMonitor: void 0 };
543
551
  case "task.done":
544
552
  if (rec.state === "review" && ev.source !== "approve") {
545
553
  return { ...rec, lastHeartbeat: now, lastEvent: ev.type };
@@ -554,19 +562,19 @@ function reduce(rec, ev, now) {
554
562
  case "task.session":
555
563
  return stampAttempt(base, { resumeRef: ev.resumeRef }, now);
556
564
  case "task.turn.started":
557
- return { ...stampAttempt(base, {}, now), state: "working", pendingTool: void 0 };
565
+ return { ...stampAttempt(base, {}, now), state: "working", pendingTool: void 0, pendingMonitor: void 0 };
558
566
  case "task.turn.completed":
559
567
  if (isStickyAttention(rec.state))
560
568
  return { ...rec, lastHeartbeat: now, lastEvent: ev.type };
561
- if (rec.pendingTool)
569
+ if (rec.pendingTool || rec.pendingMonitor)
562
570
  return stampAttempt(base, {}, now);
563
- return { ...stampAttempt(base, {}, now), state: "awaiting-input", pendingTool: void 0 };
571
+ return { ...stampAttempt(base, {}, now), state: "awaiting-input", pendingTool: void 0, pendingMonitor: void 0 };
564
572
  case "task.delta":
565
573
  return stampAttempt(base, {}, now);
566
574
  // heartbeat-only
567
575
  case "task.input.requested":
568
576
  case "task.approval.requested":
569
- return { ...stampAttempt(base, {}, now), state: "blocked", question: ev.question, pendingTool: void 0 };
577
+ return { ...stampAttempt(base, {}, now), state: "blocked", question: ev.question, pendingTool: void 0, pendingMonitor: void 0 };
570
578
  case "task.reattached":
571
579
  return stampAttempt(base, {}, now);
572
580
  case "task.first-turn.confirmed":
@@ -587,15 +595,22 @@ function reduce(rec, ev, now) {
587
595
 
588
596
  // packages/core/dist/watchdog.js
589
597
  var TOOL_STALL_BUDGET_MS = 10 * 60 * 1e3;
590
- function evaluateStall(rec, now, toolStallMs = TOOL_STALL_BUDGET_MS) {
598
+ var MONITOR_STALL_BUDGET_MS = 60 * 60 * 1e3;
599
+ function evaluateStall(rec, now, toolStallMs = TOOL_STALL_BUDGET_MS, monitorStallMs = MONITOR_STALL_BUDGET_MS) {
591
600
  if (rec.state !== "working")
592
601
  return null;
593
602
  if (rec.mode === "interactive") {
594
- if (!rec.pendingTool)
595
- return null;
596
- if (now - rec.pendingTool.since <= toolStallMs)
597
- return null;
598
- return { ...rec, state: "stalled", lastEvent: "watchdog.tool-stall" };
603
+ if (rec.pendingTool) {
604
+ if (now - rec.pendingTool.since <= toolStallMs)
605
+ return null;
606
+ return { ...rec, state: "stalled", lastEvent: "watchdog.tool-stall" };
607
+ }
608
+ if (rec.pendingMonitor) {
609
+ if (now - rec.pendingMonitor.since <= monitorStallMs)
610
+ return null;
611
+ return { ...rec, state: "stalled", lastEvent: "watchdog.monitor-stall" };
612
+ }
613
+ return null;
599
614
  }
600
615
  const liveness = rec.attempts.at(-1)?.lastHeartbeatAt ?? rec.lastHeartbeat;
601
616
  if (now - liveness <= rec.heartbeatBudgetMs)
@@ -605,7 +620,7 @@ function evaluateStall(rec, now, toolStallMs = TOOL_STALL_BUDGET_MS) {
605
620
  function recoverStall(rec, now) {
606
621
  if (rec.state !== "stalled")
607
622
  return null;
608
- return { ...rec, state: "working", lastHeartbeat: now, lastEvent: "watchdog.recover", pendingTool: void 0 };
623
+ return { ...rec, state: "working", lastHeartbeat: now, lastEvent: "watchdog.recover", pendingTool: void 0, pendingMonitor: void 0 };
609
624
  }
610
625
 
611
626
  // packages/core/dist/daemon/reduce.js
@@ -912,7 +927,7 @@ function createDaemon(deps) {
912
927
  store.delete(r.project, r.id);
913
928
  continue;
914
929
  }
915
- if (!TERMINAL_STATES.has(r.state)) {
930
+ if (!TERMINAL_STATES.has(r.state) && !isStickyAttention(r.state)) {
916
931
  const ceiling = deps.taskTimeoutMs ?? DEFAULT_TASK_TIMEOUT_MS;
917
932
  if (t - r.createdAt > ceiling) {
918
933
  const prevState = r.state;
@@ -957,7 +972,7 @@ function createDaemon(deps) {
957
972
  const idle = evaluateStall(r, t);
958
973
  if (idle) {
959
974
  store.put(idle);
960
- const synthEvent = idle.pendingTool ? { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs, tool: idle.pendingTool.name, elapsedMs: t - idle.pendingTool.since } : { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs };
975
+ const synthEvent = idle.pendingTool ? { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs, tool: idle.pendingTool.name, elapsedMs: t - idle.pendingTool.since } : idle.pendingMonitor ? { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs, tool: "Monitor", elapsedMs: t - idle.pendingMonitor.since } : { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs };
961
976
  firePush(deps, r.project, r.state, idle, synthEvent, lastCaptainTurnAt.get(r.id));
962
977
  continue;
963
978
  }
@@ -2087,9 +2102,11 @@ function buildSurfaceProbe(ctx, probes, daemonCmux) {
2087
2102
  // packages/core/dist/delivery/defer-delivery.js
2088
2103
  var DeferDelivery = class extends Error {
2089
2104
  draft;
2090
- constructor(draft = null) {
2105
+ reason;
2106
+ constructor(draft = null, reason = "draft") {
2091
2107
  super("deferred: captain composing");
2092
2108
  this.draft = draft;
2109
+ this.reason = reason;
2093
2110
  this.name = "DeferDelivery";
2094
2111
  }
2095
2112
  };
@@ -2107,6 +2124,7 @@ var CaptainDelivery = class {
2107
2124
  deferCounts = /* @__PURE__ */ new Map();
2108
2125
  lastContent = /* @__PURE__ */ new Map();
2109
2126
  stableCounts = /* @__PURE__ */ new Map();
2127
+ lastReason = /* @__PURE__ */ new Map();
2110
2128
  constructor(opts) {
2111
2129
  this.opts = opts;
2112
2130
  }
@@ -2129,29 +2147,40 @@ var CaptainDelivery = class {
2129
2147
  this.deferCounts.delete(seq);
2130
2148
  this.stableCounts.delete(seq);
2131
2149
  this.lastContent.delete(seq);
2150
+ this.lastReason.delete(seq);
2132
2151
  return { delivered: true };
2133
2152
  } catch (e) {
2134
2153
  if (e instanceof DeferDelivery) {
2135
2154
  this.deferCounts.set(seq, deferCount + 1);
2136
2155
  const content = e.draft;
2156
+ let stableCount;
2137
2157
  if (content && content === this.lastContent.get(seq)) {
2138
- this.stableCounts.set(seq, (this.stableCounts.get(seq) ?? 0) + 1);
2158
+ stableCount = (this.stableCounts.get(seq) ?? 0) + 1;
2159
+ this.stableCounts.set(seq, stableCount);
2139
2160
  } else {
2161
+ stableCount = 0;
2140
2162
  this.stableCounts.set(seq, 0);
2141
2163
  }
2142
2164
  this.lastContent.set(seq, content);
2143
- return { deferred: true };
2165
+ const reason = e.reason !== "draft" ? e.reason : stableCount >= this.opts.stableProbePolls ? "stable" : "draft";
2166
+ this.lastReason.set(seq, reason);
2167
+ return { deferred: true, reason };
2144
2168
  }
2145
- return { deferred: true };
2169
+ this.lastReason.set(seq, "unknown");
2170
+ return { deferred: true, reason: "unknown" };
2146
2171
  }
2147
2172
  }
2148
2173
  /** Read-only. Never mutates — safe to poll from the snapshot assembler every tick. */
2149
2174
  stats() {
2150
2175
  let maxDeferCount = 0;
2151
- for (const c of this.deferCounts.values())
2152
- if (c > maxDeferCount)
2176
+ let reason;
2177
+ for (const [seq, c] of this.deferCounts) {
2178
+ if (c > maxDeferCount) {
2153
2179
  maxDeferCount = c;
2154
- return { maxDeferCount, stuck: maxDeferCount >= this.opts.maxDefers };
2180
+ reason = this.lastReason.get(seq);
2181
+ }
2182
+ }
2183
+ return { maxDeferCount, stuck: maxDeferCount >= this.opts.maxDefers, reason };
2155
2184
  }
2156
2185
  };
2157
2186
 
@@ -2251,6 +2280,10 @@ function createDelivery(ctx, daemonCmux) {
2251
2280
  const notifyFault = ctx.notifyFault ?? (() => {
2252
2281
  });
2253
2282
  const defaultNotify = async (args) => {
2283
+ const fresh = store.get(args.project, args.record.id);
2284
+ if (fresh && TERMINAL_STATES.has(fresh.state) && fresh.state !== args.record.state) {
2285
+ return;
2286
+ }
2254
2287
  try {
2255
2288
  await appendToMailbox({
2256
2289
  stateRoot,
@@ -2340,16 +2373,19 @@ function createDelivery(ctx, daemonCmux) {
2340
2373
  log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=delivered`);
2341
2374
  await writeCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER, lastAckedSeq: entry.seq });
2342
2375
  } else {
2343
- log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=deferred`);
2376
+ const { maxDeferCount } = d.stats();
2377
+ if (maxDeferCount === 1 || maxDeferCount % 30 === 0) {
2378
+ log(`delivery seq=${entry.seq} kind=${entry.kind} outcome=deferred project=${project} reason=${result.reason}`);
2379
+ }
2344
2380
  break;
2345
2381
  }
2346
2382
  }
2347
2383
  const stuck = d.stats().stuck;
2348
2384
  if (stuck && !stuckNotified.has(project)) {
2349
2385
  stuckNotified.add(project);
2350
- const { maxDeferCount } = d.stats();
2351
- log(`delivery stuck project=${project} deferCount=${maxDeferCount}`);
2352
- 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.`;
2386
+ const { maxDeferCount, reason } = d.stats();
2387
+ log(`delivery stuck project=${project} deferCount=${maxDeferCount} reason=${reason ?? "unknown"}`);
2388
+ const text = reason === "modal" ? `\u26A0\uFE0F DELIVERY STUCK: a modal question is open in your captain pane and has blocked pending notification(s) for ${maxDeferCount}+ retries. This keeps retrying safely and will deliver automatically once you answer or dismiss it.` : `\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.`;
2353
2389
  appendCaptainMessage({ stateRoot, project, text, source: "daemon" }).catch((e) => log(`delivery stuck alert failed project=${project}: ${e.message}`));
2354
2390
  Promise.resolve(notifyFault(project, text)).catch((e) => log(`delivery stuck fault-notify failed project=${project}: ${e.message}`));
2355
2391
  telegramBridge?.pushRaw(project, text);
@@ -5049,9 +5085,9 @@ function createCmuxDriver() {
5049
5085
  }
5050
5086
  const draft = parseDraftFromScreen(screen);
5051
5087
  if (draft === null)
5052
- throw new DeferDelivery(null);
5088
+ throw new DeferDelivery(null, "no-box");
5053
5089
  if (hasModalOptionList(screen))
5054
- throw new DeferDelivery(null);
5090
+ throw new DeferDelivery(null, "modal");
5055
5091
  if (draft === "") {
5056
5092
  await deliver();
5057
5093
  return;
@@ -5712,6 +5748,7 @@ function installClaudeHooks(opts = {}) {
5712
5748
  });
5713
5749
  let settings = {};
5714
5750
  const raw = readFile6(settingsPath);
5751
+ const hadExistingSettings = raw !== void 0;
5715
5752
  if (raw) {
5716
5753
  try {
5717
5754
  settings = JSON.parse(raw);
@@ -5724,6 +5761,7 @@ function installClaudeHooks(opts = {}) {
5724
5761
  }
5725
5762
  const hooks = settings.hooks;
5726
5763
  let changed = false;
5764
+ const repaired = [];
5727
5765
  for (const [eventName, sub, matcher] of CLAUDE_HOOK_EVENTS) {
5728
5766
  if (!Array.isArray(hooks[eventName])) {
5729
5767
  hooks[eventName] = [];
@@ -5735,6 +5773,26 @@ function installClaudeHooks(opts = {}) {
5735
5773
  if (!alreadyPresent) {
5736
5774
  entries.push({ matcher: hookMatcher, hooks: [{ type: "command", command, timeout: 10 }] });
5737
5775
  changed = true;
5776
+ repaired.push(`${eventName}/${sub}`);
5777
+ }
5778
+ }
5779
+ if (repaired.length > 0 && hadExistingSettings) {
5780
+ log(`native-hook: repaired ${repaired.length} missing squadrant hook(s) in ${settingsPath} [${repaired.join(", ")}] \u2014 WARNING: blocked-signalling or lifecycle tracking may have been broken until this run`);
5781
+ }
5782
+ if (opts.claudeEnv && Object.keys(opts.claudeEnv).length > 0) {
5783
+ if (typeof settings.env !== "object" || settings.env === null || Array.isArray(settings.env)) {
5784
+ settings.env = {};
5785
+ }
5786
+ const env = settings.env;
5787
+ for (const [key, value] of Object.entries(opts.claudeEnv)) {
5788
+ if (key in env) {
5789
+ if (env[key] !== value) {
5790
+ log(`native-hook: claudeEnv key '${key}' already set to '${String(env[key])}' in ${settingsPath} \u2014 not overwriting with '${value}'`);
5791
+ }
5792
+ continue;
5793
+ }
5794
+ env[key] = value;
5795
+ changed = true;
5738
5796
  }
5739
5797
  }
5740
5798
  if (changed) {
@@ -5771,9 +5829,9 @@ var NativeHookSource = class {
5771
5829
  cache = /* @__PURE__ */ new Map();
5772
5830
  active = false;
5773
5831
  constructor(opts = {}) {
5774
- this.hookInstall = opts.hookInstall ?? {};
5775
5832
  this.log = opts.log ?? (() => {
5776
5833
  });
5834
+ this.hookInstall = { log: this.log, ...opts.hookInstall };
5777
5835
  }
5778
5836
  start(deps) {
5779
5837
  this.deps = deps;
@@ -6085,7 +6143,7 @@ function startSquadrantd(opts = {}) {
6085
6143
  log
6086
6144
  });
6087
6145
  const cmuxStoreSource = new CmuxStoreSource({ log });
6088
- const nativeHookSource = new NativeHookSource({ log });
6146
+ const nativeHookSource = new NativeHookSource({ log, hookInstall: { claudeEnv: loadConfig().defaults.claudeEnv } });
6089
6147
  ctx.codexDriver = codexDriver;
6090
6148
  ctx.opencodeBridge = opencodeBridge;
6091
6149
  ctx.cmuxEventsBridge = cmuxEventsBridge;