squadrant 0.21.1 → 0.22.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.
@@ -135,12 +135,7 @@ function getDefaultConfig() {
135
135
  // resolver fallback is the safety net).
136
136
  controlChannel: { claude: "on", opencode: "on" },
137
137
  crewRouting: {
138
- rules: [
139
- { tier: "extreme", match: "redesign|architect|rewrite|from scratch|deep reasoning", agent: "claude", model: "opus" },
140
- { tier: "hard", match: "refactor|migrate|implement|feature|daemon|control-plane", agent: "claude", model: "sonnet" },
141
- { tier: "mobile", match: "mobile|ios|swift|android|kotlin|react native", agent: "codex" },
142
- { tier: "daily", match: "typo|rename|bump|docs|comment|lint|format", agent: "opencode" }
143
- ]
138
+ rules: DEFAULT_CREW_ROUTING_RULES.map((r) => ({ ...r }))
144
139
  }
145
140
  },
146
141
  metrics: {
@@ -182,7 +177,7 @@ function saveConfig(config, configPath = DEFAULT_CONFIG_PATH) {
182
177
  function resolveHome(p) {
183
178
  return p.startsWith("~") ? p.replace("~", os.homedir()) : p;
184
179
  }
185
- var GATE_MODES, GATE_POLICIES, DEFAULT_CONFIG_PATH, CONFIG_DIR, DAEMON_SOCK_PATH;
180
+ var GATE_MODES, GATE_POLICIES, DEFAULT_CONFIG_PATH, CONFIG_DIR, DAEMON_SOCK_PATH, DEFAULT_CREW_ROUTING_RULES;
186
181
  var init_config = __esm({
187
182
  "packages/shared/dist/config.js"() {
188
183
  init_config_io();
@@ -191,6 +186,19 @@ var init_config = __esm({
191
186
  DEFAULT_CONFIG_PATH = process.env.SQUADRANT_CONFIG || path2.join(os.homedir(), ".config", "squadrant", "config.json");
192
187
  CONFIG_DIR = path2.dirname(DEFAULT_CONFIG_PATH);
193
188
  DAEMON_SOCK_PATH = path2.join(CONFIG_DIR, "squadrant.sock");
189
+ DEFAULT_CREW_ROUTING_RULES = [
190
+ { tier: "extreme", match: "redesign|architect|rewrite|from scratch|deep reasoning", agent: "claude", model: "opus" },
191
+ { tier: "hard", match: "refactor|migrate|implement|feature|daemon|control-plane", agent: "claude", model: "sonnet" },
192
+ { tier: "mobile", match: "mobile|ios|swift|android|kotlin|react native", agent: "codex" },
193
+ { tier: "daily", match: "typo|rename|bump|docs|comment|lint|format", agent: "opencode" }
194
+ ];
195
+ }
196
+ });
197
+
198
+ // packages/shared/dist/provider-preset.js
199
+ var init_provider_preset = __esm({
200
+ "packages/shared/dist/provider-preset.js"() {
201
+ init_config();
194
202
  }
195
203
  });
196
204
 
@@ -790,6 +798,7 @@ var init_daemon_keys = __esm({
790
798
  var init_dist = __esm({
791
799
  "packages/shared/dist/index.js"() {
792
800
  init_config();
801
+ init_provider_preset();
793
802
  init_project_config();
794
803
  init_effort();
795
804
  init_router_model();
@@ -2027,8 +2036,8 @@ function reconcileLiveness(prev, next) {
2027
2036
  if (!prev)
2028
2037
  return next;
2029
2038
  if (next.source === "scan") {
2030
- const pidAlive = next.lastSeenAt >= prev.lastSeenAt ? next.pidAlive : prev.pidAlive;
2031
- return { ...prev, pidAlive, lastSeenAt: Math.max(prev.lastSeenAt, next.lastSeenAt) };
2039
+ const pidAlive2 = next.lastSeenAt >= prev.lastSeenAt ? next.pidAlive : prev.pidAlive;
2040
+ return { ...prev, pidAlive: pidAlive2, lastSeenAt: Math.max(prev.lastSeenAt, next.lastSeenAt) };
2032
2041
  }
2033
2042
  if (next.startedAt >= prev.startedAt || next.lastState === "end")
2034
2043
  return next;
@@ -2774,6 +2783,68 @@ function createDirectSurfaceLivenessProbe(cmux2, getCaptainTitle) {
2774
2783
  }
2775
2784
  };
2776
2785
  }
2786
+ function hasModalOptionList(screen) {
2787
+ if (!screen)
2788
+ return false;
2789
+ const lines = screen.split(/\r?\n/);
2790
+ let bottomHR = -1;
2791
+ let topHR = -1;
2792
+ for (let i = lines.length - 1; i >= 0; i--) {
2793
+ if (HR_RE.test(lines[i])) {
2794
+ if (bottomHR === -1)
2795
+ bottomHR = i;
2796
+ else {
2797
+ topHR = i;
2798
+ break;
2799
+ }
2800
+ }
2801
+ }
2802
+ if (topHR === -1)
2803
+ return false;
2804
+ return lines.slice(topHR + 1, bottomHR).some((l) => /^\s*\d+\.\s/.test(l));
2805
+ }
2806
+ function classifyCaptainPane(screen, opts = {}) {
2807
+ if (screen == null || screen.trim() === "")
2808
+ return { status: "unreadable" };
2809
+ if (hasModalOptionList(screen))
2810
+ return { status: "locked" };
2811
+ if (WORKING_RE.test(screen))
2812
+ return { status: "mid-turn" };
2813
+ const idleAfterMs = opts.idleAfterMs ?? 0;
2814
+ const stale = opts.outputAgoMs !== void 0 && opts.outputAgoMs >= idleAfterMs;
2815
+ return stale ? { status: "no-session" } : { status: "mid-turn" };
2816
+ }
2817
+ function screenHash(screen) {
2818
+ let h = 2166136261;
2819
+ for (let i = 0; i < screen.length; i++) {
2820
+ h ^= screen.charCodeAt(i);
2821
+ h = Math.imul(h, 16777619) >>> 0;
2822
+ }
2823
+ return h.toString(16).padStart(8, "0");
2824
+ }
2825
+ function createCaptainPaneReader(cmux2, getCaptainTitle, opts = {}) {
2826
+ const idleAfterMs = opts.idleAfterMs ?? PANE_IDLE_AFTER_MS;
2827
+ return async (project) => {
2828
+ try {
2829
+ const wsId = await cmux2.findWorkspaceId(getCaptainTitle(project));
2830
+ if (!wsId)
2831
+ return { status: "no-session" };
2832
+ const surfaces = await cmux2.listSurfaces(wsId);
2833
+ const want = getCaptainTitle(project);
2834
+ const pane = surfaces.find((s) => s.title === want) ?? surfaces[0];
2835
+ if (!pane)
2836
+ return { status: "no-session" };
2837
+ const screen = await cmux2.readPaneScreen(pane);
2838
+ if (screen == null)
2839
+ return { status: "unreadable" };
2840
+ const outputAgoMs = opts.noteScreen?.(project, screenHash(screen));
2841
+ return classifyCaptainPane(screen, { outputAgoMs, idleAfterMs });
2842
+ } catch (e) {
2843
+ opts.log?.(`captain pane read failed project=${project}: ${e.message}`);
2844
+ return { status: "unreadable" };
2845
+ }
2846
+ };
2847
+ }
2777
2848
  function createDirectCrewPaneReader(cmux2, getCaptainTitle) {
2778
2849
  return async (rec) => {
2779
2850
  try {
@@ -2796,11 +2867,14 @@ function createDirectCrewPaneReader(cmux2, getCaptainTitle) {
2796
2867
  }
2797
2868
  };
2798
2869
  }
2799
- var TAIL_LINES;
2870
+ var TAIL_LINES, HR_RE, WORKING_RE, PANE_IDLE_AFTER_MS;
2800
2871
  var init_crew_pane_reader = __esm({
2801
2872
  "packages/core/dist/crew-pane-reader.js"() {
2802
2873
  init_dist();
2803
2874
  TAIL_LINES = 25;
2875
+ HR_RE = /^\s*─{10,}\s*$/;
2876
+ WORKING_RE = /esc to interrupt|\btokens?\b.*\b(used|left)\b|↓\s*[\d.]+\s*k?\s*tokens?\b|\(\d+m?\s*\d+s\b/i;
2877
+ PANE_IDLE_AFTER_MS = 10 * 6e4;
2804
2878
  }
2805
2879
  });
2806
2880
 
@@ -4665,8 +4739,9 @@ function createDelivery(ctx, daemonCmux, isSurfaceAlive) {
4665
4739
  if (mode !== "off")
4666
4740
  throw new DeferDelivery(null, "no-channel");
4667
4741
  }
4742
+ const paneOpts = { ...sendOpts, agent };
4668
4743
  try {
4669
- return await cmux2.send(surface, text, sendOpts);
4744
+ return await cmux2.send(surface, text, paneOpts);
4670
4745
  } catch (e) {
4671
4746
  if (!(e instanceof DeferDelivery) || e.reason !== "probe-failed")
4672
4747
  throw e;
@@ -4678,7 +4753,7 @@ function createDelivery(ctx, daemonCmux, isSurfaceAlive) {
4678
4753
  }
4679
4754
  log(`delivery project=${project}: probe-failed on ${surface.workspaceId}/${surface.surfaceId} \u2014 re-resolved to ${next.workspaceId}/${next.surfaceId}, retrying`);
4680
4755
  surface = next;
4681
- return cmux2.send(next, text, sendOpts);
4756
+ return cmux2.send(next, text, paneOpts);
4682
4757
  }
4683
4758
  });
4684
4759
  if ("delivered" in result) {
@@ -5609,16 +5684,26 @@ function createRunCommand(cliBin) {
5609
5684
  }
5610
5685
  };
5611
5686
  }
5687
+ function captainAliveStateFromHealth(rows, project) {
5688
+ const row = rows.find((h) => h.kind === "captain" && h.project === project);
5689
+ if (!row)
5690
+ return "unknown";
5691
+ if (row.state === "alive")
5692
+ return "alive";
5693
+ if (row.state === "gone" || row.state === "stopped")
5694
+ return "dead";
5695
+ return "unknown";
5696
+ }
5612
5697
  function isCaptainAliveFromHealth(rows, project) {
5613
- return rows.some((h) => h.kind === "captain" && h.project === project && h.state === "alive");
5698
+ return captainAliveStateFromHealth(rows, project) === "alive";
5614
5699
  }
5615
5700
  function createIsCaptainAlive(sock) {
5616
5701
  return async (project) => {
5617
5702
  try {
5618
5703
  const health = await sendRequest(sock, { kind: "health", project }, 5e3);
5619
- return isCaptainAliveFromHealth(health ?? [], project);
5704
+ return captainAliveStateFromHealth(health ?? [], project);
5620
5705
  } catch {
5621
- return false;
5706
+ return "unknown";
5622
5707
  }
5623
5708
  };
5624
5709
  }
@@ -5655,16 +5740,19 @@ function createEnsureCaptainAlive(deps) {
5655
5740
  const now = deps.now ?? (() => Date.now());
5656
5741
  const inFlight = /* @__PURE__ */ new Map();
5657
5742
  async function run(project) {
5658
- if (await deps.isAlive(project))
5743
+ const first = await deps.isAlive(project);
5744
+ if (first === "alive")
5659
5745
  return "alive";
5660
5746
  await deps.launch(project);
5661
5747
  const deadline = now() + warmupTimeoutMs;
5748
+ let last = first;
5662
5749
  while (now() < deadline) {
5663
- if (await deps.isAlive(project))
5750
+ last = await deps.isAlive(project);
5751
+ if (last === "alive")
5664
5752
  return "launched";
5665
5753
  await sleep3(pollMs);
5666
5754
  }
5667
- return "timeout";
5755
+ return last === "dead" ? "timeout" : "unknown";
5668
5756
  }
5669
5757
  return function ensure(project) {
5670
5758
  const existing = inFlight.get(project);
@@ -5719,6 +5807,26 @@ ${ev.question}`;
5719
5807
  function formatInbound(text) {
5720
5808
  return `\u{1F4E9} [from Telegram] ${text}`;
5721
5809
  }
5810
+ function mediaKind(m) {
5811
+ for (const [field, name] of MEDIA_KINDS) {
5812
+ if (m[field] !== void 0)
5813
+ return name;
5814
+ }
5815
+ return void 0;
5816
+ }
5817
+ function mediaMarker(kind) {
5818
+ return `[${kind} attached - not forwarded]`;
5819
+ }
5820
+ function inboundBody(text, kind) {
5821
+ if (kind === void 0)
5822
+ return text ?? "";
5823
+ const marker = mediaMarker(kind);
5824
+ return text ? `${text}
5825
+ ${marker}` : marker;
5826
+ }
5827
+ function formatMediaReceipt(kind, hasCaption) {
5828
+ return hasCaption ? `\u{1F4CE} ${kind} not forwarded \u2014 your caption reached the captain` : `\u{1F4CE} ${kind} not forwarded \u2014 the captain was told it arrived, nothing else was sent`;
5829
+ }
5722
5830
  function formatUsageLine(u) {
5723
5831
  const reqs = u.requests === 1 ? "1 req" : `${u.requests} reqs`;
5724
5832
  const named = Object.keys(u.models).filter((m) => m !== "unknown").sort((a, b) => u.models[b].costUsd - u.models[a].costUsd);
@@ -5730,8 +5838,19 @@ function maskToken(token) {
5730
5838
  return token;
5731
5839
  return "*".repeat(token.length - 4) + token.slice(-4);
5732
5840
  }
5841
+ var MEDIA_KINDS;
5733
5842
  var init_format = __esm({
5734
5843
  "packages/core/dist/telegram/format.js"() {
5844
+ MEDIA_KINDS = [
5845
+ ["photo", "photo"],
5846
+ ["voice", "voice message"],
5847
+ ["video_note", "video message"],
5848
+ ["video", "video"],
5849
+ ["audio", "audio file"],
5850
+ ["document", "document"],
5851
+ ["animation", "animation"],
5852
+ ["sticker", "sticker"]
5853
+ ];
5735
5854
  }
5736
5855
  });
5737
5856
 
@@ -5754,6 +5873,8 @@ function loadState(stateRoot) {
5754
5873
  };
5755
5874
  if (typeof data.lastUserId === "number")
5756
5875
  result.lastUserId = data.lastUserId;
5876
+ if (data.pending && Object.keys(data.pending).length > 0)
5877
+ result.pending = data.pending;
5757
5878
  return result;
5758
5879
  } catch {
5759
5880
  return { offset: 0, topics: {}, notify: {} };
@@ -5767,6 +5888,19 @@ function setTopic(stateRoot, project, topicId, scope = "project") {
5767
5888
  s.topics[topicKey(project, scope)] = topicId;
5768
5889
  saveState(stateRoot, s);
5769
5890
  }
5891
+ function pruneTopics(stateRoot, keep) {
5892
+ const s = loadState(stateRoot);
5893
+ const removed = Object.keys(s.topics).filter((key) => {
5894
+ const sep3 = key.indexOf("::");
5895
+ return !keep(sep3 === -1 ? key : key.slice(0, sep3));
5896
+ });
5897
+ if (removed.length === 0)
5898
+ return [];
5899
+ for (const key of removed)
5900
+ delete s.topics[key];
5901
+ saveState(stateRoot, s);
5902
+ return removed;
5903
+ }
5770
5904
  function isNotifyActive(stateRoot, project) {
5771
5905
  return loadState(stateRoot).notify[project] === true;
5772
5906
  }
@@ -5780,6 +5914,42 @@ function setNotify(stateRoot, project, active) {
5780
5914
  s.notify[project] = active;
5781
5915
  saveState(stateRoot, s);
5782
5916
  }
5917
+ function loadPending(stateRoot) {
5918
+ return loadState(stateRoot).pending ?? {};
5919
+ }
5920
+ function setPending(stateRoot, project, p) {
5921
+ const s = loadState(stateRoot);
5922
+ s.pending = { ...s.pending ?? {}, [project]: p };
5923
+ saveState(stateRoot, s);
5924
+ }
5925
+ function clearPending(stateRoot, project) {
5926
+ const s = loadState(stateRoot);
5927
+ if (!s.pending || !(project in s.pending))
5928
+ return;
5929
+ delete s.pending[project];
5930
+ if (Object.keys(s.pending).length === 0)
5931
+ delete s.pending;
5932
+ saveState(stateRoot, s);
5933
+ }
5934
+ function markPendingWarned(stateRoot, project, at) {
5935
+ const s = loadState(stateRoot);
5936
+ const p = s.pending?.[project];
5937
+ if (!p)
5938
+ return;
5939
+ s.pending[project] = { ...p, warnedAt: at };
5940
+ saveState(stateRoot, s);
5941
+ }
5942
+ function notePaneScreen(stateRoot, project, hash, at) {
5943
+ const s = loadState(stateRoot);
5944
+ const p = s.pending?.[project];
5945
+ if (!p)
5946
+ return void 0;
5947
+ if (p.paneHash === hash && p.paneChangedAt !== void 0)
5948
+ return at - p.paneChangedAt;
5949
+ s.pending[project] = { ...p, paneHash: hash, paneChangedAt: at };
5950
+ saveState(stateRoot, s);
5951
+ return void 0;
5952
+ }
5783
5953
  function findProjectByThread(stateRoot, threadId) {
5784
5954
  const s = loadState(stateRoot);
5785
5955
  for (const [key, id] of Object.entries(s.topics)) {
@@ -5802,17 +5972,18 @@ var init_state = __esm({
5802
5972
  function createTelegramClient(opts) {
5803
5973
  const fetchImpl = opts.fetch ?? fetch;
5804
5974
  const base = `https://api.telegram.org/bot${opts.token}`;
5805
- async function call(method, body) {
5975
+ async function call(method, body, signal) {
5806
5976
  const res = await fetchImpl(`${base}/${method}`, {
5807
5977
  method: "POST",
5808
5978
  headers: { "content-type": "application/json" },
5809
- body: JSON.stringify(body)
5979
+ body: JSON.stringify(body),
5980
+ ...signal ? { signal } : {}
5810
5981
  });
5811
5982
  const json = await res.json();
5812
5983
  if (!res.ok || !json.ok) {
5813
5984
  const code = json.error_code ?? res.status;
5814
5985
  const desc = json.description ?? "unknown error";
5815
- throw new Error(`telegram ${method} failed (${code}): ${desc}`);
5986
+ throw new TelegramApiError(code, `telegram ${method} failed (${code}): ${desc}`, json.parameters?.retry_after);
5816
5987
  }
5817
5988
  return json.result;
5818
5989
  }
@@ -5821,8 +5992,8 @@ function createTelegramClient(opts) {
5821
5992
  const r = await call("getMe", {});
5822
5993
  return { id: r.id, username: r.username };
5823
5994
  },
5824
- getUpdates(offset, timeoutSec = 50) {
5825
- return call("getUpdates", { offset, timeout: timeoutSec });
5995
+ getUpdates(offset, timeoutSec = 50, signal) {
5996
+ return call("getUpdates", { offset, timeout: timeoutSec }, signal);
5826
5997
  },
5827
5998
  async sendMessage(chatId, threadId, text, replyMarkup) {
5828
5999
  const body = { chat_id: chatId, text };
@@ -5845,6 +6016,9 @@ function createTelegramClient(opts) {
5845
6016
  const r = await call("createForumTopic", { chat_id: chatId, name });
5846
6017
  return r.message_thread_id;
5847
6018
  },
6019
+ async deleteForumTopic(chatId, threadId) {
6020
+ await call("deleteForumTopic", { chat_id: chatId, message_thread_id: threadId });
6021
+ },
5848
6022
  async setMyCommands(commands) {
5849
6023
  await call("setMyCommands", { commands });
5850
6024
  },
@@ -5853,11 +6027,241 @@ function createTelegramClient(opts) {
5853
6027
  if (threadId !== void 0)
5854
6028
  body.message_thread_id = threadId;
5855
6029
  await call("sendChatAction", body);
6030
+ },
6031
+ async setMessageReaction(chatId, messageId, emoji) {
6032
+ await call("setMessageReaction", {
6033
+ chat_id: chatId,
6034
+ message_id: messageId,
6035
+ reaction: [{ type: "emoji", emoji }]
6036
+ });
5856
6037
  }
5857
6038
  };
5858
6039
  }
6040
+ var TelegramApiError;
5859
6041
  var init_client = __esm({
5860
6042
  "packages/core/dist/telegram/client.js"() {
6043
+ TelegramApiError = class extends Error {
6044
+ code;
6045
+ retryAfterSec;
6046
+ constructor(code, message, retryAfterSec) {
6047
+ super(message);
6048
+ this.code = code;
6049
+ this.retryAfterSec = retryAfterSec;
6050
+ this.name = "TelegramApiError";
6051
+ }
6052
+ };
6053
+ }
6054
+ });
6055
+
6056
+ // packages/core/dist/telegram/inbound-lifecycle.js
6057
+ function defaultSchedule(fn, ms) {
6058
+ const t = setTimeout(fn, ms);
6059
+ t.unref?.();
6060
+ return () => clearTimeout(t);
6061
+ }
6062
+ function formatWatchdogWarning(project, read, minutes) {
6063
+ const head = `\u23F1\uFE0F [${project}] no reply for ${minutes} minutes`;
6064
+ switch (read.status) {
6065
+ case "locked":
6066
+ return `${head} \u2014 the captain is LOCKED on a prompt, waiting for a human. Answer it in the pane (or with \`squadrant crew answer\`) and it will continue.`;
6067
+ case "mid-turn":
6068
+ return `${head} \u2014 the captain is still working (a turn is in flight), just slow. No action needed yet.`;
6069
+ case "no-session":
6070
+ return `${head} \u2014 the captain looks GONE (no live session in its pane). Run \`squadrant launch ${project}\`.`;
6071
+ case "unreadable":
6072
+ return `${head} \u2014 couldn't read the captain's pane to classify it. Check it manually.`;
6073
+ }
6074
+ }
6075
+ function createInboundLifecycle(opts) {
6076
+ const { stateRoot, cfg, sendChatAction, sendReply, readPane, log } = opts;
6077
+ const now = opts.now ?? Date.now;
6078
+ const schedule = opts.schedule ?? defaultSchedule;
6079
+ const typingMs = opts.typingMs ?? TYPING_KEEPALIVE_MS;
6080
+ const watchdogMs = opts.watchdogMs ?? WATCHDOG_MS;
6081
+ const warnAtMs = watchdogMs + (opts.watchdogMs === void 0 ? WATCHDOG_GRACE_MS : 0);
6082
+ const active = /* @__PURE__ */ new Set();
6083
+ let running = false;
6084
+ function issue(project, threadId) {
6085
+ void Promise.resolve(sendChatAction(cfg.supergroupId, threadId, "typing")).catch((e) => log(`telegram typing keep-alive failed project=${project}: ${e.message}`));
6086
+ }
6087
+ function drain(project) {
6088
+ active.add(project);
6089
+ const tick = () => {
6090
+ if (!running || !active.has(project))
6091
+ return;
6092
+ const p = loadPending(stateRoot)[project];
6093
+ if (!p) {
6094
+ active.delete(project);
6095
+ return;
6096
+ }
6097
+ if (now() - p.startedAt >= warnAtMs && p.warnedAt === void 0) {
6098
+ void warn(project, p.threadId);
6099
+ }
6100
+ issue(project, p.threadId);
6101
+ schedule(tick, typingMs);
6102
+ };
6103
+ schedule(tick, typingMs);
6104
+ }
6105
+ async function warn(project, threadId) {
6106
+ markPendingWarned(stateRoot, project, now());
6107
+ let read = { status: "unreadable" };
6108
+ try {
6109
+ if (readPane)
6110
+ read = await readPane(project);
6111
+ } catch (e) {
6112
+ log(`telegram watchdog pane read failed project=${project}: ${e.message}`);
6113
+ }
6114
+ try {
6115
+ await sendReply(threadId, formatWatchdogWarning(project, read, Math.floor(watchdogMs / 6e4)));
6116
+ } catch (e) {
6117
+ log(`telegram watchdog warning failed project=${project}: ${e.message}`);
6118
+ }
6119
+ }
6120
+ return {
6121
+ start() {
6122
+ running = true;
6123
+ for (const [project, p] of Object.entries(loadPending(stateRoot))) {
6124
+ void (async () => {
6125
+ let read = { status: "unreadable" };
6126
+ try {
6127
+ if (readPane)
6128
+ read = await readPane(project);
6129
+ } catch (e) {
6130
+ log(`telegram resume pane read failed project=${project}: ${e.message}`);
6131
+ }
6132
+ const still = loadPending(stateRoot)[project];
6133
+ if (!still || still.startedAt !== p.startedAt)
6134
+ return;
6135
+ if (read.status === "no-session") {
6136
+ clearPending(stateRoot, project);
6137
+ log(`telegram typing lifecycle cleared for ${project}: captain session gone on boot`);
6138
+ return;
6139
+ }
6140
+ log(`telegram typing lifecycle resumed for ${project} (thread ${p.threadId})`);
6141
+ issue(project, p.threadId);
6142
+ drain(project);
6143
+ })();
6144
+ }
6145
+ },
6146
+ stop() {
6147
+ running = false;
6148
+ active.clear();
6149
+ },
6150
+ begin(project, threadId) {
6151
+ setPending(stateRoot, project, { threadId, startedAt: now() });
6152
+ issue(project, threadId);
6153
+ if (running && !active.has(project))
6154
+ drain(project);
6155
+ },
6156
+ pending() {
6157
+ return loadPending(stateRoot);
6158
+ }
6159
+ };
6160
+ }
6161
+ var TYPING_KEEPALIVE_MS, WATCHDOG_MS, WATCHDOG_GRACE_MS;
6162
+ var init_inbound_lifecycle = __esm({
6163
+ "packages/core/dist/telegram/inbound-lifecycle.js"() {
6164
+ init_state();
6165
+ TYPING_KEEPALIVE_MS = 4e3;
6166
+ WATCHDOG_MS = 15 * 6e4;
6167
+ WATCHDOG_GRACE_MS = 1;
6168
+ }
6169
+ });
6170
+
6171
+ // packages/core/dist/telegram/notify.js
6172
+ function runTelegramStatus(opts) {
6173
+ const tg = opts.config.telegram;
6174
+ const env = opts.env ?? process.env;
6175
+ const tokenSet = !!(tg?.botToken ?? env.TELEGRAM_BOT_TOKEN);
6176
+ const links = Object.entries(loadState(opts.stateRoot).topics).map(([key, topicId]) => {
6177
+ const sep3 = key.indexOf("::");
6178
+ return { project: key.slice(0, sep3), scope: key.slice(sep3 + 2), topicId };
6179
+ });
6180
+ return { tokenSet, supergroupId: tg?.supergroupId ?? null, links };
6181
+ }
6182
+ function runTelegramNotifySet(opts) {
6183
+ setNotify(opts.stateRoot, opts.project, opts.active);
6184
+ }
6185
+ function runTelegramNotifyPref(args) {
6186
+ const { project, dimension, value, root } = args;
6187
+ if (dimension === "crew") {
6188
+ if (!["all", "alert_only", "done_only", "none"].includes(value))
6189
+ return { ok: false, message: "crew must be all|alert_only|done_only|none" };
6190
+ saveProjectOverride(project, { telegram: { notify: { crew: value } } }, root);
6191
+ return { ok: true };
6192
+ }
6193
+ if (value !== "on" && value !== "off")
6194
+ return { ok: false, message: "cap must be on|off" };
6195
+ saveProjectOverride(project, { telegram: { notify: { cap: value === "on" } } }, root);
6196
+ return { ok: true };
6197
+ }
6198
+ function runTelegramNotifyStatus(opts) {
6199
+ const s = loadState(opts.stateRoot);
6200
+ const projects = /* @__PURE__ */ new Set();
6201
+ for (const key of Object.keys(s.topics)) {
6202
+ const sep3 = key.indexOf("::");
6203
+ projects.add(sep3 === -1 ? key : key.slice(0, sep3));
6204
+ }
6205
+ for (const p of Object.keys(s.notify))
6206
+ projects.add(p);
6207
+ return [...projects].map((project) => ({ project, active: s.notify[project] === true }));
6208
+ }
6209
+ function capAllowed(project, globalNotify, root) {
6210
+ return resolveNotify(globalNotify, loadProjectOverride(project, root)).cap;
6211
+ }
6212
+ function confirmationText(project, before, after, dim) {
6213
+ if (dim === "active")
6214
+ return `\u{1F515} ${project} \u2014 all notifications muted here. Unmute: squadrant telegram notify ${project} on`;
6215
+ if (dim === "cap")
6216
+ return `\u{1F515} ${project} \u2014 captain messages muted here. Re-enable: squadrant telegram notify ${project} cap on`;
6217
+ return `\u{1F515} ${project} \u2014 crew notifications now '${after.crew}' (was '${before.crew}'). Re-enable: squadrant telegram notify ${project} crew ${before.crew}`;
6218
+ }
6219
+ async function runNotifyConfirmation(opts) {
6220
+ const { quieter, dim } = isQuieter(opts.before, opts.after);
6221
+ if (!quieter || dim === null)
6222
+ return false;
6223
+ const topicId = loadState(opts.stateRoot).topics[topicKey(opts.project)];
6224
+ if (topicId === void 0)
6225
+ return false;
6226
+ const text = confirmationText(opts.project, opts.before, opts.after, dim);
6227
+ try {
6228
+ await opts.client.sendMessage(opts.cfg.supergroupId, topicId, text);
6229
+ return true;
6230
+ } catch {
6231
+ console.warn(`[squadrant] mute-confirmation send failed for ${opts.project} \u2014 notification preference was still saved`);
6232
+ return false;
6233
+ }
6234
+ }
6235
+ async function runTelegramSend(opts) {
6236
+ const topicId = loadState(opts.stateRoot).topics[topicKey(opts.project)];
6237
+ if (topicId === void 0) {
6238
+ throw new Error(`project "${opts.project}" is not linked \u2014 run: squadrant telegram link ${opts.project}`);
6239
+ }
6240
+ await opts.client.sendMessage(opts.cfg.supergroupId, topicId, opts.message);
6241
+ clearPending(opts.stateRoot, opts.project);
6242
+ return { chatId: opts.cfg.supergroupId, topicId };
6243
+ }
6244
+ async function runTelegramLink(opts) {
6245
+ const existing = loadState(opts.stateRoot).topics[topicKey(opts.project)];
6246
+ if (existing !== void 0)
6247
+ return { topicId: existing, created: false };
6248
+ const topicId = await opts.client.createForumTopic(opts.cfg.supergroupId, topicName(opts.project));
6249
+ const winner = loadState(opts.stateRoot).topics[topicKey(opts.project)];
6250
+ if (winner !== void 0) {
6251
+ try {
6252
+ await opts.client.deleteForumTopic?.(opts.cfg.supergroupId, topicId);
6253
+ } catch {
6254
+ }
6255
+ return { topicId: winner, created: false };
6256
+ }
6257
+ setTopic(opts.stateRoot, opts.project, topicId);
6258
+ return { topicId, created: true };
6259
+ }
6260
+ var init_notify = __esm({
6261
+ "packages/core/dist/telegram/notify.js"() {
6262
+ init_dist();
6263
+ init_state();
6264
+ init_format();
5861
6265
  }
5862
6266
  });
5863
6267
 
@@ -5945,8 +6349,10 @@ var init_tiers = __esm({
5945
6349
  });
5946
6350
 
5947
6351
  // packages/core/dist/telegram/bridge.js
6352
+ import fs11 from "fs";
5948
6353
  import os5 from "os";
5949
6354
  import path12 from "path";
6355
+ import crypto2 from "crypto";
5950
6356
  function formatInboundReceipt(project, outcome) {
5951
6357
  if (!outcome)
5952
6358
  return void 0;
@@ -5962,6 +6368,85 @@ function formatInboundReceipt(project, outcome) {
5962
6368
  return void 0;
5963
6369
  }
5964
6370
  }
6371
+ function telegramPollLockPath(token) {
6372
+ const hash = crypto2.createHash("sha256").update(token).digest("hex").slice(0, 16);
6373
+ return path12.join(os5.tmpdir(), `squadrant-telegram-poll-${hash}.lock`);
6374
+ }
6375
+ function pidAlive(pid) {
6376
+ try {
6377
+ process.kill(pid, 0);
6378
+ return true;
6379
+ } catch (e) {
6380
+ return e.code === "EPERM";
6381
+ }
6382
+ }
6383
+ function lockIsHeld(file) {
6384
+ let pid;
6385
+ let ageMs;
6386
+ try {
6387
+ const parsed = Number.parseInt(fs11.readFileSync(file, "utf8").trim(), 10);
6388
+ pid = Number.isFinite(parsed) ? parsed : void 0;
6389
+ ageMs = Date.now() - fs11.statSync(file).mtimeMs;
6390
+ } catch {
6391
+ return false;
6392
+ }
6393
+ if (pid !== void 0)
6394
+ return pidAlive(pid);
6395
+ return ageMs < POLL_LOCK_STALE_MS;
6396
+ }
6397
+ function acquirePollLock(token) {
6398
+ const file = telegramPollLockPath(token);
6399
+ for (let attempt = 0; attempt < 2; attempt++) {
6400
+ try {
6401
+ const fd = fs11.openSync(file, "wx");
6402
+ fs11.writeSync(fd, String(process.pid));
6403
+ fs11.closeSync(fd);
6404
+ return () => {
6405
+ try {
6406
+ fs11.unlinkSync(file);
6407
+ } catch {
6408
+ }
6409
+ };
6410
+ } catch (e) {
6411
+ if (e.code !== "EEXIST")
6412
+ return null;
6413
+ if (attempt === 0 && !lockIsHeld(file)) {
6414
+ try {
6415
+ fs11.unlinkSync(file);
6416
+ } catch {
6417
+ }
6418
+ continue;
6419
+ }
6420
+ return null;
6421
+ }
6422
+ }
6423
+ return null;
6424
+ }
6425
+ function defaultSchedule2(fn, ms) {
6426
+ const t = setTimeout(fn, ms);
6427
+ t.unref?.();
6428
+ return () => clearTimeout(t);
6429
+ }
6430
+ function foreignPollLockHolder(token, selfPid = process.pid) {
6431
+ const file = telegramPollLockPath(token);
6432
+ let pid;
6433
+ try {
6434
+ const parsed = Number.parseInt(fs11.readFileSync(file, "utf8").trim(), 10);
6435
+ if (!Number.isFinite(parsed))
6436
+ return null;
6437
+ pid = parsed;
6438
+ } catch {
6439
+ return null;
6440
+ }
6441
+ if (pid === selfPid)
6442
+ return null;
6443
+ return pidAlive(pid) ? pid : null;
6444
+ }
6445
+ function pollBackoffMs(err, pollMs) {
6446
+ if (err instanceof TelegramApiError && err.retryAfterSec !== void 0)
6447
+ return err.retryAfterSec * 1e3;
6448
+ return pollMs;
6449
+ }
5965
6450
  function parseNotifyPref(text) {
5966
6451
  const parts = text.trim().split(/\s+/);
5967
6452
  if (stripBotMention(parts[0] ?? "").toLowerCase() !== "/notify")
@@ -5988,9 +6473,17 @@ function notifyToggle(text) {
5988
6473
  }
5989
6474
  function createTelegramBridge(opts) {
5990
6475
  const { cfg, stateRoot, client, appendCaptainMessage: appendCaptainMessage2, log, ensureCaptainAlive, runCommand, sendReply } = opts;
6476
+ const lifecycle = opts.lifecycle;
5991
6477
  const configRoot = opts.configRoot ?? path12.join(os5.homedir(), ".config", "squadrant");
5992
6478
  const pollMs = cfg.pollMs ?? 1e3;
6479
+ const schedule = opts.schedule ?? defaultSchedule2;
6480
+ const lockToken = cfg.botToken ?? process.env.TELEGRAM_BOT_TOKEN;
6481
+ let releasePollLock = null;
6482
+ let pollAbort = null;
5993
6483
  let running = false;
6484
+ let pollStopped = false;
6485
+ let selfHealCancel = null;
6486
+ let conflictCount = 0;
5994
6487
  let lastSuccessfulPollAt = null;
5995
6488
  let lastError = null;
5996
6489
  let lastErrorAt = null;
@@ -6000,12 +6493,8 @@ function createTelegramBridge(opts) {
6000
6493
  saveState(stateRoot, s);
6001
6494
  }
6002
6495
  async function sendToTopic(project, text) {
6003
- let threadId = loadState(stateRoot).topics[topicKey(project)];
6004
- if (threadId === void 0) {
6005
- threadId = await client.createForumTopic(cfg.supergroupId, topicName(project));
6006
- setTopic(stateRoot, project, threadId);
6007
- }
6008
- await client.sendMessage(cfg.supergroupId, threadId, text);
6496
+ const { topicId } = await runTelegramLink({ project, cfg, client, stateRoot });
6497
+ await client.sendMessage(cfg.supergroupId, topicId, text);
6009
6498
  }
6010
6499
  async function deliverOutbound(project, ev) {
6011
6500
  const resolved = resolveNotify(cfg.notify, loadProjectOverride(project, configRoot));
@@ -6192,10 +6681,13 @@ ${formatUsageLine(usage2)}`;
6192
6681
  }
6193
6682
  await runChannelCommand(text, fromId, void 0);
6194
6683
  }
6195
- async function handleProjectTopic(text, threadId, fromId) {
6684
+ async function handleProjectTopic(text, threadId, fromId, messageId, media) {
6196
6685
  const resolved = findProjectByThread(stateRoot, threadId);
6197
- if (!resolved)
6686
+ if (!resolved) {
6687
+ log(`telegram inbound in unbound topic thread=${threadId} \u2014 no project linked, message dropped`);
6688
+ await reply(threadId, "\u26A0\uFE0F this topic isn't linked to a project \u2014 run: squadrant telegram link <project>");
6198
6689
  return;
6690
+ }
6199
6691
  if (isBareSpawn(text)) {
6200
6692
  if (!isControlEnabled(cfg) || !isAuthorized(fromId, cfg)) {
6201
6693
  await reply(threadId, "\u26D4 not authorized");
@@ -6245,21 +6737,16 @@ ${formatUsageLine(usage2)}`;
6245
6737
  await runChannelCommand(text, fromId, threadId);
6246
6738
  return;
6247
6739
  }
6248
- void client.sendChatAction(cfg.supergroupId, threadId, "typing").catch((e) => {
6249
- log(`telegram sendChatAction failed: ${e.message}`);
6250
- });
6740
+ if (messageId !== void 0) {
6741
+ void Promise.resolve(client.setMessageReaction?.(cfg.supergroupId, messageId, "\u{1F44D}")).catch((e) => log(`telegram stage-1 reaction failed project=${resolved.project}: ${e.message}`));
6742
+ }
6251
6743
  setNotify(stateRoot, resolved.project, true);
6252
6744
  if (ensureCaptainAlive && isControlEnabled(cfg) && isAuthorized(fromId, cfg)) {
6253
- try {
6254
- const r = await ensureCaptainAlive(resolved.project);
6745
+ void ensureCaptainAlive(resolved.project).then((r) => {
6255
6746
  if (r === "timeout") {
6256
- await reply(threadId, `\u274C couldn't reach ${resolved.project} captain \u2014 saved to mailbox, will deliver when you open the workspace.`);
6257
- } else {
6258
- await reply(threadId, `\u{1F4E8} delivered to ${resolved.project} captain`);
6747
+ log(`telegram auto-launch: warmup timed out project=${resolved.project} \u2014 delivery verdict (not this probe) decides the receipt`);
6259
6748
  }
6260
- } catch (e) {
6261
- log(`telegram auto-launch failed project=${resolved.project}: ${e.message}`);
6262
- }
6749
+ }).catch((e) => log(`telegram auto-launch failed project=${resolved.project}: ${e.message}`));
6263
6750
  }
6264
6751
  let handled = false;
6265
6752
  let outcome;
@@ -6268,13 +6755,22 @@ ${formatUsageLine(usage2)}`;
6268
6755
  handled = res.handled;
6269
6756
  outcome = res.outcome;
6270
6757
  }
6758
+ const deliveryHeld = outcome?.status === "held";
6271
6759
  if (!handled) {
6272
6760
  await appendCaptainMessage2({ stateRoot, project: resolved.project, text: formatInbound(text), source: "telegram" });
6761
+ outcome = void 0;
6273
6762
  }
6274
6763
  const receipt = formatInboundReceipt(resolved.project, outcome);
6275
6764
  if (receipt && sendReply) {
6276
6765
  await sendReply(threadId, receipt).catch((e) => log(`telegram receipt failed project=${resolved.project}: ${e.message}`));
6277
6766
  }
6767
+ if (media) {
6768
+ await reply(threadId, formatMediaReceipt(media.kind, media.hasCaption));
6769
+ }
6770
+ if (deliveryHeld)
6771
+ return;
6772
+ await reply(threadId, CAPTAIN_RECEIVED_ACK);
6773
+ lifecycle?.begin(resolved.project, threadId);
6278
6774
  }
6279
6775
  async function handleUpdate(u) {
6280
6776
  if (u.callback_query) {
@@ -6282,7 +6778,11 @@ ${formatUsageLine(usage2)}`;
6282
6778
  return;
6283
6779
  }
6284
6780
  const m = u.message;
6285
- if (!m || m.text === void 0)
6781
+ if (!m)
6782
+ return;
6783
+ const kind = mediaKind(m);
6784
+ const text = m.text ?? m.caption;
6785
+ if (text === void 0 && kind === void 0)
6286
6786
  return;
6287
6787
  if (!cfg.chats.includes(m.chat.id))
6288
6788
  return;
@@ -6296,7 +6796,7 @@ ${formatUsageLine(usage2)}`;
6296
6796
  await reply(threadId, "\u26D4 not authorized");
6297
6797
  return;
6298
6798
  }
6299
- const task = m.text.trim();
6799
+ const task = (text ?? "").trim();
6300
6800
  if (!task) {
6301
6801
  await reply(threadId, "spawn cancelled \u2014 empty task");
6302
6802
  return;
@@ -6307,39 +6807,122 @@ ${formatUsageLine(usage2)}`;
6307
6807
  return;
6308
6808
  }
6309
6809
  if (m.message_thread_id === void 0) {
6810
+ if (m.text === void 0)
6811
+ return;
6310
6812
  await handleGeneral(m.text, m.from?.id);
6311
6813
  return;
6312
6814
  }
6313
- await handleProjectTopic(m.text, m.message_thread_id, m.from?.id);
6815
+ await handleProjectTopic(inboundBody(text, kind), m.message_thread_id, m.from?.id, m.message_id, kind === void 0 ? void 0 : { kind, hasCaption: text !== void 0 });
6816
+ }
6817
+ function acquirePollLockOrLog() {
6818
+ if (!lockToken)
6819
+ return true;
6820
+ releasePollLock = acquirePollLock(lockToken);
6821
+ if (releasePollLock)
6822
+ return true;
6823
+ const holder = foreignPollLockHolder(lockToken);
6824
+ log(`telegram inbound poll disabled: another consumer already polls this bot token (single-consumer guard, #830)${holder !== null ? ` \u2014 lock held by pid ${holder}` : ""}`);
6825
+ return false;
6826
+ }
6827
+ function scheduleSelfHeal() {
6828
+ selfHealCancel?.();
6829
+ selfHealCancel = schedule(() => {
6830
+ selfHealCancel = null;
6831
+ if (pollStopped || running)
6832
+ return;
6833
+ if (!acquirePollLockOrLog()) {
6834
+ scheduleSelfHeal();
6835
+ return;
6836
+ }
6837
+ running = true;
6838
+ conflictCount = 0;
6839
+ log("telegram inbound poll re-attempting after a 409 stop (#850)");
6840
+ void pollLoop();
6841
+ }, CONFLICT_409_SELF_HEAL_MS);
6314
6842
  }
6315
6843
  async function pollLoop() {
6316
6844
  while (running) {
6845
+ const ac = new AbortController();
6846
+ pollAbort = ac;
6847
+ let waitMs = pollMs;
6317
6848
  try {
6318
6849
  const offset = loadState(stateRoot).offset;
6319
- const updates = await client.getUpdates(offset, LONG_POLL_SEC);
6850
+ const updates = await client.getUpdates(offset, LONG_POLL_SEC, ac.signal);
6851
+ conflictCount = 0;
6320
6852
  for (const u of updates) {
6321
6853
  await handleUpdate(u);
6322
6854
  persistOffset(u.update_id + 1);
6323
6855
  }
6324
6856
  lastSuccessfulPollAt = Date.now();
6325
6857
  } catch (e) {
6326
- lastError = e.message;
6858
+ if (ac.signal.aborted)
6859
+ break;
6860
+ const msg = e.message;
6861
+ lastError = msg;
6327
6862
  lastErrorAt = Date.now();
6328
- log(`telegram inbound poll failed: ${e.message}`);
6863
+ if (e instanceof TelegramApiError && e.code === 409) {
6864
+ conflictCount += 1;
6865
+ if (conflictCount >= CONFLICT_409_TERMINAL_AFTER) {
6866
+ log(`telegram inbound poll stopped: repeated 409 conflict (another getUpdates consumer holds this token) \u2014 ${msg}`);
6867
+ break;
6868
+ }
6869
+ } else {
6870
+ log(`telegram inbound poll failed: ${msg}`);
6871
+ }
6872
+ waitMs = pollBackoffMs(e, pollMs);
6873
+ } finally {
6874
+ pollAbort = null;
6329
6875
  }
6330
6876
  if (running)
6331
- await sleep2(pollMs);
6877
+ await sleep2(waitMs);
6332
6878
  }
6879
+ running = false;
6880
+ releasePollLock?.();
6881
+ releasePollLock = null;
6882
+ if (!pollStopped)
6883
+ scheduleSelfHeal();
6333
6884
  }
6334
6885
  return {
6335
6886
  start() {
6336
6887
  if (running)
6337
6888
  return;
6889
+ pollStopped = false;
6890
+ selfHealCancel?.();
6891
+ selfHealCancel = null;
6892
+ if (!acquirePollLockOrLog())
6893
+ return;
6894
+ const known = opts.registeredProjects?.();
6895
+ if (known !== void 0) {
6896
+ try {
6897
+ const removed = pruneTopics(stateRoot, (p) => known.includes(p));
6898
+ if (removed.length > 0)
6899
+ log(`telegram registry pruned ${removed.length} stale topic link(s): ${removed.join(", ")}`);
6900
+ } catch (e) {
6901
+ log(`telegram registry prune failed: ${e.message}`);
6902
+ }
6903
+ }
6338
6904
  running = true;
6905
+ conflictCount = 0;
6906
+ try {
6907
+ lifecycle?.start();
6908
+ } catch (e) {
6909
+ log(`telegram lifecycle start failed: ${e.message}`);
6910
+ }
6339
6911
  void pollLoop();
6340
6912
  },
6341
6913
  stop() {
6342
6914
  running = false;
6915
+ pollStopped = true;
6916
+ selfHealCancel?.();
6917
+ selfHealCancel = null;
6918
+ try {
6919
+ lifecycle?.stop();
6920
+ } catch (e) {
6921
+ log(`telegram lifecycle stop failed: ${e.message}`);
6922
+ }
6923
+ pollAbort?.abort();
6924
+ releasePollLock?.();
6925
+ releasePollLock = null;
6343
6926
  },
6344
6927
  pushLifecycle(project, ev) {
6345
6928
  void deliverOutbound(project, ev).catch((e) => {
@@ -6356,17 +6939,23 @@ ${formatUsageLine(usage2)}`;
6356
6939
  }
6357
6940
  };
6358
6941
  }
6359
- var LONG_POLL_SEC, sleep2, CREW_TIERS, RECOGNIZED_CHANNEL_COMMANDS;
6942
+ var CAPTAIN_RECEIVED_ACK, LONG_POLL_SEC, POLL_LOCK_STALE_MS, CONFLICT_409_TERMINAL_AFTER, CONFLICT_409_SELF_HEAL_MS, sleep2, CREW_TIERS, RECOGNIZED_CHANNEL_COMMANDS;
6360
6943
  var init_bridge = __esm({
6361
6944
  "packages/core/dist/telegram/bridge.js"() {
6362
6945
  init_dist();
6946
+ init_client();
6363
6947
  init_auth();
6364
6948
  init_commands();
6365
6949
  init_format();
6950
+ init_notify();
6366
6951
  init_panels();
6367
6952
  init_state();
6368
6953
  init_tiers();
6954
+ CAPTAIN_RECEIVED_ACK = "\u2705 captain received";
6369
6955
  LONG_POLL_SEC = 50;
6956
+ POLL_LOCK_STALE_MS = 2 * 6e4;
6957
+ CONFLICT_409_TERMINAL_AFTER = 3;
6958
+ CONFLICT_409_SELF_HEAL_MS = 5 * 6e4;
6370
6959
  sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
6371
6960
  CREW_TIERS = ["all", "alert_only", "done_only", "none"];
6372
6961
  RECOGNIZED_CHANNEL_COMMANDS = /* @__PURE__ */ new Set(["status", "projects", "crews", "launch", "effort", "spawn"]);
@@ -6411,7 +7000,7 @@ var init_restart_daemon = __esm({
6411
7000
  });
6412
7001
 
6413
7002
  // packages/core/dist/telegram/setup.js
6414
- import fs11 from "fs";
7003
+ import fs12 from "fs";
6415
7004
  function resolveSetupGroup(existingSupergroupId, opts) {
6416
7005
  if (existingSupergroupId !== void 0 && !opts.redetect)
6417
7006
  return "reuse";
@@ -6462,7 +7051,7 @@ function writeTelegramConfig(configPath, opts) {
6462
7051
  let config;
6463
7052
  let raw = null;
6464
7053
  try {
6465
- raw = fs11.readFileSync(configPath, "utf-8");
7054
+ raw = fs12.readFileSync(configPath, "utf-8");
6466
7055
  } catch (err) {
6467
7056
  if (err.code !== "ENOENT") {
6468
7057
  throw new Error(`refusing to overwrite unreadable config at ${configPath}: ${String(err)}`);
@@ -6501,94 +7090,6 @@ var init_setup = __esm({
6501
7090
  }
6502
7091
  });
6503
7092
 
6504
- // packages/core/dist/telegram/notify.js
6505
- function runTelegramStatus(opts) {
6506
- const tg = opts.config.telegram;
6507
- const env = opts.env ?? process.env;
6508
- const tokenSet = !!(tg?.botToken ?? env.TELEGRAM_BOT_TOKEN);
6509
- const links = Object.entries(loadState(opts.stateRoot).topics).map(([key, topicId]) => {
6510
- const sep3 = key.indexOf("::");
6511
- return { project: key.slice(0, sep3), scope: key.slice(sep3 + 2), topicId };
6512
- });
6513
- return { tokenSet, supergroupId: tg?.supergroupId ?? null, links };
6514
- }
6515
- function runTelegramNotifySet(opts) {
6516
- setNotify(opts.stateRoot, opts.project, opts.active);
6517
- }
6518
- function runTelegramNotifyPref(args) {
6519
- const { project, dimension, value, root } = args;
6520
- if (dimension === "crew") {
6521
- if (!["all", "alert_only", "done_only", "none"].includes(value))
6522
- return { ok: false, message: "crew must be all|alert_only|done_only|none" };
6523
- saveProjectOverride(project, { telegram: { notify: { crew: value } } }, root);
6524
- return { ok: true };
6525
- }
6526
- if (value !== "on" && value !== "off")
6527
- return { ok: false, message: "cap must be on|off" };
6528
- saveProjectOverride(project, { telegram: { notify: { cap: value === "on" } } }, root);
6529
- return { ok: true };
6530
- }
6531
- function runTelegramNotifyStatus(opts) {
6532
- const s = loadState(opts.stateRoot);
6533
- const projects = /* @__PURE__ */ new Set();
6534
- for (const key of Object.keys(s.topics)) {
6535
- const sep3 = key.indexOf("::");
6536
- projects.add(sep3 === -1 ? key : key.slice(0, sep3));
6537
- }
6538
- for (const p of Object.keys(s.notify))
6539
- projects.add(p);
6540
- return [...projects].map((project) => ({ project, active: s.notify[project] === true }));
6541
- }
6542
- function capAllowed(project, globalNotify, root) {
6543
- return resolveNotify(globalNotify, loadProjectOverride(project, root)).cap;
6544
- }
6545
- function confirmationText(project, before, after, dim) {
6546
- if (dim === "active")
6547
- return `\u{1F515} ${project} \u2014 all notifications muted here. Unmute: squadrant telegram notify ${project} on`;
6548
- if (dim === "cap")
6549
- return `\u{1F515} ${project} \u2014 captain messages muted here. Re-enable: squadrant telegram notify ${project} cap on`;
6550
- return `\u{1F515} ${project} \u2014 crew notifications now '${after.crew}' (was '${before.crew}'). Re-enable: squadrant telegram notify ${project} crew ${before.crew}`;
6551
- }
6552
- async function runNotifyConfirmation(opts) {
6553
- const { quieter, dim } = isQuieter(opts.before, opts.after);
6554
- if (!quieter || dim === null)
6555
- return false;
6556
- const topicId = loadState(opts.stateRoot).topics[topicKey(opts.project)];
6557
- if (topicId === void 0)
6558
- return false;
6559
- const text = confirmationText(opts.project, opts.before, opts.after, dim);
6560
- try {
6561
- await opts.client.sendMessage(opts.cfg.supergroupId, topicId, text);
6562
- return true;
6563
- } catch {
6564
- console.warn(`[squadrant] mute-confirmation send failed for ${opts.project} \u2014 notification preference was still saved`);
6565
- return false;
6566
- }
6567
- }
6568
- async function runTelegramSend(opts) {
6569
- const topicId = loadState(opts.stateRoot).topics[topicKey(opts.project)];
6570
- if (topicId === void 0) {
6571
- throw new Error(`project "${opts.project}" is not linked \u2014 run: squadrant telegram link ${opts.project}`);
6572
- }
6573
- await opts.client.sendMessage(opts.cfg.supergroupId, topicId, opts.message);
6574
- return { chatId: opts.cfg.supergroupId, topicId };
6575
- }
6576
- async function runTelegramLink(opts) {
6577
- const existing = loadState(opts.stateRoot).topics[topicKey(opts.project)];
6578
- if (existing !== void 0)
6579
- return { topicId: existing, created: false };
6580
- const topicId = await opts.client.createForumTopic(opts.cfg.supergroupId, topicName(opts.project));
6581
- setTopic(opts.stateRoot, opts.project, topicId);
6582
- return { topicId, created: true };
6583
- }
6584
- var init_notify = __esm({
6585
- "packages/core/dist/telegram/notify.js"() {
6586
- init_dist();
6587
- init_state();
6588
- init_format();
6589
- }
6590
- });
6591
-
6592
7093
  // packages/core/dist/telegram/index.js
6593
7094
  var init_telegram = __esm({
6594
7095
  "packages/core/dist/telegram/index.js"() {
@@ -6600,6 +7101,7 @@ var init_telegram = __esm({
6600
7101
  init_format();
6601
7102
  init_state();
6602
7103
  init_client();
7104
+ init_inbound_lifecycle();
6603
7105
  init_bridge();
6604
7106
  init_setup();
6605
7107
  init_notify();
@@ -7865,7 +8367,7 @@ var init_launch_workspace = __esm({
7865
8367
  });
7866
8368
 
7867
8369
  // packages/core/dist/side-session.js
7868
- import fs12 from "fs";
8370
+ import fs13 from "fs";
7869
8371
  function sideTitleFor(project, name) {
7870
8372
  return `\u{1F5D2} ${project}:${name}`;
7871
8373
  }
@@ -7967,7 +8469,7 @@ async function runSideClose(runtime, workspaceId, project, name, projPath, workt
7967
8469
  await runtime.closePane(pane);
7968
8470
  if (projPath) {
7969
8471
  const wtPath = worktreePath(projPath, worktreeDir, project, name);
7970
- if (fs12.existsSync(wtPath)) {
8472
+ if (fs13.existsSync(wtPath)) {
7971
8473
  try {
7972
8474
  removeWorktree(projPath, wtPath);
7973
8475
  } catch (e) {
@@ -8404,8 +8906,10 @@ __export(dist_exports, {
8404
8906
  AGENT_BINS: () => AGENT_BINS,
8405
8907
  ANTHROPIC_ONLY_REQUEST_FIELDS: () => ANTHROPIC_ONLY_REQUEST_FIELDS,
8406
8908
  BOT_COMMANDS: () => BOT_COMMANDS,
8909
+ CAPTAIN_RECEIVED_ACK: () => CAPTAIN_RECEIVED_ACK,
8407
8910
  CC_SOCKS_DIR: () => CC_SOCKS_DIR,
8408
8911
  CMUX_PRESERVE_CLAUDE_AUTH_ENV: () => CMUX_PRESERVE_CLAUDE_AUTH_ENV,
8912
+ CONFLICT_409_SELF_HEAL_MS: () => CONFLICT_409_SELF_HEAL_MS,
8409
8913
  CREW_GONE_MS: () => CREW_GONE_MS,
8410
8914
  CREW_STALE_MS: () => CREW_STALE_MS,
8411
8915
  CaptainDelivery: () => CaptainDelivery,
@@ -8425,6 +8929,7 @@ __export(dist_exports, {
8425
8929
  LivenessRegistry: () => LivenessRegistry,
8426
8930
  MONITOR_STALL_BUDGET_MS: () => MONITOR_STALL_BUDGET_MS,
8427
8931
  OPERATOR_INITIATED_COMMANDS: () => OPERATOR_INITIATED_COMMANDS,
8932
+ PANE_IDLE_AFTER_MS: () => PANE_IDLE_AFTER_MS,
8428
8933
  PROBE_QUIET_MS: () => PROBE_QUIET_MS,
8429
8934
  PROTOCOL_VERSION: () => PROTOCOL_VERSION,
8430
8935
  READ_ONLY_CREW_SUBCOMMANDS: () => READ_ONLY_CREW_SUBCOMMANDS,
@@ -8437,6 +8942,10 @@ __export(dist_exports, {
8437
8942
  TERMINAL_RECORD_KEEP_PER_PROJECT: () => TERMINAL_RECORD_KEEP_PER_PROJECT,
8438
8943
  TERMINAL_RECORD_TTL_MS: () => TERMINAL_RECORD_TTL_MS,
8439
8944
  TOOL_STALL_BUDGET_MS: () => TOOL_STALL_BUDGET_MS,
8945
+ TYPING_KEEPALIVE_MS: () => TYPING_KEEPALIVE_MS,
8946
+ TelegramApiError: () => TelegramApiError,
8947
+ WATCHDOG_GRACE_MS: () => WATCHDOG_GRACE_MS,
8948
+ WATCHDOG_MS: () => WATCHDOG_MS,
8440
8949
  WORK_ITEM_TTL_MS: () => WORK_ITEM_TTL_MS,
8441
8950
  WRITABLE_CONFIG_KEYS: () => WRITABLE_CONFIG_KEYS,
8442
8951
  _resetRestartInFlightForTest: () => _resetRestartInFlightForTest,
@@ -8458,15 +8967,19 @@ __export(dist_exports, {
8458
8967
  callClassifier: () => callClassifier,
8459
8968
  capAllowed: () => capAllowed,
8460
8969
  capOutput: () => capOutput,
8970
+ captainAliveStateFromHealth: () => captainAliveStateFromHealth,
8461
8971
  captainRecordPath: () => captainRecordPath,
8462
8972
  captainSocketPath: () => captainSocketPath,
8463
8973
  checkFact: () => checkFact,
8974
+ classifyCaptainPane: () => classifyCaptainPane,
8464
8975
  classifyHealth: () => classifyHealth,
8465
8976
  claudeEnvShadowsRouter: () => claudeEnvShadowsRouter,
8977
+ clearPending: () => clearPending,
8466
8978
  closeWorkItem: () => closeWorkItem,
8467
8979
  computeTemplateHash: () => computeTemplateHash,
8468
8980
  consumeExitMarker: () => consumeExitMarker,
8469
8981
  createAttach: () => createAttach,
8982
+ createCaptainPaneReader: () => createCaptainPaneReader,
8470
8983
  createCrewPaneReader: () => createCrewPaneReader,
8471
8984
  createDaemon: () => createDaemon,
8472
8985
  createDecoder: () => createDecoder,
@@ -8475,6 +8988,7 @@ __export(dist_exports, {
8475
8988
  createDirectSurfaceLivenessProbe: () => createDirectSurfaceLivenessProbe,
8476
8989
  createEnsureCaptainAlive: () => createEnsureCaptainAlive,
8477
8990
  createEventsSource: () => createEventsSource,
8991
+ createInboundLifecycle: () => createInboundLifecycle,
8478
8992
  createInteractiveProbe: () => createInteractiveProbe,
8479
8993
  createIsCaptainAlive: () => createIsCaptainAlive,
8480
8994
  createLaunch: () => createLaunch,
@@ -8530,12 +9044,16 @@ __export(dist_exports, {
8530
9044
  formatInbound: () => formatInbound,
8531
9045
  formatInboundReceipt: () => formatInboundReceipt,
8532
9046
  formatLifecycle: () => formatLifecycle,
9047
+ formatMediaReceipt: () => formatMediaReceipt,
8533
9048
  formatPermissionDecision: () => formatPermissionDecision,
8534
9049
  formatUsageLine: () => formatUsageLine,
9050
+ formatWatchdogWarning: () => formatWatchdogWarning,
8535
9051
  freshTrace: () => freshTrace,
8536
9052
  gateCacheKey: () => gateCacheKey,
8537
9053
  getDaemonPid: () => getDaemonPid,
9054
+ hasModalOptionList: () => hasModalOptionList,
8538
9055
  healCmdFor: () => healCmdFor,
9056
+ inboundBody: () => inboundBody,
8539
9057
  isAuthorized: () => isAuthorized,
8540
9058
  isBareSpawn: () => isBareSpawn,
8541
9059
  isCaptainAlive: () => isCaptainAlive,
@@ -8557,18 +9075,23 @@ __export(dist_exports, {
8557
9075
  kickstartArgv: () => kickstartArgv,
8558
9076
  launchOneWorkspace: () => launchOneWorkspace,
8559
9077
  listSessions: () => listSessions,
9078
+ loadPending: () => loadPending,
8560
9079
  loadSessions: () => loadSessions,
8561
9080
  loadState: () => loadState,
8562
9081
  mailboxStats: () => mailboxStats,
8563
9082
  makeGate: () => makeGate,
9083
+ markPendingWarned: () => markPendingWarned,
8564
9084
  maskToken: () => maskToken,
8565
9085
  matchTier1Deny: () => matchTier1Deny,
9086
+ mediaKind: () => mediaKind,
9087
+ mediaMarker: () => mediaMarker,
8566
9088
  mergeClaudeEnvRouterSettings: () => mergeClaudeEnvRouterSettings,
8567
9089
  mintToken: () => mintToken,
8568
9090
  nameFromTitle: () => nameFromTitle,
8569
9091
  newestSessionInDirectory: () => newestSessionInDirectory,
8570
9092
  nextAutoName: () => nextAutoName,
8571
9093
  niceCrewCommand: () => niceCrewCommand,
9094
+ notePaneScreen: () => notePaneScreen,
8572
9095
  notifyToggle: () => notifyToggle,
8573
9096
  parseBearer: () => parseBearer,
8574
9097
  parseClassifierVerdict: () => parseClassifierVerdict,
@@ -8577,6 +9100,7 @@ __export(dist_exports, {
8577
9100
  parseNotifyPref: () => parseNotifyPref,
8578
9101
  parseProgramArgs: () => parseProgramArgs,
8579
9102
  plistPath: () => plistPath,
9103
+ pollBackoffMs: () => pollBackoffMs,
8580
9104
  pollNewestSessionInDirectory: () => pollNewestSessionInDirectory,
8581
9105
  prepareCaptainRoute: () => prepareCaptainRoute,
8582
9106
  printForeignInstallError: () => printForeignInstallError,
@@ -8584,6 +9108,7 @@ __export(dist_exports, {
8584
9108
  probeCaptainChannel: () => probeCaptainChannel,
8585
9109
  programArgsBlock: () => programArgsBlock,
8586
9110
  projectHealth: () => projectHealth,
9111
+ pruneTopics: () => pruneTopics,
8587
9112
  purgeExpiredWorkItems: () => purgeExpiredWorkItems,
8588
9113
  readCaptainAddress: () => readCaptainAddress,
8589
9114
  readCursor: () => readCursor,
@@ -8650,9 +9175,11 @@ __export(dist_exports, {
8650
9175
  saveSessions: () => saveSessions,
8651
9176
  saveState: () => saveState,
8652
9177
  screenHasSplashMarker: () => screenHasSplashMarker,
9178
+ screenHash: () => screenHash,
8653
9179
  sendRequest: () => sendRequest,
8654
9180
  setLastUserId: () => setLastUserId,
8655
9181
  setNotify: () => setNotify,
9182
+ setPending: () => setPending,
8656
9183
  setTopic: () => setTopic,
8657
9184
  shellQuote: () => shellQuote,
8658
9185
  shouldBuildRouterService: () => shouldBuildRouterService,
@@ -8665,6 +9192,7 @@ __export(dist_exports, {
8665
9192
  startServer: () => startServer,
8666
9193
  stripBotMention: () => stripBotMention,
8667
9194
  surfaceVerdict: () => surfaceVerdict,
9195
+ telegramPollLockPath: () => telegramPollLockPath,
8668
9196
  timeoutGate: () => timeoutGate,
8669
9197
  titleFor: () => titleFor,
8670
9198
  toControlEvent: () => toControlEvent,
@@ -8758,7 +9286,7 @@ import { readdirSync as readdirSync4, readFileSync as readFileSync12 } from "fs"
8758
9286
  import { join as join18 } from "path";
8759
9287
 
8760
9288
  // packages/agents/dist/claude/registry.js
8761
- import fs13 from "fs";
9289
+ import fs14 from "fs";
8762
9290
  import { join as join17 } from "path";
8763
9291
  import { homedir as homedir7 } from "os";
8764
9292
  var CLAUDE_SESSIONS_DIR = join17(homedir7(), ".claude", "sessions");
@@ -8803,11 +9331,11 @@ function toLifecycleSnapshot(entry, taskId, alive, now) {
8803
9331
  function readClaudeStatusBySocketPath(socketPath) {
8804
9332
  let files;
8805
9333
  try {
8806
- files = fs13.readdirSync(CLAUDE_SESSIONS_DIR);
9334
+ files = fs14.readdirSync(CLAUDE_SESSIONS_DIR);
8807
9335
  } catch {
8808
9336
  return void 0;
8809
9337
  }
8810
- const entries = parseRegistryDir(files, (name) => fs13.readFileSync(join17(CLAUDE_SESSIONS_DIR, name), "utf8"));
9338
+ const entries = parseRegistryDir(files, (name) => fs14.readFileSync(join17(CLAUDE_SESSIONS_DIR, name), "utf8"));
8811
9339
  const entry = entries.find((e) => e.messagingSocketPath === socketPath);
8812
9340
  if (!entry)
8813
9341
  return void 0;
@@ -8830,7 +9358,7 @@ import { homedir as homedir8 } from "os";
8830
9358
  var SQUADRANT_STATE_DIR = join19(homedir8(), ".config", "squadrant", "state");
8831
9359
 
8832
9360
  // packages/agents/dist/drivers/launch-cmd.js
8833
- import fs14 from "fs";
9361
+ import fs15 from "fs";
8834
9362
  import path13 from "path";
8835
9363
 
8836
9364
  // packages/agents/dist/projection/cursor.js
@@ -8854,7 +9382,7 @@ import path17 from "path";
8854
9382
  import os9 from "os";
8855
9383
 
8856
9384
  // packages/agents/dist/projection/opencode-skills.js
8857
- import fs15 from "fs";
9385
+ import fs16 from "fs";
8858
9386
  import os10 from "os";
8859
9387
  import path18 from "path";
8860
9388
  var OPENCODE_SKILL_MARKER = ".squadrant-managed";
@@ -8876,62 +9404,62 @@ function parseSkillName(raw) {
8876
9404
  }
8877
9405
  function listSourceSkills(sourceSkillsDir) {
8878
9406
  const out = /* @__PURE__ */ new Map();
8879
- if (!fs15.existsSync(sourceSkillsDir))
9407
+ if (!fs16.existsSync(sourceSkillsDir))
8880
9408
  return out;
8881
- for (const entry of fs15.readdirSync(sourceSkillsDir, { withFileTypes: true })) {
9409
+ for (const entry of fs16.readdirSync(sourceSkillsDir, { withFileTypes: true })) {
8882
9410
  if (!entry.isDirectory())
8883
9411
  continue;
8884
9412
  const skillPath = path18.join(sourceSkillsDir, entry.name, "SKILL.md");
8885
- if (!fs15.existsSync(skillPath))
9413
+ if (!fs16.existsSync(skillPath))
8886
9414
  continue;
8887
- const name = parseSkillName(fs15.readFileSync(skillPath, "utf-8"));
9415
+ const name = parseSkillName(fs16.readFileSync(skillPath, "utf-8"));
8888
9416
  if (name && SAFE_NAME.test(name))
8889
9417
  out.set(name, skillPath);
8890
9418
  }
8891
9419
  return out;
8892
9420
  }
8893
9421
  function hasMarker(dir) {
8894
- return fs15.existsSync(path18.join(dir, OPENCODE_SKILL_MARKER));
9422
+ return fs16.existsSync(path18.join(dir, OPENCODE_SKILL_MARKER));
8895
9423
  }
8896
9424
  function syncOpencodeSkills(opts) {
8897
9425
  const result = { written: [], unchanged: [], removed: [], skipped: [] };
8898
9426
  const wanted = listSourceSkills(opts.sourceSkillsDir);
8899
- if (wanted.size === 0 && !fs15.existsSync(opts.sourceSkillsDir))
9427
+ if (wanted.size === 0 && !fs16.existsSync(opts.sourceSkillsDir))
8900
9428
  return result;
8901
9429
  if (!opts.dryRun)
8902
- fs15.mkdirSync(opts.skillsRoot, { recursive: true });
9430
+ fs16.mkdirSync(opts.skillsRoot, { recursive: true });
8903
9431
  for (const [name, srcPath] of wanted) {
8904
9432
  const targetDir = path18.join(opts.skillsRoot, name);
8905
9433
  const targetSkill = path18.join(targetDir, "SKILL.md");
8906
9434
  const managed = hasMarker(targetDir);
8907
- if (fs15.existsSync(targetSkill) && !managed) {
9435
+ if (fs16.existsSync(targetSkill) && !managed) {
8908
9436
  result.skipped.push({ name, reason: "foreign-collision" });
8909
9437
  continue;
8910
9438
  }
8911
- const current = fs15.existsSync(targetSkill) ? fs15.readFileSync(targetSkill) : null;
8912
- if (current && current.equals(fs15.readFileSync(srcPath))) {
9439
+ const current = fs16.existsSync(targetSkill) ? fs16.readFileSync(targetSkill) : null;
9440
+ if (current && current.equals(fs16.readFileSync(srcPath))) {
8913
9441
  if (!managed && !opts.dryRun) {
8914
- fs15.writeFileSync(path18.join(targetDir, OPENCODE_SKILL_MARKER), MARKER_BODY);
9442
+ fs16.writeFileSync(path18.join(targetDir, OPENCODE_SKILL_MARKER), MARKER_BODY);
8915
9443
  }
8916
9444
  result.unchanged.push(name);
8917
9445
  continue;
8918
9446
  }
8919
9447
  if (!opts.dryRun) {
8920
- fs15.mkdirSync(targetDir, { recursive: true });
8921
- fs15.copyFileSync(srcPath, targetSkill);
8922
- fs15.writeFileSync(path18.join(targetDir, OPENCODE_SKILL_MARKER), MARKER_BODY);
9448
+ fs16.mkdirSync(targetDir, { recursive: true });
9449
+ fs16.copyFileSync(srcPath, targetSkill);
9450
+ fs16.writeFileSync(path18.join(targetDir, OPENCODE_SKILL_MARKER), MARKER_BODY);
8923
9451
  }
8924
9452
  result.written.push(name);
8925
9453
  }
8926
- if (fs15.existsSync(opts.skillsRoot)) {
8927
- for (const entry of fs15.readdirSync(opts.skillsRoot, { withFileTypes: true })) {
9454
+ if (fs16.existsSync(opts.skillsRoot)) {
9455
+ for (const entry of fs16.readdirSync(opts.skillsRoot, { withFileTypes: true })) {
8928
9456
  if (!entry.isDirectory() || wanted.has(entry.name))
8929
9457
  continue;
8930
9458
  const dir = path18.join(opts.skillsRoot, entry.name);
8931
9459
  if (!hasMarker(dir))
8932
9460
  continue;
8933
9461
  if (!opts.dryRun)
8934
- fs15.rmSync(dir, { recursive: true, force: true });
9462
+ fs16.rmSync(dir, { recursive: true, force: true });
8935
9463
  result.removed.push(entry.name);
8936
9464
  }
8937
9465
  }
@@ -10105,6 +10633,9 @@ var ClaudePeerChannel = class {
10105
10633
  return { status: "accepted", via: this.name, confirmed: true };
10106
10634
  }
10107
10635
  }
10636
+ if (statusBefore === "busy" || statusBefore === "shell") {
10637
+ return { status: "queued", via: this.name };
10638
+ }
10108
10639
  if (statusBefore === "idle") {
10109
10640
  await this.deps.sleep(window);
10110
10641
  const afterInfo = this.deps.statusFor(taskId);
@@ -10243,7 +10774,7 @@ var ClaudeReceiptListener = class {
10243
10774
  };
10244
10775
 
10245
10776
  // packages/agents/dist/claude/api-key-approval.js
10246
- import fs16 from "fs";
10777
+ import fs17 from "fs";
10247
10778
  import os12 from "os";
10248
10779
  import path20 from "path";
10249
10780
 
@@ -10485,11 +11016,11 @@ function parseDraftFromScreen(screen) {
10485
11016
  if (!screen)
10486
11017
  return null;
10487
11018
  const lines = screen.split(/\r?\n/);
10488
- const HR_RE = /^\s*─{10,}\s*$/;
11019
+ const HR_RE2 = /^\s*─{10,}\s*$/;
10489
11020
  let bottomHR = -1;
10490
11021
  let topHR = -1;
10491
11022
  for (let i = lines.length - 1; i >= 0; i--) {
10492
- if (HR_RE.test(lines[i])) {
11023
+ if (HR_RE2.test(lines[i])) {
10493
11024
  if (bottomHR === -1) {
10494
11025
  bottomHR = i;
10495
11026
  } else {
@@ -10527,11 +11058,11 @@ function hasCCInputBox(screen) {
10527
11058
  if (!screen)
10528
11059
  return false;
10529
11060
  const lines = screen.split(/\r?\n/);
10530
- const HR_RE = /^\s*─{10,}\s*$/;
11061
+ const HR_RE2 = /^\s*─{10,}\s*$/;
10531
11062
  let bottomHR = -1;
10532
11063
  let topHR = -1;
10533
11064
  for (let i = lines.length - 1; i >= 0; i--) {
10534
- if (HR_RE.test(lines[i])) {
11065
+ if (HR_RE2.test(lines[i])) {
10535
11066
  if (bottomHR === -1)
10536
11067
  bottomHR = i;
10537
11068
  else {
@@ -10544,15 +11075,15 @@ function hasCCInputBox(screen) {
10544
11075
  return false;
10545
11076
  return lines.slice(topHR + 1, bottomHR).some((l) => /[>❯]/.test(l));
10546
11077
  }
10547
- function hasModalOptionList(screen) {
11078
+ function hasModalOptionList2(screen) {
10548
11079
  if (!screen)
10549
11080
  return false;
10550
11081
  const lines = screen.split(/\r?\n/);
10551
- const HR_RE = /^\s*─{10,}\s*$/;
11082
+ const HR_RE2 = /^\s*─{10,}\s*$/;
10552
11083
  let bottomHR = -1;
10553
11084
  let topHR = -1;
10554
11085
  for (let i = lines.length - 1; i >= 0; i--) {
10555
- if (HR_RE.test(lines[i])) {
11086
+ if (HR_RE2.test(lines[i])) {
10556
11087
  if (bottomHR === -1)
10557
11088
  bottomHR = i;
10558
11089
  else {
@@ -10569,11 +11100,11 @@ function readInputBoxRaw(screen, opts) {
10569
11100
  if (!screen)
10570
11101
  return null;
10571
11102
  const lines = screen.split(/\r?\n/);
10572
- const HR_RE = /^\s*─{10,}\s*$/;
11103
+ const HR_RE2 = /^\s*─{10,}\s*$/;
10573
11104
  let bottomHR = -1;
10574
11105
  let topHR = -1;
10575
11106
  for (let i = lines.length - 1; i >= 0; i--) {
10576
- if (HR_RE.test(lines[i])) {
11107
+ if (HR_RE2.test(lines[i])) {
10577
11108
  if (bottomHR === -1)
10578
11109
  bottomHR = i;
10579
11110
  else {
@@ -10603,6 +11134,34 @@ function classifyStartupSurface(screen) {
10603
11134
  return "idle";
10604
11135
  return "loading";
10605
11136
  }
11137
+ var OC_BOX_BOTTOM_RE = /^\s*╹[▀]{6,}\s*$/;
11138
+ var OC_BOX_SIDE_RE = /^\s*┃/;
11139
+ var OC_BOX_STATUS_RE = /^Build\s*·/i;
11140
+ var OC_BUSY_RE = /esc\s+interrupt/i;
11141
+ function parseOpencodeDraftFromScreen(screen) {
11142
+ if (!screen)
11143
+ return null;
11144
+ if (OC_BUSY_RE.test(screen))
11145
+ return null;
11146
+ const lines = screen.split(/\r?\n/);
11147
+ let bottom = -1;
11148
+ for (let i = lines.length - 1; i >= 0; i--) {
11149
+ if (OC_BOX_BOTTOM_RE.test(lines[i])) {
11150
+ bottom = i;
11151
+ break;
11152
+ }
11153
+ }
11154
+ if (bottom === -1)
11155
+ return null;
11156
+ const content = [];
11157
+ for (let i = bottom - 1; i >= 0 && OC_BOX_SIDE_RE.test(lines[i]); i--) {
11158
+ content.push(lines[i].replace(/^\s*┃\s*/, "").replace(/[\s┃│]+$/, ""));
11159
+ }
11160
+ if (content.length === 0)
11161
+ return null;
11162
+ const hasDraft = content.some((l) => l !== "" && !OC_BOX_STATUS_RE.test(l) && !screenHasSplashMarker(l, "Ask anything"));
11163
+ return hasDraft ? null : "";
11164
+ }
10606
11165
  function sendDebugEnabled() {
10607
11166
  return !!process.env.SQUADRANT_DEBUG_SEND;
10608
11167
  }
@@ -10818,10 +11377,10 @@ function createCmuxDriver() {
10818
11377
  `);
10819
11378
  throw new DeferDelivery(null, "probe-failed");
10820
11379
  }
10821
- const draft = parseDraftFromScreen(screen);
11380
+ const draft = opts?.agent === "opencode" ? parseOpencodeDraftFromScreen(screen) : parseDraftFromScreen(screen);
10822
11381
  if (draft === null)
10823
11382
  throw new DeferDelivery(null, "no-box");
10824
- if (hasModalOptionList(screen))
11383
+ if (hasModalOptionList2(screen))
10825
11384
  throw new DeferDelivery(null, "modal");
10826
11385
  if (draft === "") {
10827
11386
  await deliver();
@@ -11015,7 +11574,7 @@ var NotifierRegistry = class {
11015
11574
  };
11016
11575
 
11017
11576
  // packages/workspaces/dist/workspaces/obsidian.js
11018
- import fs17 from "fs/promises";
11577
+ import fs18 from "fs/promises";
11019
11578
  import { existsSync as existsSync11 } from "fs";
11020
11579
  import path21 from "path";
11021
11580
 
@@ -11811,7 +12370,7 @@ async function settleInputBox(runtime, pane) {
11811
12370
  }
11812
12371
  async function confirmedSendToPane(runtime, pane, message) {
11813
12372
  const preSendScreen = await runtime.readPaneScreen(pane) ?? "";
11814
- if (hasModalOptionList(preSendScreen)) {
12373
+ if (hasModalOptionList2(preSendScreen)) {
11815
12374
  return { delivered: false, blockedByModal: true };
11816
12375
  }
11817
12376
  await runtime.pasteToPane(pane, message);
@@ -11914,7 +12473,7 @@ async function maybeBroadcastDaemonRestart(opts) {
11914
12473
 
11915
12474
  // packages/cli/src/lib/captain-channel-factory.ts
11916
12475
  import { createServer as createServer4, connect as netConnect } from "net";
11917
- import fs18 from "fs";
12476
+ import fs19 from "fs";
11918
12477
  import { join as join26 } from "path";
11919
12478
  import { randomUUID as randomUUID5 } from "crypto";
11920
12479
  import chalk2 from "chalk";
@@ -11923,15 +12482,15 @@ var shared;
11923
12482
  var registryEntryPath = () => join26(CLAUDE_SESSIONS_DIR, `${process.pid}.json`);
11924
12483
  function unregisterSenderIdentity() {
11925
12484
  try {
11926
- fs18.unlinkSync(registryEntryPath());
12485
+ fs19.unlinkSync(registryEntryPath());
11927
12486
  } catch {
11928
12487
  }
11929
12488
  }
11930
12489
  function registerSenderIdentity(socketPath) {
11931
12490
  try {
11932
12491
  unregisterSenderIdentity();
11933
- fs18.mkdirSync(CLAUDE_SESSIONS_DIR, { recursive: true });
11934
- fs18.writeFileSync(
12492
+ fs19.mkdirSync(CLAUDE_SESSIONS_DIR, { recursive: true });
12493
+ fs19.writeFileSync(
11935
12494
  registryEntryPath(),
11936
12495
  JSON.stringify({
11937
12496
  pid: process.pid,
@@ -11957,7 +12516,7 @@ async function sharedReceiptListener() {
11957
12516
  // must never be the reason we refuse to start.
11958
12517
  unlinkStale: (p) => {
11959
12518
  try {
11960
- fs18.unlinkSync(p);
12519
+ fs19.unlinkSync(p);
11961
12520
  } catch {
11962
12521
  }
11963
12522
  },
@@ -12054,6 +12613,33 @@ function buildTelegramBridge(cfg, stateRoot, log, deliverInbound, usageFor) {
12054
12613
  });
12055
12614
  const runCommand = createRunCommand(CLI_BIN);
12056
12615
  const sendReply = (threadId, text, replyMarkup) => client.sendMessage(cfg.supergroupId, threadId, text, replyMarkup);
12616
+ const lifecycle = createInboundLifecycle({
12617
+ stateRoot,
12618
+ cfg: { supergroupId: cfg.supergroupId },
12619
+ sendChatAction: (chatId, threadId, action) => client.sendChatAction(chatId, threadId, action),
12620
+ sendReply: async (threadId, text) => {
12621
+ await client.sendMessage(cfg.supergroupId, threadId, text);
12622
+ },
12623
+ readPane: createCaptainPaneReader(
12624
+ new DaemonCmux(createCmuxDriver()),
12625
+ (project) => {
12626
+ try {
12627
+ return loadConfig().projects[project]?.captainName ?? `${project}-captain`;
12628
+ } catch {
12629
+ return `${project}-captain`;
12630
+ }
12631
+ },
12632
+ {
12633
+ log,
12634
+ // #839 staleness, measured for real: cmux has no pane activity
12635
+ // timestamp, so the reader hashes the screen and asks the persisted
12636
+ // pending store how long that exact screen has been up. Persisted, so a
12637
+ // daemon restart cannot hand a dead captain a fresh 15 minutes.
12638
+ noteScreen: (project, hash) => notePaneScreen(stateRoot, project, hash, Date.now())
12639
+ }
12640
+ ),
12641
+ log
12642
+ });
12057
12643
  return createTelegramBridge({
12058
12644
  cfg,
12059
12645
  stateRoot,
@@ -12065,7 +12651,20 @@ function buildTelegramBridge(cfg, stateRoot, log, deliverInbound, usageFor) {
12065
12651
  runCommand,
12066
12652
  sendReply,
12067
12653
  deliverInbound,
12068
- usageFor
12654
+ usageFor,
12655
+ lifecycle,
12656
+ // #321 registry prune. Read the file directly rather than via loadConfig():
12657
+ // loadConfig swallows every failure and returns defaults, so an unreadable
12658
+ // config would look like "no projects registered" and the prune would delete
12659
+ // every link on the machine. `undefined` correctly means "don't prune".
12660
+ registeredProjects: () => {
12661
+ try {
12662
+ const parsed = JSON.parse(readFileSync19(join27(dirname6(stateRoot), "config.json"), "utf8"));
12663
+ return Object.keys(parsed.projects ?? {});
12664
+ } catch {
12665
+ return void 0;
12666
+ }
12667
+ }
12069
12668
  });
12070
12669
  }
12071
12670
  function buildNotifyFault(log) {