squadrant 0.21.0 → 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
 
@@ -3471,6 +3545,11 @@ function assertBackendUsable(o) {
3471
3545
  function shouldBuildRouterService(router, isVitest) {
3472
3546
  return !!router && !isVitest;
3473
3547
  }
3548
+ function claudeEnvShadowsRouter(claudeEnv) {
3549
+ if (!claudeEnv)
3550
+ return [];
3551
+ return Object.keys(claudeEnv).filter((key) => key.startsWith("ANTHROPIC_"));
3552
+ }
3474
3553
  var init_router_resolution = __esm({
3475
3554
  "packages/core/dist/router-resolution.js"() {
3476
3555
  init_dist();
@@ -3497,6 +3576,14 @@ function buildRouterEnv(creds, model) {
3497
3576
  env.ANTHROPIC_MODEL = model;
3498
3577
  return env;
3499
3578
  }
3579
+ function mergeClaudeEnvRouterSettings(routerEnv, claudeEnv) {
3580
+ const merged = { ...routerEnv };
3581
+ for (const [key, value] of Object.entries(claudeEnv ?? {})) {
3582
+ if (!key.startsWith("ANTHROPIC_"))
3583
+ merged[key] = value;
3584
+ }
3585
+ return merged;
3586
+ }
3500
3587
  function formatCustomHeaders(headers) {
3501
3588
  const entries = Object.entries(headers ?? {});
3502
3589
  if (entries.length === 0)
@@ -3810,10 +3897,18 @@ async function runCrewSpawn(input, config, deps) {
3810
3897
  if (agentName === "claude") {
3811
3898
  let routerEnv = {};
3812
3899
  if (backend !== "native") {
3900
+ if (!deps.writeRouterSettings) {
3901
+ throw new Error(`backend '${backend}' requires a per-spawn --settings writer to outrank defaults.claudeEnv, but the spawn path has none \u2014 refusing to launch a routed session that would silently bypass the router shim`);
3902
+ }
3813
3903
  if (!deps.routerCredentials) {
3814
3904
  throw new Error(`backend '${backend}' requires router credentials, but the spawn path has no daemon credentials provider`);
3815
3905
  }
3816
3906
  routerEnv = buildRouterEnv(await deps.routerCredentials({ project: input.project, backend }), crewModel);
3907
+ const shadowed = claudeEnvShadowsRouter(config.defaults.claudeEnv);
3908
+ if (shadowed.length > 0) {
3909
+ process.stderr.write(`\u26A0\uFE0F backend '${backend}' selected but defaults.claudeEnv sets ${shadowed.join(", ")} \u2014 these would shadow the router shim. Overriding via a per-spawn --settings env block (outranks ~/.claude/settings.json).
3910
+ `);
3911
+ }
3817
3912
  }
3818
3913
  ensureSocksDir();
3819
3914
  const messagingSocketPath = path9.join(CC_SOCKS_DIR, `squadrant-${randomUUID3()}.sock`);
@@ -3826,6 +3921,12 @@ async function runCrewSpawn(input, config, deps) {
3826
3921
  name,
3827
3922
  messagingSocketPath
3828
3923
  });
3924
+ const routerSettingsPath = Object.keys(routerEnv).length > 0 ? deps.writeRouterSettings({
3925
+ stateRoot: STATE_ROOT,
3926
+ project: input.project,
3927
+ taskId: rec.id,
3928
+ env: mergeClaudeEnvRouterSettings(routerEnv, config.defaults.claudeEnv)
3929
+ }) : void 0;
3829
3930
  let hooksInstalled = false;
3830
3931
  try {
3831
3932
  deps.writeSettingsLocal(spawnCwd);
@@ -3841,18 +3942,25 @@ async function runCrewSpawn(input, config, deps) {
3841
3942
  messagingSocketPath,
3842
3943
  // Permission mode is config-driven so squadrant can default crews to 'auto'
3843
3944
  // or keep the semi-automatic 'acceptEdits' gate. Falls back to 'acceptEdits'.
3844
- permissionMode: config.defaults.permissions?.crew ?? "acceptEdits",
3945
+ // #772: a router-backed crew MUST use 'default' — 'auto' makes the built-in
3946
+ // Sonnet-5 classifier own PermissionRequest and the gate yields, which is
3947
+ // exactly the fail-closed dead end U7 exists to fix. The rubric owns the
3948
+ // permission mode for a routed spawn; the operator's config cannot override
3949
+ // it back into the broken path.
3950
+ permissionMode: backend !== "native" ? "default" : config.defaults.permissions?.crew ?? "acceptEdits",
3845
3951
  // #708: self-describing name so ListAgents/the registry can tell this
3846
3952
  // crew apart from an unrelated session instead of an auto-derived cwd
3847
3953
  // basename (only the claude driver reads this — other agents ignore it).
3848
3954
  sessionName: crewSessionName(input.project, name),
3849
3955
  ...crewModel ? { model: crewModel } : {},
3850
- ...crewThinking ? { thinking: crewThinking } : {}
3956
+ ...crewThinking ? { thinking: crewThinking } : {},
3957
+ ...routerSettingsPath ? { settingsPath: routerSettingsPath } : {}
3851
3958
  });
3852
3959
  const direction2 = input.direction ?? "tab";
3853
3960
  const title2 = titleFor(input.project, name);
3854
3961
  const pane2 = await deps.runtime.newPane({ workspaceId: captain.id, direction: direction2, title: title2 });
3855
- const envPrefix = `SQUADRANT_CREW_TASK_ID=${rec.id} SQUADRANT_CREW_PROJECT=${input.project}`;
3962
+ const gateEnv = backend !== "native" ? " SQUADRANT_GATE=on" : "";
3963
+ const envPrefix = `SQUADRANT_CREW_TASK_ID=${rec.id} SQUADRANT_CREW_PROJECT=${input.project}${gateEnv}`;
3856
3964
  const routerPrefix = Object.keys(routerEnv).length > 0 ? ` ${renderEnvAssignments(routerEnv)}` : "";
3857
3965
  await deps.runtime.sendToPane(pane2, `cd ${shellQuote(spawnCwd)} && ${envPrefix}${routerPrefix} ${niceCrewCommand(cliCommand2)}`);
3858
3966
  const preLaunchScreen = await deps.runtime.readPaneScreen(pane2) ?? "";
@@ -4631,8 +4739,9 @@ function createDelivery(ctx, daemonCmux, isSurfaceAlive) {
4631
4739
  if (mode !== "off")
4632
4740
  throw new DeferDelivery(null, "no-channel");
4633
4741
  }
4742
+ const paneOpts = { ...sendOpts, agent };
4634
4743
  try {
4635
- return await cmux2.send(surface, text, sendOpts);
4744
+ return await cmux2.send(surface, text, paneOpts);
4636
4745
  } catch (e) {
4637
4746
  if (!(e instanceof DeferDelivery) || e.reason !== "probe-failed")
4638
4747
  throw e;
@@ -4644,7 +4753,7 @@ function createDelivery(ctx, daemonCmux, isSurfaceAlive) {
4644
4753
  }
4645
4754
  log(`delivery project=${project}: probe-failed on ${surface.workspaceId}/${surface.surfaceId} \u2014 re-resolved to ${next.workspaceId}/${next.surfaceId}, retrying`);
4646
4755
  surface = next;
4647
- return cmux2.send(next, text, sendOpts);
4756
+ return cmux2.send(next, text, paneOpts);
4648
4757
  }
4649
4758
  });
4650
4759
  if ("delivered" in result) {
@@ -5575,16 +5684,26 @@ function createRunCommand(cliBin) {
5575
5684
  }
5576
5685
  };
5577
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
+ }
5578
5697
  function isCaptainAliveFromHealth(rows, project) {
5579
- return rows.some((h) => h.kind === "captain" && h.project === project && h.state === "alive");
5698
+ return captainAliveStateFromHealth(rows, project) === "alive";
5580
5699
  }
5581
5700
  function createIsCaptainAlive(sock) {
5582
5701
  return async (project) => {
5583
5702
  try {
5584
5703
  const health = await sendRequest(sock, { kind: "health", project }, 5e3);
5585
- return isCaptainAliveFromHealth(health ?? [], project);
5704
+ return captainAliveStateFromHealth(health ?? [], project);
5586
5705
  } catch {
5587
- return false;
5706
+ return "unknown";
5588
5707
  }
5589
5708
  };
5590
5709
  }
@@ -5621,16 +5740,19 @@ function createEnsureCaptainAlive(deps) {
5621
5740
  const now = deps.now ?? (() => Date.now());
5622
5741
  const inFlight = /* @__PURE__ */ new Map();
5623
5742
  async function run(project) {
5624
- if (await deps.isAlive(project))
5743
+ const first = await deps.isAlive(project);
5744
+ if (first === "alive")
5625
5745
  return "alive";
5626
5746
  await deps.launch(project);
5627
5747
  const deadline = now() + warmupTimeoutMs;
5748
+ let last = first;
5628
5749
  while (now() < deadline) {
5629
- if (await deps.isAlive(project))
5750
+ last = await deps.isAlive(project);
5751
+ if (last === "alive")
5630
5752
  return "launched";
5631
5753
  await sleep3(pollMs);
5632
5754
  }
5633
- return "timeout";
5755
+ return last === "dead" ? "timeout" : "unknown";
5634
5756
  }
5635
5757
  return function ensure(project) {
5636
5758
  const existing = inFlight.get(project);
@@ -5685,6 +5807,26 @@ ${ev.question}`;
5685
5807
  function formatInbound(text) {
5686
5808
  return `\u{1F4E9} [from Telegram] ${text}`;
5687
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
+ }
5688
5830
  function formatUsageLine(u) {
5689
5831
  const reqs = u.requests === 1 ? "1 req" : `${u.requests} reqs`;
5690
5832
  const named = Object.keys(u.models).filter((m) => m !== "unknown").sort((a, b) => u.models[b].costUsd - u.models[a].costUsd);
@@ -5696,8 +5838,19 @@ function maskToken(token) {
5696
5838
  return token;
5697
5839
  return "*".repeat(token.length - 4) + token.slice(-4);
5698
5840
  }
5841
+ var MEDIA_KINDS;
5699
5842
  var init_format = __esm({
5700
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
+ ];
5701
5854
  }
5702
5855
  });
5703
5856
 
@@ -5720,6 +5873,8 @@ function loadState(stateRoot) {
5720
5873
  };
5721
5874
  if (typeof data.lastUserId === "number")
5722
5875
  result.lastUserId = data.lastUserId;
5876
+ if (data.pending && Object.keys(data.pending).length > 0)
5877
+ result.pending = data.pending;
5723
5878
  return result;
5724
5879
  } catch {
5725
5880
  return { offset: 0, topics: {}, notify: {} };
@@ -5733,6 +5888,19 @@ function setTopic(stateRoot, project, topicId, scope = "project") {
5733
5888
  s.topics[topicKey(project, scope)] = topicId;
5734
5889
  saveState(stateRoot, s);
5735
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
+ }
5736
5904
  function isNotifyActive(stateRoot, project) {
5737
5905
  return loadState(stateRoot).notify[project] === true;
5738
5906
  }
@@ -5746,6 +5914,42 @@ function setNotify(stateRoot, project, active) {
5746
5914
  s.notify[project] = active;
5747
5915
  saveState(stateRoot, s);
5748
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
+ }
5749
5953
  function findProjectByThread(stateRoot, threadId) {
5750
5954
  const s = loadState(stateRoot);
5751
5955
  for (const [key, id] of Object.entries(s.topics)) {
@@ -5768,17 +5972,18 @@ var init_state = __esm({
5768
5972
  function createTelegramClient(opts) {
5769
5973
  const fetchImpl = opts.fetch ?? fetch;
5770
5974
  const base = `https://api.telegram.org/bot${opts.token}`;
5771
- async function call(method, body) {
5975
+ async function call(method, body, signal) {
5772
5976
  const res = await fetchImpl(`${base}/${method}`, {
5773
5977
  method: "POST",
5774
5978
  headers: { "content-type": "application/json" },
5775
- body: JSON.stringify(body)
5979
+ body: JSON.stringify(body),
5980
+ ...signal ? { signal } : {}
5776
5981
  });
5777
5982
  const json = await res.json();
5778
5983
  if (!res.ok || !json.ok) {
5779
5984
  const code = json.error_code ?? res.status;
5780
5985
  const desc = json.description ?? "unknown error";
5781
- throw new Error(`telegram ${method} failed (${code}): ${desc}`);
5986
+ throw new TelegramApiError(code, `telegram ${method} failed (${code}): ${desc}`, json.parameters?.retry_after);
5782
5987
  }
5783
5988
  return json.result;
5784
5989
  }
@@ -5787,8 +5992,8 @@ function createTelegramClient(opts) {
5787
5992
  const r = await call("getMe", {});
5788
5993
  return { id: r.id, username: r.username };
5789
5994
  },
5790
- getUpdates(offset, timeoutSec = 50) {
5791
- return call("getUpdates", { offset, timeout: timeoutSec });
5995
+ getUpdates(offset, timeoutSec = 50, signal) {
5996
+ return call("getUpdates", { offset, timeout: timeoutSec }, signal);
5792
5997
  },
5793
5998
  async sendMessage(chatId, threadId, text, replyMarkup) {
5794
5999
  const body = { chat_id: chatId, text };
@@ -5811,6 +6016,9 @@ function createTelegramClient(opts) {
5811
6016
  const r = await call("createForumTopic", { chat_id: chatId, name });
5812
6017
  return r.message_thread_id;
5813
6018
  },
6019
+ async deleteForumTopic(chatId, threadId) {
6020
+ await call("deleteForumTopic", { chat_id: chatId, message_thread_id: threadId });
6021
+ },
5814
6022
  async setMyCommands(commands) {
5815
6023
  await call("setMyCommands", { commands });
5816
6024
  },
@@ -5819,11 +6027,241 @@ function createTelegramClient(opts) {
5819
6027
  if (threadId !== void 0)
5820
6028
  body.message_thread_id = threadId;
5821
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
+ });
5822
6037
  }
5823
6038
  };
5824
6039
  }
6040
+ var TelegramApiError;
5825
6041
  var init_client = __esm({
5826
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();
5827
6265
  }
5828
6266
  });
5829
6267
 
@@ -5911,8 +6349,10 @@ var init_tiers = __esm({
5911
6349
  });
5912
6350
 
5913
6351
  // packages/core/dist/telegram/bridge.js
6352
+ import fs11 from "fs";
5914
6353
  import os5 from "os";
5915
6354
  import path12 from "path";
6355
+ import crypto2 from "crypto";
5916
6356
  function formatInboundReceipt(project, outcome) {
5917
6357
  if (!outcome)
5918
6358
  return void 0;
@@ -5928,6 +6368,85 @@ function formatInboundReceipt(project, outcome) {
5928
6368
  return void 0;
5929
6369
  }
5930
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
+ }
5931
6450
  function parseNotifyPref(text) {
5932
6451
  const parts = text.trim().split(/\s+/);
5933
6452
  if (stripBotMention(parts[0] ?? "").toLowerCase() !== "/notify")
@@ -5954,9 +6473,17 @@ function notifyToggle(text) {
5954
6473
  }
5955
6474
  function createTelegramBridge(opts) {
5956
6475
  const { cfg, stateRoot, client, appendCaptainMessage: appendCaptainMessage2, log, ensureCaptainAlive, runCommand, sendReply } = opts;
6476
+ const lifecycle = opts.lifecycle;
5957
6477
  const configRoot = opts.configRoot ?? path12.join(os5.homedir(), ".config", "squadrant");
5958
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;
5959
6483
  let running = false;
6484
+ let pollStopped = false;
6485
+ let selfHealCancel = null;
6486
+ let conflictCount = 0;
5960
6487
  let lastSuccessfulPollAt = null;
5961
6488
  let lastError = null;
5962
6489
  let lastErrorAt = null;
@@ -5966,12 +6493,8 @@ function createTelegramBridge(opts) {
5966
6493
  saveState(stateRoot, s);
5967
6494
  }
5968
6495
  async function sendToTopic(project, text) {
5969
- let threadId = loadState(stateRoot).topics[topicKey(project)];
5970
- if (threadId === void 0) {
5971
- threadId = await client.createForumTopic(cfg.supergroupId, topicName(project));
5972
- setTopic(stateRoot, project, threadId);
5973
- }
5974
- await client.sendMessage(cfg.supergroupId, threadId, text);
6496
+ const { topicId } = await runTelegramLink({ project, cfg, client, stateRoot });
6497
+ await client.sendMessage(cfg.supergroupId, topicId, text);
5975
6498
  }
5976
6499
  async function deliverOutbound(project, ev) {
5977
6500
  const resolved = resolveNotify(cfg.notify, loadProjectOverride(project, configRoot));
@@ -6158,10 +6681,13 @@ ${formatUsageLine(usage2)}`;
6158
6681
  }
6159
6682
  await runChannelCommand(text, fromId, void 0);
6160
6683
  }
6161
- async function handleProjectTopic(text, threadId, fromId) {
6684
+ async function handleProjectTopic(text, threadId, fromId, messageId, media) {
6162
6685
  const resolved = findProjectByThread(stateRoot, threadId);
6163
- 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>");
6164
6689
  return;
6690
+ }
6165
6691
  if (isBareSpawn(text)) {
6166
6692
  if (!isControlEnabled(cfg) || !isAuthorized(fromId, cfg)) {
6167
6693
  await reply(threadId, "\u26D4 not authorized");
@@ -6211,21 +6737,16 @@ ${formatUsageLine(usage2)}`;
6211
6737
  await runChannelCommand(text, fromId, threadId);
6212
6738
  return;
6213
6739
  }
6214
- void client.sendChatAction(cfg.supergroupId, threadId, "typing").catch((e) => {
6215
- log(`telegram sendChatAction failed: ${e.message}`);
6216
- });
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
+ }
6217
6743
  setNotify(stateRoot, resolved.project, true);
6218
6744
  if (ensureCaptainAlive && isControlEnabled(cfg) && isAuthorized(fromId, cfg)) {
6219
- try {
6220
- const r = await ensureCaptainAlive(resolved.project);
6745
+ void ensureCaptainAlive(resolved.project).then((r) => {
6221
6746
  if (r === "timeout") {
6222
- await reply(threadId, `\u274C couldn't reach ${resolved.project} captain \u2014 saved to mailbox, will deliver when you open the workspace.`);
6223
- } else {
6224
- 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`);
6225
6748
  }
6226
- } catch (e) {
6227
- log(`telegram auto-launch failed project=${resolved.project}: ${e.message}`);
6228
- }
6749
+ }).catch((e) => log(`telegram auto-launch failed project=${resolved.project}: ${e.message}`));
6229
6750
  }
6230
6751
  let handled = false;
6231
6752
  let outcome;
@@ -6234,13 +6755,22 @@ ${formatUsageLine(usage2)}`;
6234
6755
  handled = res.handled;
6235
6756
  outcome = res.outcome;
6236
6757
  }
6758
+ const deliveryHeld = outcome?.status === "held";
6237
6759
  if (!handled) {
6238
6760
  await appendCaptainMessage2({ stateRoot, project: resolved.project, text: formatInbound(text), source: "telegram" });
6761
+ outcome = void 0;
6239
6762
  }
6240
6763
  const receipt = formatInboundReceipt(resolved.project, outcome);
6241
6764
  if (receipt && sendReply) {
6242
6765
  await sendReply(threadId, receipt).catch((e) => log(`telegram receipt failed project=${resolved.project}: ${e.message}`));
6243
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);
6244
6774
  }
6245
6775
  async function handleUpdate(u) {
6246
6776
  if (u.callback_query) {
@@ -6248,7 +6778,11 @@ ${formatUsageLine(usage2)}`;
6248
6778
  return;
6249
6779
  }
6250
6780
  const m = u.message;
6251
- 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)
6252
6786
  return;
6253
6787
  if (!cfg.chats.includes(m.chat.id))
6254
6788
  return;
@@ -6262,7 +6796,7 @@ ${formatUsageLine(usage2)}`;
6262
6796
  await reply(threadId, "\u26D4 not authorized");
6263
6797
  return;
6264
6798
  }
6265
- const task = m.text.trim();
6799
+ const task = (text ?? "").trim();
6266
6800
  if (!task) {
6267
6801
  await reply(threadId, "spawn cancelled \u2014 empty task");
6268
6802
  return;
@@ -6273,39 +6807,122 @@ ${formatUsageLine(usage2)}`;
6273
6807
  return;
6274
6808
  }
6275
6809
  if (m.message_thread_id === void 0) {
6810
+ if (m.text === void 0)
6811
+ return;
6276
6812
  await handleGeneral(m.text, m.from?.id);
6277
6813
  return;
6278
6814
  }
6279
- 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);
6280
6842
  }
6281
6843
  async function pollLoop() {
6282
6844
  while (running) {
6845
+ const ac = new AbortController();
6846
+ pollAbort = ac;
6847
+ let waitMs = pollMs;
6283
6848
  try {
6284
6849
  const offset = loadState(stateRoot).offset;
6285
- const updates = await client.getUpdates(offset, LONG_POLL_SEC);
6850
+ const updates = await client.getUpdates(offset, LONG_POLL_SEC, ac.signal);
6851
+ conflictCount = 0;
6286
6852
  for (const u of updates) {
6287
6853
  await handleUpdate(u);
6288
6854
  persistOffset(u.update_id + 1);
6289
6855
  }
6290
6856
  lastSuccessfulPollAt = Date.now();
6291
6857
  } catch (e) {
6292
- lastError = e.message;
6858
+ if (ac.signal.aborted)
6859
+ break;
6860
+ const msg = e.message;
6861
+ lastError = msg;
6293
6862
  lastErrorAt = Date.now();
6294
- 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;
6295
6875
  }
6296
6876
  if (running)
6297
- await sleep2(pollMs);
6877
+ await sleep2(waitMs);
6298
6878
  }
6879
+ running = false;
6880
+ releasePollLock?.();
6881
+ releasePollLock = null;
6882
+ if (!pollStopped)
6883
+ scheduleSelfHeal();
6299
6884
  }
6300
6885
  return {
6301
6886
  start() {
6302
6887
  if (running)
6303
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
+ }
6304
6904
  running = true;
6905
+ conflictCount = 0;
6906
+ try {
6907
+ lifecycle?.start();
6908
+ } catch (e) {
6909
+ log(`telegram lifecycle start failed: ${e.message}`);
6910
+ }
6305
6911
  void pollLoop();
6306
6912
  },
6307
6913
  stop() {
6308
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;
6309
6926
  },
6310
6927
  pushLifecycle(project, ev) {
6311
6928
  void deliverOutbound(project, ev).catch((e) => {
@@ -6322,17 +6939,23 @@ ${formatUsageLine(usage2)}`;
6322
6939
  }
6323
6940
  };
6324
6941
  }
6325
- 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;
6326
6943
  var init_bridge = __esm({
6327
6944
  "packages/core/dist/telegram/bridge.js"() {
6328
6945
  init_dist();
6946
+ init_client();
6329
6947
  init_auth();
6330
6948
  init_commands();
6331
6949
  init_format();
6950
+ init_notify();
6332
6951
  init_panels();
6333
6952
  init_state();
6334
6953
  init_tiers();
6954
+ CAPTAIN_RECEIVED_ACK = "\u2705 captain received";
6335
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;
6336
6959
  sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
6337
6960
  CREW_TIERS = ["all", "alert_only", "done_only", "none"];
6338
6961
  RECOGNIZED_CHANNEL_COMMANDS = /* @__PURE__ */ new Set(["status", "projects", "crews", "launch", "effort", "spawn"]);
@@ -6377,7 +7000,7 @@ var init_restart_daemon = __esm({
6377
7000
  });
6378
7001
 
6379
7002
  // packages/core/dist/telegram/setup.js
6380
- import fs11 from "fs";
7003
+ import fs12 from "fs";
6381
7004
  function resolveSetupGroup(existingSupergroupId, opts) {
6382
7005
  if (existingSupergroupId !== void 0 && !opts.redetect)
6383
7006
  return "reuse";
@@ -6428,7 +7051,7 @@ function writeTelegramConfig(configPath, opts) {
6428
7051
  let config;
6429
7052
  let raw = null;
6430
7053
  try {
6431
- raw = fs11.readFileSync(configPath, "utf-8");
7054
+ raw = fs12.readFileSync(configPath, "utf-8");
6432
7055
  } catch (err) {
6433
7056
  if (err.code !== "ENOENT") {
6434
7057
  throw new Error(`refusing to overwrite unreadable config at ${configPath}: ${String(err)}`);
@@ -6467,94 +7090,6 @@ var init_setup = __esm({
6467
7090
  }
6468
7091
  });
6469
7092
 
6470
- // packages/core/dist/telegram/notify.js
6471
- function runTelegramStatus(opts) {
6472
- const tg = opts.config.telegram;
6473
- const env = opts.env ?? process.env;
6474
- const tokenSet = !!(tg?.botToken ?? env.TELEGRAM_BOT_TOKEN);
6475
- const links = Object.entries(loadState(opts.stateRoot).topics).map(([key, topicId]) => {
6476
- const sep3 = key.indexOf("::");
6477
- return { project: key.slice(0, sep3), scope: key.slice(sep3 + 2), topicId };
6478
- });
6479
- return { tokenSet, supergroupId: tg?.supergroupId ?? null, links };
6480
- }
6481
- function runTelegramNotifySet(opts) {
6482
- setNotify(opts.stateRoot, opts.project, opts.active);
6483
- }
6484
- function runTelegramNotifyPref(args) {
6485
- const { project, dimension, value, root } = args;
6486
- if (dimension === "crew") {
6487
- if (!["all", "alert_only", "done_only", "none"].includes(value))
6488
- return { ok: false, message: "crew must be all|alert_only|done_only|none" };
6489
- saveProjectOverride(project, { telegram: { notify: { crew: value } } }, root);
6490
- return { ok: true };
6491
- }
6492
- if (value !== "on" && value !== "off")
6493
- return { ok: false, message: "cap must be on|off" };
6494
- saveProjectOverride(project, { telegram: { notify: { cap: value === "on" } } }, root);
6495
- return { ok: true };
6496
- }
6497
- function runTelegramNotifyStatus(opts) {
6498
- const s = loadState(opts.stateRoot);
6499
- const projects = /* @__PURE__ */ new Set();
6500
- for (const key of Object.keys(s.topics)) {
6501
- const sep3 = key.indexOf("::");
6502
- projects.add(sep3 === -1 ? key : key.slice(0, sep3));
6503
- }
6504
- for (const p of Object.keys(s.notify))
6505
- projects.add(p);
6506
- return [...projects].map((project) => ({ project, active: s.notify[project] === true }));
6507
- }
6508
- function capAllowed(project, globalNotify, root) {
6509
- return resolveNotify(globalNotify, loadProjectOverride(project, root)).cap;
6510
- }
6511
- function confirmationText(project, before, after, dim) {
6512
- if (dim === "active")
6513
- return `\u{1F515} ${project} \u2014 all notifications muted here. Unmute: squadrant telegram notify ${project} on`;
6514
- if (dim === "cap")
6515
- return `\u{1F515} ${project} \u2014 captain messages muted here. Re-enable: squadrant telegram notify ${project} cap on`;
6516
- return `\u{1F515} ${project} \u2014 crew notifications now '${after.crew}' (was '${before.crew}'). Re-enable: squadrant telegram notify ${project} crew ${before.crew}`;
6517
- }
6518
- async function runNotifyConfirmation(opts) {
6519
- const { quieter, dim } = isQuieter(opts.before, opts.after);
6520
- if (!quieter || dim === null)
6521
- return false;
6522
- const topicId = loadState(opts.stateRoot).topics[topicKey(opts.project)];
6523
- if (topicId === void 0)
6524
- return false;
6525
- const text = confirmationText(opts.project, opts.before, opts.after, dim);
6526
- try {
6527
- await opts.client.sendMessage(opts.cfg.supergroupId, topicId, text);
6528
- return true;
6529
- } catch {
6530
- console.warn(`[squadrant] mute-confirmation send failed for ${opts.project} \u2014 notification preference was still saved`);
6531
- return false;
6532
- }
6533
- }
6534
- async function runTelegramSend(opts) {
6535
- const topicId = loadState(opts.stateRoot).topics[topicKey(opts.project)];
6536
- if (topicId === void 0) {
6537
- throw new Error(`project "${opts.project}" is not linked \u2014 run: squadrant telegram link ${opts.project}`);
6538
- }
6539
- await opts.client.sendMessage(opts.cfg.supergroupId, topicId, opts.message);
6540
- return { chatId: opts.cfg.supergroupId, topicId };
6541
- }
6542
- async function runTelegramLink(opts) {
6543
- const existing = loadState(opts.stateRoot).topics[topicKey(opts.project)];
6544
- if (existing !== void 0)
6545
- return { topicId: existing, created: false };
6546
- const topicId = await opts.client.createForumTopic(opts.cfg.supergroupId, topicName(opts.project));
6547
- setTopic(opts.stateRoot, opts.project, topicId);
6548
- return { topicId, created: true };
6549
- }
6550
- var init_notify = __esm({
6551
- "packages/core/dist/telegram/notify.js"() {
6552
- init_dist();
6553
- init_state();
6554
- init_format();
6555
- }
6556
- });
6557
-
6558
7093
  // packages/core/dist/telegram/index.js
6559
7094
  var init_telegram = __esm({
6560
7095
  "packages/core/dist/telegram/index.js"() {
@@ -6566,6 +7101,7 @@ var init_telegram = __esm({
6566
7101
  init_format();
6567
7102
  init_state();
6568
7103
  init_client();
7104
+ init_inbound_lifecycle();
6569
7105
  init_bridge();
6570
7106
  init_setup();
6571
7107
  init_notify();
@@ -6760,6 +7296,26 @@ function writeJson(res, status, body) {
6760
7296
  res.writeHead(status, { "content-type": "application/json" });
6761
7297
  res.end(JSON.stringify(body));
6762
7298
  }
7299
+ function pathnameOf(url) {
7300
+ if (!url)
7301
+ return "";
7302
+ const q = url.indexOf("?");
7303
+ return q === -1 ? url : url.slice(0, q);
7304
+ }
7305
+ function modelsList(ids) {
7306
+ const data = ids.map((id) => ({
7307
+ type: "model",
7308
+ id,
7309
+ display_name: id,
7310
+ created_at: "1970-01-01T00:00:00Z"
7311
+ }));
7312
+ return {
7313
+ data,
7314
+ has_more: false,
7315
+ first_id: ids[0] ?? null,
7316
+ last_id: ids[ids.length - 1] ?? null
7317
+ };
7318
+ }
6763
7319
  function buildUpstreamHeaders(req, upstream, stream) {
6764
7320
  const headers = {
6765
7321
  "content-type": "application/json",
@@ -6788,6 +7344,7 @@ function createRouterShim(opts) {
6788
7344
  let lastError;
6789
7345
  let upstreamReachable = true;
6790
7346
  const upstreamUrl = joinUrl(opts.upstream.baseUrl, "/v1/messages");
7347
+ const advertisedModels = new Set(opts.models ?? []);
6791
7348
  function emitUsage(u) {
6792
7349
  try {
6793
7350
  opts.onUsage?.(u);
@@ -6817,6 +7374,8 @@ function createRouterShim(opts) {
6817
7374
  const body = parsed;
6818
7375
  const wantsStream = body.stream === true;
6819
7376
  const model = typeof body.model === "string" ? body.model : void 0;
7377
+ if (model)
7378
+ advertisedModels.add(model);
6820
7379
  const sanitized = sanitizeRequest(body, opts.upstream);
6821
7380
  let upstreamRes;
6822
7381
  try {
@@ -6889,8 +7448,20 @@ function createRouterShim(opts) {
6889
7448
  emitUsage(usage2);
6890
7449
  writeJson(res, upstreamRes.status, upstreamBody);
6891
7450
  }
7451
+ async function handleCountTokens(req, res) {
7452
+ let text;
7453
+ try {
7454
+ text = await readBody(req);
7455
+ } catch {
7456
+ const e = anthropicError(400, "invalid_request_error", "malformed request body");
7457
+ writeJson(res, e.status, e.body);
7458
+ return;
7459
+ }
7460
+ writeJson(res, 200, { input_tokens: Math.max(1, Math.ceil(text.length / 4)) });
7461
+ }
6892
7462
  function handler(req, res) {
6893
- if (req.method === "POST" && req.url === "/v1/messages") {
7463
+ const path23 = pathnameOf(req.url);
7464
+ if (req.method === "POST" && path23 === "/v1/messages") {
6894
7465
  void handleMessages(req, res).catch((err) => {
6895
7466
  log(`router internal error: ${err instanceof Error ? err.message : String(err)}`);
6896
7467
  if (!res.headersSent) {
@@ -6902,7 +7473,18 @@ function createRouterShim(opts) {
6902
7473
  });
6903
7474
  return;
6904
7475
  }
6905
- if (req.method === "GET" && req.url === "/healthz") {
7476
+ if (req.method === "POST" && path23 === "/v1/messages/count_tokens") {
7477
+ void handleCountTokens(req, res).catch(() => {
7478
+ if (!res.headersSent)
7479
+ writeJson(res, 502, anthropicError(502, "api_error", "router internal error").body);
7480
+ });
7481
+ return;
7482
+ }
7483
+ if (req.method === "GET" && path23 === "/v1/models") {
7484
+ writeJson(res, 200, modelsList([...advertisedModels]));
7485
+ return;
7486
+ }
7487
+ if (req.method === "GET" && path23 === "/healthz") {
6906
7488
  writeJson(res, 200, { ready: server?.listening === true, upstreamReachable, lastError });
6907
7489
  return;
6908
7490
  }
@@ -7027,12 +7609,17 @@ function createRouterService(router, projects, deps = {}) {
7027
7609
  tokenByProject.set(p, t);
7028
7610
  projectTokens.set(t, p);
7029
7611
  }
7612
+ const models = [
7613
+ ...deps.models ?? [],
7614
+ ...Object.values(router.models ?? {}).map((m) => m.upstream)
7615
+ ];
7030
7616
  const ledger = createUsageLedger();
7031
7617
  const shim = createRouterShim({
7032
7618
  upstream,
7033
7619
  projectTokens,
7034
7620
  port: router.port ?? 0,
7035
7621
  log,
7622
+ models,
7036
7623
  onUsage: (u) => ledger.record(u),
7037
7624
  ...deps.fetch ? { fetch: deps.fetch } : {}
7038
7625
  });
@@ -7095,12 +7682,53 @@ var init_router = __esm({
7095
7682
  }
7096
7683
  });
7097
7684
 
7685
+ // packages/core/dist/captain-router.js
7686
+ async function prepareCaptainRoute(o) {
7687
+ assertBackendUsable({ backend: o.backend, agent: o.agentName, router: o.config.defaults.router });
7688
+ if (o.backend === "native") {
7689
+ return { backend: "native", model: o.model, permissionMode: o.configuredPermissionMode, env: {} };
7690
+ }
7691
+ if (!o.deps.routerCredentials) {
7692
+ throw new Error(`captain launch: backend '${o.backend}' requires router credentials from the daemon, but this launch path has no credentials provider`);
7693
+ }
7694
+ if (!o.deps.writeRouterSettings) {
7695
+ throw new Error(`captain launch: backend '${o.backend}' requires a per-spawn --settings writer to outrank defaults.claudeEnv, but this launch path has none \u2014 refusing to launch a routed captain that would silently bypass the router shim`);
7696
+ }
7697
+ const model = resolveRouterModel(o.model, o.agentName, o.config.defaults.router);
7698
+ const creds = await o.deps.routerCredentials({ project: o.project, backend: o.backend });
7699
+ const routerEnv = buildRouterEnv(creds, model);
7700
+ const shadowed = claudeEnvShadowsRouter(o.config.defaults.claudeEnv);
7701
+ if (shadowed.length > 0) {
7702
+ o.warn?.(`backend '${o.backend}' selected but defaults.claudeEnv sets ${shadowed.join(", ")} \u2014 these would shadow the router shim. Overriding via a per-spawn --settings env block (outranks ~/.claude/settings.json).`);
7703
+ }
7704
+ const settingsPath = o.deps.writeRouterSettings({
7705
+ stateRoot: o.stateRoot,
7706
+ project: o.project,
7707
+ taskId: "captain",
7708
+ env: mergeClaudeEnvRouterSettings(routerEnv, o.config.defaults.claudeEnv)
7709
+ });
7710
+ return {
7711
+ backend: o.backend,
7712
+ model,
7713
+ permissionMode: "default",
7714
+ env: { ...routerEnv, SQUADRANT_GATE: "on" },
7715
+ settingsPath
7716
+ };
7717
+ }
7718
+ var init_captain_router = __esm({
7719
+ "packages/core/dist/captain-router.js"() {
7720
+ init_dist();
7721
+ init_router_resolution();
7722
+ init_env();
7723
+ }
7724
+ });
7725
+
7098
7726
  // packages/core/dist/permission-gate.js
7099
7727
  import { createHash } from "crypto";
7100
7728
  import { mkdirSync as mkdirSync7, readFileSync as readFileSync11, renameSync as renameSync4, writeFileSync as writeFileSync10, unlinkSync as unlinkSync4 } from "fs";
7101
7729
  import { dirname as dirname5, join as join15 } from "path";
7102
7730
  function isGateSession(env) {
7103
- return Boolean(env.SQUADRANT_CREW_TASK_ID) || env[SIDE_SESSION_ENV] === "1";
7731
+ return Boolean(env.SQUADRANT_CREW_TASK_ID) || env[SIDE_SESSION_ENV] === "1" || env.SQUADRANT_ROLE === "captain";
7104
7732
  }
7105
7733
  function resolveGateMode(env, gate) {
7106
7734
  const fromEnv = env.SQUADRANT_GATE;
@@ -7296,7 +7924,15 @@ async function callClassifier(o) {
7296
7924
  return "ask";
7297
7925
  }
7298
7926
  const data = await res.json();
7927
+ if (data?.stop_reason === "max_tokens") {
7928
+ o.log?.("gate: classifier exhausted max_tokens before a verdict \u2014 asking");
7929
+ return "ask";
7930
+ }
7299
7931
  const text = Array.isArray(data?.content) ? data.content.filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join(" ") : "";
7932
+ if (!text.trim()) {
7933
+ o.log?.("gate: classifier returned no text verdict \u2014 asking");
7934
+ return "ask";
7935
+ }
7300
7936
  return parseClassifierVerdict(text);
7301
7937
  } catch (e) {
7302
7938
  o.log?.(`gate: classifier unavailable (${e.message}) \u2014 asking`);
@@ -7315,7 +7951,7 @@ ${toolPayload}`).digest("hex");
7315
7951
  }
7316
7952
  async function evaluatePermissionRequest(input) {
7317
7953
  if (!isGateSession(input.env)) {
7318
- return { decision: "yield", reason: "not a squadrant crew/side session" };
7954
+ return { decision: "yield", reason: "not a squadrant crew/side/captain session" };
7319
7955
  }
7320
7956
  const gate = input.config.defaults.gate;
7321
7957
  const mode = resolveGateMode(input.env, gate);
@@ -7401,7 +8037,7 @@ var init_permission_gate = __esm({
7401
8037
  DEFAULT_CACHE_TTL_MS = 10 * 60 * 1e3;
7402
8038
  DEFAULT_CACHE_MAX_ENTRIES = 500;
7403
8039
  DEFAULT_CLASSIFIER_TIMEOUT_MS = 5e3;
7404
- CLASSIFIER_MAX_TOKENS = 8;
8040
+ CLASSIFIER_MAX_TOKENS = 256;
7405
8041
  MAX_PAYLOAD_CHARS = 2e3;
7406
8042
  DEFAULT_GATE_DENY_RULES = [
7407
8043
  // Flag-order-agnostic: both recursive and force must be present, in any order,
@@ -7731,7 +8367,7 @@ var init_launch_workspace = __esm({
7731
8367
  });
7732
8368
 
7733
8369
  // packages/core/dist/side-session.js
7734
- import fs12 from "fs";
8370
+ import fs13 from "fs";
7735
8371
  function sideTitleFor(project, name) {
7736
8372
  return `\u{1F5D2} ${project}:${name}`;
7737
8373
  }
@@ -7833,7 +8469,7 @@ async function runSideClose(runtime, workspaceId, project, name, projPath, workt
7833
8469
  await runtime.closePane(pane);
7834
8470
  if (projPath) {
7835
8471
  const wtPath = worktreePath(projPath, worktreeDir, project, name);
7836
- if (fs12.existsSync(wtPath)) {
8472
+ if (fs13.existsSync(wtPath)) {
7837
8473
  try {
7838
8474
  removeWorktree(projPath, wtPath);
7839
8475
  } catch (e) {
@@ -8270,8 +8906,10 @@ __export(dist_exports, {
8270
8906
  AGENT_BINS: () => AGENT_BINS,
8271
8907
  ANTHROPIC_ONLY_REQUEST_FIELDS: () => ANTHROPIC_ONLY_REQUEST_FIELDS,
8272
8908
  BOT_COMMANDS: () => BOT_COMMANDS,
8909
+ CAPTAIN_RECEIVED_ACK: () => CAPTAIN_RECEIVED_ACK,
8273
8910
  CC_SOCKS_DIR: () => CC_SOCKS_DIR,
8274
8911
  CMUX_PRESERVE_CLAUDE_AUTH_ENV: () => CMUX_PRESERVE_CLAUDE_AUTH_ENV,
8912
+ CONFLICT_409_SELF_HEAL_MS: () => CONFLICT_409_SELF_HEAL_MS,
8275
8913
  CREW_GONE_MS: () => CREW_GONE_MS,
8276
8914
  CREW_STALE_MS: () => CREW_STALE_MS,
8277
8915
  CaptainDelivery: () => CaptainDelivery,
@@ -8291,6 +8929,7 @@ __export(dist_exports, {
8291
8929
  LivenessRegistry: () => LivenessRegistry,
8292
8930
  MONITOR_STALL_BUDGET_MS: () => MONITOR_STALL_BUDGET_MS,
8293
8931
  OPERATOR_INITIATED_COMMANDS: () => OPERATOR_INITIATED_COMMANDS,
8932
+ PANE_IDLE_AFTER_MS: () => PANE_IDLE_AFTER_MS,
8294
8933
  PROBE_QUIET_MS: () => PROBE_QUIET_MS,
8295
8934
  PROTOCOL_VERSION: () => PROTOCOL_VERSION,
8296
8935
  READ_ONLY_CREW_SUBCOMMANDS: () => READ_ONLY_CREW_SUBCOMMANDS,
@@ -8303,6 +8942,10 @@ __export(dist_exports, {
8303
8942
  TERMINAL_RECORD_KEEP_PER_PROJECT: () => TERMINAL_RECORD_KEEP_PER_PROJECT,
8304
8943
  TERMINAL_RECORD_TTL_MS: () => TERMINAL_RECORD_TTL_MS,
8305
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,
8306
8949
  WORK_ITEM_TTL_MS: () => WORK_ITEM_TTL_MS,
8307
8950
  WRITABLE_CONFIG_KEYS: () => WRITABLE_CONFIG_KEYS,
8308
8951
  _resetRestartInFlightForTest: () => _resetRestartInFlightForTest,
@@ -8324,14 +8967,19 @@ __export(dist_exports, {
8324
8967
  callClassifier: () => callClassifier,
8325
8968
  capAllowed: () => capAllowed,
8326
8969
  capOutput: () => capOutput,
8970
+ captainAliveStateFromHealth: () => captainAliveStateFromHealth,
8327
8971
  captainRecordPath: () => captainRecordPath,
8328
8972
  captainSocketPath: () => captainSocketPath,
8329
8973
  checkFact: () => checkFact,
8974
+ classifyCaptainPane: () => classifyCaptainPane,
8330
8975
  classifyHealth: () => classifyHealth,
8976
+ claudeEnvShadowsRouter: () => claudeEnvShadowsRouter,
8977
+ clearPending: () => clearPending,
8331
8978
  closeWorkItem: () => closeWorkItem,
8332
8979
  computeTemplateHash: () => computeTemplateHash,
8333
8980
  consumeExitMarker: () => consumeExitMarker,
8334
8981
  createAttach: () => createAttach,
8982
+ createCaptainPaneReader: () => createCaptainPaneReader,
8335
8983
  createCrewPaneReader: () => createCrewPaneReader,
8336
8984
  createDaemon: () => createDaemon,
8337
8985
  createDecoder: () => createDecoder,
@@ -8340,6 +8988,7 @@ __export(dist_exports, {
8340
8988
  createDirectSurfaceLivenessProbe: () => createDirectSurfaceLivenessProbe,
8341
8989
  createEnsureCaptainAlive: () => createEnsureCaptainAlive,
8342
8990
  createEventsSource: () => createEventsSource,
8991
+ createInboundLifecycle: () => createInboundLifecycle,
8343
8992
  createInteractiveProbe: () => createInteractiveProbe,
8344
8993
  createIsCaptainAlive: () => createIsCaptainAlive,
8345
8994
  createLaunch: () => createLaunch,
@@ -8395,12 +9044,16 @@ __export(dist_exports, {
8395
9044
  formatInbound: () => formatInbound,
8396
9045
  formatInboundReceipt: () => formatInboundReceipt,
8397
9046
  formatLifecycle: () => formatLifecycle,
9047
+ formatMediaReceipt: () => formatMediaReceipt,
8398
9048
  formatPermissionDecision: () => formatPermissionDecision,
8399
9049
  formatUsageLine: () => formatUsageLine,
9050
+ formatWatchdogWarning: () => formatWatchdogWarning,
8400
9051
  freshTrace: () => freshTrace,
8401
9052
  gateCacheKey: () => gateCacheKey,
8402
9053
  getDaemonPid: () => getDaemonPid,
9054
+ hasModalOptionList: () => hasModalOptionList,
8403
9055
  healCmdFor: () => healCmdFor,
9056
+ inboundBody: () => inboundBody,
8404
9057
  isAuthorized: () => isAuthorized,
8405
9058
  isBareSpawn: () => isBareSpawn,
8406
9059
  isCaptainAlive: () => isCaptainAlive,
@@ -8422,17 +9075,23 @@ __export(dist_exports, {
8422
9075
  kickstartArgv: () => kickstartArgv,
8423
9076
  launchOneWorkspace: () => launchOneWorkspace,
8424
9077
  listSessions: () => listSessions,
9078
+ loadPending: () => loadPending,
8425
9079
  loadSessions: () => loadSessions,
8426
9080
  loadState: () => loadState,
8427
9081
  mailboxStats: () => mailboxStats,
8428
9082
  makeGate: () => makeGate,
9083
+ markPendingWarned: () => markPendingWarned,
8429
9084
  maskToken: () => maskToken,
8430
9085
  matchTier1Deny: () => matchTier1Deny,
9086
+ mediaKind: () => mediaKind,
9087
+ mediaMarker: () => mediaMarker,
9088
+ mergeClaudeEnvRouterSettings: () => mergeClaudeEnvRouterSettings,
8431
9089
  mintToken: () => mintToken,
8432
9090
  nameFromTitle: () => nameFromTitle,
8433
9091
  newestSessionInDirectory: () => newestSessionInDirectory,
8434
9092
  nextAutoName: () => nextAutoName,
8435
9093
  niceCrewCommand: () => niceCrewCommand,
9094
+ notePaneScreen: () => notePaneScreen,
8436
9095
  notifyToggle: () => notifyToggle,
8437
9096
  parseBearer: () => parseBearer,
8438
9097
  parseClassifierVerdict: () => parseClassifierVerdict,
@@ -8441,12 +9100,15 @@ __export(dist_exports, {
8441
9100
  parseNotifyPref: () => parseNotifyPref,
8442
9101
  parseProgramArgs: () => parseProgramArgs,
8443
9102
  plistPath: () => plistPath,
9103
+ pollBackoffMs: () => pollBackoffMs,
8444
9104
  pollNewestSessionInDirectory: () => pollNewestSessionInDirectory,
9105
+ prepareCaptainRoute: () => prepareCaptainRoute,
8445
9106
  printForeignInstallError: () => printForeignInstallError,
8446
9107
  printVersionUpgradeNotice: () => printVersionUpgradeNotice,
8447
9108
  probeCaptainChannel: () => probeCaptainChannel,
8448
9109
  programArgsBlock: () => programArgsBlock,
8449
9110
  projectHealth: () => projectHealth,
9111
+ pruneTopics: () => pruneTopics,
8450
9112
  purgeExpiredWorkItems: () => purgeExpiredWorkItems,
8451
9113
  readCaptainAddress: () => readCaptainAddress,
8452
9114
  readCursor: () => readCursor,
@@ -8513,9 +9175,11 @@ __export(dist_exports, {
8513
9175
  saveSessions: () => saveSessions,
8514
9176
  saveState: () => saveState,
8515
9177
  screenHasSplashMarker: () => screenHasSplashMarker,
9178
+ screenHash: () => screenHash,
8516
9179
  sendRequest: () => sendRequest,
8517
9180
  setLastUserId: () => setLastUserId,
8518
9181
  setNotify: () => setNotify,
9182
+ setPending: () => setPending,
8519
9183
  setTopic: () => setTopic,
8520
9184
  shellQuote: () => shellQuote,
8521
9185
  shouldBuildRouterService: () => shouldBuildRouterService,
@@ -8528,6 +9192,7 @@ __export(dist_exports, {
8528
9192
  startServer: () => startServer,
8529
9193
  stripBotMention: () => stripBotMention,
8530
9194
  surfaceVerdict: () => surfaceVerdict,
9195
+ telegramPollLockPath: () => telegramPollLockPath,
8531
9196
  timeoutGate: () => timeoutGate,
8532
9197
  titleFor: () => titleFor,
8533
9198
  toControlEvent: () => toControlEvent,
@@ -8574,6 +9239,7 @@ var init_dist2 = __esm({
8574
9239
  init_router();
8575
9240
  init_crew_routing();
8576
9241
  init_router_resolution();
9242
+ init_captain_router();
8577
9243
  init_permission_gate();
8578
9244
  init_restart_daemon();
8579
9245
  init_group_dispatch();
@@ -8620,7 +9286,7 @@ import { readdirSync as readdirSync4, readFileSync as readFileSync12 } from "fs"
8620
9286
  import { join as join18 } from "path";
8621
9287
 
8622
9288
  // packages/agents/dist/claude/registry.js
8623
- import fs13 from "fs";
9289
+ import fs14 from "fs";
8624
9290
  import { join as join17 } from "path";
8625
9291
  import { homedir as homedir7 } from "os";
8626
9292
  var CLAUDE_SESSIONS_DIR = join17(homedir7(), ".claude", "sessions");
@@ -8665,11 +9331,11 @@ function toLifecycleSnapshot(entry, taskId, alive, now) {
8665
9331
  function readClaudeStatusBySocketPath(socketPath) {
8666
9332
  let files;
8667
9333
  try {
8668
- files = fs13.readdirSync(CLAUDE_SESSIONS_DIR);
9334
+ files = fs14.readdirSync(CLAUDE_SESSIONS_DIR);
8669
9335
  } catch {
8670
9336
  return void 0;
8671
9337
  }
8672
- 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"));
8673
9339
  const entry = entries.find((e) => e.messagingSocketPath === socketPath);
8674
9340
  if (!entry)
8675
9341
  return void 0;
@@ -8692,7 +9358,7 @@ import { homedir as homedir8 } from "os";
8692
9358
  var SQUADRANT_STATE_DIR = join19(homedir8(), ".config", "squadrant", "state");
8693
9359
 
8694
9360
  // packages/agents/dist/drivers/launch-cmd.js
8695
- import fs14 from "fs";
9361
+ import fs15 from "fs";
8696
9362
  import path13 from "path";
8697
9363
 
8698
9364
  // packages/agents/dist/projection/cursor.js
@@ -8716,7 +9382,7 @@ import path17 from "path";
8716
9382
  import os9 from "os";
8717
9383
 
8718
9384
  // packages/agents/dist/projection/opencode-skills.js
8719
- import fs15 from "fs";
9385
+ import fs16 from "fs";
8720
9386
  import os10 from "os";
8721
9387
  import path18 from "path";
8722
9388
  var OPENCODE_SKILL_MARKER = ".squadrant-managed";
@@ -8738,62 +9404,62 @@ function parseSkillName(raw) {
8738
9404
  }
8739
9405
  function listSourceSkills(sourceSkillsDir) {
8740
9406
  const out = /* @__PURE__ */ new Map();
8741
- if (!fs15.existsSync(sourceSkillsDir))
9407
+ if (!fs16.existsSync(sourceSkillsDir))
8742
9408
  return out;
8743
- for (const entry of fs15.readdirSync(sourceSkillsDir, { withFileTypes: true })) {
9409
+ for (const entry of fs16.readdirSync(sourceSkillsDir, { withFileTypes: true })) {
8744
9410
  if (!entry.isDirectory())
8745
9411
  continue;
8746
9412
  const skillPath = path18.join(sourceSkillsDir, entry.name, "SKILL.md");
8747
- if (!fs15.existsSync(skillPath))
9413
+ if (!fs16.existsSync(skillPath))
8748
9414
  continue;
8749
- const name = parseSkillName(fs15.readFileSync(skillPath, "utf-8"));
9415
+ const name = parseSkillName(fs16.readFileSync(skillPath, "utf-8"));
8750
9416
  if (name && SAFE_NAME.test(name))
8751
9417
  out.set(name, skillPath);
8752
9418
  }
8753
9419
  return out;
8754
9420
  }
8755
9421
  function hasMarker(dir) {
8756
- return fs15.existsSync(path18.join(dir, OPENCODE_SKILL_MARKER));
9422
+ return fs16.existsSync(path18.join(dir, OPENCODE_SKILL_MARKER));
8757
9423
  }
8758
9424
  function syncOpencodeSkills(opts) {
8759
9425
  const result = { written: [], unchanged: [], removed: [], skipped: [] };
8760
9426
  const wanted = listSourceSkills(opts.sourceSkillsDir);
8761
- if (wanted.size === 0 && !fs15.existsSync(opts.sourceSkillsDir))
9427
+ if (wanted.size === 0 && !fs16.existsSync(opts.sourceSkillsDir))
8762
9428
  return result;
8763
9429
  if (!opts.dryRun)
8764
- fs15.mkdirSync(opts.skillsRoot, { recursive: true });
9430
+ fs16.mkdirSync(opts.skillsRoot, { recursive: true });
8765
9431
  for (const [name, srcPath] of wanted) {
8766
9432
  const targetDir = path18.join(opts.skillsRoot, name);
8767
9433
  const targetSkill = path18.join(targetDir, "SKILL.md");
8768
9434
  const managed = hasMarker(targetDir);
8769
- if (fs15.existsSync(targetSkill) && !managed) {
9435
+ if (fs16.existsSync(targetSkill) && !managed) {
8770
9436
  result.skipped.push({ name, reason: "foreign-collision" });
8771
9437
  continue;
8772
9438
  }
8773
- const current = fs15.existsSync(targetSkill) ? fs15.readFileSync(targetSkill) : null;
8774
- 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))) {
8775
9441
  if (!managed && !opts.dryRun) {
8776
- fs15.writeFileSync(path18.join(targetDir, OPENCODE_SKILL_MARKER), MARKER_BODY);
9442
+ fs16.writeFileSync(path18.join(targetDir, OPENCODE_SKILL_MARKER), MARKER_BODY);
8777
9443
  }
8778
9444
  result.unchanged.push(name);
8779
9445
  continue;
8780
9446
  }
8781
9447
  if (!opts.dryRun) {
8782
- fs15.mkdirSync(targetDir, { recursive: true });
8783
- fs15.copyFileSync(srcPath, targetSkill);
8784
- 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);
8785
9451
  }
8786
9452
  result.written.push(name);
8787
9453
  }
8788
- if (fs15.existsSync(opts.skillsRoot)) {
8789
- 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 })) {
8790
9456
  if (!entry.isDirectory() || wanted.has(entry.name))
8791
9457
  continue;
8792
9458
  const dir = path18.join(opts.skillsRoot, entry.name);
8793
9459
  if (!hasMarker(dir))
8794
9460
  continue;
8795
9461
  if (!opts.dryRun)
8796
- fs15.rmSync(dir, { recursive: true, force: true });
9462
+ fs16.rmSync(dir, { recursive: true, force: true });
8797
9463
  result.removed.push(entry.name);
8798
9464
  }
8799
9465
  }
@@ -9967,6 +10633,9 @@ var ClaudePeerChannel = class {
9967
10633
  return { status: "accepted", via: this.name, confirmed: true };
9968
10634
  }
9969
10635
  }
10636
+ if (statusBefore === "busy" || statusBefore === "shell") {
10637
+ return { status: "queued", via: this.name };
10638
+ }
9970
10639
  if (statusBefore === "idle") {
9971
10640
  await this.deps.sleep(window);
9972
10641
  const afterInfo = this.deps.statusFor(taskId);
@@ -10105,7 +10774,7 @@ var ClaudeReceiptListener = class {
10105
10774
  };
10106
10775
 
10107
10776
  // packages/agents/dist/claude/api-key-approval.js
10108
- import fs16 from "fs";
10777
+ import fs17 from "fs";
10109
10778
  import os12 from "os";
10110
10779
  import path20 from "path";
10111
10780
 
@@ -10347,11 +11016,11 @@ function parseDraftFromScreen(screen) {
10347
11016
  if (!screen)
10348
11017
  return null;
10349
11018
  const lines = screen.split(/\r?\n/);
10350
- const HR_RE = /^\s*─{10,}\s*$/;
11019
+ const HR_RE2 = /^\s*─{10,}\s*$/;
10351
11020
  let bottomHR = -1;
10352
11021
  let topHR = -1;
10353
11022
  for (let i = lines.length - 1; i >= 0; i--) {
10354
- if (HR_RE.test(lines[i])) {
11023
+ if (HR_RE2.test(lines[i])) {
10355
11024
  if (bottomHR === -1) {
10356
11025
  bottomHR = i;
10357
11026
  } else {
@@ -10389,11 +11058,11 @@ function hasCCInputBox(screen) {
10389
11058
  if (!screen)
10390
11059
  return false;
10391
11060
  const lines = screen.split(/\r?\n/);
10392
- const HR_RE = /^\s*─{10,}\s*$/;
11061
+ const HR_RE2 = /^\s*─{10,}\s*$/;
10393
11062
  let bottomHR = -1;
10394
11063
  let topHR = -1;
10395
11064
  for (let i = lines.length - 1; i >= 0; i--) {
10396
- if (HR_RE.test(lines[i])) {
11065
+ if (HR_RE2.test(lines[i])) {
10397
11066
  if (bottomHR === -1)
10398
11067
  bottomHR = i;
10399
11068
  else {
@@ -10406,15 +11075,15 @@ function hasCCInputBox(screen) {
10406
11075
  return false;
10407
11076
  return lines.slice(topHR + 1, bottomHR).some((l) => /[>❯]/.test(l));
10408
11077
  }
10409
- function hasModalOptionList(screen) {
11078
+ function hasModalOptionList2(screen) {
10410
11079
  if (!screen)
10411
11080
  return false;
10412
11081
  const lines = screen.split(/\r?\n/);
10413
- const HR_RE = /^\s*─{10,}\s*$/;
11082
+ const HR_RE2 = /^\s*─{10,}\s*$/;
10414
11083
  let bottomHR = -1;
10415
11084
  let topHR = -1;
10416
11085
  for (let i = lines.length - 1; i >= 0; i--) {
10417
- if (HR_RE.test(lines[i])) {
11086
+ if (HR_RE2.test(lines[i])) {
10418
11087
  if (bottomHR === -1)
10419
11088
  bottomHR = i;
10420
11089
  else {
@@ -10431,11 +11100,11 @@ function readInputBoxRaw(screen, opts) {
10431
11100
  if (!screen)
10432
11101
  return null;
10433
11102
  const lines = screen.split(/\r?\n/);
10434
- const HR_RE = /^\s*─{10,}\s*$/;
11103
+ const HR_RE2 = /^\s*─{10,}\s*$/;
10435
11104
  let bottomHR = -1;
10436
11105
  let topHR = -1;
10437
11106
  for (let i = lines.length - 1; i >= 0; i--) {
10438
- if (HR_RE.test(lines[i])) {
11107
+ if (HR_RE2.test(lines[i])) {
10439
11108
  if (bottomHR === -1)
10440
11109
  bottomHR = i;
10441
11110
  else {
@@ -10465,6 +11134,34 @@ function classifyStartupSurface(screen) {
10465
11134
  return "idle";
10466
11135
  return "loading";
10467
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
+ }
10468
11165
  function sendDebugEnabled() {
10469
11166
  return !!process.env.SQUADRANT_DEBUG_SEND;
10470
11167
  }
@@ -10680,10 +11377,10 @@ function createCmuxDriver() {
10680
11377
  `);
10681
11378
  throw new DeferDelivery(null, "probe-failed");
10682
11379
  }
10683
- const draft = parseDraftFromScreen(screen);
11380
+ const draft = opts?.agent === "opencode" ? parseOpencodeDraftFromScreen(screen) : parseDraftFromScreen(screen);
10684
11381
  if (draft === null)
10685
11382
  throw new DeferDelivery(null, "no-box");
10686
- if (hasModalOptionList(screen))
11383
+ if (hasModalOptionList2(screen))
10687
11384
  throw new DeferDelivery(null, "modal");
10688
11385
  if (draft === "") {
10689
11386
  await deliver();
@@ -10877,7 +11574,7 @@ var NotifierRegistry = class {
10877
11574
  };
10878
11575
 
10879
11576
  // packages/workspaces/dist/workspaces/obsidian.js
10880
- import fs17 from "fs/promises";
11577
+ import fs18 from "fs/promises";
10881
11578
  import { existsSync as existsSync11 } from "fs";
10882
11579
  import path21 from "path";
10883
11580
 
@@ -11673,7 +12370,7 @@ async function settleInputBox(runtime, pane) {
11673
12370
  }
11674
12371
  async function confirmedSendToPane(runtime, pane, message) {
11675
12372
  const preSendScreen = await runtime.readPaneScreen(pane) ?? "";
11676
- if (hasModalOptionList(preSendScreen)) {
12373
+ if (hasModalOptionList2(preSendScreen)) {
11677
12374
  return { delivered: false, blockedByModal: true };
11678
12375
  }
11679
12376
  await runtime.pasteToPane(pane, message);
@@ -11776,7 +12473,7 @@ async function maybeBroadcastDaemonRestart(opts) {
11776
12473
 
11777
12474
  // packages/cli/src/lib/captain-channel-factory.ts
11778
12475
  import { createServer as createServer4, connect as netConnect } from "net";
11779
- import fs18 from "fs";
12476
+ import fs19 from "fs";
11780
12477
  import { join as join26 } from "path";
11781
12478
  import { randomUUID as randomUUID5 } from "crypto";
11782
12479
  import chalk2 from "chalk";
@@ -11785,15 +12482,15 @@ var shared;
11785
12482
  var registryEntryPath = () => join26(CLAUDE_SESSIONS_DIR, `${process.pid}.json`);
11786
12483
  function unregisterSenderIdentity() {
11787
12484
  try {
11788
- fs18.unlinkSync(registryEntryPath());
12485
+ fs19.unlinkSync(registryEntryPath());
11789
12486
  } catch {
11790
12487
  }
11791
12488
  }
11792
12489
  function registerSenderIdentity(socketPath) {
11793
12490
  try {
11794
12491
  unregisterSenderIdentity();
11795
- fs18.mkdirSync(CLAUDE_SESSIONS_DIR, { recursive: true });
11796
- fs18.writeFileSync(
12492
+ fs19.mkdirSync(CLAUDE_SESSIONS_DIR, { recursive: true });
12493
+ fs19.writeFileSync(
11797
12494
  registryEntryPath(),
11798
12495
  JSON.stringify({
11799
12496
  pid: process.pid,
@@ -11819,7 +12516,7 @@ async function sharedReceiptListener() {
11819
12516
  // must never be the reason we refuse to start.
11820
12517
  unlinkStale: (p) => {
11821
12518
  try {
11822
- fs18.unlinkSync(p);
12519
+ fs19.unlinkSync(p);
11823
12520
  } catch {
11824
12521
  }
11825
12522
  },
@@ -11916,6 +12613,33 @@ function buildTelegramBridge(cfg, stateRoot, log, deliverInbound, usageFor) {
11916
12613
  });
11917
12614
  const runCommand = createRunCommand(CLI_BIN);
11918
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
+ });
11919
12643
  return createTelegramBridge({
11920
12644
  cfg,
11921
12645
  stateRoot,
@@ -11927,7 +12651,20 @@ function buildTelegramBridge(cfg, stateRoot, log, deliverInbound, usageFor) {
11927
12651
  runCommand,
11928
12652
  sendReply,
11929
12653
  deliverInbound,
11930
- 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
+ }
11931
12668
  });
11932
12669
  }
11933
12670
  function buildNotifyFault(log) {