@integrity-labs/agt-cli 0.19.13 → 0.19.15

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.
@@ -100,7 +100,7 @@ async function spawnPairSession(session) {
100
100
  return { ok: true };
101
101
  } catch {
102
102
  }
103
- const { resolveClaudeBinary } = await import("./persistent-session-XBRQN7XE.js");
103
+ const { resolveClaudeBinary } = await import("./persistent-session-M2GVL6Z6.js");
104
104
  const claudeBin = resolveClaudeBinary();
105
105
  try {
106
106
  await execFileAsync("tmux", [
@@ -357,4 +357,4 @@ export {
357
357
  startClaudePair,
358
358
  submitClaudePairCode
359
359
  };
360
- //# sourceMappingURL=claude-pair-runtime-SJDLJNYF.js.map
360
+ //# sourceMappingURL=claude-pair-runtime-UF4OMFCA.js.map
@@ -22,7 +22,7 @@ import {
22
22
  resolveChannels,
23
23
  resolveDmTarget,
24
24
  wrapScheduledTaskPrompt
25
- } from "../chunk-SUUTWC6M.js";
25
+ } from "../chunk-DVWBVANP.js";
26
26
  import {
27
27
  findTaskByTemplate,
28
28
  getProjectDir,
@@ -39,15 +39,17 @@ import {
39
39
  isAgentIdle,
40
40
  isSessionHealthy,
41
41
  isStaleForToday,
42
+ parsePsRows,
42
43
  peekCurrentSession,
43
44
  prepareForRespawn,
45
+ reapOrphanChannelMcps,
44
46
  resetRestartCount,
45
47
  resolveClaudeBinary,
46
48
  sanitizeMcpJson,
47
49
  startPersistentSession,
48
50
  stopAllSessionsAndWait,
49
51
  stopPersistentSession
50
- } from "../chunk-QFWR2NV5.js";
52
+ } from "../chunk-3K3RO5NS.js";
51
53
 
52
54
  // src/lib/manager-worker.ts
53
55
  import { createHash } from "crypto";
@@ -58,6 +60,188 @@ import { join as join4, dirname } from "path";
58
60
  import { homedir as homedir3 } from "os";
59
61
  import { fileURLToPath } from "url";
60
62
 
63
+ // src/lib/stale-mcp-reaper.ts
64
+ import { execFileSync } from "child_process";
65
+ function parseEnvIntegrationsVars(content) {
66
+ const names = /* @__PURE__ */ new Set();
67
+ for (const raw of content.split(/\r?\n/)) {
68
+ const line = raw.trim();
69
+ if (!line || line.startsWith("#")) continue;
70
+ const eq = line.indexOf("=");
71
+ if (eq <= 0) continue;
72
+ const name = line.slice(0, eq).trim();
73
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) continue;
74
+ names.add(name);
75
+ }
76
+ return [...names];
77
+ }
78
+ function findMcpServersUsingVars(mcp, changedVars) {
79
+ const changedSet = new Set(changedVars);
80
+ if (!mcp?.mcpServers || changedSet.size === 0) return [];
81
+ const result = [];
82
+ for (const [serverKey, entry] of Object.entries(mcp.mcpServers)) {
83
+ if (!entry || typeof entry !== "object") continue;
84
+ const env = entry.env;
85
+ if (!env || typeof env !== "object") continue;
86
+ let matches = false;
87
+ for (const value of Object.values(env)) {
88
+ if (typeof value !== "string") continue;
89
+ const placeholderMatches = value.match(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g);
90
+ if (placeholderMatches) {
91
+ for (const ph of placeholderMatches) {
92
+ const name = ph.slice(2, -1);
93
+ if (changedSet.has(name)) {
94
+ matches = true;
95
+ break;
96
+ }
97
+ }
98
+ }
99
+ if (matches) break;
100
+ }
101
+ if (matches) result.push(serverKey);
102
+ }
103
+ return result;
104
+ }
105
+ function buildArgvMatchers(serverKeys) {
106
+ const patterns = [];
107
+ for (const key of serverKeys) {
108
+ const safe = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
109
+ patterns.push(new RegExp(`\\b${safe}\\b`));
110
+ if (safe.includes("_")) {
111
+ const dashed = safe.replace(/_/g, "-");
112
+ patterns.push(new RegExp(`\\b${dashed}\\b`));
113
+ }
114
+ }
115
+ return patterns;
116
+ }
117
+ function buildClaudeAgentMatcher(codeName) {
118
+ const safe = codeName.replace(/[^A-Za-z0-9_-]/g, "");
119
+ return new RegExp(`\\bclaude\\b.*--name\\s+agt-${safe}(?=\\s|$)`);
120
+ }
121
+ function findMcpChildrenForAgent(args) {
122
+ const { rows, codeName, serverKeys } = args;
123
+ const maxDepth = args.maxDepth ?? 8;
124
+ if (serverKeys.length === 0 || rows.length === 0) return [];
125
+ const claudeMatcher = buildClaudeAgentMatcher(codeName);
126
+ const argvMatchers = buildArgvMatchers(serverKeys);
127
+ const byPid = new Map(rows.map((r) => [r.pid, r]));
128
+ const matched = [];
129
+ for (const row of rows) {
130
+ if (!argvMatchers.some((re) => re.test(row.args))) continue;
131
+ if (/\bclaude\b/.test(row.args) && row.args.includes(`--name agt-${codeName}`)) continue;
132
+ let cur = byPid.get(row.ppid);
133
+ let belongs = false;
134
+ for (let depth = 0; depth < maxDepth && cur; depth++) {
135
+ if (claudeMatcher.test(cur.args)) {
136
+ belongs = true;
137
+ break;
138
+ }
139
+ if (cur.pid === 1) break;
140
+ cur = byPid.get(cur.ppid);
141
+ }
142
+ if (belongs) matched.push(row.pid);
143
+ }
144
+ return matched;
145
+ }
146
+ function reapStaleMcpChildren(args) {
147
+ const { log: log2, codeName, serverKeys, graceMs = 5e3 } = args;
148
+ if (serverKeys.length === 0) return [];
149
+ const runPs = args.runPs ?? (() => execFileSync("ps", ["-eo", "pid,ppid,args"], { encoding: "utf-8", timeout: 5e3 }));
150
+ const killProcess = args.killProcess ?? ((pid, signal) => {
151
+ try {
152
+ process.kill(pid, signal);
153
+ } catch {
154
+ }
155
+ });
156
+ const isAlive = args.isAlive ?? ((pid) => {
157
+ try {
158
+ process.kill(pid, 0);
159
+ return true;
160
+ } catch {
161
+ return false;
162
+ }
163
+ });
164
+ let psOutput;
165
+ try {
166
+ psOutput = runPs();
167
+ } catch (err) {
168
+ log2(`[stale-mcp-reaper] ps invocation failed for '${codeName}': ${err.message} \u2014 skipping reap`);
169
+ return [];
170
+ }
171
+ const rows = parsePsRows(psOutput);
172
+ const targets = findMcpChildrenForAgent({ rows, codeName, serverKeys });
173
+ if (targets.length === 0) return [];
174
+ const byPid = new Map(rows.map((r) => [r.pid, r]));
175
+ const describe = (pid) => {
176
+ const argv = byPid.get(pid)?.args ?? "";
177
+ const pkgMatch = argv.match(/(@[a-z0-9_-]+\/[a-z0-9_-]+|[a-z0-9_-]+-mcp-server|[a-z0-9_-]+-mcp\b)/i);
178
+ return pkgMatch ? `${pkgMatch[1]} (pid ${pid})` : `pid ${pid}`;
179
+ };
180
+ log2(
181
+ `[stale-mcp-reaper] '${codeName}': rotating ${targets.length} stale MCP child(ren) for [${serverKeys.join(", ")}]: ${targets.map(describe).join(", ")}`
182
+ );
183
+ for (const pid of targets) {
184
+ killProcess(pid, "SIGTERM");
185
+ }
186
+ setTimeout(() => {
187
+ try {
188
+ let freshPsOutput;
189
+ try {
190
+ freshPsOutput = runPs();
191
+ } catch (err) {
192
+ log2(`[stale-mcp-reaper] '${codeName}': fresh ps for SIGKILL re-verify failed: ${err.message} \u2014 skipping SIGKILL pass`);
193
+ return;
194
+ }
195
+ const stillOwned = new Set(
196
+ findMcpChildrenForAgent({
197
+ rows: parsePsRows(freshPsOutput),
198
+ codeName,
199
+ serverKeys
200
+ })
201
+ );
202
+ const stragglers = targets.filter((pid) => isAlive(pid) && stillOwned.has(pid));
203
+ if (stragglers.length === 0) return;
204
+ log2(
205
+ `[stale-mcp-reaper] '${codeName}': ${stragglers.length} child(ren) survived SIGTERM; sending SIGKILL: ${stragglers.map(describe).join(", ")}`
206
+ );
207
+ for (const pid of stragglers) {
208
+ killProcess(pid, "SIGKILL");
209
+ }
210
+ } catch (err) {
211
+ log2(`[stale-mcp-reaper] '${codeName}': error in SIGKILL pass: ${err.message}`);
212
+ }
213
+ }, graceMs).unref();
214
+ return targets;
215
+ }
216
+
217
+ // src/lib/channel-restart-decision.ts
218
+ function launchableChannelIds(channelConfigs) {
219
+ if (!channelConfigs) return /* @__PURE__ */ new Set();
220
+ const result = /* @__PURE__ */ new Set();
221
+ for (const [channelId, entry] of Object.entries(channelConfigs)) {
222
+ if (!entry) continue;
223
+ if (entry.config == null) continue;
224
+ if (entry.status !== "active" && entry.status !== "pending") continue;
225
+ result.add(channelId);
226
+ }
227
+ return result;
228
+ }
229
+ function decideChannelRestart(input) {
230
+ const { previousChannelIds, currentChannelIds, sessionMode, framework, sessionHealthy } = input;
231
+ if (previousChannelIds === void 0) {
232
+ return { restart: false, added: [], removed: [] };
233
+ }
234
+ const added = [...currentChannelIds].filter((c) => !previousChannelIds.has(c));
235
+ const removed = [...previousChannelIds].filter((c) => !currentChannelIds.has(c));
236
+ if (added.length === 0 && removed.length === 0) {
237
+ return { restart: false, added, removed };
238
+ }
239
+ if (sessionMode !== "persistent") return { restart: false, added, removed };
240
+ if (framework !== "claude-code") return { restart: false, added, removed };
241
+ if (!sessionHealthy) return { restart: false, added, removed };
242
+ return { restart: true, added, removed };
243
+ }
244
+
61
245
  // src/lib/integration-context-render.ts
62
246
  var PLUGIN_CONTEXT_PLACEHOLDER_RE = /\{\{\s*context\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g;
63
247
  var TEAM_OVERRIDES_HEADER = "## Team Overrides\n\n> **The following overrides anything you've read above.** If any rule here conflicts with earlier instructions in this skill, follow what is written here. These are user-supplied directives that take precedence over the plugin's default guidance.\n";
@@ -620,7 +804,7 @@ function saveChannelHashCache(source, configDir) {
620
804
  }
621
805
 
622
806
  // src/lib/channel-sweep.ts
623
- import { execFileSync } from "child_process";
807
+ import { execFileSync as execFileSync2 } from "child_process";
624
808
  var CHANNEL_BASENAMES = [
625
809
  "slack-channel",
626
810
  "direct-chat-channel",
@@ -783,7 +967,7 @@ function resolveLiveAnchorPids(agentCodeNames) {
783
967
  for (const codeName of agentCodeNames) {
784
968
  const pids = /* @__PURE__ */ new Set();
785
969
  try {
786
- const out = execFileSync("tmux", ["list-panes", "-t", `agt-${codeName}`, "-F", "#{pane_pid}"], {
970
+ const out = execFileSync2("tmux", ["list-panes", "-t", `agt-${codeName}`, "-F", "#{pane_pid}"], {
787
971
  encoding: "utf-8",
788
972
  timeout: 2e3,
789
973
  stdio: ["ignore", "pipe", "ignore"]
@@ -803,7 +987,7 @@ async function sweepChannelProcesses(opts) {
803
987
  const kill = opts.killFn ?? defaultKill;
804
988
  let psOutput = "";
805
989
  try {
806
- psOutput = execFileSync("ps", ["eww", "-o", "pid=,ppid=,etime=,command="], {
990
+ psOutput = execFileSync2("ps", ["eww", "-o", "pid=,ppid=,etime=,command="], {
807
991
  encoding: "utf-8",
808
992
  timeout: 5e3,
809
993
  maxBuffer: 10 * 1024 * 1024
@@ -840,7 +1024,7 @@ function defaultKillSignal(pid, signal) {
840
1024
  }
841
1025
  }
842
1026
  function defaultPs() {
843
- return execFileSync("ps", ["eww", "-o", "pid=,ppid=,etime=,command="], {
1027
+ return execFileSync2("ps", ["eww", "-o", "pid=,ppid=,etime=,command="], {
844
1028
  encoding: "utf-8",
845
1029
  timeout: 5e3,
846
1030
  maxBuffer: 10 * 1024 * 1024
@@ -1554,6 +1738,28 @@ var KNOWN_SAFE_TAIL_SIGNATURES = /* @__PURE__ */ new Set(["session_id_in_use"]);
1554
1738
  var knownVersions = /* @__PURE__ */ new Map();
1555
1739
  var knownStatuses = /* @__PURE__ */ new Map();
1556
1740
  var knownChannels = /* @__PURE__ */ new Map();
1741
+ var pendingSessionRestarts = /* @__PURE__ */ new Map();
1742
+ function scheduleSessionRestart(codeName, delayMs, reason) {
1743
+ const existing = pendingSessionRestarts.get(codeName);
1744
+ if (existing) {
1745
+ clearTimeout(existing);
1746
+ log(`[hot-reload] Coalesced restart for '${codeName}': replacing pending timer with ${reason}`);
1747
+ }
1748
+ const timer = setTimeout(() => {
1749
+ pendingSessionRestarts.delete(codeName);
1750
+ stopPersistentSession(codeName, log);
1751
+ log(`[hot-reload] Session stopped for '${codeName}' \u2014 will respawn with ${reason}`);
1752
+ }, delayMs);
1753
+ timer.unref?.();
1754
+ pendingSessionRestarts.set(codeName, timer);
1755
+ }
1756
+ function cancelPendingSessionRestart(codeName) {
1757
+ const existing = pendingSessionRestarts.get(codeName);
1758
+ if (!existing) return;
1759
+ clearTimeout(existing);
1760
+ pendingSessionRestarts.delete(codeName);
1761
+ log(`[hot-reload] Cancelled pending restart timer for '${codeName}' (another teardown path is handling it)`);
1762
+ }
1557
1763
  var writtenHashes = /* @__PURE__ */ new Map();
1558
1764
  var knownSecretsHashes = /* @__PURE__ */ new Map();
1559
1765
  var knownChannelConfigHashes = /* @__PURE__ */ new Map();
@@ -1630,10 +1836,10 @@ function clearAgentCaches(agentId, codeName) {
1630
1836
  var cachedFrameworkVersion = null;
1631
1837
  var lastVersionCheckAt = 0;
1632
1838
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
1633
- var agtCliVersion = true ? "0.19.13" : "dev";
1634
- function resolveBrewPath(execFileSync2) {
1839
+ var agtCliVersion = true ? "0.19.15" : "dev";
1840
+ function resolveBrewPath(execFileSync3) {
1635
1841
  try {
1636
- const out = execFileSync2("which", ["brew"], { timeout: 5e3 }).toString().trim();
1842
+ const out = execFileSync3("which", ["brew"], { timeout: 5e3 }).toString().trim();
1637
1843
  if (out) return out;
1638
1844
  } catch {
1639
1845
  }
@@ -1675,9 +1881,9 @@ async function ensureToolkitCli(toolkitSlug) {
1675
1881
  }
1676
1882
  const { binary, installer, package: pkg, script } = integration.cli_tool;
1677
1883
  const resolvedInstaller = installer ?? "manual";
1678
- const { execFileSync: execFileSync2, execSync } = await import("child_process");
1884
+ const { execFileSync: execFileSync3, execSync } = await import("child_process");
1679
1885
  try {
1680
- execFileSync2("which", [binary], { timeout: 5e3, stdio: "pipe" });
1886
+ execFileSync3("which", [binary], { timeout: 5e3, stdio: "pipe" });
1681
1887
  toolkitCliEnsured.add(toolkitSlug);
1682
1888
  toolkitCliRetryAfter.delete(toolkitSlug);
1683
1889
  toolkitCliFailureCount.delete(toolkitSlug);
@@ -1698,14 +1904,14 @@ async function ensureToolkitCli(toolkitSlug) {
1698
1904
  return;
1699
1905
  }
1700
1906
  log(`[toolkit-install] ${toolkitSlug}: installing via npm (${pkg})\u2026`);
1701
- execFileSync2("npm", ["install", "-g", pkg], { timeout: 18e4, stdio: "pipe" });
1907
+ execFileSync3("npm", ["install", "-g", pkg], { timeout: 18e4, stdio: "pipe" });
1702
1908
  } else if (resolvedInstaller === "brew") {
1703
1909
  if (!pkg) {
1704
1910
  log(`[toolkit-install] ${toolkitSlug}: installer=brew but no package declared`);
1705
1911
  toolkitCliEnsured.add(toolkitSlug);
1706
1912
  return;
1707
1913
  }
1708
- const brewPath = resolveBrewPath(execFileSync2);
1914
+ const brewPath = resolveBrewPath(execFileSync3);
1709
1915
  if (!brewPath) {
1710
1916
  log(`[toolkit-install] ${toolkitSlug}: installer=brew but Homebrew not available \u2014 install manually: brew install ${pkg}`);
1711
1917
  toolkitCliEnsured.add(toolkitSlug);
@@ -1715,9 +1921,9 @@ async function ensureToolkitCli(toolkitSlug) {
1715
1921
  const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
1716
1922
  log(`[toolkit-install] ${toolkitSlug}: installing via brew (${pkg})\u2026`);
1717
1923
  if (isRoot) {
1718
- execFileSync2("sudo", ["-u", "ec2-user", "-H", brewPath, "install", pkg], { timeout: 18e4, stdio: "pipe", cwd: "/tmp" });
1924
+ execFileSync3("sudo", ["-u", "ec2-user", "-H", brewPath, "install", pkg], { timeout: 18e4, stdio: "pipe", cwd: "/tmp" });
1719
1925
  } else {
1720
- execFileSync2(brewPath, ["install", pkg], { timeout: 18e4, stdio: "pipe" });
1926
+ execFileSync3(brewPath, ["install", pkg], { timeout: 18e4, stdio: "pipe" });
1721
1927
  }
1722
1928
  } else if (resolvedInstaller === "script") {
1723
1929
  if (!script) {
@@ -1737,7 +1943,7 @@ async function ensureToolkitCli(toolkitSlug) {
1737
1943
  process.env.PATH = `${brewBinDir}:${process.env.PATH ?? ""}`;
1738
1944
  }
1739
1945
  try {
1740
- execFileSync2("which", [binary], { timeout: 5e3, stdio: "pipe" });
1946
+ execFileSync3("which", [binary], { timeout: 5e3, stdio: "pipe" });
1741
1947
  log(`[toolkit-install] ${toolkitSlug}: installed \u2014 ${binary} now on PATH`);
1742
1948
  toolkitCliEnsured.add(toolkitSlug);
1743
1949
  toolkitCliRetryAfter.delete(toolkitSlug);
@@ -1790,8 +1996,8 @@ async function ensureFrameworkBinary(frameworkId) {
1790
1996
  if (frameworkId !== "claude-code") return;
1791
1997
  if (frameworkBinaryChecked.has(frameworkId)) return;
1792
1998
  frameworkBinaryChecked.add(frameworkId);
1793
- const { execFileSync: execFileSync2 } = await import("child_process");
1794
- const brewPath = resolveBrewPath(execFileSync2);
1999
+ const { execFileSync: execFileSync3 } = await import("child_process");
2000
+ const brewPath = resolveBrewPath(execFileSync3);
1795
2001
  if (!brewPath) {
1796
2002
  log("Homebrew not found (no `brew` on PATH, no /home/linuxbrew/.linuxbrew/bin/brew). Cannot auto-install Claude Code. Install manually: https://claude.ai/download");
1797
2003
  return;
@@ -1807,7 +2013,7 @@ async function ensureFrameworkBinary(frameworkId) {
1807
2013
  let claudeExists = existsSync3("/home/linuxbrew/.linuxbrew/bin/claude");
1808
2014
  if (!claudeExists) {
1809
2015
  try {
1810
- execFileSync2("which", ["claude"], { timeout: 5e3 });
2016
+ execFileSync3("which", ["claude"], { timeout: 5e3 });
1811
2017
  claudeExists = true;
1812
2018
  } catch {
1813
2019
  }
@@ -1899,16 +2105,16 @@ async function checkAndUpdateCli() {
1899
2105
  }
1900
2106
  }
1901
2107
  async function checkAndUpdateCliViaBrew() {
1902
- const { execFileSync: execFileSync2 } = await import("child_process");
1903
- const brewPath = resolveBrewPath(execFileSync2);
2108
+ const { execFileSync: execFileSync3 } = await import("child_process");
2109
+ const brewPath = resolveBrewPath(execFileSync3);
1904
2110
  if (!brewPath) return;
1905
2111
  try {
1906
- execFileSync2(brewPath, ["update", "--quiet"], { timeout: 6e4, stdio: "pipe" });
2112
+ execFileSync3(brewPath, ["update", "--quiet"], { timeout: 6e4, stdio: "pipe" });
1907
2113
  } catch (err) {
1908
2114
  log(`[self-update] brew update failed (continuing with stale cache): ${err.message}`);
1909
2115
  }
1910
2116
  try {
1911
- const outdated = execFileSync2(brewPath, ["outdated", "--json=v2"], {
2117
+ const outdated = execFileSync3(brewPath, ["outdated", "--json=v2"], {
1912
2118
  timeout: 3e4,
1913
2119
  encoding: "utf-8"
1914
2120
  });
@@ -1919,7 +2125,7 @@ async function checkAndUpdateCliViaBrew() {
1919
2125
  const latest = agtOutdated.current_version ?? "unknown";
1920
2126
  log(`[self-update] agt CLI update available: ${installed} \u2192 ${latest}. Upgrading via brew...`);
1921
2127
  try {
1922
- execFileSync2(brewPath, ["upgrade", "integrity-labs/tap/agt"], {
2128
+ execFileSync3(brewPath, ["upgrade", "integrity-labs/tap/agt"], {
1923
2129
  timeout: 12e4,
1924
2130
  stdio: "pipe"
1925
2131
  });
@@ -1938,7 +2144,7 @@ async function checkAndUpdateCliViaBrew() {
1938
2144
  }
1939
2145
  }
1940
2146
  async function checkAndUpdateCliViaNpm() {
1941
- const { execFileSync: execFileSync2 } = await import("child_process");
2147
+ const { execFileSync: execFileSync3 } = await import("child_process");
1942
2148
  if (agtCliVersion === "dev") return;
1943
2149
  let latest;
1944
2150
  try {
@@ -1987,7 +2193,7 @@ async function checkAndUpdateCliViaNpm() {
1987
2193
  "--registry=https://registry.npmjs.org"
1988
2194
  ];
1989
2195
  try {
1990
- execFileSync2(cmd, args, { timeout: 18e4, stdio: "pipe" });
2196
+ execFileSync3(cmd, args, { timeout: 18e4, stdio: "pipe" });
1991
2197
  log(`[self-update] agt CLI upgraded to ${latest}. Scheduling manager restart so the new binary takes effect.`);
1992
2198
  restartAfterUpgrade = true;
1993
2199
  pendingUpgradeVersion = latest;
@@ -2473,6 +2679,7 @@ async function pollCycle() {
2473
2679
  log,
2474
2680
  resolveFramework: (codeName) => agentFrameworkCache.get(codeName) ?? null,
2475
2681
  stopSession: (codeName) => {
2682
+ cancelPendingSessionRestart(codeName);
2476
2683
  stopPersistentSession(codeName, log);
2477
2684
  persistentSessionAgents.delete(codeName);
2478
2685
  claudeAuthTupleBySession.delete(codeName);
@@ -2533,7 +2740,7 @@ async function pollCycle() {
2533
2740
  }
2534
2741
  try {
2535
2742
  const { detectHostSecurity } = await import("../host-security-6PDFG7F5.js");
2536
- const { collectDiagnostics } = await import("../persistent-session-XBRQN7XE.js");
2743
+ const { collectDiagnostics } = await import("../persistent-session-M2GVL6Z6.js");
2537
2744
  const diagCodeNames = [...persistentSessionAgents];
2538
2745
  const agentDiagnostics = diagCodeNames.length > 0 ? collectDiagnostics(diagCodeNames) : void 0;
2539
2746
  let tailscaleHostname;
@@ -2633,6 +2840,7 @@ async function pollCycle() {
2633
2840
  log(`Agent '${prev.codeName}' removed from host (deleted or unassigned)`);
2634
2841
  const adapter = resolveAgentFramework(prev.codeName);
2635
2842
  await stopGatewayIfRunning(prev.codeName, adapter);
2843
+ cancelPendingSessionRestart(prev.codeName);
2636
2844
  stopPersistentSession(prev.codeName, log);
2637
2845
  try {
2638
2846
  const { execSync: es } = await import("child_process");
@@ -2770,6 +2978,7 @@ async function processAgent(agent, agentStates) {
2770
2978
  if (agent.status === "draft" || agent.status === "paused") {
2771
2979
  log(`Agent '${agent.code_name}' is ${agent.status}, skipping provisioning`);
2772
2980
  await stopGatewayIfRunning(agent.code_name, adapter);
2981
+ cancelPendingSessionRestart(agent.code_name);
2773
2982
  stopPersistentSession(agent.code_name, log);
2774
2983
  try {
2775
2984
  const { execSync: es } = await import("child_process");
@@ -2798,6 +3007,7 @@ async function processAgent(agent, agentStates) {
2798
3007
  if (agent.status === "revoked") {
2799
3008
  log(`Agent '${agent.code_name}' is revoked, cleaning up`);
2800
3009
  await stopGatewayIfRunning(agent.code_name, adapter);
3010
+ cancelPendingSessionRestart(agent.code_name);
2801
3011
  stopPersistentSession(agent.code_name, log);
2802
3012
  try {
2803
3013
  const { execSync: es } = await import("child_process");
@@ -2905,9 +3115,10 @@ async function processAgent(agent, agentStates) {
2905
3115
  const toolsVersion = refreshData.tools.version;
2906
3116
  const known = knownVersions.get(agent.agent_id);
2907
3117
  let lastProvisionAt = state.agents.find((a) => a.agentId === agent.agent_id)?.lastProvisionAt ?? null;
2908
- const currentChannelIds = new Set(Object.keys(refreshData.channel_configs ?? {}));
3118
+ const currentChannelIds = launchableChannelIds(refreshData.channel_configs);
2909
3119
  const previousChannelIds = knownChannels.get(agent.agent_id);
2910
3120
  const channelsChanged = !previousChannelIds || currentChannelIds.size !== previousChannelIds.size || [...currentChannelIds].some((ch) => !previousChannelIds.has(ch)) || [...previousChannelIds].some((ch) => !currentChannelIds.has(ch));
3121
+ let channelConfigConverged = true;
2911
3122
  if (previousChannelIds && channelsChanged && frameworkAdapter.removeChannelCredentials) {
2912
3123
  for (const ch of previousChannelIds) {
2913
3124
  if (!currentChannelIds.has(ch)) {
@@ -2915,12 +3126,12 @@ async function processAgent(agent, agentStates) {
2915
3126
  frameworkAdapter.removeChannelCredentials(agent.code_name, ch);
2916
3127
  log(`Removed ${ch} credentials for '${agent.code_name}'`);
2917
3128
  } catch (err) {
3129
+ channelConfigConverged = false;
2918
3130
  log(`Failed to remove ${ch} credentials for '${agent.code_name}': ${err.message}`);
2919
3131
  }
2920
3132
  }
2921
3133
  }
2922
3134
  }
2923
- knownChannels.set(agent.agent_id, currentChannelIds);
2924
3135
  try {
2925
3136
  const artifacts = generateArtifacts(agent, refreshData, frameworkAdapter);
2926
3137
  const changedFiles = [];
@@ -3133,6 +3344,7 @@ async function processAgent(agent, agentStates) {
3133
3344
  saveChannelHashCache2();
3134
3345
  log(`Channel credentials written for '${agent.code_name}/${channelId}' (reason=${reason}, hash=${configHash.slice(0, 8)}${prevHash ? `, prev=${prevHash.slice(0, 8)}` : ""})`);
3135
3346
  } catch (err) {
3347
+ channelConfigConverged = false;
3136
3348
  log(`Failed to write channel credentials for '${agent.code_name}/${channelId}': ${err.message}`);
3137
3349
  }
3138
3350
  }
@@ -3145,6 +3357,32 @@ async function processAgent(agent, agentStates) {
3145
3357
  }
3146
3358
  }
3147
3359
  }
3360
+ if (channelConfigConverged) {
3361
+ knownChannels.set(agent.agent_id, currentChannelIds);
3362
+ } else {
3363
+ log(`[channels] Credential sync did not converge for '${agent.code_name}' \u2014 leaving diff live for next tick retry`);
3364
+ }
3365
+ const restartDecision = channelConfigConverged ? decideChannelRestart({
3366
+ previousChannelIds,
3367
+ currentChannelIds,
3368
+ sessionMode: refreshData.agent.session_mode,
3369
+ framework: agentFrameworkCache.get(agent.code_name) ?? "openclaw",
3370
+ sessionHealthy: isSessionHealthy(agent.code_name)
3371
+ }) : { restart: false, added: [], removed: [] };
3372
+ if (restartDecision.restart) {
3373
+ const reasonParts = [];
3374
+ if (restartDecision.added.length > 0) reasonParts.push(`added=${restartDecision.added.join(",")}`);
3375
+ if (restartDecision.removed.length > 0) reasonParts.push(`removed=${restartDecision.removed.join(",")}`);
3376
+ const reason = reasonParts.join(" ");
3377
+ log(`[hot-reload] Channel set changed for '${agent.code_name}' (${reason}) \u2014 restarting session`);
3378
+ const restartNotice = restartDecision.added.length > 0 ? `New channels have been wired up (${restartDecision.added.join(", ")}). Note: channels require a session restart to attach their MCP servers as channel listeners. Your manager will restart your session shortly.` : `Channels were removed (${restartDecision.removed.join(", ")}). Your manager will restart your session shortly so the launch flags drop those channels.`;
3379
+ const delivered = await injectMessage(agent.code_name, "system", restartNotice, { task_name: "channel-update" }, log).catch(() => false);
3380
+ const delay = delivered ? 8e3 : 3e3;
3381
+ if (!delivered) {
3382
+ log(`[hot-reload] Inject notification unconfirmed for '${agent.code_name}' \u2014 proceeding with shorter delay`);
3383
+ }
3384
+ scheduleSessionRestart(agent.code_name, delay, "new channel set");
3385
+ }
3148
3386
  const agentSessionMode = refreshData.agent.session_mode;
3149
3387
  if (agentSessionMode === "persistent" && (agentFrameworkCache.get(agent.code_name) ?? "openclaw") === "claude-code") {
3150
3388
  try {
@@ -3250,12 +3488,33 @@ async function processAgent(agent, agentStates) {
3250
3488
  log(`Integrations provisioned for '${agent.code_name}' (${integrations.length} integration(s))`);
3251
3489
  const fw = agentFrameworkCache.get(agent.code_name) ?? "openclaw";
3252
3490
  if (fw === "claude-code" && isSessionHealthy(agent.code_name)) {
3491
+ let affectedServerKeys = [];
3492
+ try {
3493
+ const projectDir = join4(homedir3(), ".augmented", agent.code_name, "project");
3494
+ const envIntPath = join4(projectDir, ".env.integrations");
3495
+ const projectMcpPath = join4(projectDir, ".mcp.json");
3496
+ const envContent = readFileSync3(envIntPath, "utf-8");
3497
+ const mcpContent = readFileSync3(projectMcpPath, "utf-8");
3498
+ const changedVars = parseEnvIntegrationsVars(envContent);
3499
+ const mcpJson = JSON.parse(mcpContent);
3500
+ affectedServerKeys = findMcpServersUsingVars(mcpJson, changedVars);
3501
+ } catch (err) {
3502
+ log(`[hot-reload] Failed to compute affected MCP servers for '${agent.code_name}': ${err.message}`);
3503
+ }
3504
+ if (affectedServerKeys.length > 0) {
3505
+ reapStaleMcpChildren({
3506
+ log,
3507
+ codeName: agent.code_name,
3508
+ serverKeys: affectedServerKeys
3509
+ });
3510
+ }
3253
3511
  const names = integrations.map((i) => i.display_name || i.definition_id).join(", ");
3254
- injectMessage(agent.code_name, "system", `Your integrations have been updated. You now have access to: ${names}. Your .env.integrations and CLAUDE.md have been refreshed \u2014 credentials are available immediately via environment variables.`, {
3512
+ const reapNote = affectedServerKeys.length > 0 ? ` The MCP servers that depend on rotating credentials (${affectedServerKeys.join(", ")}) have been signalled to reconnect.` : "";
3513
+ injectMessage(agent.code_name, "system", `Your integrations have been refreshed. You have access to: ${names}.${reapNote}`, {
3255
3514
  task_name: "integration-update"
3256
3515
  }, log).catch(() => {
3257
3516
  });
3258
- log(`[hot-reload] Notified '${agent.code_name}' about integration update: ${names}`);
3517
+ log(`[hot-reload] Notified '${agent.code_name}' about integration update: ${names} (reaped ${affectedServerKeys.length} stale MCP server(s))`);
3259
3518
  }
3260
3519
  needsGatewayRestart = true;
3261
3520
  }
@@ -3334,10 +3593,7 @@ async function processAgent(agent, agentStates) {
3334
3593
  if (!delivered) {
3335
3594
  log(`[hot-reload] Inject notification unconfirmed for '${agent.code_name}' \u2014 proceeding with shorter delay`);
3336
3595
  }
3337
- setTimeout(() => {
3338
- stopPersistentSession(agent.code_name, log);
3339
- log(`[hot-reload] Session stopped for '${agent.code_name}' \u2014 will respawn with new MCP servers`);
3340
- }, delay);
3596
+ scheduleSessionRestart(agent.code_name, delay, "new MCP servers");
3341
3597
  }
3342
3598
  }
3343
3599
  } catch (err) {
@@ -4327,6 +4583,7 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
4327
4583
  const msg = err.message;
4328
4584
  log(`[persistent-session] Failed to resolve auth for '${codeName}': ${msg} \u2014 refusing to spawn`);
4329
4585
  if (isSessionHealthy(codeName)) {
4586
+ cancelPendingSessionRestart(codeName);
4330
4587
  stopPersistentSession(codeName, log);
4331
4588
  persistentSessionAgents.delete(codeName);
4332
4589
  claudeAuthTupleBySession.delete(codeName);
@@ -4349,6 +4606,7 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
4349
4606
  const recordedAuthTuple = claudeAuthTupleBySession.get(codeName);
4350
4607
  if (recordedAuthTuple && recordedAuthTuple !== currentAuthTuple && isSessionHealthy(codeName)) {
4351
4608
  log(`[persistent-session] Auth config changed for '${codeName}' (${recordedAuthTuple} \u2192 ${currentAuthTuple}) \u2014 restarting session`);
4609
+ cancelPendingSessionRestart(codeName);
4352
4610
  stopPersistentSession(codeName, log);
4353
4611
  persistentSessionAgents.delete(codeName);
4354
4612
  }
@@ -4360,6 +4618,7 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
4360
4618
  log(
4361
4619
  `[persistent-session] Day rollover for '${codeName}' (yesterday=${current.date}) \u2014 agent idle, restarting to mint fresh session`
4362
4620
  );
4621
+ cancelPendingSessionRestart(codeName);
4363
4622
  stopPersistentSession(codeName, log);
4364
4623
  persistentSessionAgents.delete(codeName);
4365
4624
  } else {
@@ -5739,7 +5998,7 @@ async function processClaudePairSessions(agents) {
5739
5998
  killPairSession,
5740
5999
  pairTmuxSession,
5741
6000
  finalizeClaudePairOnboarding
5742
- } = await import("../claude-pair-runtime-SJDLJNYF.js");
6001
+ } = await import("../claude-pair-runtime-UF4OMFCA.js");
5743
6002
  for (const pairId of pendingResp.cancelled_pair_ids ?? []) {
5744
6003
  log(`[claude-pair] sweeping orphan tmux session for pair ${pairId.slice(0, 8)}`);
5745
6004
  const killed = await killPairSession(pairTmuxSession(pairId));
@@ -6301,6 +6560,7 @@ function startManager(opts) {
6301
6560
  `[startup] worker pid=${process.pid} ppid=${process.ppid} node=${process.version} log=${join4(homedir3(), ".augmented", "manager.log")}`
6302
6561
  );
6303
6562
  deployMcpAssets();
6563
+ reapOrphanChannelMcps({ log });
6304
6564
  void ensureHostFrameworkBinaries();
6305
6565
  startPolling();
6306
6566
  }