@tritard/waterbrother 0.16.76 → 0.16.78

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.16.76",
3
+ "version": "0.16.78",
4
4
  "description": "Waterbrother: bring-your-own-model coding CLI with local tools, sessions, operator modes, and approval controls",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -3023,6 +3023,15 @@ function createTelegramBridgeHostRecord({ sessionId, cwd, label = "", surface =
3023
3023
  };
3024
3024
  }
3025
3025
 
3026
+ function upsertTelegramBridgeHostEntry(hosts = [], nextHost = {}) {
3027
+ const list = Array.isArray(hosts) ? [...hosts] : [];
3028
+ const pid = Number(nextHost?.pid || 0);
3029
+ if (!pid) return list;
3030
+ const filtered = list.filter((host) => Number(host?.pid || 0) !== pid);
3031
+ filtered.unshift(nextHost);
3032
+ return filtered.slice(0, 20);
3033
+ }
3034
+
3026
3035
  async function syncSharedTelegramBridgeAgent({ cwd, host = {}, actor = {} }) {
3027
3036
  try {
3028
3037
  const project = await loadSharedProject(cwd);
@@ -3052,6 +3061,7 @@ async function syncSharedTelegramBridgeAgent({ cwd, host = {}, actor = {} }) {
3052
3061
  async function registerTelegramBridgeHost({ sessionId, cwd, label = "", surface = "live-tui", ownerId = "", ownerName = "", provider = "", model = "", runtimeProfile = "", actor = null }) {
3053
3062
  const bridge = await loadGatewayBridge(TELEGRAM_BRIDGE_SERVICE);
3054
3063
  bridge.activeHost = createTelegramBridgeHostRecord({ sessionId, cwd, label, surface, ownerId, ownerName, provider, model, runtimeProfile });
3064
+ bridge.hosts = upsertTelegramBridgeHostEntry(bridge.hosts, bridge.activeHost);
3055
3065
  bridge.deliveredReplies = Array.isArray(bridge.deliveredReplies) ? bridge.deliveredReplies.slice(-50) : [];
3056
3066
  bridge.pendingRequests = Array.isArray(bridge.pendingRequests) ? bridge.pendingRequests : [];
3057
3067
  await saveGatewayBridge(TELEGRAM_BRIDGE_SERVICE, bridge);
@@ -3079,6 +3089,7 @@ async function touchTelegramBridgeHost({ sessionId, cwd, label = "", surface = "
3079
3089
  updatedAt: new Date().toISOString()
3080
3090
  };
3081
3091
  }
3092
+ bridge.hosts = upsertTelegramBridgeHostEntry(bridge.hosts, bridge.activeHost);
3082
3093
  await saveGatewayBridge(TELEGRAM_BRIDGE_SERVICE, bridge);
3083
3094
  await syncSharedTelegramBridgeAgent({ cwd, host: bridge.activeHost, actor: actor || { id: ownerId, name: ownerName } });
3084
3095
  return bridge.activeHost;
@@ -3088,8 +3099,9 @@ async function clearTelegramBridgeHost() {
3088
3099
  const bridge = await loadGatewayBridge(TELEGRAM_BRIDGE_SERVICE);
3089
3100
  if (Number(bridge.activeHost?.pid || 0) === process.pid) {
3090
3101
  bridge.activeHost = { pid: 0, sessionId: "", cwd: "", label: "", surface: "", ownerId: "", ownerName: "", provider: "", model: "", runtimeProfile: "", startedAt: "", updatedAt: "" };
3091
- await saveGatewayBridge(TELEGRAM_BRIDGE_SERVICE, bridge);
3092
3102
  }
3103
+ bridge.hosts = Array.isArray(bridge.hosts) ? bridge.hosts.filter((host) => Number(host?.pid || 0) !== process.pid) : [];
3104
+ await saveGatewayBridge(TELEGRAM_BRIDGE_SERVICE, bridge);
3093
3105
  }
3094
3106
 
3095
3107
  async function dequeueTelegramBridgeRequest({ cwd }) {
@@ -3111,6 +3123,7 @@ async function dequeueTelegramBridgeRequest({ cwd }) {
3111
3123
  ...activeHost,
3112
3124
  updatedAt: new Date().toISOString()
3113
3125
  };
3126
+ bridge.hosts = upsertTelegramBridgeHostEntry(bridge.hosts, bridge.activeHost);
3114
3127
  await saveGatewayBridge(TELEGRAM_BRIDGE_SERVICE, bridge);
3115
3128
  return next;
3116
3129
  }
@@ -3135,6 +3148,9 @@ async function deliverTelegramBridgeReply(request, { sessionId, content = "", er
3135
3148
  sessionId: String(sessionId || "").trim() || String(bridge.activeHost.sessionId || "")
3136
3149
  };
3137
3150
  }
3151
+ if (Number(bridge.activeHost?.pid || 0) === process.pid) {
3152
+ bridge.hosts = upsertTelegramBridgeHostEntry(bridge.hosts, bridge.activeHost);
3153
+ }
3138
3154
  await saveGatewayBridge(TELEGRAM_BRIDGE_SERVICE, bridge);
3139
3155
  }
3140
3156
 
@@ -75,29 +75,39 @@ function normalizeBridgeReply(parsed = {}) {
75
75
  }
76
76
 
77
77
  function normalizeGatewayBridge(parsed = {}) {
78
+ const normalizeHost = (host = {}) => ({
79
+ pid: Number.isFinite(Number(host.pid)) ? Math.floor(Number(host.pid)) : 0,
80
+ sessionId: String(host.sessionId || "").trim(),
81
+ cwd: String(host.cwd || "").trim(),
82
+ label: String(host.label || "").trim(),
83
+ surface: String(host.surface || "").trim(),
84
+ ownerId: String(host.ownerId || "").trim(),
85
+ ownerName: String(host.ownerName || "").trim(),
86
+ provider: String(host.provider || "").trim(),
87
+ model: String(host.model || "").trim(),
88
+ runtimeProfile: String(host.runtimeProfile || "").trim(),
89
+ startedAt: String(host.startedAt || "").trim(),
90
+ updatedAt: String(host.updatedAt || "").trim()
91
+ });
78
92
  const pendingRequests = Array.isArray(parsed?.pendingRequests)
79
93
  ? parsed.pendingRequests.map(normalizeBridgeRequest).filter((item) => item.id)
80
94
  : [];
81
95
  const deliveredReplies = Array.isArray(parsed?.deliveredReplies)
82
96
  ? parsed.deliveredReplies.map(normalizeBridgeReply).filter((item) => item.requestId)
83
97
  : [];
98
+ const hosts = Array.isArray(parsed?.hosts)
99
+ ? parsed.hosts.map(normalizeHost).filter((host) => Number(host.pid || 0) > 0)
100
+ : [];
101
+ const activeHost = parsed?.activeHost && typeof parsed.activeHost === "object"
102
+ ? normalizeHost(parsed.activeHost)
103
+ : normalizeHost();
104
+ const mergedHosts = [...hosts];
105
+ if (Number(activeHost.pid || 0) > 0 && !mergedHosts.some((host) => Number(host.pid || 0) === Number(activeHost.pid || 0))) {
106
+ mergedHosts.unshift(activeHost);
107
+ }
84
108
  return {
85
- activeHost: parsed?.activeHost && typeof parsed.activeHost === "object"
86
- ? {
87
- pid: Number.isFinite(Number(parsed.activeHost.pid)) ? Math.floor(Number(parsed.activeHost.pid)) : 0,
88
- sessionId: String(parsed.activeHost.sessionId || "").trim(),
89
- cwd: String(parsed.activeHost.cwd || "").trim(),
90
- label: String(parsed.activeHost.label || "").trim(),
91
- surface: String(parsed.activeHost.surface || "").trim(),
92
- ownerId: String(parsed.activeHost.ownerId || "").trim(),
93
- ownerName: String(parsed.activeHost.ownerName || "").trim(),
94
- provider: String(parsed.activeHost.provider || "").trim(),
95
- model: String(parsed.activeHost.model || "").trim(),
96
- runtimeProfile: String(parsed.activeHost.runtimeProfile || "").trim(),
97
- startedAt: String(parsed.activeHost.startedAt || "").trim(),
98
- updatedAt: String(parsed.activeHost.updatedAt || "").trim()
99
- }
100
- : { pid: 0, sessionId: "", cwd: "", label: "", surface: "", ownerId: "", ownerName: "", provider: "", model: "", runtimeProfile: "", startedAt: "", updatedAt: "" },
109
+ activeHost,
110
+ hosts: mergedHosts.slice(-20),
101
111
  pendingRequests,
102
112
  deliveredReplies
103
113
  };
package/src/gateway.js CHANGED
@@ -419,6 +419,60 @@ function chooseExecutorAgent(project, fallbackExecutor = {}) {
419
419
  return null;
420
420
  }
421
421
 
422
+ function getAgentRuntimeKey(agent = {}) {
423
+ const provider = String(agent?.provider || "").trim();
424
+ const model = String(agent?.model || "").trim();
425
+ return provider && model ? `${provider}/${model}` : "";
426
+ }
427
+
428
+ function listRuntimeGroups(project, fallbackExecutor = {}) {
429
+ const groups = new Map();
430
+ for (const agent of listProjectAgents(project)) {
431
+ const runtime = getAgentRuntimeKey(agent);
432
+ if (!runtime) continue;
433
+ if (!groups.has(runtime)) {
434
+ groups.set(runtime, []);
435
+ }
436
+ groups.get(runtime).push(agent);
437
+ }
438
+ if (!groups.size && fallbackExecutor?.provider && fallbackExecutor?.model) {
439
+ const runtime = `${fallbackExecutor.provider}/${fallbackExecutor.model}`;
440
+ groups.set(runtime, [{
441
+ id: "",
442
+ ownerId: String(project?.activeOperator?.id || "").trim(),
443
+ ownerName: String(project?.activeOperator?.name || project?.activeOperator?.id || "").trim(),
444
+ label: String(project?.activeOperator?.name || project?.activeOperator?.id || "active terminal").trim(),
445
+ surface: String(fallbackExecutor.surface || "").trim(),
446
+ role: "executor",
447
+ provider: String(fallbackExecutor.provider || "").trim(),
448
+ model: String(fallbackExecutor.model || "").trim(),
449
+ runtimeProfile: String(fallbackExecutor.runtimeProfile || "").trim(),
450
+ sessionId: String(fallbackExecutor.hostSessionId || "").trim()
451
+ }]);
452
+ }
453
+ return [...groups.entries()].map(([runtime, agents]) => ({ runtime, agents }));
454
+ }
455
+
456
+ function summarizeRuntimeConflict(project, fallbackExecutor = {}) {
457
+ const groups = listRuntimeGroups(project, fallbackExecutor).sort((left, right) => right.agents.length - left.agents.length);
458
+ const uniqueRuntimes = groups.filter((group) => String(group.runtime || "").trim());
459
+ if (!uniqueRuntimes.length) {
460
+ return { hasConflict: false, runtimeCount: 0, groups: [] };
461
+ }
462
+ return {
463
+ hasConflict: uniqueRuntimes.length > 1,
464
+ runtimeCount: uniqueRuntimes.length,
465
+ groups: uniqueRuntimes
466
+ };
467
+ }
468
+
469
+ function formatBridgeHostLabel(host = {}) {
470
+ const owner = String(host?.ownerName || host?.ownerId || "").trim();
471
+ const label = String(host?.label || "").trim();
472
+ const runtime = host?.provider && host?.model ? `${host.provider}/${host.model}` : "";
473
+ return [owner || label, label && label !== owner ? `(${label})` : "", runtime ? `[${runtime}]` : ""].filter(Boolean).join(" ").trim();
474
+ }
475
+
422
476
  function getLatestBlockingReviewPolicy(project) {
423
477
  const events = Array.isArray(project?.recentEvents) ? [...project.recentEvents] : [];
424
478
  const ordered = events
@@ -486,6 +540,7 @@ function formatTelegramSummaryMarkup({ cwd, project, chatId = "", title = "", ex
486
540
  if (project?.enabled) {
487
541
  const participants = listProjectParticipants(project);
488
542
  const agents = listProjectAgents(project);
543
+ const runtimeConflict = summarizeRuntimeConflict(project, executor);
489
544
  lines.push(`room mode: <code>${escapeTelegramHtml(project.roomMode || "chat")}</code>`);
490
545
  lines.push(`bound chat: <code>${escapeTelegramHtml(roomLabel)}</code>`);
491
546
  lines.push(`active operator: <code>${escapeTelegramHtml(activeOperator)}</code>`);
@@ -493,6 +548,9 @@ function formatTelegramSummaryMarkup({ cwd, project, chatId = "", title = "", ex
493
548
  if (agents.length) {
494
549
  lines.push(`agents: <code>${escapeTelegramHtml(String(agents.length))}</code>`);
495
550
  }
551
+ if (runtimeConflict.runtimeCount) {
552
+ lines.push(`runtime split: <code>${escapeTelegramHtml(runtimeConflict.hasConflict ? `yes (${runtimeConflict.runtimeCount})` : "no")}</code>`);
553
+ }
496
554
  if (executor?.surface) lines.push(`executor: <code>${escapeTelegramHtml(executor.surface)}</code>`);
497
555
  if (executor?.provider && executor?.model) {
498
556
  lines.push(`runtime: <code>${escapeTelegramHtml(`${executor.provider}/${executor.model}`)}</code>`);
@@ -679,9 +737,19 @@ function parseTelegramStateIntent(text = "") {
679
737
  if (/\bwho are the bots\b/.test(lower) || /\bwho are the agents\b/.test(lower) || /\bwhat bots are here\b/.test(lower) || /\bwhat agents are here\b/.test(lower)) {
680
738
  return { action: "agent-list" };
681
739
  }
740
+ if (/\bwhich terminals are live\b/.test(lower) || /\bwhich bots are live\b/.test(lower) || /\bwho is live\b/.test(lower) || /\bwhat terminals are live\b/.test(lower)) {
741
+ return { action: "live-hosts" };
742
+ }
743
+ if (/\bmodel conflict\b/.test(lower) || /\bruntime conflict\b/.test(lower) || /\bmodel split\b/.test(lower) || /\bruntime split\b/.test(lower) || /\bcompare models\b/.test(lower) || /\bcompare bots\b/.test(lower)) {
744
+ return { action: "model-conflict" };
745
+ }
682
746
  if (/\bwhich (?:bot|agent|terminal) should take this\b/.test(lower) || /\bwho should take this\b/.test(lower) || /\bwho should handle this\b/.test(lower)) {
683
747
  return { action: "executor-recommendation" };
684
748
  }
749
+ const agentPerspectiveMatch = value.match(/^(?:what does|how does)\s+(.+?)['’]s\s+model\s+think\??$/i);
750
+ if (agentPerspectiveMatch?.[1]) {
751
+ return { action: "agent-perspective", target: agentPerspectiveMatch[1].trim() };
752
+ }
685
753
  const agentModelMatch = value.match(/^(?:what model is|which model is|what runtime is)\s+(.+?)['’]s\s+(?:bot|terminal)(?:\s+on)?\??$/i);
686
754
  if (agentModelMatch?.[1]) {
687
755
  return { action: "agent-model", target: agentModelMatch[1].trim() };
@@ -795,6 +863,8 @@ function formatTelegramRoomMarkup(project, options = {}) {
795
863
  const agentLines = agents.length
796
864
  ? agents.map((agent) => `• ${escapeTelegramHtml(formatAgentLabel(agent) || agent.id || "agent")} <code>${escapeTelegramHtml(agent.surface || "unknown")}</code>`)
797
865
  : ["• none"];
866
+ const runtimeConflict = summarizeRuntimeConflict(project, executor);
867
+ const liveHosts = Array.isArray(options.liveHosts) ? options.liveHosts : [];
798
868
  const executorBits = [
799
869
  `surface: <code>${escapeTelegramHtml(executor.surface || "telegram")}</code>`,
800
870
  `provider: <code>${escapeTelegramHtml(executor.provider || "unknown")}</code>`,
@@ -819,6 +889,21 @@ function formatTelegramRoomMarkup(project, options = {}) {
819
889
  `pending invites: <code>${pendingInviteCount}</code>`,
820
890
  "<b>Executor</b>",
821
891
  ...executorBits,
892
+ "<b>Runtime Split</b>",
893
+ runtimeConflict.runtimeCount
894
+ ? `status: <code>${escapeTelegramHtml(runtimeConflict.hasConflict ? `split (${runtimeConflict.runtimeCount})` : "aligned")}</code>`
895
+ : "status: <code>unknown</code>",
896
+ ...runtimeConflict.groups.map((group) => {
897
+ const owners = group.agents
898
+ .map((agent) => String(agent.ownerName || agent.label || agent.ownerId || agent.id || "").trim())
899
+ .filter(Boolean);
900
+ return `• <code>${escapeTelegramHtml(group.runtime)}</code>${owners.length ? ` — ${escapeTelegramHtml(owners.join(", "))}` : ""}`;
901
+ }),
902
+ "<b>Live Terminals</b>",
903
+ `count: <code>${escapeTelegramHtml(String(liveHosts.length))}</code>`,
904
+ ...(liveHosts.length
905
+ ? liveHosts.map((host) => `• ${escapeTelegramHtml(formatBridgeHostLabel(host) || host.sessionId || "terminal")} <code>${escapeTelegramHtml(host.surface || "live-tui")}</code>`)
906
+ : ["• none"]),
822
907
  "<b>Task Summary</b>",
823
908
  `total: <code>${tasks.length}</code>`,
824
909
  ...[...byState.entries()].map(([state, count]) => `${escapeTelegramHtml(state)}: <code>${count}</code>`),
@@ -1998,6 +2083,65 @@ class TelegramGateway {
1998
2083
  ].join("\n");
1999
2084
  }
2000
2085
 
2086
+ if (intent.action === "live-hosts") {
2087
+ const hosts = await this.getLiveBridgeHosts({ cwd });
2088
+ if (!hosts.length) {
2089
+ return "<b>Live terminals</b>\n• none";
2090
+ }
2091
+ return [
2092
+ "<b>Live terminals</b>",
2093
+ ...hosts.map((host) => {
2094
+ const roleAgent = listProjectAgents(project).find((agent) => String(agent?.sessionId || "").trim() === String(host?.sessionId || "").trim()) || null;
2095
+ return `• ${escapeTelegramHtml(formatBridgeHostLabel(host) || host.sessionId || "terminal")} <i>(${escapeTelegramHtml(roleAgent?.role || "live")})</i> <code>${escapeTelegramHtml(host.surface || "live-tui")}</code>`;
2096
+ })
2097
+ ].join("\n");
2098
+ }
2099
+
2100
+ if (intent.action === "model-conflict") {
2101
+ if (!project?.enabled) {
2102
+ return "This project is not shared.";
2103
+ }
2104
+ const summary = summarizeRuntimeConflict(project, executor);
2105
+ if (!summary.runtimeCount) {
2106
+ return "<b>Model conflict check</b>\nNo registered terminal runtime is known yet.";
2107
+ }
2108
+ const lines = [
2109
+ "<b>Model conflict check</b>",
2110
+ summary.hasConflict
2111
+ ? `This room has a runtime split across <code>${escapeTelegramHtml(String(summary.runtimeCount))}</code> models.`
2112
+ : "This room is currently aligned on one runtime."
2113
+ ];
2114
+ for (const group of summary.groups) {
2115
+ const owners = group.agents
2116
+ .map((agent) => String(agent.ownerName || agent.label || agent.ownerId || agent.id || "").trim())
2117
+ .filter(Boolean);
2118
+ lines.push(`• <code>${escapeTelegramHtml(group.runtime)}</code>${owners.length ? ` — ${escapeTelegramHtml(owners.join(", "))}` : ""}`);
2119
+ }
2120
+ if (summary.hasConflict) {
2121
+ lines.push("If you want a second opinion, assign a reviewer or ask whose bot should handle the work.");
2122
+ }
2123
+ return lines.join("\n");
2124
+ }
2125
+
2126
+ if (intent.action === "agent-perspective") {
2127
+ if (!project?.enabled) {
2128
+ return "This project is not shared.";
2129
+ }
2130
+ const agent = resolveProjectAgent(project, intent.target);
2131
+ if (!agent) {
2132
+ return `No terminal found for ${escapeTelegramHtml(intent.target || "that person")} in this room yet.`;
2133
+ }
2134
+ const runtime = agent.provider && agent.model ? `${agent.provider}/${agent.model}` : "unknown";
2135
+ return [
2136
+ "<b>Model perspective</b>",
2137
+ `person: <code>${escapeTelegramHtml(agent.ownerName || agent.ownerId || agent.label || agent.id || "-")}</code>`,
2138
+ `terminal: <code>${escapeTelegramHtml(agent.label || agent.id || "unknown")}</code>`,
2139
+ `runtime: <code>${escapeTelegramHtml(runtime)}</code>`,
2140
+ `role: <code>${escapeTelegramHtml(agent.role || "standby")}</code>`,
2141
+ "This shows which model would speak for that terminal. To get its perspective in the room, assign it as reviewer or executor."
2142
+ ].join("\n");
2143
+ }
2144
+
2001
2145
  if (intent.action === "agent-model") {
2002
2146
  if (!project?.enabled) {
2003
2147
  return "This project is not shared.";
@@ -2027,12 +2171,20 @@ class TelegramGateway {
2027
2171
  const operatorLabel = project.activeOperator?.id
2028
2172
  ? `${project.activeOperator.name || project.activeOperator.id}`
2029
2173
  : "none";
2174
+ const liveHosts = await this.getLiveBridgeHosts({ cwd });
2175
+ const isLive = liveHosts.some((host) => {
2176
+ const ownerId = String(host?.ownerId || "").trim();
2177
+ const sessionId = String(host?.sessionId || "").trim();
2178
+ return (ownerId && ownerId === String(agent.ownerId || "").trim())
2179
+ || (sessionId && sessionId === String(agent.sessionId || "").trim());
2180
+ });
2030
2181
  return [
2031
2182
  "<b>Execution recommendation</b>",
2032
2183
  `take this on: <code>${escapeTelegramHtml(agent.ownerName || agent.label || agent.ownerId || agent.id || "unknown")}</code>`,
2033
2184
  `role: <code>${escapeTelegramHtml(agent.role || "executor")}</code>`,
2034
2185
  `surface: <code>${escapeTelegramHtml(agent.surface || "unknown")}</code>`,
2035
2186
  `runtime: <code>${escapeTelegramHtml(agent.provider && agent.model ? `${agent.provider}/${agent.model}` : "unknown")}</code>`,
2187
+ `live now: <code>${escapeTelegramHtml(isLive ? "yes" : "no")}</code>`,
2036
2188
  `active operator: <code>${escapeTelegramHtml(operatorLabel)}</code>`
2037
2189
  ].join("\n");
2038
2190
  }
@@ -2379,6 +2531,7 @@ class TelegramGateway {
2379
2531
  async buildRoomMarkup(message, sessionId) {
2380
2532
  const { project } = await this.bindSharedRoomForMessage(message, sessionId);
2381
2533
  const host = await this.getLiveBridgeHost();
2534
+ const liveHosts = await this.getLiveBridgeHosts({ cwd: project?.cwd || this.cwd });
2382
2535
  const executor = {
2383
2536
  surface: host?.surface || (host ? "live-tui" : "telegram-fallback"),
2384
2537
  provider: host?.provider || this.runtime.provider,
@@ -2390,10 +2543,10 @@ class TelegramGateway {
2390
2543
  ? `${formatTelegramSummaryMarkup({
2391
2544
  cwd: project.cwd || this.cwd,
2392
2545
  project,
2393
- chatId: String(message.chat.id),
2394
- title: String(message.chat.title || "").trim(),
2395
- executor
2396
- })}\n\n${formatTelegramRoomMarkup(project, { executor })}`
2546
+ chatId: String(message.chat.id),
2547
+ title: String(message.chat.title || "").trim(),
2548
+ executor
2549
+ })}\n\n${formatTelegramRoomMarkup(project, { executor, liveHosts })}`
2397
2550
  : "<b>Shared room</b>\nThis project is not shared.";
2398
2551
  }
2399
2552
 
@@ -2444,6 +2597,39 @@ class TelegramGateway {
2444
2597
  return host;
2445
2598
  }
2446
2599
 
2600
+ async getLiveBridgeHosts({ cwd = "" } = {}) {
2601
+ const bridge = await loadGatewayBridge("telegram");
2602
+ const hosts = Array.isArray(bridge.hosts) ? bridge.hosts : [];
2603
+ const nextHosts = [];
2604
+ let changed = false;
2605
+ for (const host of hosts) {
2606
+ const pid = Number(host?.pid || 0);
2607
+ if (!Number.isFinite(pid) || pid <= 0) {
2608
+ changed = true;
2609
+ continue;
2610
+ }
2611
+ try {
2612
+ process.kill(pid, 0);
2613
+ } catch {
2614
+ changed = true;
2615
+ continue;
2616
+ }
2617
+ if (cwd && host.cwd && String(host.cwd) !== String(cwd || "")) {
2618
+ continue;
2619
+ }
2620
+ nextHosts.push(host);
2621
+ }
2622
+ if (changed || nextHosts.length !== hosts.length) {
2623
+ bridge.hosts = nextHosts;
2624
+ const activePid = Number(bridge.activeHost?.pid || 0);
2625
+ if (activePid > 0 && !nextHosts.some((host) => Number(host?.pid || 0) === activePid)) {
2626
+ bridge.activeHost = { pid: 0, sessionId: "", cwd: "", label: "", surface: "", ownerId: "", ownerName: "", provider: "", model: "", runtimeProfile: "", startedAt: "", updatedAt: "" };
2627
+ }
2628
+ await saveGatewayBridge("telegram", bridge);
2629
+ }
2630
+ return nextHosts;
2631
+ }
2632
+
2447
2633
  async runPromptViaBridge(message, sessionId, promptText, options = {}) {
2448
2634
  const host = await this.getLiveBridgeHost();
2449
2635
  if (!host) {