@integrity-labs/agt-cli 0.27.7-test.7 → 0.27.8-test.10

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.
@@ -15,7 +15,7 @@ import {
15
15
  provisionOrientHook,
16
16
  provisionStopHook,
17
17
  requireHost
18
- } from "../chunk-2G35ES7G.js";
18
+ } from "../chunk-2E5OABKX.js";
19
19
  import {
20
20
  getProjectDir as getProjectDir2,
21
21
  getReadyTasks,
@@ -46,7 +46,7 @@ import {
46
46
  stopAllSessionsAndWait,
47
47
  stopPersistentSession,
48
48
  takeZombieDetection
49
- } from "../chunk-GN4XPQWJ.js";
49
+ } from "../chunk-JIETOJQV.js";
50
50
  import {
51
51
  KANBAN_CHECK_COMMAND,
52
52
  appendDmFooter,
@@ -69,7 +69,7 @@ import {
69
69
  resolveConnectivityProbe,
70
70
  resolveDmTarget,
71
71
  wrapScheduledTaskPrompt
72
- } from "../chunk-YSBGIXJG.js";
72
+ } from "../chunk-6HFXSNNY.js";
73
73
  import {
74
74
  parsePsRows,
75
75
  reapOrphanChannelMcps
@@ -77,10 +77,10 @@ import {
77
77
 
78
78
  // src/lib/manager-worker.ts
79
79
  import { createHash as createHash3 } from "crypto";
80
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, appendFileSync, mkdirSync as mkdirSync2, chmodSync, existsSync as existsSync4, rmSync as rmSync2, readdirSync as readdirSync3, statSync as statSync2, unlinkSync, copyFileSync } from "fs";
80
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, appendFileSync, mkdirSync as mkdirSync4, chmodSync, existsSync as existsSync5, rmSync as rmSync2, readdirSync as readdirSync3, statSync as statSync2, unlinkSync, copyFileSync } from "fs";
81
81
  import https from "https";
82
82
  import { execFileSync as syncExecFile } from "child_process";
83
- import { join as join6, dirname } from "path";
83
+ import { join as join6, dirname as dirname3 } from "path";
84
84
  import { homedir as homedir4 } from "os";
85
85
  import { fileURLToPath } from "url";
86
86
 
@@ -578,6 +578,37 @@ function decideChannelRestart(input) {
578
578
  return { restart: true, added, removed };
579
579
  }
580
580
 
581
+ // src/lib/sender-policy-restart-decision.ts
582
+ function decideSenderPolicyRestart(input) {
583
+ const {
584
+ previousHash,
585
+ currentHash,
586
+ sessionMode,
587
+ framework,
588
+ sessionHealthy,
589
+ channelSetRestartAlreadyScheduled
590
+ } = input;
591
+ if (previousHash === void 0) {
592
+ return { restart: false, firstPoll: true, changed: false };
593
+ }
594
+ if (previousHash === currentHash) {
595
+ return { restart: false, firstPoll: false, changed: false };
596
+ }
597
+ if (sessionMode !== "persistent") {
598
+ return { restart: false, firstPoll: false, changed: true };
599
+ }
600
+ if (framework !== "claude-code") {
601
+ return { restart: false, firstPoll: false, changed: true };
602
+ }
603
+ if (!sessionHealthy) {
604
+ return { restart: false, firstPoll: false, changed: true };
605
+ }
606
+ if (channelSetRestartAlreadyScheduled) {
607
+ return { restart: false, firstPoll: false, changed: true };
608
+ }
609
+ return { restart: true, firstPoll: false, changed: true };
610
+ }
611
+
581
612
  // src/lib/restart-breaker.ts
582
613
  function reaperRestartBreakerReason(activeKeys) {
583
614
  return activeKeys.length >= 2 ? "mcp-presence-reaper" : void 0;
@@ -910,6 +941,142 @@ function runCliProbe(binary, args, opts = {}) {
910
941
  });
911
942
  }
912
943
 
944
+ // src/lib/self-update-coalesce.ts
945
+ import { readFileSync, writeFileSync, mkdirSync } from "fs";
946
+ import { dirname } from "path";
947
+ var DEFAULT_SELF_UPDATE_COALESCE_MS = 30 * 60 * 1e3;
948
+ function resolveCoalesceWindowMs(env = process.env) {
949
+ const raw = env.AGT_SELF_UPDATE_COALESCE_MS;
950
+ if (raw == null || raw === "") return DEFAULT_SELF_UPDATE_COALESCE_MS;
951
+ const parsed = Number(raw);
952
+ if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_SELF_UPDATE_COALESCE_MS;
953
+ return Math.trunc(parsed);
954
+ }
955
+ function readLastSelfUpdateAppliedMs(markerPath, now = Date.now(), read = (p) => readFileSync(p, "utf-8")) {
956
+ let raw;
957
+ try {
958
+ raw = read(markerPath);
959
+ } catch {
960
+ return null;
961
+ }
962
+ const v = parseInt(raw.trim(), 10);
963
+ if (!Number.isFinite(v) || v <= 0) return null;
964
+ if (v > now) return null;
965
+ return v;
966
+ }
967
+ function stampLastSelfUpdateApplied(markerPath, now = Date.now(), write = (p, v) => {
968
+ mkdirSync(dirname(p), { recursive: true });
969
+ writeFileSync(p, v);
970
+ }) {
971
+ try {
972
+ write(markerPath, String(now));
973
+ } catch {
974
+ }
975
+ }
976
+ function decideSelfUpdateCoalesce(inputs) {
977
+ const { windowMs, lastAppliedMs, now } = inputs;
978
+ if (windowMs <= 0) return { proceed: true, remainingMs: 0, lastAppliedMs: null };
979
+ if (lastAppliedMs == null) return { proceed: true, remainingMs: 0, lastAppliedMs: null };
980
+ const elapsed = now - lastAppliedMs;
981
+ if (elapsed < 0) return { proceed: true, remainingMs: 0, lastAppliedMs: null };
982
+ if (elapsed >= windowMs) return { proceed: true, remainingMs: 0, lastAppliedMs: null };
983
+ return { proceed: false, remainingMs: windowMs - elapsed, lastAppliedMs };
984
+ }
985
+ function formatCoalesceDeferLogLine(opts) {
986
+ const remainingSec = Math.ceil(opts.remainingMs / 1e3);
987
+ const windowMin = Math.round(opts.windowMs / 6e4);
988
+ return `[self-update] coalescing \u2014 ${opts.installed} \u2192 ${opts.latest} (${opts.channelLabel}) available, deferring for ${remainingSec}s (last restart inside ${windowMin}min window). See ENG-5862. Override with AGT_SELF_UPDATE_COALESCE_MS=0 during incident response.`;
989
+ }
990
+
991
+ // src/lib/atomic-write.ts
992
+ import { closeSync, fsyncSync, openSync, writeSync, renameSync, mkdirSync as mkdirSync2 } from "fs";
993
+ import { dirname as dirname2 } from "path";
994
+ function atomicWriteFileSync(path, data) {
995
+ const dirPath = dirname2(path);
996
+ const tmpPath = `${path}.tmp.${process.pid}.${Math.random().toString(36).slice(2, 8)}`;
997
+ try {
998
+ mkdirSync2(dirPath, { recursive: true });
999
+ } catch {
1000
+ }
1001
+ const fd = openSync(tmpPath, "w", 420);
1002
+ try {
1003
+ writeSync(fd, data);
1004
+ try {
1005
+ fsyncSync(fd);
1006
+ } catch {
1007
+ }
1008
+ } finally {
1009
+ closeSync(fd);
1010
+ }
1011
+ renameSync(tmpPath, path);
1012
+ try {
1013
+ const dirFd = openSync(dirPath, "r");
1014
+ try {
1015
+ fsyncSync(dirFd);
1016
+ } finally {
1017
+ closeSync(dirFd);
1018
+ }
1019
+ } catch {
1020
+ }
1021
+ }
1022
+
1023
+ // src/lib/claude-pid-tracker.ts
1024
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
1025
+ function readPidFile(path) {
1026
+ if (!existsSync(path)) return { version: 1, spawns: [] };
1027
+ try {
1028
+ const raw = JSON.parse(readFileSync2(path, "utf-8"));
1029
+ if (raw.version !== 1 || !Array.isArray(raw.spawns)) return { version: 1, spawns: [] };
1030
+ const spawns = raw.spawns.filter(
1031
+ (s) => !!s && typeof s.pid === "number" && Number.isFinite(s.pid) && s.pid > 0
1032
+ );
1033
+ return { version: 1, spawns };
1034
+ } catch {
1035
+ return { version: 1, spawns: [] };
1036
+ }
1037
+ }
1038
+ function writePidFile(path, file) {
1039
+ atomicWriteFileSync(path, JSON.stringify(file, null, 2));
1040
+ }
1041
+ function addSpawn(path, record) {
1042
+ const file = readPidFile(path);
1043
+ const filtered = file.spawns.filter((s) => s.pid !== record.pid);
1044
+ writePidFile(path, { version: 1, spawns: [...filtered, record] });
1045
+ }
1046
+ function removeSpawn(path, pid) {
1047
+ const file = readPidFile(path);
1048
+ const next = file.spawns.filter((s) => s.pid !== pid);
1049
+ if (next.length !== file.spawns.length) writePidFile(path, { version: 1, spawns: next });
1050
+ }
1051
+ function decideReaperActions(file, isAlive, looksLikeClaude = () => true) {
1052
+ const toKill = [];
1053
+ const alreadyDead = [];
1054
+ const pidReusedSkipped = [];
1055
+ for (const s of file.spawns) {
1056
+ if (!isAlive(s.pid)) {
1057
+ alreadyDead.push(s);
1058
+ continue;
1059
+ }
1060
+ if (!looksLikeClaude(s.pid)) {
1061
+ pidReusedSkipped.push(s);
1062
+ continue;
1063
+ }
1064
+ toKill.push(s);
1065
+ }
1066
+ const toResetKanban = toKill.filter(
1067
+ (s) => s.kind === "kanban-work" && typeof s.kanban_card_id === "string" && s.kanban_card_id.length > 0
1068
+ );
1069
+ return { toKill, alreadyDead, pidReusedSkipped, toResetKanban };
1070
+ }
1071
+ function formatDrainShutdownLine(opts) {
1072
+ const killed = opts.killedPids && opts.killedPids.length > 0 ? ` killed_pids=${opts.killedPids.join(",")}` : "";
1073
+ const note = opts.note ? ` note="${opts.note.replace(/"/g, '\\"')}"` : "";
1074
+ return `[drain] step=${opts.step} elapsed_ms=${opts.elapsedMs}${killed}${note}`;
1075
+ }
1076
+ function formatReaperBootLine(opts) {
1077
+ return `[drain] step=boot-reaper total=${opts.totalRecorded} killed=${opts.killed} already_dead=${opts.alreadyDead} pid_reused_skipped=${opts.pidReusedSkipped} kanban_reset=${opts.kanbanReset}`;
1078
+ }
1079
+
913
1080
  // src/lib/usage-banner-monitor.ts
914
1081
  var MIN_CHECK_INTERVAL_MS = 6e4;
915
1082
  var PANE_TAIL_LINES_FOR_BANNER = 200;
@@ -966,7 +1133,7 @@ async function maybeReportUsageBanner(args) {
966
1133
  }
967
1134
 
968
1135
  // src/lib/token-usage-monitor.ts
969
- import { readdirSync, readFileSync, statSync } from "fs";
1136
+ import { readdirSync, readFileSync as readFileSync3, statSync } from "fs";
970
1137
  import { join } from "path";
971
1138
  var MIN_CHECK_INTERVAL_MS2 = 6e4;
972
1139
  var TRANSCRIPT_MTIME_WINDOW_MS = 2 * 24 * 60 * 60 * 1e3;
@@ -1012,7 +1179,7 @@ async function maybeReportTokenUsage(args) {
1012
1179
  }
1013
1180
  let content;
1014
1181
  try {
1015
- content = readFileSync(path, "utf-8");
1182
+ content = readFileSync3(path, "utf-8");
1016
1183
  } catch (err) {
1017
1184
  log2(`[token-usage] read failed for '${codeName}/${name}': ${err.message}`);
1018
1185
  continue;
@@ -1093,7 +1260,7 @@ async function maybeReportTokenUsage(args) {
1093
1260
  }
1094
1261
 
1095
1262
  // src/lib/activity-cache-monitor.ts
1096
- import { existsSync, readFileSync as readFileSync2 } from "fs";
1263
+ import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
1097
1264
  import { homedir } from "os";
1098
1265
  import { join as join2 } from "path";
1099
1266
  var MIN_CHECK_INTERVAL_MS3 = 6e4;
@@ -1140,12 +1307,12 @@ async function maybeReportActivityCache(args) {
1140
1307
  const nowMs = now.getTime();
1141
1308
  if (nowMs - state3.lastCheckedAt < MIN_CHECK_INTERVAL_MS3) return;
1142
1309
  state3.lastCheckedAt = nowMs;
1143
- if (!existsSync(STATS_CACHE_PATH)) {
1310
+ if (!existsSync2(STATS_CACHE_PATH)) {
1144
1311
  return;
1145
1312
  }
1146
1313
  let raw;
1147
1314
  try {
1148
- raw = readFileSync2(STATS_CACHE_PATH, "utf-8");
1315
+ raw = readFileSync4(STATS_CACHE_PATH, "utf-8");
1149
1316
  } catch (err) {
1150
1317
  log2(`[activity-cache] readFileSync failed: ${err.message}`);
1151
1318
  return;
@@ -1712,7 +1879,7 @@ function normalize(value) {
1712
1879
  }
1713
1880
 
1714
1881
  // src/lib/channel-hash-cache.ts
1715
- import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync } from "fs";
1882
+ import { existsSync as existsSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
1716
1883
  import { join as join4 } from "path";
1717
1884
  var CACHE_FILENAME = "channel-hash-cache.json";
1718
1885
  function getChannelHashCacheFile(configDir) {
@@ -1720,10 +1887,10 @@ function getChannelHashCacheFile(configDir) {
1720
1887
  }
1721
1888
  function loadChannelHashCache(target, configDir) {
1722
1889
  const path = getChannelHashCacheFile(configDir);
1723
- if (!existsSync2(path)) return;
1890
+ if (!existsSync3(path)) return;
1724
1891
  let parsed;
1725
1892
  try {
1726
- parsed = JSON.parse(readFileSync3(path, "utf-8"));
1893
+ parsed = JSON.parse(readFileSync5(path, "utf-8"));
1727
1894
  } catch {
1728
1895
  return;
1729
1896
  }
@@ -1737,7 +1904,7 @@ function saveChannelHashCache(source, configDir) {
1737
1904
  const obj = {};
1738
1905
  for (const [key, value] of source) obj[key] = value;
1739
1906
  try {
1740
- writeFileSync(path, JSON.stringify(obj, null, 2));
1907
+ writeFileSync2(path, JSON.stringify(obj, null, 2));
1741
1908
  } catch {
1742
1909
  }
1743
1910
  }
@@ -2210,6 +2377,16 @@ var agentState = {
2210
2377
  knownIntegrationHashes: /* @__PURE__ */ new Map(),
2211
2378
  knownManagedMcpHashes: /* @__PURE__ */ new Map(),
2212
2379
  knownManagedMcpStructure: /* @__PURE__ */ new Map(),
2380
+ /**
2381
+ * ENG-5858: hash of the effective sender_policy returned by
2382
+ * /host/refresh for this agent. Tracked separately from
2383
+ * `knownChannelConfigHashes` because the channel-cred hash is
2384
+ * per-channel (cleared/rewritten per channel) whereas sender_policy is
2385
+ * agent-wide. A change here triggers a session restart so the
2386
+ * slack-channel / teams-channel MCP children pick up the new env vars
2387
+ * (they read SLACK_SENDER_POLICY / MSTEAMS_SENDER_POLICY only at boot).
2388
+ */
2389
+ knownSenderPolicyHashes: /* @__PURE__ */ new Map(),
2213
2390
  // ---------------------------------------------------------------------------
2214
2391
  // codeName-keyed
2215
2392
  // ---------------------------------------------------------------------------
@@ -2239,6 +2416,7 @@ function clearAgentState(agentId, codeName) {
2239
2416
  agentState.knownIntegrationHashes.delete(agentId);
2240
2417
  agentState.knownManagedMcpHashes.delete(agentId);
2241
2418
  agentState.knownManagedMcpStructure.delete(agentId);
2419
+ agentState.knownSenderPolicyHashes.delete(agentId);
2242
2420
  agentState.agentDisplayNames.delete(codeName);
2243
2421
  agentState.codeNameToAgentId.delete(codeName);
2244
2422
  let channelCacheMutated = false;
@@ -2257,7 +2435,7 @@ function clearAgentState(agentId, codeName) {
2257
2435
  }
2258
2436
 
2259
2437
  // src/lib/restart-flags.ts
2260
- import { existsSync as existsSync3, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync4, renameSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
2438
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync6, renameSync as renameSync2, rmSync, writeFileSync as writeFileSync3 } from "fs";
2261
2439
  import { homedir as homedir3 } from "os";
2262
2440
  import { join as join5 } from "path";
2263
2441
  import { randomUUID } from "crypto";
@@ -2269,12 +2447,12 @@ function flagPath(codeName) {
2269
2447
  }
2270
2448
  function readRestartFlags() {
2271
2449
  const dir = restartFlagsDir();
2272
- if (!existsSync3(dir)) return [];
2450
+ if (!existsSync4(dir)) return [];
2273
2451
  const out = [];
2274
2452
  for (const entry of readdirSync2(dir)) {
2275
2453
  if (!entry.endsWith(".flag")) continue;
2276
2454
  try {
2277
- const raw = readFileSync4(join5(dir, entry), "utf8");
2455
+ const raw = readFileSync6(join5(dir, entry), "utf8");
2278
2456
  const parsed = JSON.parse(raw);
2279
2457
  if (typeof parsed.codeName !== "string" || parsed.codeName.length === 0) {
2280
2458
  parsed.codeName = entry.replace(/\.flag$/, "");
@@ -2292,7 +2470,7 @@ function readRestartFlags() {
2292
2470
  }
2293
2471
  function deleteRestartFlag(codeName) {
2294
2472
  const path = flagPath(codeName);
2295
- if (existsSync3(path)) {
2473
+ if (existsSync4(path)) {
2296
2474
  rmSync(path, { force: true });
2297
2475
  }
2298
2476
  }
@@ -2973,7 +3151,7 @@ var runningMcpHashes = /* @__PURE__ */ new Map();
2973
3151
  var runningMcpServerKeys = /* @__PURE__ */ new Map();
2974
3152
  function projectMcpHash(_codeName, projectDir) {
2975
3153
  try {
2976
- const raw = readFileSync5(join6(projectDir, ".mcp.json"), "utf-8");
3154
+ const raw = readFileSync7(join6(projectDir, ".mcp.json"), "utf-8");
2977
3155
  return createHash3("sha256").update(canonicalJson(JSON.parse(raw))).digest("hex");
2978
3156
  } catch {
2979
3157
  return null;
@@ -2981,7 +3159,7 @@ function projectMcpHash(_codeName, projectDir) {
2981
3159
  }
2982
3160
  function projectMcpKeys(_codeName, projectDir) {
2983
3161
  try {
2984
- const raw = readFileSync5(join6(projectDir, ".mcp.json"), "utf-8");
3162
+ const raw = readFileSync7(join6(projectDir, ".mcp.json"), "utf-8");
2985
3163
  const parsed = JSON.parse(raw);
2986
3164
  const servers = parsed.mcpServers;
2987
3165
  if (!servers || typeof servers !== "object") return /* @__PURE__ */ new Set();
@@ -2992,7 +3170,7 @@ function projectMcpKeys(_codeName, projectDir) {
2992
3170
  }
2993
3171
  function readMcpHttpServerConfig(projectDir, serverKey) {
2994
3172
  try {
2995
- const raw = readFileSync5(join6(projectDir, ".mcp.json"), "utf-8");
3173
+ const raw = readFileSync7(join6(projectDir, ".mcp.json"), "utf-8");
2996
3174
  const servers = JSON.parse(raw).mcpServers ?? {};
2997
3175
  const entry = servers[serverKey];
2998
3176
  if (entry && typeof entry.url === "string" && (entry.type === "http" || entry.type === void 0)) {
@@ -3166,7 +3344,7 @@ var cachedFrameworkVersion = null;
3166
3344
  var lastVersionCheckAt = 0;
3167
3345
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
3168
3346
  var lastResponsivenessProbeAt = 0;
3169
- var agtCliVersion = true ? "0.27.7-test.7" : "dev";
3347
+ var agtCliVersion = true ? "0.27.8-test.10" : "dev";
3170
3348
  function resolveBrewPath(execFileSync4) {
3171
3349
  try {
3172
3350
  const out = execFileSync4("which", ["brew"], { timeout: 5e3 }).toString().trim();
@@ -3179,7 +3357,7 @@ function resolveBrewPath(execFileSync4) {
3179
3357
  "/usr/local/bin/brew"
3180
3358
  ];
3181
3359
  for (const path of fallbacks) {
3182
- if (existsSync4(path)) return path;
3360
+ if (existsSync5(path)) return path;
3183
3361
  }
3184
3362
  return null;
3185
3363
  }
@@ -3189,7 +3367,7 @@ function claudeBinaryInstalled(execFileSync4) {
3189
3367
  "/opt/homebrew/bin/claude",
3190
3368
  "/usr/local/bin/claude"
3191
3369
  ];
3192
- if (canonical.some((path) => existsSync4(path))) return true;
3370
+ if (canonical.some((path) => existsSync5(path))) return true;
3193
3371
  try {
3194
3372
  execFileSync4("which", ["claude"], { timeout: 5e3 });
3195
3373
  return true;
@@ -3261,7 +3439,7 @@ async function ensureToolkitCli(toolkitSlug) {
3261
3439
  toolkitCliEnsured.add(toolkitSlug);
3262
3440
  return;
3263
3441
  }
3264
- brewBinDir = dirname(brewPath);
3442
+ brewBinDir = dirname3(brewPath);
3265
3443
  const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
3266
3444
  log(`[toolkit-install] ${toolkitSlug}: installing via brew (${pkg})\u2026`);
3267
3445
  if (isRoot) {
@@ -3342,8 +3520,8 @@ function claudeManagedSettingsPath() {
3342
3520
  function ensureClaudeManagedSettings(path = claudeManagedSettingsPath()) {
3343
3521
  try {
3344
3522
  let settings = {};
3345
- if (existsSync4(path)) {
3346
- const raw = readFileSync5(path, "utf-8").trim();
3523
+ if (existsSync5(path)) {
3524
+ const raw = readFileSync7(path, "utf-8").trim();
3347
3525
  if (raw) {
3348
3526
  let parsed;
3349
3527
  try {
@@ -3359,8 +3537,8 @@ function ensureClaudeManagedSettings(path = claudeManagedSettingsPath()) {
3359
3537
  }
3360
3538
  if (settings.channelsEnabled === true) return;
3361
3539
  settings.channelsEnabled = true;
3362
- mkdirSync2(dirname(path), { recursive: true });
3363
- writeFileSync3(path, `${JSON.stringify(settings, null, 2)}
3540
+ mkdirSync4(dirname3(path), { recursive: true });
3541
+ writeFileSync4(path, `${JSON.stringify(settings, null, 2)}
3364
3542
  `);
3365
3543
  log(`[managed-settings] set channelsEnabled:true in ${path} (ENG-5786 \u2014 unblocks Claude Code channels)`);
3366
3544
  } catch (err) {
@@ -3399,11 +3577,11 @@ async function ensureFrameworkBinary(frameworkId) {
3399
3577
  log(`Claude Code install failed: ${err.message}`);
3400
3578
  return;
3401
3579
  }
3402
- const brewBinDir = dirname(brewPath);
3580
+ const brewBinDir = dirname3(brewPath);
3403
3581
  if (!process.env.PATH?.split(":").includes(brewBinDir)) {
3404
3582
  process.env.PATH = `${brewBinDir}:${process.env.PATH ?? ""}`;
3405
3583
  }
3406
- if (existsSync4("/home/linuxbrew/.linuxbrew/bin/claude")) {
3584
+ if (existsSync5("/home/linuxbrew/.linuxbrew/bin/claude")) {
3407
3585
  log("Claude Code installed successfully");
3408
3586
  } else {
3409
3587
  log("Claude Code install completed but binary not found at expected path \u2014 check brew logs");
@@ -3419,13 +3597,13 @@ function claudeCodeUpgradeMarkerPath() {
3419
3597
  }
3420
3598
  function stampClaudeCodeUpgradeMarker() {
3421
3599
  try {
3422
- writeFileSync3(claudeCodeUpgradeMarkerPath(), String(Date.now()));
3600
+ writeFileSync4(claudeCodeUpgradeMarkerPath(), String(Date.now()));
3423
3601
  } catch {
3424
3602
  }
3425
3603
  }
3426
3604
  function claudeCodeUpgradeThrottled() {
3427
3605
  try {
3428
- const lastCheck = parseInt(readFileSync5(claudeCodeUpgradeMarkerPath(), "utf-8").trim(), 10);
3606
+ const lastCheck = parseInt(readFileSync7(claudeCodeUpgradeMarkerPath(), "utf-8").trim(), 10);
3429
3607
  if (!Number.isFinite(lastCheck)) return false;
3430
3608
  return Date.now() - lastCheck < CLAUDE_CODE_UPGRADE_CHECK_INTERVAL_MS;
3431
3609
  } catch {
@@ -3474,6 +3652,9 @@ ${r.stderr}`;
3474
3652
  });
3475
3653
  }
3476
3654
  var UPDATE_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
3655
+ function selfUpdateAppliedMarkerPath() {
3656
+ return join6(homedir4(), ".augmented", ".last-self-update-applied");
3657
+ }
3477
3658
  var selfUpdateUpToDateLogged = false;
3478
3659
  var restartAfterUpgrade = false;
3479
3660
  var pendingUpgradeVersion = null;
@@ -3537,6 +3718,22 @@ async function checkAndUpdateCliViaBrew() {
3537
3718
  if (agtOutdated) {
3538
3719
  const installed = agtOutdated.installed_versions?.[0] ?? "unknown";
3539
3720
  const latest = agtOutdated.current_version ?? "unknown";
3721
+ const coalesceWindowMs = resolveCoalesceWindowMs();
3722
+ const coalesce = decideSelfUpdateCoalesce({
3723
+ windowMs: coalesceWindowMs,
3724
+ lastAppliedMs: readLastSelfUpdateAppliedMs(selfUpdateAppliedMarkerPath()),
3725
+ now: Date.now()
3726
+ });
3727
+ if (!coalesce.proceed) {
3728
+ log(formatCoalesceDeferLogLine({
3729
+ installed,
3730
+ latest,
3731
+ channelLabel: "brew",
3732
+ remainingMs: coalesce.remainingMs,
3733
+ windowMs: coalesceWindowMs
3734
+ }));
3735
+ return;
3736
+ }
3540
3737
  log(`[self-update] agt CLI update available: ${installed} \u2192 ${latest}. Upgrading via brew...`);
3541
3738
  try {
3542
3739
  execFileSync4(brewPath, ["upgrade", "integrity-labs/tap/agt"], {
@@ -3544,6 +3741,7 @@ async function checkAndUpdateCliViaBrew() {
3544
3741
  stdio: "pipe"
3545
3742
  });
3546
3743
  log(`[self-update] agt CLI upgraded to ${latest}. Scheduling manager restart so the new binary takes effect.`);
3744
+ stampLastSelfUpdateApplied(selfUpdateAppliedMarkerPath());
3547
3745
  restartAfterUpgrade = true;
3548
3746
  pendingUpgradeVersion = latest;
3549
3747
  } catch (err) {
@@ -3604,6 +3802,22 @@ async function checkAndUpdateCliViaNpm() {
3604
3802
  }
3605
3803
  return;
3606
3804
  }
3805
+ const coalesceWindowMs = resolveCoalesceWindowMs();
3806
+ const coalesce = decideSelfUpdateCoalesce({
3807
+ windowMs: coalesceWindowMs,
3808
+ lastAppliedMs: readLastSelfUpdateAppliedMs(selfUpdateAppliedMarkerPath()),
3809
+ now: Date.now()
3810
+ });
3811
+ if (!coalesce.proceed) {
3812
+ log(formatCoalesceDeferLogLine({
3813
+ installed: agtCliVersion,
3814
+ latest,
3815
+ channelLabel: `npm, channel=${channel}`,
3816
+ remainingMs: coalesce.remainingMs,
3817
+ windowMs: coalesceWindowMs
3818
+ }));
3819
+ return;
3820
+ }
3607
3821
  log(`[self-update] agt CLI update available: ${agtCliVersion} \u2192 ${latest} (channel=${channel}). Upgrading via npm...`);
3608
3822
  const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
3609
3823
  const cmd = isRoot ? "npm" : "sudo";
@@ -3623,6 +3837,7 @@ async function checkAndUpdateCliViaNpm() {
3623
3837
  try {
3624
3838
  execFileSync4(cmd, args, { timeout: 18e4, stdio: "pipe" });
3625
3839
  log(`[self-update] agt CLI upgraded to ${latest}. Scheduling manager restart so the new binary takes effect.`);
3840
+ stampLastSelfUpdateApplied(selfUpdateAppliedMarkerPath());
3626
3841
  restartAfterUpgrade = true;
3627
3842
  pendingUpgradeVersion = latest;
3628
3843
  } catch (err) {
@@ -3685,7 +3900,7 @@ async function applyClaudeAuthToEnv(childEnv, label) {
3685
3900
  const claudeDir = join6(homedir4(), ".claude");
3686
3901
  for (const filename of [".credentials.json", "credentials.json"]) {
3687
3902
  const p = join6(claudeDir, filename);
3688
- if (existsSync4(p)) {
3903
+ if (existsSync5(p)) {
3689
3904
  try {
3690
3905
  rmSync2(p, { force: true });
3691
3906
  log(`[${label}] Removed ${p} (api_key mode \u2014 preventing OAuth fallback)`);
@@ -3699,14 +3914,14 @@ async function applyClaudeAuthToEnv(childEnv, label) {
3699
3914
  }
3700
3915
  function loadGatewayPorts() {
3701
3916
  try {
3702
- return JSON.parse(readFileSync5(GATEWAY_PORTS_FILE, "utf-8"));
3917
+ return JSON.parse(readFileSync7(GATEWAY_PORTS_FILE, "utf-8"));
3703
3918
  } catch {
3704
3919
  return {};
3705
3920
  }
3706
3921
  }
3707
3922
  function saveGatewayPorts(ports) {
3708
- mkdirSync2(AUGMENTED_DIR, { recursive: true });
3709
- writeFileSync3(GATEWAY_PORTS_FILE, JSON.stringify(ports, null, 2));
3923
+ mkdirSync4(AUGMENTED_DIR, { recursive: true });
3924
+ writeFileSync4(GATEWAY_PORTS_FILE, JSON.stringify(ports, null, 2));
3710
3925
  }
3711
3926
  function allocatePort(codeName) {
3712
3927
  const ports = loadGatewayPorts();
@@ -3743,7 +3958,7 @@ function saveChannelHashCache2() {
3743
3958
  function send(msg) {
3744
3959
  if (msg.type === "state-update") {
3745
3960
  try {
3746
- writeFileSync3(getStateFile(), JSON.stringify(msg.state, null, 2));
3961
+ atomicWriteFileSync(getStateFile(), JSON.stringify(msg.state, null, 2));
3747
3962
  } catch {
3748
3963
  }
3749
3964
  }
@@ -3775,8 +3990,8 @@ function log(msg) {
3775
3990
  if (!managerLogPath) {
3776
3991
  try {
3777
3992
  managerLogPath = join6(homedir4(), ".augmented", "manager.log");
3778
- mkdirSync2(dirname(managerLogPath), { recursive: true });
3779
- if (existsSync4(managerLogPath)) {
3993
+ mkdirSync4(dirname3(managerLogPath), { recursive: true });
3994
+ if (existsSync5(managerLogPath)) {
3780
3995
  chmodSync(managerLogPath, 384);
3781
3996
  }
3782
3997
  } catch {
@@ -3804,7 +4019,7 @@ function sha256(content) {
3804
4019
  }
3805
4020
  function hashFile(filePath) {
3806
4021
  try {
3807
- const content = readFileSync5(filePath, "utf-8");
4022
+ const content = readFileSync7(filePath, "utf-8");
3808
4023
  return sha256(content);
3809
4024
  } catch {
3810
4025
  return null;
@@ -3828,7 +4043,7 @@ function parseSkillFrontmatter(content) {
3828
4043
  return out;
3829
4044
  }
3830
4045
  async function refreshSkillsIndexInClaudeMd(configDir, codeName, log2) {
3831
- const { readdirSync: readdirSync4, readFileSync: rfs, existsSync: ex, writeFileSync: writeFileSync4 } = await import("fs");
4046
+ const { readdirSync: readdirSync4, readFileSync: rfs, existsSync: ex, writeFileSync: writeFileSync5 } = await import("fs");
3832
4047
  const skillsDir = join6(configDir, codeName, "project", ".claude", "skills");
3833
4048
  const claudeMdPath = join6(configDir, codeName, "project", "CLAUDE.md");
3834
4049
  if (!ex(skillsDir) || !ex(claudeMdPath)) return;
@@ -3876,7 +4091,7 @@ ${SKILLS_INDEX_END}`;
3876
4091
  next = current.trimEnd() + "\n\n" + section + "\n";
3877
4092
  }
3878
4093
  if (next !== current) {
3879
- writeFileSync4(claudeMdPath, next, "utf-8");
4094
+ writeFileSync5(claudeMdPath, next, "utf-8");
3880
4095
  log2(`Refreshed skills index in CLAUDE.md for '${codeName}' (${entries.length} skills)`);
3881
4096
  }
3882
4097
  }
@@ -3885,7 +4100,7 @@ async function migrateToProfiles() {
3885
4100
  const sharedConfigPath = join6(homeDir, ".openclaw", "openclaw.json");
3886
4101
  let sharedConfig;
3887
4102
  try {
3888
- sharedConfig = JSON.parse(readFileSync5(sharedConfigPath, "utf-8"));
4103
+ sharedConfig = JSON.parse(readFileSync7(sharedConfigPath, "utf-8"));
3889
4104
  } catch {
3890
4105
  return;
3891
4106
  }
@@ -3899,7 +4114,7 @@ async function migrateToProfiles() {
3899
4114
  if (!codeName) continue;
3900
4115
  if (codeName === "main") continue;
3901
4116
  const profileDir = join6(homeDir, `.openclaw-${codeName}`);
3902
- if (existsSync4(join6(profileDir, "openclaw.json"))) continue;
4117
+ if (existsSync5(join6(profileDir, "openclaw.json"))) continue;
3903
4118
  log(`Migrating agent '${codeName}' to per-agent profile`);
3904
4119
  if (adapter.seedProfileConfig) {
3905
4120
  adapter.seedProfileConfig(codeName);
@@ -3907,10 +4122,10 @@ async function migrateToProfiles() {
3907
4122
  const sharedAuthDir = join6(homeDir, ".openclaw", "agents", codeName, "agent");
3908
4123
  const profileAuthDir = join6(profileDir, "agents", codeName, "agent");
3909
4124
  const authFile = join6(sharedAuthDir, "auth-profiles.json");
3910
- if (existsSync4(authFile)) {
3911
- mkdirSync2(profileAuthDir, { recursive: true });
3912
- const authContent = readFileSync5(authFile, "utf-8");
3913
- writeFileSync3(join6(profileAuthDir, "auth-profiles.json"), authContent);
4125
+ if (existsSync5(authFile)) {
4126
+ mkdirSync4(profileAuthDir, { recursive: true });
4127
+ const authContent = readFileSync7(authFile, "utf-8");
4128
+ writeFileSync4(join6(profileAuthDir, "auth-profiles.json"), authContent);
3914
4129
  }
3915
4130
  allocatePort(codeName);
3916
4131
  migrated++;
@@ -3948,7 +4163,7 @@ function readGatewayToken(codeName) {
3948
4163
  }
3949
4164
  const homeDir = process.env["HOME"] ?? "/tmp";
3950
4165
  try {
3951
- const cfg = JSON.parse(readFileSync5(join6(homeDir, `.openclaw-${codeName}`, "openclaw.json"), "utf-8"));
4166
+ const cfg = JSON.parse(readFileSync7(join6(homeDir, `.openclaw-${codeName}`, "openclaw.json"), "utf-8"));
3952
4167
  return cfg?.gateway?.auth?.token;
3953
4168
  } catch {
3954
4169
  return void 0;
@@ -3958,9 +4173,9 @@ var GATEWAY_HUNG_TIMEOUT_MS = 5 * 6e4;
3958
4173
  function isGatewayHung(codeName) {
3959
4174
  const homeDir = process.env["HOME"] ?? "/tmp";
3960
4175
  const jobsPath = join6(homeDir, `.openclaw-${codeName}`, "cron", "jobs.json");
3961
- if (!existsSync4(jobsPath)) return false;
4176
+ if (!existsSync5(jobsPath)) return false;
3962
4177
  try {
3963
- const data = JSON.parse(readFileSync5(jobsPath, "utf-8"));
4178
+ const data = JSON.parse(readFileSync7(jobsPath, "utf-8"));
3964
4179
  const jobs = data.jobs ?? data;
3965
4180
  if (!Array.isArray(jobs)) return false;
3966
4181
  const now = Date.now();
@@ -4000,12 +4215,12 @@ async function ensureGatewayRunning(codeName, adapter) {
4000
4215
  try {
4001
4216
  const homeDir = process.env["HOME"] ?? "/tmp";
4002
4217
  const configPath = join6(homeDir, `.openclaw-${codeName}`, "openclaw.json");
4003
- if (existsSync4(configPath)) {
4004
- const cfg = JSON.parse(readFileSync5(configPath, "utf-8"));
4218
+ if (existsSync5(configPath)) {
4219
+ const cfg = JSON.parse(readFileSync7(configPath, "utf-8"));
4005
4220
  if (cfg.gateway?.port !== status.port) {
4006
4221
  if (!cfg.gateway) cfg.gateway = {};
4007
4222
  cfg.gateway.port = status.port;
4008
- writeFileSync3(configPath, JSON.stringify(cfg, null, 2));
4223
+ writeFileSync4(configPath, JSON.stringify(cfg, null, 2));
4009
4224
  }
4010
4225
  }
4011
4226
  } catch {
@@ -4026,11 +4241,11 @@ async function ensureGatewayRunning(codeName, adapter) {
4026
4241
  try {
4027
4242
  const homeDir = process.env["HOME"] ?? "/tmp";
4028
4243
  const configPath = join6(homeDir, `.openclaw-${codeName}`, "openclaw.json");
4029
- if (existsSync4(configPath)) {
4030
- const cfg = JSON.parse(readFileSync5(configPath, "utf-8"));
4244
+ if (existsSync5(configPath)) {
4245
+ const cfg = JSON.parse(readFileSync7(configPath, "utf-8"));
4031
4246
  if (!cfg.gateway) cfg.gateway = {};
4032
4247
  cfg.gateway.port = port;
4033
- writeFileSync3(configPath, JSON.stringify(cfg, null, 2));
4248
+ writeFileSync4(configPath, JSON.stringify(cfg, null, 2));
4034
4249
  }
4035
4250
  } catch {
4036
4251
  }
@@ -4180,7 +4395,7 @@ async function pollCycle() {
4180
4395
  }
4181
4396
  try {
4182
4397
  const { detectHostSecurity } = await import("../host-security-6PDFG7F5.js");
4183
- const { collectDiagnostics } = await import("../persistent-session-ICYFLUAM.js");
4398
+ const { collectDiagnostics } = await import("../persistent-session-HSHIZC2Q.js");
4184
4399
  const diagCodeNames = [...agentState.persistentSessionAgents];
4185
4400
  const agentDiagnostics = diagCodeNames.length > 0 ? collectDiagnostics(diagCodeNames) : void 0;
4186
4401
  let tailscaleHostname;
@@ -4248,7 +4463,7 @@ async function pollCycle() {
4248
4463
  const {
4249
4464
  collectResponsivenessProbes,
4250
4465
  getResponsivenessIntervalMs
4251
- } = await import("../responsiveness-probe-WZNQ2762.js");
4466
+ } = await import("../responsiveness-probe-RYKS5TRP.js");
4252
4467
  const probeIntervalMs = getResponsivenessIntervalMs();
4253
4468
  if (now - lastResponsivenessProbeAt > probeIntervalMs) {
4254
4469
  const probeCodeNames = [...agentState.persistentSessionAgents];
@@ -4363,7 +4578,7 @@ async function pollCycle() {
4363
4578
  if (restartAckStateChanged) {
4364
4579
  try {
4365
4580
  const ackedState = { ...state4, agents: agentStates };
4366
- writeFileSync3(getStateFile(), JSON.stringify(ackedState, null, 2));
4581
+ atomicWriteFileSync(getStateFile(), JSON.stringify(ackedState, null, 2));
4367
4582
  } catch (err) {
4368
4583
  log(`[restart] failed to persist ack immediately: ${err.message}`);
4369
4584
  }
@@ -4583,7 +4798,7 @@ async function processAgent(agent, agentStates) {
4583
4798
  const residuals = {
4584
4799
  gatewayRunning: gatewayLiveness.running,
4585
4800
  portAllocated: Object.prototype.hasOwnProperty.call(ports, agent.code_name),
4586
- provisionDirExists: existsSync4(agentDir)
4801
+ provisionDirExists: existsSync5(agentDir)
4587
4802
  };
4588
4803
  if (!hasRevokedResiduals(residuals)) {
4589
4804
  agentStates.push({
@@ -4742,7 +4957,7 @@ async function processAgent(agent, agentStates) {
4742
4957
  try {
4743
4958
  const artifacts = generateArtifacts(agent, refreshData, frameworkAdapter);
4744
4959
  const changedFiles = [];
4745
- mkdirSync2(agentDir, { recursive: true });
4960
+ mkdirSync4(agentDir, { recursive: true });
4746
4961
  for (const artifact of artifacts) {
4747
4962
  const filePath = join6(agentDir, artifact.relativePath);
4748
4963
  let existingHash;
@@ -4764,7 +4979,7 @@ async function processAgent(agent, agentStates) {
4764
4979
  newHash = sha256(stripDynamicSections(artifact.content));
4765
4980
  try {
4766
4981
  const projectClaudeMd = join6(config.configDir, agent.code_name, "project", "CLAUDE.md");
4767
- const existing = readFileSync5(projectClaudeMd, "utf-8");
4982
+ const existing = readFileSync7(projectClaudeMd, "utf-8");
4768
4983
  existingHash = sha256(stripDynamicSections(existing));
4769
4984
  } catch {
4770
4985
  existingHash = null;
@@ -4782,7 +4997,7 @@ async function processAgent(agent, agentStates) {
4782
4997
  const generatorKeys = Object.keys(generatorServers);
4783
4998
  let existingRaw = "";
4784
4999
  try {
4785
- existingRaw = readFileSync5(filePath, "utf-8");
5000
+ existingRaw = readFileSync7(filePath, "utf-8");
4786
5001
  } catch {
4787
5002
  }
4788
5003
  const existingServers = parseMcp(existingRaw);
@@ -4804,18 +5019,18 @@ async function processAgent(agent, agentStates) {
4804
5019
  }
4805
5020
  }
4806
5021
  if (changedFiles.length > 0) {
4807
- const isFirst = !existsSync4(join6(agentDir, "CHARTER.md"));
5022
+ const isFirst = !existsSync5(join6(agentDir, "CHARTER.md"));
4808
5023
  const verb = isFirst ? "Provisioning" : "Updating";
4809
5024
  const fileNames = changedFiles.map((f) => f.relativePath).join(", ");
4810
5025
  log(`${verb} '${agent.code_name}': ${fileNames}`);
4811
5026
  for (const file of changedFiles) {
4812
5027
  const filePath = join6(agentDir, file.relativePath);
4813
- mkdirSync2(dirname(filePath), { recursive: true });
4814
- writeFileSync3(filePath, file.content);
5028
+ mkdirSync4(dirname3(filePath), { recursive: true });
5029
+ writeFileSync4(filePath, file.content);
4815
5030
  }
4816
5031
  try {
4817
5032
  const provSkillsDir = join6(agentDir, ".claude", "skills");
4818
- if (existsSync4(provSkillsDir)) {
5033
+ if (existsSync5(provSkillsDir)) {
4819
5034
  for (const folder of readdirSync3(provSkillsDir)) {
4820
5035
  if (folder.startsWith("knowledge-")) {
4821
5036
  try {
@@ -4897,7 +5112,7 @@ async function processAgent(agent, agentStates) {
4897
5112
  }
4898
5113
  let lastDriftCheckAt = now;
4899
5114
  const written = agentState.writtenHashes.get(agent.agent_id);
4900
- if (written && existsSync4(agentDir)) {
5115
+ if (written && existsSync5(agentDir)) {
4901
5116
  const driftedFiles = [];
4902
5117
  for (const [file, expectedHash] of written) {
4903
5118
  const localHash = hashFile(join6(agentDir, file));
@@ -4975,14 +5190,16 @@ async function processAgent(agent, agentStates) {
4975
5190
  } : void 0;
4976
5191
  const peersForHash = channelId === "telegram" ? extractCharterTelegramPeers(refreshData.charter?.raw_content ?? "", gateContext) : channelId === "slack" ? extractCharterSlackPeers(refreshData.charter?.raw_content ?? "", gateContext) : null;
4977
5192
  const sessionModeForHash = refreshData.agent.session_mode;
4978
- const CHANNEL_WRITE_VERSION = 5;
5193
+ const senderPolicyForHash = refreshData.sender_policy ?? null;
5194
+ const CHANNEL_WRITE_VERSION = 8;
4979
5195
  const configHash = createHash3("sha256").update(
4980
5196
  canonicalJson({
4981
5197
  writeVersion: CHANNEL_WRITE_VERSION,
4982
5198
  config: entry.config,
4983
5199
  team: teamSettingsForHash,
4984
5200
  peers: peersForHash,
4985
- sessionMode: sessionModeForHash ?? null
5201
+ sessionMode: sessionModeForHash ?? null,
5202
+ senderPolicy: senderPolicyForHash
4986
5203
  })
4987
5204
  ).digest("hex");
4988
5205
  const cacheKey = `${agent.agent_id}:${channelId}`;
@@ -5008,6 +5225,7 @@ async function processAgent(agent, agentStates) {
5008
5225
  const telegramPeerDisabled = channelId === "telegram" ? peerDisabledMode === "all" : void 0;
5009
5226
  const telegramPeers = channelId === "telegram" ? extractCharterTelegramPeers(refreshData.charter?.raw_content ?? "", gateContext) : void 0;
5010
5227
  const slackPeers = channelId === "slack" ? extractCharterSlackPeers(refreshData.charter?.raw_content ?? "", gateContext) : void 0;
5228
+ const senderPolicyForCall = channelId === "slack" || channelId === "msteams" ? refreshData.sender_policy ?? void 0 : void 0;
5011
5229
  frameworkAdapter.writeChannelCredentials(
5012
5230
  agent.code_name,
5013
5231
  channelId,
@@ -5019,7 +5237,8 @@ async function processAgent(agent, agentStates) {
5019
5237
  peerDisabled,
5020
5238
  telegramPeers,
5021
5239
  slackPeers,
5022
- agentTimezone
5240
+ agentTimezone,
5241
+ senderPolicy: senderPolicyForCall
5023
5242
  }
5024
5243
  );
5025
5244
  agentState.knownChannelConfigHashes.set(cacheKey, configHash);
@@ -5065,18 +5284,58 @@ async function processAgent(agent, agentStates) {
5065
5284
  }
5066
5285
  scheduleSessionRestart(agent.code_name, delay, "new channel set");
5067
5286
  }
5287
+ if (channelConfigConverged) {
5288
+ const hasSenderPolicyChannel = currentChannelIds.has("slack") || currentChannelIds.has("msteams");
5289
+ const senderPolicyForRestartHash = refreshData.sender_policy ?? null;
5290
+ const senderPolicyHash = createHash3("sha256").update(canonicalJson({ senderPolicy: senderPolicyForRestartHash })).digest("hex");
5291
+ const prevSenderPolicyHash = agentState.knownSenderPolicyHashes.get(agent.agent_id);
5292
+ const senderPolicyDecision = hasSenderPolicyChannel ? decideSenderPolicyRestart({
5293
+ previousHash: prevSenderPolicyHash,
5294
+ currentHash: senderPolicyHash,
5295
+ sessionMode: refreshData.agent.session_mode,
5296
+ framework: agentFrameworkCache.get(agent.code_name) ?? "openclaw",
5297
+ sessionHealthy: isSessionHealthy(agent.code_name),
5298
+ channelSetRestartAlreadyScheduled: restartDecision.restart
5299
+ }) : { restart: false, firstPoll: prevSenderPolicyHash === void 0, changed: false };
5300
+ if (senderPolicyDecision.restart) {
5301
+ log(
5302
+ `[hot-reload] sender_policy changed for '${agent.code_name}' (${prevSenderPolicyHash?.slice(0, 8) ?? "first"} \u2192 ${senderPolicyHash.slice(0, 8)}) \u2014 restarting session`
5303
+ );
5304
+ const notice = "Your sender_policy has been updated. Restarting session shortly so the channel MCP servers pick up the new gate.";
5305
+ const delivered = await injectMessage(
5306
+ agent.code_name,
5307
+ "system",
5308
+ notice,
5309
+ { task_name: "sender-policy-update" },
5310
+ log
5311
+ ).catch(() => false);
5312
+ const delay = delivered ? 8e3 : 3e3;
5313
+ if (!delivered) {
5314
+ log(
5315
+ `[hot-reload] Inject notification unconfirmed for '${agent.code_name}' \u2014 proceeding with shorter delay`
5316
+ );
5317
+ }
5318
+ scheduleSessionRestart(
5319
+ agent.code_name,
5320
+ delay,
5321
+ "sender_policy change",
5322
+ "sender-policy-change"
5323
+ );
5324
+ }
5325
+ agentState.knownSenderPolicyHashes.set(agent.agent_id, senderPolicyHash);
5326
+ }
5068
5327
  const agentSessionMode = refreshData.agent.session_mode;
5069
5328
  if (agentSessionMode === "persistent" && (agentFrameworkCache.get(agent.code_name) ?? "openclaw") === "claude-code") {
5070
5329
  try {
5071
5330
  const agentProvisionDir = agentDir;
5072
5331
  const projectDir = join6(homedir4(), ".augmented", agent.code_name, "project");
5073
- mkdirSync2(agentProvisionDir, { recursive: true });
5074
- mkdirSync2(projectDir, { recursive: true });
5332
+ mkdirSync4(agentProvisionDir, { recursive: true });
5333
+ mkdirSync4(projectDir, { recursive: true });
5075
5334
  const provisionMcpPath = join6(agentProvisionDir, ".mcp.json");
5076
5335
  const projectMcpPath = join6(projectDir, ".mcp.json");
5077
5336
  let mcpConfig = { mcpServers: {} };
5078
5337
  try {
5079
- mcpConfig = JSON.parse(readFileSync5(provisionMcpPath, "utf-8"));
5338
+ mcpConfig = JSON.parse(readFileSync7(provisionMcpPath, "utf-8"));
5080
5339
  if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
5081
5340
  } catch {
5082
5341
  }
@@ -5086,7 +5345,7 @@ async function processAgent(agent, agentStates) {
5086
5345
  const tz = directChatTeamSettings?.["timezone"];
5087
5346
  return typeof tz === "string" && tz.trim() !== "" ? tz.trim() : void 0;
5088
5347
  })();
5089
- if (existsSync4(localDirectChatChannel) && !mcpConfig.mcpServers["direct-chat"]) {
5348
+ if (existsSync5(localDirectChatChannel) && !mcpConfig.mcpServers["direct-chat"]) {
5090
5349
  mcpConfig.mcpServers["direct-chat"] = {
5091
5350
  command: "node",
5092
5351
  args: [localDirectChatChannel],
@@ -5098,12 +5357,12 @@ async function processAgent(agent, agentStates) {
5098
5357
  }
5099
5358
  };
5100
5359
  const serialized = JSON.stringify(mcpConfig, null, 2);
5101
- writeFileSync3(provisionMcpPath, serialized);
5102
- writeFileSync3(projectMcpPath, serialized);
5360
+ writeFileSync4(provisionMcpPath, serialized);
5361
+ writeFileSync4(projectMcpPath, serialized);
5103
5362
  log(`Channel credentials written for '${agent.code_name}/direct-chat'`);
5104
5363
  }
5105
5364
  const staleChannelsPath = join6(projectDir, ".mcp-channels.json");
5106
- if (existsSync4(staleChannelsPath)) {
5365
+ if (existsSync5(staleChannelsPath)) {
5107
5366
  try {
5108
5367
  rmSync2(staleChannelsPath, { force: true });
5109
5368
  } catch {
@@ -5181,7 +5440,7 @@ async function processAgent(agent, agentStates) {
5181
5440
  const envIntPath = join6(projectDir, ".env.integrations");
5182
5441
  let preWriteEnv;
5183
5442
  try {
5184
- preWriteEnv = readFileSync5(envIntPath, "utf-8");
5443
+ preWriteEnv = readFileSync7(envIntPath, "utf-8");
5185
5444
  } catch {
5186
5445
  preWriteEnv = void 0;
5187
5446
  }
@@ -5194,8 +5453,8 @@ async function processAgent(agent, agentStates) {
5194
5453
  if (fw === "claude-code" && isSessionHealthy(agent.code_name)) {
5195
5454
  try {
5196
5455
  const projectMcpPath = join6(projectDir, ".mcp.json");
5197
- const postWriteEnv = readFileSync5(envIntPath, "utf-8");
5198
- const mcpContent = readFileSync5(projectMcpPath, "utf-8");
5456
+ const postWriteEnv = readFileSync7(envIntPath, "utf-8");
5457
+ const mcpContent = readFileSync7(projectMcpPath, "utf-8");
5199
5458
  const changedVars = diffEnvIntegrations(preWriteEnv, postWriteEnv);
5200
5459
  const mcpJsonForReap = JSON.parse(mcpContent);
5201
5460
  const affectedServerKeys = findMcpServersUsingVars(mcpJsonForReap, changedVars);
@@ -5263,8 +5522,8 @@ async function processAgent(agent, agentStates) {
5263
5522
  const mcpPath = frameworkAdapter.getMcpPath(agent.code_name);
5264
5523
  if (mcpPath) {
5265
5524
  try {
5266
- const { readFileSync: readFileSync6 } = await import("fs");
5267
- const mcpConfig = JSON.parse(readFileSync6(mcpPath, "utf-8"));
5525
+ const { readFileSync: readFileSync8 } = await import("fs");
5526
+ const mcpConfig = JSON.parse(readFileSync8(mcpPath, "utf-8"));
5268
5527
  if (mcpConfig.mcpServers) {
5269
5528
  const managedPrefixes = [
5270
5529
  "composio_",
@@ -5361,7 +5620,7 @@ async function processAgent(agent, agentStates) {
5361
5620
  if (frameworkAdapter.installPlugin) {
5362
5621
  try {
5363
5622
  const pluginPath = join6(process.cwd(), "packages", "openclaw-plugin-augmented", "src", "index.ts");
5364
- if (existsSync4(pluginPath)) {
5623
+ if (existsSync5(pluginPath)) {
5365
5624
  frameworkAdapter.installPlugin(agent.code_name, "augmented", pluginPath, {
5366
5625
  agtHost: requireHost(),
5367
5626
  agtApiKey: getApiKey() ?? void 0,
@@ -5440,7 +5699,7 @@ async function processAgent(agent, agentStates) {
5440
5699
  // install target but cheap to sweep.
5441
5700
  join6(agentDir, ".claude", "skills")
5442
5701
  ];
5443
- const existingDirs = candidateSkillDirs.filter((d) => existsSync4(d));
5702
+ const existingDirs = candidateSkillDirs.filter((d) => existsSync5(d));
5444
5703
  const discoveredEntries = /* @__PURE__ */ new Set();
5445
5704
  for (const dir of existingDirs) {
5446
5705
  try {
@@ -5455,7 +5714,7 @@ async function processAgent(agent, agentStates) {
5455
5714
  const removeSkillFolder = (entry, reason) => {
5456
5715
  for (const dir of existingDirs) {
5457
5716
  const p = join6(dir, entry);
5458
- if (existsSync4(p)) {
5717
+ if (existsSync5(p)) {
5459
5718
  rmSync3(p, { recursive: true, force: true });
5460
5719
  }
5461
5720
  }
@@ -5617,7 +5876,7 @@ async function processAgent(agent, agentStates) {
5617
5876
  let mcpJsonParsed = null;
5618
5877
  try {
5619
5878
  const mcpPath = join6(getProjectDir(agent.code_name), ".mcp.json");
5620
- mcpJsonParsed = JSON.parse(readFileSync5(mcpPath, "utf-8"));
5879
+ mcpJsonParsed = JSON.parse(readFileSync7(mcpPath, "utf-8"));
5621
5880
  } catch {
5622
5881
  }
5623
5882
  reapMissingMcpSessions({
@@ -5772,9 +6031,9 @@ async function processAgent(agent, agentStates) {
5772
6031
  if (agentFw === "openclaw" && gatewayRunning && gatewayPort) {
5773
6032
  const homeDir = process.env["HOME"] ?? "/tmp";
5774
6033
  const jobsPath = join6(homeDir, `.openclaw-${agent.code_name}`, "cron", "jobs.json");
5775
- if (existsSync4(jobsPath)) {
6034
+ if (existsSync5(jobsPath)) {
5776
6035
  try {
5777
- const jobsData = JSON.parse(readFileSync5(jobsPath, "utf-8"));
6036
+ const jobsData = JSON.parse(readFileSync7(jobsPath, "utf-8"));
5778
6037
  const kanbanJob = (jobsData.jobs ?? []).find(
5779
6038
  (j) => typeof j.name === "string" && j.name.includes("kanban-work")
5780
6039
  );
@@ -5908,7 +6167,7 @@ In progress for ${age} minutes \u2014 auto-failed`).catch(() => {
5908
6167
  }
5909
6168
  }
5910
6169
  const trackedFiles = frameworkAdapter.driftTrackedFiles();
5911
- if (trackedFiles.length > 0 && existsSync4(agentDir)) {
6170
+ if (trackedFiles.length > 0 && existsSync5(agentDir)) {
5912
6171
  const hashes = /* @__PURE__ */ new Map();
5913
6172
  for (const file of trackedFiles) {
5914
6173
  const h = hashFile(join6(agentDir, file));
@@ -5955,9 +6214,9 @@ function cleanupStaleSessions(codeName) {
5955
6214
  }
5956
6215
  function cleanupCronSessions(sessionsDir, keepCount) {
5957
6216
  const indexPath = join6(sessionsDir, "sessions.json");
5958
- if (!existsSync4(indexPath)) return;
6217
+ if (!existsSync5(indexPath)) return;
5959
6218
  try {
5960
- const raw = readFileSync5(indexPath, "utf-8");
6219
+ const raw = readFileSync7(indexPath, "utf-8");
5961
6220
  const index = JSON.parse(raw);
5962
6221
  const cronRunKeys = Object.keys(index).filter((k) => k.includes(":cron:") && k.includes(":run:")).map((k) => ({
5963
6222
  key: k,
@@ -5972,7 +6231,7 @@ function cleanupCronSessions(sessionsDir, keepCount) {
5972
6231
  if (entry.sessionId) {
5973
6232
  const sessionFile = join6(sessionsDir, `${entry.sessionId}.jsonl`);
5974
6233
  try {
5975
- if (existsSync4(sessionFile)) {
6234
+ if (existsSync5(sessionFile)) {
5976
6235
  unlinkSync(sessionFile);
5977
6236
  deletedFiles++;
5978
6237
  }
@@ -5993,7 +6252,7 @@ function cleanupCronSessions(sessionsDir, keepCount) {
5993
6252
  if (parentSessionId) {
5994
6253
  try {
5995
6254
  const f = join6(sessionsDir, `${parentSessionId}.jsonl`);
5996
- if (existsSync4(f)) {
6255
+ if (existsSync5(f)) {
5997
6256
  unlinkSync(f);
5998
6257
  deletedFiles++;
5999
6258
  }
@@ -6002,7 +6261,7 @@ function cleanupCronSessions(sessionsDir, keepCount) {
6002
6261
  }
6003
6262
  }
6004
6263
  }
6005
- writeFileSync3(indexPath, JSON.stringify(index));
6264
+ writeFileSync4(indexPath, JSON.stringify(index));
6006
6265
  if (toDelete.length > 0) {
6007
6266
  log(`Cleaned ${toDelete.length} cron session(s) and ${deletedFiles} file(s) from ${sessionsDir}`);
6008
6267
  }
@@ -6011,9 +6270,9 @@ function cleanupCronSessions(sessionsDir, keepCount) {
6011
6270
  }
6012
6271
  var STALE_RUN_TIMEOUT_MS = 5 * 6e4;
6013
6272
  function clearStaleCronRunState(jobsPath) {
6014
- if (!existsSync4(jobsPath)) return;
6273
+ if (!existsSync5(jobsPath)) return;
6015
6274
  try {
6016
- const raw = readFileSync5(jobsPath, "utf-8");
6275
+ const raw = readFileSync7(jobsPath, "utf-8");
6017
6276
  const data = JSON.parse(raw);
6018
6277
  const jobs = data.jobs ?? data;
6019
6278
  if (!Array.isArray(jobs)) return;
@@ -6038,13 +6297,13 @@ function clearStaleCronRunState(jobsPath) {
6038
6297
  }
6039
6298
  }
6040
6299
  if (changed) {
6041
- writeFileSync3(jobsPath, JSON.stringify(data, null, 2));
6300
+ writeFileSync4(jobsPath, JSON.stringify(data, null, 2));
6042
6301
  }
6043
6302
  } catch {
6044
6303
  }
6045
6304
  }
6046
6305
  function cleanupOldFiles(dir, maxAgeDays, ext) {
6047
- if (!existsSync4(dir)) return;
6306
+ if (!existsSync5(dir)) return;
6048
6307
  const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3;
6049
6308
  let removed = 0;
6050
6309
  try {
@@ -6069,6 +6328,25 @@ function cleanupOldFiles(dir, maxAgeDays, ext) {
6069
6328
  var inFlightClaudeTasks = /* @__PURE__ */ new Set();
6070
6329
  var claudeTaskConcurrency = /* @__PURE__ */ new Map();
6071
6330
  var MAX_CLAUDE_CONCURRENCY = 2;
6331
+ function claudePidFilePath() {
6332
+ return join6(homedir4(), ".augmented", "manager-claude-pids.json");
6333
+ }
6334
+ var inFlightClaudePids = /* @__PURE__ */ new Map();
6335
+ function registerClaudeSpawn(record) {
6336
+ inFlightClaudePids.set(record.pid, record);
6337
+ try {
6338
+ addSpawn(claudePidFilePath(), record);
6339
+ } catch (err) {
6340
+ log(`[drain] pid-file write failed: ${err.message}`);
6341
+ }
6342
+ }
6343
+ function unregisterClaudeSpawn(pid) {
6344
+ inFlightClaudePids.delete(pid);
6345
+ try {
6346
+ removeSpawn(claudePidFilePath(), pid);
6347
+ } catch {
6348
+ }
6349
+ }
6072
6350
  var claudeSchedulerStates = /* @__PURE__ */ new Map();
6073
6351
  async function syncAndCheckClaudeScheduler(agent, tasks, boardItems, refreshData) {
6074
6352
  const codeName = agent.code_name;
@@ -6340,9 +6618,9 @@ async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
6340
6618
  try {
6341
6619
  const claudeMdPath = join6(projectDir, "CLAUDE.md");
6342
6620
  const serverNames = [];
6343
- if (existsSync4(mcpConfigPath)) {
6621
+ if (existsSync5(mcpConfigPath)) {
6344
6622
  try {
6345
- const d = JSON.parse(readFileSync5(mcpConfigPath, "utf-8"));
6623
+ const d = JSON.parse(readFileSync7(mcpConfigPath, "utf-8"));
6346
6624
  if (d.mcpServers) serverNames.push(...Object.keys(d.mcpServers));
6347
6625
  } catch {
6348
6626
  }
@@ -6361,14 +6639,14 @@ async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
6361
6639
  "--allowedTools",
6362
6640
  allowedTools
6363
6641
  ];
6364
- if (existsSync4(claudeMdPath)) {
6642
+ if (existsSync5(claudeMdPath)) {
6365
6643
  claudeArgs.push("--system-prompt-file", claudeMdPath);
6366
6644
  }
6367
6645
  const childEnv = { ...process.env };
6368
6646
  const envIntPath = join6(projectDir, ".env.integrations");
6369
- if (existsSync4(envIntPath)) {
6647
+ if (existsSync5(envIntPath)) {
6370
6648
  try {
6371
- for (const line of readFileSync5(envIntPath, "utf-8").split("\n")) {
6649
+ for (const line of readFileSync7(envIntPath, "utf-8").split("\n")) {
6372
6650
  if (!line || line.startsWith("#") || !line.includes("=")) continue;
6373
6651
  const eqIdx = line.indexOf("=");
6374
6652
  childEnv[line.slice(0, eqIdx)] = line.slice(eqIdx + 1);
@@ -6392,11 +6670,21 @@ async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
6392
6670
  runId = startResult.run_id;
6393
6671
  kanbanItemId = startResult.kanban_item_id;
6394
6672
  if (runId) childEnv["AGT_RUN_ID"] = runId;
6673
+ const claudeKind = task.templateId === "kanban-work" ? "kanban-work" : "scheduled-task";
6395
6674
  const { stdout, stderr } = await execFilePromiseLong(resolveClaudeBinary(), claudeArgs, {
6396
6675
  cwd: projectDir,
6397
6676
  timeout: 3e5,
6398
6677
  stdin: "ignore",
6399
- env: childEnv
6678
+ env: childEnv,
6679
+ onSpawn: (pid) => registerClaudeSpawn({
6680
+ pid,
6681
+ started_at: Date.now(),
6682
+ kind: claudeKind,
6683
+ agent_id: agentId,
6684
+ agent_code_name: codeName,
6685
+ kanban_card_id: kanbanItemId ?? void 0
6686
+ }),
6687
+ onExit: (pid) => unregisterClaudeSpawn(pid)
6400
6688
  });
6401
6689
  if (stderr) {
6402
6690
  log(`[claude-scheduler] Task '${task.name}' stderr for '${codeName}': ${stderr.slice(0, 500)}`);
@@ -7170,9 +7458,9 @@ ${escapeXml(msg.content)}
7170
7458
  const projDir = ccProjectDir(agent.codeName);
7171
7459
  const mcpConfigPath = join6(projDir, ".mcp.json");
7172
7460
  const serverNames = [];
7173
- if (existsSync4(mcpConfigPath)) {
7461
+ if (existsSync5(mcpConfigPath)) {
7174
7462
  try {
7175
- const d = JSON.parse(readFileSync5(mcpConfigPath, "utf-8"));
7463
+ const d = JSON.parse(readFileSync7(mcpConfigPath, "utf-8"));
7176
7464
  if (d.mcpServers) serverNames.push(...Object.keys(d.mcpServers));
7177
7465
  } catch {
7178
7466
  }
@@ -7192,14 +7480,14 @@ ${escapeXml(msg.content)}
7192
7480
  allowedTools
7193
7481
  ];
7194
7482
  const chatClaudeMd = join6(projDir, "CLAUDE.md");
7195
- if (existsSync4(chatClaudeMd)) {
7483
+ if (existsSync5(chatClaudeMd)) {
7196
7484
  chatArgs.push("--system-prompt-file", chatClaudeMd);
7197
7485
  }
7198
7486
  const envIntPath = join6(projDir, ".env.integrations");
7199
7487
  const childEnv = { ...process.env };
7200
- if (existsSync4(envIntPath)) {
7488
+ if (existsSync5(envIntPath)) {
7201
7489
  try {
7202
- for (const line of readFileSync5(envIntPath, "utf-8").split("\n")) {
7490
+ for (const line of readFileSync7(envIntPath, "utf-8").split("\n")) {
7203
7491
  if (!line || line.startsWith("#") || !line.includes("=")) continue;
7204
7492
  const eqIdx = line.indexOf("=");
7205
7493
  childEnv[line.slice(0, eqIdx)] = line.slice(eqIdx + 1);
@@ -7212,7 +7500,19 @@ ${escapeXml(msg.content)}
7212
7500
  } catch (err) {
7213
7501
  throw new Error(`Auth resolve failed for '${agent.codeName}': ${err.message}`);
7214
7502
  }
7215
- const { stdout } = await execFilePromiseLong(resolveClaudeBinary(), chatArgs, { cwd: projDir, stdin: "ignore", env: childEnv });
7503
+ const { stdout } = await execFilePromiseLong(resolveClaudeBinary(), chatArgs, {
7504
+ cwd: projDir,
7505
+ stdin: "ignore",
7506
+ env: childEnv,
7507
+ onSpawn: (pid) => registerClaudeSpawn({
7508
+ pid,
7509
+ started_at: Date.now(),
7510
+ kind: "direct-chat",
7511
+ agent_id: agent.agentId,
7512
+ agent_code_name: agent.codeName
7513
+ }),
7514
+ onExit: (pid) => unregisterClaudeSpawn(pid)
7515
+ });
7216
7516
  reply = stdout.trim() || "[No response from agent]";
7217
7517
  } else {
7218
7518
  const { stdout } = await execFilePromiseLong("openclaw", [
@@ -7564,8 +7864,8 @@ function getBuiltInSkillContent(skillId) {
7564
7864
  join6(new URL(".", import.meta.url).pathname, "..", "..", "..", "..", "skills", skillId, "SKILL.md")
7565
7865
  ];
7566
7866
  for (const candidate of candidates) {
7567
- if (existsSync4(candidate)) {
7568
- const content = readFileSync5(candidate, "utf-8");
7867
+ if (existsSync5(candidate)) {
7868
+ const content = readFileSync7(candidate, "utf-8");
7569
7869
  const files = [{ relativePath: "SKILL.md", content }];
7570
7870
  builtInSkillCache.set(skillId, files);
7571
7871
  return files;
@@ -7736,6 +8036,12 @@ async function execFilePromiseLong(cmd, args, opts) {
7736
8036
  stdio: [opts?.stdin === "ignore" ? "ignore" : "pipe", "pipe", "pipe"],
7737
8037
  ...opts?.env ? { env: opts.env } : {}
7738
8038
  });
8039
+ if (opts?.onSpawn && typeof child.pid === "number") {
8040
+ try {
8041
+ opts.onSpawn(child.pid);
8042
+ } catch {
8043
+ }
8044
+ }
7739
8045
  let stdout = "";
7740
8046
  let stderr = "";
7741
8047
  child.stdout?.on("data", (d) => {
@@ -7750,6 +8056,12 @@ async function execFilePromiseLong(cmd, args, opts) {
7750
8056
  }, opts?.timeout ?? 12e4);
7751
8057
  child.on("close", (code) => {
7752
8058
  clearTimeout(timer);
8059
+ if (opts?.onExit && typeof child.pid === "number") {
8060
+ try {
8061
+ opts.onExit(child.pid);
8062
+ } catch {
8063
+ }
8064
+ }
7753
8065
  if (code !== 0) reject(new ChildProcessError(code, stdout, stderr));
7754
8066
  else resolve({ stdout, stderr });
7755
8067
  });
@@ -8196,7 +8508,7 @@ async function processClaudePairSessions(agents) {
8196
8508
  killPairSession,
8197
8509
  pairTmuxSession,
8198
8510
  finalizeClaudePairOnboarding
8199
- } = await import("../claude-pair-runtime-ZBQKBBMT.js");
8511
+ } = await import("../claude-pair-runtime-3UHTNKZ4.js");
8200
8512
  for (const pairId of pendingResp.cancelled_pair_ids ?? []) {
8201
8513
  log(`[claude-pair] sweeping orphan tmux session for pair ${pairId.slice(0, 8)}`);
8202
8514
  const killed = await killPairSession(pairTmuxSession(pairId));
@@ -8518,14 +8830,14 @@ async function syncMemories(agent, configDir, log2) {
8518
8830
  }
8519
8831
  pendingFreshMemorySync.delete(agent.agent_id);
8520
8832
  }
8521
- if (existsSync4(memoryDir)) {
8833
+ if (existsSync5(memoryDir)) {
8522
8834
  const prevHashes = memoryFileHashes.get(agent.agent_id) ?? /* @__PURE__ */ new Map();
8523
8835
  const currentHashes = /* @__PURE__ */ new Map();
8524
8836
  const changedMemories = [];
8525
8837
  for (const file of readdirSync3(memoryDir)) {
8526
8838
  if (!file.endsWith(".md")) continue;
8527
8839
  try {
8528
- const raw = readFileSync5(join6(memoryDir, file), "utf-8");
8840
+ const raw = readFileSync7(join6(memoryDir, file), "utf-8");
8529
8841
  const fileHash = createHash3("sha256").update(raw).digest("hex").slice(0, 16);
8530
8842
  currentHashes.set(file, fileHash);
8531
8843
  if (prevHashes.get(file) === fileHash) continue;
@@ -8550,7 +8862,7 @@ async function syncMemories(agent, configDir, log2) {
8550
8862
  } catch (err) {
8551
8863
  for (const mem of changedMemories) {
8552
8864
  for (const [file] of currentHashes) {
8553
- const parsed = parseMemoryFile(readFileSync5(join6(memoryDir, file), "utf-8"), file.replace(/\.md$/, ""));
8865
+ const parsed = parseMemoryFile(readFileSync7(join6(memoryDir, file), "utf-8"), file.replace(/\.md$/, ""));
8554
8866
  if (parsed?.name === mem.name) currentHashes.delete(file);
8555
8867
  }
8556
8868
  }
@@ -8563,7 +8875,7 @@ async function syncMemories(agent, configDir, log2) {
8563
8875
  }
8564
8876
  }
8565
8877
  async function downloadMemories(agent, memoryDir, log2, { force }) {
8566
- const localFiles = existsSync4(memoryDir) ? readdirSync3(memoryDir).filter((f) => f.endsWith(".md")).sort() : [];
8878
+ const localFiles = existsSync5(memoryDir) ? readdirSync3(memoryDir).filter((f) => f.endsWith(".md")).sort() : [];
8567
8879
  const localListHash = createHash3("sha256").update(localFiles.join(",")).digest("hex").slice(0, 16);
8568
8880
  const prevLocalHash = lastLocalFileHash.get(agent.agent_id);
8569
8881
  const prevDownload = lastDownloadHash.get(agent.agent_id);
@@ -8578,7 +8890,7 @@ async function downloadMemories(agent, memoryDir, log2, { force }) {
8578
8890
  lastDownloadHash.set(agent.agent_id, responseHash);
8579
8891
  lastLocalFileHash.set(agent.agent_id, localListHash);
8580
8892
  if (dbMemories.memories?.length) {
8581
- mkdirSync2(memoryDir, { recursive: true });
8893
+ mkdirSync4(memoryDir, { recursive: true });
8582
8894
  let written = 0;
8583
8895
  let overwritten = 0;
8584
8896
  for (let i = 0; i < dbMemories.memories.length; i++) {
@@ -8594,17 +8906,17 @@ description: ${JSON.stringify(mem.content.slice(0, 200))}
8594
8906
 
8595
8907
  ${mem.content}
8596
8908
  `;
8597
- if (existsSync4(filePath)) {
8909
+ if (existsSync5(filePath)) {
8598
8910
  let existing = "";
8599
8911
  try {
8600
- existing = readFileSync5(filePath, "utf-8");
8912
+ existing = readFileSync7(filePath, "utf-8");
8601
8913
  } catch {
8602
8914
  }
8603
8915
  if (existing === desired) continue;
8604
- writeFileSync3(filePath, desired);
8916
+ writeFileSync4(filePath, desired);
8605
8917
  overwritten++;
8606
8918
  } else {
8607
- writeFileSync3(filePath, desired);
8919
+ writeFileSync4(filePath, desired);
8608
8920
  written++;
8609
8921
  }
8610
8922
  }
@@ -8621,7 +8933,7 @@ ${mem.content}
8621
8933
  }
8622
8934
  }
8623
8935
  async function cleanupAgentFiles(codeName, agentDir) {
8624
- if (existsSync4(agentDir)) {
8936
+ if (existsSync5(agentDir)) {
8625
8937
  try {
8626
8938
  rmSync2(agentDir, { recursive: true, force: true });
8627
8939
  log(`Removed provision directory for '${codeName}'`);
@@ -8777,9 +9089,24 @@ async function stopPolling(opts = {}) {
8777
9089
  process.exit(opts.forcedExitCode ?? 1);
8778
9090
  }, 15e3);
8779
9091
  shutdownTimer.unref();
9092
+ const drainStartedAt = Date.now();
9093
+ const livePids = [...inFlightClaudePids.keys()];
9094
+ if (livePids.length > 0) {
9095
+ for (const pid of livePids) {
9096
+ try {
9097
+ process.kill(pid, "SIGTERM");
9098
+ } catch {
9099
+ }
9100
+ }
9101
+ log(formatDrainShutdownLine({ step: "sigterm-claude-pids", elapsedMs: Date.now() - drainStartedAt, killedPids: livePids }));
9102
+ } else {
9103
+ log(formatDrainShutdownLine({ step: "sigterm-claude-pids", elapsedMs: 0, note: "no in-flight claude -p children" }));
9104
+ }
9105
+ const subsysStartedAt = Date.now();
8780
9106
  stopCaffeinate();
8781
9107
  stopRealtimeChat();
8782
9108
  stopGatewayPool();
9109
+ log(formatDrainShutdownLine({ step: "stop-subsystems", elapsedMs: Date.now() - subsysStartedAt }));
8783
9110
  for (const codeName of [...scheduledRunsByCode.keys()]) {
8784
9111
  closeScheduledRunsForCode(codeName, "cancelled", "manager shutdown");
8785
9112
  }
@@ -8795,8 +9122,8 @@ function startManager(opts) {
8795
9122
  config = opts;
8796
9123
  try {
8797
9124
  const stateFile = getStateFile();
8798
- if (existsSync4(stateFile)) {
8799
- const raw = readFileSync5(stateFile, "utf-8");
9125
+ if (existsSync5(stateFile)) {
9126
+ const raw = readFileSync7(stateFile, "utf-8");
8800
9127
  const parsed = JSON.parse(raw);
8801
9128
  if (Array.isArray(parsed.agents)) {
8802
9129
  state4.agents = parsed.agents;
@@ -8816,8 +9143,77 @@ function startManager(opts) {
8816
9143
  );
8817
9144
  deployMcpAssets();
8818
9145
  reapOrphanChannelMcps({ log });
8819
- void ensureHostFrameworkBinaries();
8820
- startPolling();
9146
+ void (async () => {
9147
+ try {
9148
+ await reapOrphanedClaudePids();
9149
+ } catch (err) {
9150
+ log(`[drain] boot reaper failed: ${err.message}`);
9151
+ }
9152
+ void ensureHostFrameworkBinaries();
9153
+ startPolling();
9154
+ })();
9155
+ }
9156
+ async function reapOrphanedClaudePids() {
9157
+ const path = claudePidFilePath();
9158
+ const file = readPidFile(path);
9159
+ if (file.spawns.length === 0) return;
9160
+ const looksLikeClaude = (pid) => {
9161
+ if (process.platform !== "linux") return true;
9162
+ try {
9163
+ const comm = readFileSync7(`/proc/${pid}/comm`, "utf-8").trim().toLowerCase();
9164
+ return comm.includes("claude");
9165
+ } catch {
9166
+ return false;
9167
+ }
9168
+ };
9169
+ const decision = decideReaperActions(
9170
+ file,
9171
+ (pid) => {
9172
+ try {
9173
+ process.kill(pid, 0);
9174
+ return true;
9175
+ } catch {
9176
+ return false;
9177
+ }
9178
+ },
9179
+ looksLikeClaude
9180
+ );
9181
+ for (const spawn of decision.toKill) {
9182
+ try {
9183
+ process.kill(spawn.pid, "SIGKILL");
9184
+ } catch (err) {
9185
+ log(`[drain] reaper SIGKILL pid=${spawn.pid} failed: ${err.message}`);
9186
+ }
9187
+ }
9188
+ for (const spawn of decision.pidReusedSkipped) {
9189
+ log(
9190
+ `[drain] reaper skipped pid=${spawn.pid} agent=${spawn.agent_code_name} card=${spawn.kanban_card_id ?? "none"} \u2014 PID reuse suspected (identity probe rejected)`
9191
+ );
9192
+ }
9193
+ let kanbanReset = 0;
9194
+ for (const spawn of decision.toResetKanban) {
9195
+ try {
9196
+ await api.post("/host/kanban", {
9197
+ agent_id: spawn.agent_id,
9198
+ update: [{ id: spawn.kanban_card_id, status: "backlog" }]
9199
+ });
9200
+ kanbanReset += 1;
9201
+ } catch (err) {
9202
+ log(`[drain] reaper kanban-reset failed for card=${spawn.kanban_card_id} agent=${spawn.agent_code_name}: ${err.message}`);
9203
+ }
9204
+ }
9205
+ log(formatReaperBootLine({
9206
+ totalRecorded: file.spawns.length,
9207
+ killed: decision.toKill.length,
9208
+ alreadyDead: decision.alreadyDead.length,
9209
+ pidReusedSkipped: decision.pidReusedSkipped.length,
9210
+ kanbanReset
9211
+ }));
9212
+ try {
9213
+ writePidFile(path, { version: 1, spawns: [] });
9214
+ } catch (err) {
9215
+ log(`[drain] reaper file truncate failed: ${err.message}`);
9216
+ }
8821
9217
  }
8822
9218
  async function ensureHostFrameworkBinaries() {
8823
9219
  const apiKey = getApiKey();
@@ -8875,17 +9271,17 @@ function restartRunningChannelMcps(basenames) {
8875
9271
  }
8876
9272
  function deployMcpAssets() {
8877
9273
  const targetDir = join6(homedir4(), ".augmented", "_mcp");
8878
- mkdirSync2(targetDir, { recursive: true });
8879
- const moduleDir = dirname(fileURLToPath(import.meta.url));
9274
+ mkdirSync4(targetDir, { recursive: true });
9275
+ const moduleDir = dirname3(fileURLToPath(import.meta.url));
8880
9276
  let mcpSourceDir = "";
8881
9277
  let dir = moduleDir;
8882
9278
  for (let i = 0; i < 6; i++) {
8883
9279
  const candidate = join6(dir, "dist", "mcp");
8884
- if (existsSync4(join6(candidate, "index.js"))) {
9280
+ if (existsSync5(join6(candidate, "index.js"))) {
8885
9281
  mcpSourceDir = candidate;
8886
9282
  break;
8887
9283
  }
8888
- const parent = dirname(dir);
9284
+ const parent = dirname3(dir);
8889
9285
  if (parent === dir) break;
8890
9286
  dir = parent;
8891
9287
  }
@@ -8896,8 +9292,8 @@ function deployMcpAssets() {
8896
9292
  const changedBasenames = [];
8897
9293
  const fileHash = (p) => {
8898
9294
  try {
8899
- if (!existsSync4(p)) return null;
8900
- return createHash3("sha256").update(readFileSync5(p)).digest("hex");
9295
+ if (!existsSync5(p)) return null;
9296
+ return createHash3("sha256").update(readFileSync7(p)).digest("hex");
8901
9297
  } catch {
8902
9298
  return null;
8903
9299
  }
@@ -8910,7 +9306,7 @@ function deployMcpAssets() {
8910
9306
  for (const file of ["index.js", "slack-channel.js", "direct-chat-channel.js", "telegram-channel.js"]) {
8911
9307
  const src = join6(mcpSourceDir, file);
8912
9308
  const dst = join6(targetDir, file);
8913
- if (!existsSync4(src)) continue;
9309
+ if (!existsSync5(src)) continue;
8914
9310
  const before = fileHash(dst);
8915
9311
  try {
8916
9312
  copyFileSync(src, dst);
@@ -8930,20 +9326,20 @@ function deployMcpAssets() {
8930
9326
  const localMcpPath = join6(targetDir, "index.js");
8931
9327
  try {
8932
9328
  const agentsDir = join6(homedir4(), ".augmented", "agents");
8933
- if (existsSync4(agentsDir)) {
9329
+ if (existsSync5(agentsDir)) {
8934
9330
  for (const entry of readdirSync3(agentsDir, { withFileTypes: true })) {
8935
9331
  if (!entry.isDirectory()) continue;
8936
9332
  for (const subdir of ["provision", "project"]) {
8937
9333
  const mcpJsonPath = join6(agentsDir, entry.name, subdir, ".mcp.json");
8938
9334
  try {
8939
- const raw = readFileSync5(mcpJsonPath, "utf-8");
9335
+ const raw = readFileSync7(mcpJsonPath, "utf-8");
8940
9336
  if (!raw.includes("@integrity-labs/augmented-mcp")) continue;
8941
9337
  const mcpConfig = JSON.parse(raw);
8942
9338
  const augServer = mcpConfig.mcpServers?.["augmented"];
8943
9339
  if (!augServer) continue;
8944
9340
  augServer.command = "node";
8945
9341
  augServer.args = [localMcpPath];
8946
- writeFileSync3(mcpJsonPath, JSON.stringify(mcpConfig, null, 2));
9342
+ writeFileSync4(mcpJsonPath, JSON.stringify(mcpConfig, null, 2));
8947
9343
  log(`[manager] Patched ${entry.name}/${subdir}/.mcp.json: npx \u2192 node`);
8948
9344
  } catch {
8949
9345
  }