@integrity-labs/agt-cli 0.27.29 → 0.27.31

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-HX74HMG4.js";
18
+ } from "../chunk-E6MGOZTL.js";
19
19
  import {
20
20
  getProjectDir as getProjectDir2,
21
21
  getReadyTasks,
@@ -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 readFileSync6, writeFileSync as writeFileSync4, appendFileSync, mkdirSync as mkdirSync3, 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 as dirname2 } 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
 
@@ -988,6 +988,95 @@ function formatCoalesceDeferLogLine(opts) {
988
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
989
  }
990
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
+
991
1080
  // src/lib/usage-banner-monitor.ts
992
1081
  var MIN_CHECK_INTERVAL_MS = 6e4;
993
1082
  var PANE_TAIL_LINES_FOR_BANNER = 200;
@@ -1044,7 +1133,7 @@ async function maybeReportUsageBanner(args) {
1044
1133
  }
1045
1134
 
1046
1135
  // src/lib/token-usage-monitor.ts
1047
- import { readdirSync, readFileSync as readFileSync2, statSync } from "fs";
1136
+ import { readdirSync, readFileSync as readFileSync3, statSync } from "fs";
1048
1137
  import { join } from "path";
1049
1138
  var MIN_CHECK_INTERVAL_MS2 = 6e4;
1050
1139
  var TRANSCRIPT_MTIME_WINDOW_MS = 2 * 24 * 60 * 60 * 1e3;
@@ -1090,7 +1179,7 @@ async function maybeReportTokenUsage(args) {
1090
1179
  }
1091
1180
  let content;
1092
1181
  try {
1093
- content = readFileSync2(path, "utf-8");
1182
+ content = readFileSync3(path, "utf-8");
1094
1183
  } catch (err) {
1095
1184
  log2(`[token-usage] read failed for '${codeName}/${name}': ${err.message}`);
1096
1185
  continue;
@@ -1171,7 +1260,7 @@ async function maybeReportTokenUsage(args) {
1171
1260
  }
1172
1261
 
1173
1262
  // src/lib/activity-cache-monitor.ts
1174
- import { existsSync, readFileSync as readFileSync3 } from "fs";
1263
+ import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
1175
1264
  import { homedir } from "os";
1176
1265
  import { join as join2 } from "path";
1177
1266
  var MIN_CHECK_INTERVAL_MS3 = 6e4;
@@ -1218,12 +1307,12 @@ async function maybeReportActivityCache(args) {
1218
1307
  const nowMs = now.getTime();
1219
1308
  if (nowMs - state3.lastCheckedAt < MIN_CHECK_INTERVAL_MS3) return;
1220
1309
  state3.lastCheckedAt = nowMs;
1221
- if (!existsSync(STATS_CACHE_PATH)) {
1310
+ if (!existsSync2(STATS_CACHE_PATH)) {
1222
1311
  return;
1223
1312
  }
1224
1313
  let raw;
1225
1314
  try {
1226
- raw = readFileSync3(STATS_CACHE_PATH, "utf-8");
1315
+ raw = readFileSync4(STATS_CACHE_PATH, "utf-8");
1227
1316
  } catch (err) {
1228
1317
  log2(`[activity-cache] readFileSync failed: ${err.message}`);
1229
1318
  return;
@@ -1790,7 +1879,7 @@ function normalize(value) {
1790
1879
  }
1791
1880
 
1792
1881
  // src/lib/channel-hash-cache.ts
1793
- import { existsSync as existsSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
1882
+ import { existsSync as existsSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
1794
1883
  import { join as join4 } from "path";
1795
1884
  var CACHE_FILENAME = "channel-hash-cache.json";
1796
1885
  function getChannelHashCacheFile(configDir) {
@@ -1798,10 +1887,10 @@ function getChannelHashCacheFile(configDir) {
1798
1887
  }
1799
1888
  function loadChannelHashCache(target, configDir) {
1800
1889
  const path = getChannelHashCacheFile(configDir);
1801
- if (!existsSync2(path)) return;
1890
+ if (!existsSync3(path)) return;
1802
1891
  let parsed;
1803
1892
  try {
1804
- parsed = JSON.parse(readFileSync4(path, "utf-8"));
1893
+ parsed = JSON.parse(readFileSync5(path, "utf-8"));
1805
1894
  } catch {
1806
1895
  return;
1807
1896
  }
@@ -2346,7 +2435,7 @@ function clearAgentState(agentId, codeName) {
2346
2435
  }
2347
2436
 
2348
2437
  // src/lib/restart-flags.ts
2349
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync5, renameSync, rmSync, writeFileSync as writeFileSync3 } 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";
2350
2439
  import { homedir as homedir3 } from "os";
2351
2440
  import { join as join5 } from "path";
2352
2441
  import { randomUUID } from "crypto";
@@ -2358,12 +2447,12 @@ function flagPath(codeName) {
2358
2447
  }
2359
2448
  function readRestartFlags() {
2360
2449
  const dir = restartFlagsDir();
2361
- if (!existsSync3(dir)) return [];
2450
+ if (!existsSync4(dir)) return [];
2362
2451
  const out = [];
2363
2452
  for (const entry of readdirSync2(dir)) {
2364
2453
  if (!entry.endsWith(".flag")) continue;
2365
2454
  try {
2366
- const raw = readFileSync5(join5(dir, entry), "utf8");
2455
+ const raw = readFileSync6(join5(dir, entry), "utf8");
2367
2456
  const parsed = JSON.parse(raw);
2368
2457
  if (typeof parsed.codeName !== "string" || parsed.codeName.length === 0) {
2369
2458
  parsed.codeName = entry.replace(/\.flag$/, "");
@@ -2381,7 +2470,7 @@ function readRestartFlags() {
2381
2470
  }
2382
2471
  function deleteRestartFlag(codeName) {
2383
2472
  const path = flagPath(codeName);
2384
- if (existsSync3(path)) {
2473
+ if (existsSync4(path)) {
2385
2474
  rmSync(path, { force: true });
2386
2475
  }
2387
2476
  }
@@ -3062,7 +3151,7 @@ var runningMcpHashes = /* @__PURE__ */ new Map();
3062
3151
  var runningMcpServerKeys = /* @__PURE__ */ new Map();
3063
3152
  function projectMcpHash(_codeName, projectDir) {
3064
3153
  try {
3065
- const raw = readFileSync6(join6(projectDir, ".mcp.json"), "utf-8");
3154
+ const raw = readFileSync7(join6(projectDir, ".mcp.json"), "utf-8");
3066
3155
  return createHash3("sha256").update(canonicalJson(JSON.parse(raw))).digest("hex");
3067
3156
  } catch {
3068
3157
  return null;
@@ -3070,7 +3159,7 @@ function projectMcpHash(_codeName, projectDir) {
3070
3159
  }
3071
3160
  function projectMcpKeys(_codeName, projectDir) {
3072
3161
  try {
3073
- const raw = readFileSync6(join6(projectDir, ".mcp.json"), "utf-8");
3162
+ const raw = readFileSync7(join6(projectDir, ".mcp.json"), "utf-8");
3074
3163
  const parsed = JSON.parse(raw);
3075
3164
  const servers = parsed.mcpServers;
3076
3165
  if (!servers || typeof servers !== "object") return /* @__PURE__ */ new Set();
@@ -3081,7 +3170,7 @@ function projectMcpKeys(_codeName, projectDir) {
3081
3170
  }
3082
3171
  function readMcpHttpServerConfig(projectDir, serverKey) {
3083
3172
  try {
3084
- const raw = readFileSync6(join6(projectDir, ".mcp.json"), "utf-8");
3173
+ const raw = readFileSync7(join6(projectDir, ".mcp.json"), "utf-8");
3085
3174
  const servers = JSON.parse(raw).mcpServers ?? {};
3086
3175
  const entry = servers[serverKey];
3087
3176
  if (entry && typeof entry.url === "string" && (entry.type === "http" || entry.type === void 0)) {
@@ -3255,7 +3344,7 @@ var cachedFrameworkVersion = null;
3255
3344
  var lastVersionCheckAt = 0;
3256
3345
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
3257
3346
  var lastResponsivenessProbeAt = 0;
3258
- var agtCliVersion = true ? "0.27.29" : "dev";
3347
+ var agtCliVersion = true ? "0.27.31" : "dev";
3259
3348
  function resolveBrewPath(execFileSync4) {
3260
3349
  try {
3261
3350
  const out = execFileSync4("which", ["brew"], { timeout: 5e3 }).toString().trim();
@@ -3268,7 +3357,7 @@ function resolveBrewPath(execFileSync4) {
3268
3357
  "/usr/local/bin/brew"
3269
3358
  ];
3270
3359
  for (const path of fallbacks) {
3271
- if (existsSync4(path)) return path;
3360
+ if (existsSync5(path)) return path;
3272
3361
  }
3273
3362
  return null;
3274
3363
  }
@@ -3278,7 +3367,7 @@ function claudeBinaryInstalled(execFileSync4) {
3278
3367
  "/opt/homebrew/bin/claude",
3279
3368
  "/usr/local/bin/claude"
3280
3369
  ];
3281
- if (canonical.some((path) => existsSync4(path))) return true;
3370
+ if (canonical.some((path) => existsSync5(path))) return true;
3282
3371
  try {
3283
3372
  execFileSync4("which", ["claude"], { timeout: 5e3 });
3284
3373
  return true;
@@ -3350,7 +3439,7 @@ async function ensureToolkitCli(toolkitSlug) {
3350
3439
  toolkitCliEnsured.add(toolkitSlug);
3351
3440
  return;
3352
3441
  }
3353
- brewBinDir = dirname2(brewPath);
3442
+ brewBinDir = dirname3(brewPath);
3354
3443
  const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
3355
3444
  log(`[toolkit-install] ${toolkitSlug}: installing via brew (${pkg})\u2026`);
3356
3445
  if (isRoot) {
@@ -3431,8 +3520,8 @@ function claudeManagedSettingsPath() {
3431
3520
  function ensureClaudeManagedSettings(path = claudeManagedSettingsPath()) {
3432
3521
  try {
3433
3522
  let settings = {};
3434
- if (existsSync4(path)) {
3435
- const raw = readFileSync6(path, "utf-8").trim();
3523
+ if (existsSync5(path)) {
3524
+ const raw = readFileSync7(path, "utf-8").trim();
3436
3525
  if (raw) {
3437
3526
  let parsed;
3438
3527
  try {
@@ -3448,7 +3537,7 @@ function ensureClaudeManagedSettings(path = claudeManagedSettingsPath()) {
3448
3537
  }
3449
3538
  if (settings.channelsEnabled === true) return;
3450
3539
  settings.channelsEnabled = true;
3451
- mkdirSync3(dirname2(path), { recursive: true });
3540
+ mkdirSync4(dirname3(path), { recursive: true });
3452
3541
  writeFileSync4(path, `${JSON.stringify(settings, null, 2)}
3453
3542
  `);
3454
3543
  log(`[managed-settings] set channelsEnabled:true in ${path} (ENG-5786 \u2014 unblocks Claude Code channels)`);
@@ -3488,11 +3577,11 @@ async function ensureFrameworkBinary(frameworkId) {
3488
3577
  log(`Claude Code install failed: ${err.message}`);
3489
3578
  return;
3490
3579
  }
3491
- const brewBinDir = dirname2(brewPath);
3580
+ const brewBinDir = dirname3(brewPath);
3492
3581
  if (!process.env.PATH?.split(":").includes(brewBinDir)) {
3493
3582
  process.env.PATH = `${brewBinDir}:${process.env.PATH ?? ""}`;
3494
3583
  }
3495
- if (existsSync4("/home/linuxbrew/.linuxbrew/bin/claude")) {
3584
+ if (existsSync5("/home/linuxbrew/.linuxbrew/bin/claude")) {
3496
3585
  log("Claude Code installed successfully");
3497
3586
  } else {
3498
3587
  log("Claude Code install completed but binary not found at expected path \u2014 check brew logs");
@@ -3514,7 +3603,7 @@ function stampClaudeCodeUpgradeMarker() {
3514
3603
  }
3515
3604
  function claudeCodeUpgradeThrottled() {
3516
3605
  try {
3517
- const lastCheck = parseInt(readFileSync6(claudeCodeUpgradeMarkerPath(), "utf-8").trim(), 10);
3606
+ const lastCheck = parseInt(readFileSync7(claudeCodeUpgradeMarkerPath(), "utf-8").trim(), 10);
3518
3607
  if (!Number.isFinite(lastCheck)) return false;
3519
3608
  return Date.now() - lastCheck < CLAUDE_CODE_UPGRADE_CHECK_INTERVAL_MS;
3520
3609
  } catch {
@@ -3811,7 +3900,7 @@ async function applyClaudeAuthToEnv(childEnv, label) {
3811
3900
  const claudeDir = join6(homedir4(), ".claude");
3812
3901
  for (const filename of [".credentials.json", "credentials.json"]) {
3813
3902
  const p = join6(claudeDir, filename);
3814
- if (existsSync4(p)) {
3903
+ if (existsSync5(p)) {
3815
3904
  try {
3816
3905
  rmSync2(p, { force: true });
3817
3906
  log(`[${label}] Removed ${p} (api_key mode \u2014 preventing OAuth fallback)`);
@@ -3825,13 +3914,13 @@ async function applyClaudeAuthToEnv(childEnv, label) {
3825
3914
  }
3826
3915
  function loadGatewayPorts() {
3827
3916
  try {
3828
- return JSON.parse(readFileSync6(GATEWAY_PORTS_FILE, "utf-8"));
3917
+ return JSON.parse(readFileSync7(GATEWAY_PORTS_FILE, "utf-8"));
3829
3918
  } catch {
3830
3919
  return {};
3831
3920
  }
3832
3921
  }
3833
3922
  function saveGatewayPorts(ports) {
3834
- mkdirSync3(AUGMENTED_DIR, { recursive: true });
3923
+ mkdirSync4(AUGMENTED_DIR, { recursive: true });
3835
3924
  writeFileSync4(GATEWAY_PORTS_FILE, JSON.stringify(ports, null, 2));
3836
3925
  }
3837
3926
  function allocatePort(codeName) {
@@ -3869,7 +3958,7 @@ function saveChannelHashCache2() {
3869
3958
  function send(msg) {
3870
3959
  if (msg.type === "state-update") {
3871
3960
  try {
3872
- writeFileSync4(getStateFile(), JSON.stringify(msg.state, null, 2));
3961
+ atomicWriteFileSync(getStateFile(), JSON.stringify(msg.state, null, 2));
3873
3962
  } catch {
3874
3963
  }
3875
3964
  }
@@ -3901,8 +3990,8 @@ function log(msg) {
3901
3990
  if (!managerLogPath) {
3902
3991
  try {
3903
3992
  managerLogPath = join6(homedir4(), ".augmented", "manager.log");
3904
- mkdirSync3(dirname2(managerLogPath), { recursive: true });
3905
- if (existsSync4(managerLogPath)) {
3993
+ mkdirSync4(dirname3(managerLogPath), { recursive: true });
3994
+ if (existsSync5(managerLogPath)) {
3906
3995
  chmodSync(managerLogPath, 384);
3907
3996
  }
3908
3997
  } catch {
@@ -3930,7 +4019,7 @@ function sha256(content) {
3930
4019
  }
3931
4020
  function hashFile(filePath) {
3932
4021
  try {
3933
- const content = readFileSync6(filePath, "utf-8");
4022
+ const content = readFileSync7(filePath, "utf-8");
3934
4023
  return sha256(content);
3935
4024
  } catch {
3936
4025
  return null;
@@ -4011,7 +4100,7 @@ async function migrateToProfiles() {
4011
4100
  const sharedConfigPath = join6(homeDir, ".openclaw", "openclaw.json");
4012
4101
  let sharedConfig;
4013
4102
  try {
4014
- sharedConfig = JSON.parse(readFileSync6(sharedConfigPath, "utf-8"));
4103
+ sharedConfig = JSON.parse(readFileSync7(sharedConfigPath, "utf-8"));
4015
4104
  } catch {
4016
4105
  return;
4017
4106
  }
@@ -4025,7 +4114,7 @@ async function migrateToProfiles() {
4025
4114
  if (!codeName) continue;
4026
4115
  if (codeName === "main") continue;
4027
4116
  const profileDir = join6(homeDir, `.openclaw-${codeName}`);
4028
- if (existsSync4(join6(profileDir, "openclaw.json"))) continue;
4117
+ if (existsSync5(join6(profileDir, "openclaw.json"))) continue;
4029
4118
  log(`Migrating agent '${codeName}' to per-agent profile`);
4030
4119
  if (adapter.seedProfileConfig) {
4031
4120
  adapter.seedProfileConfig(codeName);
@@ -4033,9 +4122,9 @@ async function migrateToProfiles() {
4033
4122
  const sharedAuthDir = join6(homeDir, ".openclaw", "agents", codeName, "agent");
4034
4123
  const profileAuthDir = join6(profileDir, "agents", codeName, "agent");
4035
4124
  const authFile = join6(sharedAuthDir, "auth-profiles.json");
4036
- if (existsSync4(authFile)) {
4037
- mkdirSync3(profileAuthDir, { recursive: true });
4038
- const authContent = readFileSync6(authFile, "utf-8");
4125
+ if (existsSync5(authFile)) {
4126
+ mkdirSync4(profileAuthDir, { recursive: true });
4127
+ const authContent = readFileSync7(authFile, "utf-8");
4039
4128
  writeFileSync4(join6(profileAuthDir, "auth-profiles.json"), authContent);
4040
4129
  }
4041
4130
  allocatePort(codeName);
@@ -4074,7 +4163,7 @@ function readGatewayToken(codeName) {
4074
4163
  }
4075
4164
  const homeDir = process.env["HOME"] ?? "/tmp";
4076
4165
  try {
4077
- const cfg = JSON.parse(readFileSync6(join6(homeDir, `.openclaw-${codeName}`, "openclaw.json"), "utf-8"));
4166
+ const cfg = JSON.parse(readFileSync7(join6(homeDir, `.openclaw-${codeName}`, "openclaw.json"), "utf-8"));
4078
4167
  return cfg?.gateway?.auth?.token;
4079
4168
  } catch {
4080
4169
  return void 0;
@@ -4084,9 +4173,9 @@ var GATEWAY_HUNG_TIMEOUT_MS = 5 * 6e4;
4084
4173
  function isGatewayHung(codeName) {
4085
4174
  const homeDir = process.env["HOME"] ?? "/tmp";
4086
4175
  const jobsPath = join6(homeDir, `.openclaw-${codeName}`, "cron", "jobs.json");
4087
- if (!existsSync4(jobsPath)) return false;
4176
+ if (!existsSync5(jobsPath)) return false;
4088
4177
  try {
4089
- const data = JSON.parse(readFileSync6(jobsPath, "utf-8"));
4178
+ const data = JSON.parse(readFileSync7(jobsPath, "utf-8"));
4090
4179
  const jobs = data.jobs ?? data;
4091
4180
  if (!Array.isArray(jobs)) return false;
4092
4181
  const now = Date.now();
@@ -4126,8 +4215,8 @@ async function ensureGatewayRunning(codeName, adapter) {
4126
4215
  try {
4127
4216
  const homeDir = process.env["HOME"] ?? "/tmp";
4128
4217
  const configPath = join6(homeDir, `.openclaw-${codeName}`, "openclaw.json");
4129
- if (existsSync4(configPath)) {
4130
- const cfg = JSON.parse(readFileSync6(configPath, "utf-8"));
4218
+ if (existsSync5(configPath)) {
4219
+ const cfg = JSON.parse(readFileSync7(configPath, "utf-8"));
4131
4220
  if (cfg.gateway?.port !== status.port) {
4132
4221
  if (!cfg.gateway) cfg.gateway = {};
4133
4222
  cfg.gateway.port = status.port;
@@ -4152,8 +4241,8 @@ async function ensureGatewayRunning(codeName, adapter) {
4152
4241
  try {
4153
4242
  const homeDir = process.env["HOME"] ?? "/tmp";
4154
4243
  const configPath = join6(homeDir, `.openclaw-${codeName}`, "openclaw.json");
4155
- if (existsSync4(configPath)) {
4156
- const cfg = JSON.parse(readFileSync6(configPath, "utf-8"));
4244
+ if (existsSync5(configPath)) {
4245
+ const cfg = JSON.parse(readFileSync7(configPath, "utf-8"));
4157
4246
  if (!cfg.gateway) cfg.gateway = {};
4158
4247
  cfg.gateway.port = port;
4159
4248
  writeFileSync4(configPath, JSON.stringify(cfg, null, 2));
@@ -4489,7 +4578,7 @@ async function pollCycle() {
4489
4578
  if (restartAckStateChanged) {
4490
4579
  try {
4491
4580
  const ackedState = { ...state4, agents: agentStates };
4492
- writeFileSync4(getStateFile(), JSON.stringify(ackedState, null, 2));
4581
+ atomicWriteFileSync(getStateFile(), JSON.stringify(ackedState, null, 2));
4493
4582
  } catch (err) {
4494
4583
  log(`[restart] failed to persist ack immediately: ${err.message}`);
4495
4584
  }
@@ -4709,7 +4798,7 @@ async function processAgent(agent, agentStates) {
4709
4798
  const residuals = {
4710
4799
  gatewayRunning: gatewayLiveness.running,
4711
4800
  portAllocated: Object.prototype.hasOwnProperty.call(ports, agent.code_name),
4712
- provisionDirExists: existsSync4(agentDir)
4801
+ provisionDirExists: existsSync5(agentDir)
4713
4802
  };
4714
4803
  if (!hasRevokedResiduals(residuals)) {
4715
4804
  agentStates.push({
@@ -4868,7 +4957,7 @@ async function processAgent(agent, agentStates) {
4868
4957
  try {
4869
4958
  const artifacts = generateArtifacts(agent, refreshData, frameworkAdapter);
4870
4959
  const changedFiles = [];
4871
- mkdirSync3(agentDir, { recursive: true });
4960
+ mkdirSync4(agentDir, { recursive: true });
4872
4961
  for (const artifact of artifacts) {
4873
4962
  const filePath = join6(agentDir, artifact.relativePath);
4874
4963
  let existingHash;
@@ -4890,7 +4979,7 @@ async function processAgent(agent, agentStates) {
4890
4979
  newHash = sha256(stripDynamicSections(artifact.content));
4891
4980
  try {
4892
4981
  const projectClaudeMd = join6(config.configDir, agent.code_name, "project", "CLAUDE.md");
4893
- const existing = readFileSync6(projectClaudeMd, "utf-8");
4982
+ const existing = readFileSync7(projectClaudeMd, "utf-8");
4894
4983
  existingHash = sha256(stripDynamicSections(existing));
4895
4984
  } catch {
4896
4985
  existingHash = null;
@@ -4908,7 +4997,7 @@ async function processAgent(agent, agentStates) {
4908
4997
  const generatorKeys = Object.keys(generatorServers);
4909
4998
  let existingRaw = "";
4910
4999
  try {
4911
- existingRaw = readFileSync6(filePath, "utf-8");
5000
+ existingRaw = readFileSync7(filePath, "utf-8");
4912
5001
  } catch {
4913
5002
  }
4914
5003
  const existingServers = parseMcp(existingRaw);
@@ -4930,18 +5019,18 @@ async function processAgent(agent, agentStates) {
4930
5019
  }
4931
5020
  }
4932
5021
  if (changedFiles.length > 0) {
4933
- const isFirst = !existsSync4(join6(agentDir, "CHARTER.md"));
5022
+ const isFirst = !existsSync5(join6(agentDir, "CHARTER.md"));
4934
5023
  const verb = isFirst ? "Provisioning" : "Updating";
4935
5024
  const fileNames = changedFiles.map((f) => f.relativePath).join(", ");
4936
5025
  log(`${verb} '${agent.code_name}': ${fileNames}`);
4937
5026
  for (const file of changedFiles) {
4938
5027
  const filePath = join6(agentDir, file.relativePath);
4939
- mkdirSync3(dirname2(filePath), { recursive: true });
5028
+ mkdirSync4(dirname3(filePath), { recursive: true });
4940
5029
  writeFileSync4(filePath, file.content);
4941
5030
  }
4942
5031
  try {
4943
5032
  const provSkillsDir = join6(agentDir, ".claude", "skills");
4944
- if (existsSync4(provSkillsDir)) {
5033
+ if (existsSync5(provSkillsDir)) {
4945
5034
  for (const folder of readdirSync3(provSkillsDir)) {
4946
5035
  if (folder.startsWith("knowledge-")) {
4947
5036
  try {
@@ -5023,7 +5112,7 @@ async function processAgent(agent, agentStates) {
5023
5112
  }
5024
5113
  let lastDriftCheckAt = now;
5025
5114
  const written = agentState.writtenHashes.get(agent.agent_id);
5026
- if (written && existsSync4(agentDir)) {
5115
+ if (written && existsSync5(agentDir)) {
5027
5116
  const driftedFiles = [];
5028
5117
  for (const [file, expectedHash] of written) {
5029
5118
  const localHash = hashFile(join6(agentDir, file));
@@ -5240,13 +5329,13 @@ async function processAgent(agent, agentStates) {
5240
5329
  try {
5241
5330
  const agentProvisionDir = agentDir;
5242
5331
  const projectDir = join6(homedir4(), ".augmented", agent.code_name, "project");
5243
- mkdirSync3(agentProvisionDir, { recursive: true });
5244
- mkdirSync3(projectDir, { recursive: true });
5332
+ mkdirSync4(agentProvisionDir, { recursive: true });
5333
+ mkdirSync4(projectDir, { recursive: true });
5245
5334
  const provisionMcpPath = join6(agentProvisionDir, ".mcp.json");
5246
5335
  const projectMcpPath = join6(projectDir, ".mcp.json");
5247
5336
  let mcpConfig = { mcpServers: {} };
5248
5337
  try {
5249
- mcpConfig = JSON.parse(readFileSync6(provisionMcpPath, "utf-8"));
5338
+ mcpConfig = JSON.parse(readFileSync7(provisionMcpPath, "utf-8"));
5250
5339
  if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
5251
5340
  } catch {
5252
5341
  }
@@ -5256,7 +5345,7 @@ async function processAgent(agent, agentStates) {
5256
5345
  const tz = directChatTeamSettings?.["timezone"];
5257
5346
  return typeof tz === "string" && tz.trim() !== "" ? tz.trim() : void 0;
5258
5347
  })();
5259
- if (existsSync4(localDirectChatChannel) && !mcpConfig.mcpServers["direct-chat"]) {
5348
+ if (existsSync5(localDirectChatChannel) && !mcpConfig.mcpServers["direct-chat"]) {
5260
5349
  mcpConfig.mcpServers["direct-chat"] = {
5261
5350
  command: "node",
5262
5351
  args: [localDirectChatChannel],
@@ -5273,7 +5362,7 @@ async function processAgent(agent, agentStates) {
5273
5362
  log(`Channel credentials written for '${agent.code_name}/direct-chat'`);
5274
5363
  }
5275
5364
  const staleChannelsPath = join6(projectDir, ".mcp-channels.json");
5276
- if (existsSync4(staleChannelsPath)) {
5365
+ if (existsSync5(staleChannelsPath)) {
5277
5366
  try {
5278
5367
  rmSync2(staleChannelsPath, { force: true });
5279
5368
  } catch {
@@ -5351,7 +5440,7 @@ async function processAgent(agent, agentStates) {
5351
5440
  const envIntPath = join6(projectDir, ".env.integrations");
5352
5441
  let preWriteEnv;
5353
5442
  try {
5354
- preWriteEnv = readFileSync6(envIntPath, "utf-8");
5443
+ preWriteEnv = readFileSync7(envIntPath, "utf-8");
5355
5444
  } catch {
5356
5445
  preWriteEnv = void 0;
5357
5446
  }
@@ -5364,8 +5453,8 @@ async function processAgent(agent, agentStates) {
5364
5453
  if (fw === "claude-code" && isSessionHealthy(agent.code_name)) {
5365
5454
  try {
5366
5455
  const projectMcpPath = join6(projectDir, ".mcp.json");
5367
- const postWriteEnv = readFileSync6(envIntPath, "utf-8");
5368
- const mcpContent = readFileSync6(projectMcpPath, "utf-8");
5456
+ const postWriteEnv = readFileSync7(envIntPath, "utf-8");
5457
+ const mcpContent = readFileSync7(projectMcpPath, "utf-8");
5369
5458
  const changedVars = diffEnvIntegrations(preWriteEnv, postWriteEnv);
5370
5459
  const mcpJsonForReap = JSON.parse(mcpContent);
5371
5460
  const affectedServerKeys = findMcpServersUsingVars(mcpJsonForReap, changedVars);
@@ -5433,8 +5522,8 @@ async function processAgent(agent, agentStates) {
5433
5522
  const mcpPath = frameworkAdapter.getMcpPath(agent.code_name);
5434
5523
  if (mcpPath) {
5435
5524
  try {
5436
- const { readFileSync: readFileSync7 } = await import("fs");
5437
- const mcpConfig = JSON.parse(readFileSync7(mcpPath, "utf-8"));
5525
+ const { readFileSync: readFileSync8 } = await import("fs");
5526
+ const mcpConfig = JSON.parse(readFileSync8(mcpPath, "utf-8"));
5438
5527
  if (mcpConfig.mcpServers) {
5439
5528
  const managedPrefixes = [
5440
5529
  "composio_",
@@ -5531,7 +5620,7 @@ async function processAgent(agent, agentStates) {
5531
5620
  if (frameworkAdapter.installPlugin) {
5532
5621
  try {
5533
5622
  const pluginPath = join6(process.cwd(), "packages", "openclaw-plugin-augmented", "src", "index.ts");
5534
- if (existsSync4(pluginPath)) {
5623
+ if (existsSync5(pluginPath)) {
5535
5624
  frameworkAdapter.installPlugin(agent.code_name, "augmented", pluginPath, {
5536
5625
  agtHost: requireHost(),
5537
5626
  agtApiKey: getApiKey() ?? void 0,
@@ -5610,7 +5699,7 @@ async function processAgent(agent, agentStates) {
5610
5699
  // install target but cheap to sweep.
5611
5700
  join6(agentDir, ".claude", "skills")
5612
5701
  ];
5613
- const existingDirs = candidateSkillDirs.filter((d) => existsSync4(d));
5702
+ const existingDirs = candidateSkillDirs.filter((d) => existsSync5(d));
5614
5703
  const discoveredEntries = /* @__PURE__ */ new Set();
5615
5704
  for (const dir of existingDirs) {
5616
5705
  try {
@@ -5625,7 +5714,7 @@ async function processAgent(agent, agentStates) {
5625
5714
  const removeSkillFolder = (entry, reason) => {
5626
5715
  for (const dir of existingDirs) {
5627
5716
  const p = join6(dir, entry);
5628
- if (existsSync4(p)) {
5717
+ if (existsSync5(p)) {
5629
5718
  rmSync3(p, { recursive: true, force: true });
5630
5719
  }
5631
5720
  }
@@ -5787,7 +5876,7 @@ async function processAgent(agent, agentStates) {
5787
5876
  let mcpJsonParsed = null;
5788
5877
  try {
5789
5878
  const mcpPath = join6(getProjectDir(agent.code_name), ".mcp.json");
5790
- mcpJsonParsed = JSON.parse(readFileSync6(mcpPath, "utf-8"));
5879
+ mcpJsonParsed = JSON.parse(readFileSync7(mcpPath, "utf-8"));
5791
5880
  } catch {
5792
5881
  }
5793
5882
  reapMissingMcpSessions({
@@ -5942,9 +6031,9 @@ async function processAgent(agent, agentStates) {
5942
6031
  if (agentFw === "openclaw" && gatewayRunning && gatewayPort) {
5943
6032
  const homeDir = process.env["HOME"] ?? "/tmp";
5944
6033
  const jobsPath = join6(homeDir, `.openclaw-${agent.code_name}`, "cron", "jobs.json");
5945
- if (existsSync4(jobsPath)) {
6034
+ if (existsSync5(jobsPath)) {
5946
6035
  try {
5947
- const jobsData = JSON.parse(readFileSync6(jobsPath, "utf-8"));
6036
+ const jobsData = JSON.parse(readFileSync7(jobsPath, "utf-8"));
5948
6037
  const kanbanJob = (jobsData.jobs ?? []).find(
5949
6038
  (j) => typeof j.name === "string" && j.name.includes("kanban-work")
5950
6039
  );
@@ -6078,7 +6167,7 @@ In progress for ${age} minutes \u2014 auto-failed`).catch(() => {
6078
6167
  }
6079
6168
  }
6080
6169
  const trackedFiles = frameworkAdapter.driftTrackedFiles();
6081
- if (trackedFiles.length > 0 && existsSync4(agentDir)) {
6170
+ if (trackedFiles.length > 0 && existsSync5(agentDir)) {
6082
6171
  const hashes = /* @__PURE__ */ new Map();
6083
6172
  for (const file of trackedFiles) {
6084
6173
  const h = hashFile(join6(agentDir, file));
@@ -6125,9 +6214,9 @@ function cleanupStaleSessions(codeName) {
6125
6214
  }
6126
6215
  function cleanupCronSessions(sessionsDir, keepCount) {
6127
6216
  const indexPath = join6(sessionsDir, "sessions.json");
6128
- if (!existsSync4(indexPath)) return;
6217
+ if (!existsSync5(indexPath)) return;
6129
6218
  try {
6130
- const raw = readFileSync6(indexPath, "utf-8");
6219
+ const raw = readFileSync7(indexPath, "utf-8");
6131
6220
  const index = JSON.parse(raw);
6132
6221
  const cronRunKeys = Object.keys(index).filter((k) => k.includes(":cron:") && k.includes(":run:")).map((k) => ({
6133
6222
  key: k,
@@ -6142,7 +6231,7 @@ function cleanupCronSessions(sessionsDir, keepCount) {
6142
6231
  if (entry.sessionId) {
6143
6232
  const sessionFile = join6(sessionsDir, `${entry.sessionId}.jsonl`);
6144
6233
  try {
6145
- if (existsSync4(sessionFile)) {
6234
+ if (existsSync5(sessionFile)) {
6146
6235
  unlinkSync(sessionFile);
6147
6236
  deletedFiles++;
6148
6237
  }
@@ -6163,7 +6252,7 @@ function cleanupCronSessions(sessionsDir, keepCount) {
6163
6252
  if (parentSessionId) {
6164
6253
  try {
6165
6254
  const f = join6(sessionsDir, `${parentSessionId}.jsonl`);
6166
- if (existsSync4(f)) {
6255
+ if (existsSync5(f)) {
6167
6256
  unlinkSync(f);
6168
6257
  deletedFiles++;
6169
6258
  }
@@ -6181,9 +6270,9 @@ function cleanupCronSessions(sessionsDir, keepCount) {
6181
6270
  }
6182
6271
  var STALE_RUN_TIMEOUT_MS = 5 * 6e4;
6183
6272
  function clearStaleCronRunState(jobsPath) {
6184
- if (!existsSync4(jobsPath)) return;
6273
+ if (!existsSync5(jobsPath)) return;
6185
6274
  try {
6186
- const raw = readFileSync6(jobsPath, "utf-8");
6275
+ const raw = readFileSync7(jobsPath, "utf-8");
6187
6276
  const data = JSON.parse(raw);
6188
6277
  const jobs = data.jobs ?? data;
6189
6278
  if (!Array.isArray(jobs)) return;
@@ -6214,7 +6303,7 @@ function clearStaleCronRunState(jobsPath) {
6214
6303
  }
6215
6304
  }
6216
6305
  function cleanupOldFiles(dir, maxAgeDays, ext) {
6217
- if (!existsSync4(dir)) return;
6306
+ if (!existsSync5(dir)) return;
6218
6307
  const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3;
6219
6308
  let removed = 0;
6220
6309
  try {
@@ -6239,6 +6328,25 @@ function cleanupOldFiles(dir, maxAgeDays, ext) {
6239
6328
  var inFlightClaudeTasks = /* @__PURE__ */ new Set();
6240
6329
  var claudeTaskConcurrency = /* @__PURE__ */ new Map();
6241
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
+ }
6242
6350
  var claudeSchedulerStates = /* @__PURE__ */ new Map();
6243
6351
  async function syncAndCheckClaudeScheduler(agent, tasks, boardItems, refreshData) {
6244
6352
  const codeName = agent.code_name;
@@ -6510,9 +6618,9 @@ async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
6510
6618
  try {
6511
6619
  const claudeMdPath = join6(projectDir, "CLAUDE.md");
6512
6620
  const serverNames = [];
6513
- if (existsSync4(mcpConfigPath)) {
6621
+ if (existsSync5(mcpConfigPath)) {
6514
6622
  try {
6515
- const d = JSON.parse(readFileSync6(mcpConfigPath, "utf-8"));
6623
+ const d = JSON.parse(readFileSync7(mcpConfigPath, "utf-8"));
6516
6624
  if (d.mcpServers) serverNames.push(...Object.keys(d.mcpServers));
6517
6625
  } catch {
6518
6626
  }
@@ -6531,14 +6639,14 @@ async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
6531
6639
  "--allowedTools",
6532
6640
  allowedTools
6533
6641
  ];
6534
- if (existsSync4(claudeMdPath)) {
6642
+ if (existsSync5(claudeMdPath)) {
6535
6643
  claudeArgs.push("--system-prompt-file", claudeMdPath);
6536
6644
  }
6537
6645
  const childEnv = { ...process.env };
6538
6646
  const envIntPath = join6(projectDir, ".env.integrations");
6539
- if (existsSync4(envIntPath)) {
6647
+ if (existsSync5(envIntPath)) {
6540
6648
  try {
6541
- for (const line of readFileSync6(envIntPath, "utf-8").split("\n")) {
6649
+ for (const line of readFileSync7(envIntPath, "utf-8").split("\n")) {
6542
6650
  if (!line || line.startsWith("#") || !line.includes("=")) continue;
6543
6651
  const eqIdx = line.indexOf("=");
6544
6652
  childEnv[line.slice(0, eqIdx)] = line.slice(eqIdx + 1);
@@ -6562,11 +6670,21 @@ async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
6562
6670
  runId = startResult.run_id;
6563
6671
  kanbanItemId = startResult.kanban_item_id;
6564
6672
  if (runId) childEnv["AGT_RUN_ID"] = runId;
6673
+ const claudeKind = task.templateId === "kanban-work" ? "kanban-work" : "scheduled-task";
6565
6674
  const { stdout, stderr } = await execFilePromiseLong(resolveClaudeBinary(), claudeArgs, {
6566
6675
  cwd: projectDir,
6567
6676
  timeout: 3e5,
6568
6677
  stdin: "ignore",
6569
- 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)
6570
6688
  });
6571
6689
  if (stderr) {
6572
6690
  log(`[claude-scheduler] Task '${task.name}' stderr for '${codeName}': ${stderr.slice(0, 500)}`);
@@ -7340,9 +7458,9 @@ ${escapeXml(msg.content)}
7340
7458
  const projDir = ccProjectDir(agent.codeName);
7341
7459
  const mcpConfigPath = join6(projDir, ".mcp.json");
7342
7460
  const serverNames = [];
7343
- if (existsSync4(mcpConfigPath)) {
7461
+ if (existsSync5(mcpConfigPath)) {
7344
7462
  try {
7345
- const d = JSON.parse(readFileSync6(mcpConfigPath, "utf-8"));
7463
+ const d = JSON.parse(readFileSync7(mcpConfigPath, "utf-8"));
7346
7464
  if (d.mcpServers) serverNames.push(...Object.keys(d.mcpServers));
7347
7465
  } catch {
7348
7466
  }
@@ -7362,14 +7480,14 @@ ${escapeXml(msg.content)}
7362
7480
  allowedTools
7363
7481
  ];
7364
7482
  const chatClaudeMd = join6(projDir, "CLAUDE.md");
7365
- if (existsSync4(chatClaudeMd)) {
7483
+ if (existsSync5(chatClaudeMd)) {
7366
7484
  chatArgs.push("--system-prompt-file", chatClaudeMd);
7367
7485
  }
7368
7486
  const envIntPath = join6(projDir, ".env.integrations");
7369
7487
  const childEnv = { ...process.env };
7370
- if (existsSync4(envIntPath)) {
7488
+ if (existsSync5(envIntPath)) {
7371
7489
  try {
7372
- for (const line of readFileSync6(envIntPath, "utf-8").split("\n")) {
7490
+ for (const line of readFileSync7(envIntPath, "utf-8").split("\n")) {
7373
7491
  if (!line || line.startsWith("#") || !line.includes("=")) continue;
7374
7492
  const eqIdx = line.indexOf("=");
7375
7493
  childEnv[line.slice(0, eqIdx)] = line.slice(eqIdx + 1);
@@ -7382,7 +7500,19 @@ ${escapeXml(msg.content)}
7382
7500
  } catch (err) {
7383
7501
  throw new Error(`Auth resolve failed for '${agent.codeName}': ${err.message}`);
7384
7502
  }
7385
- 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
+ });
7386
7516
  reply = stdout.trim() || "[No response from agent]";
7387
7517
  } else {
7388
7518
  const { stdout } = await execFilePromiseLong("openclaw", [
@@ -7734,8 +7864,8 @@ function getBuiltInSkillContent(skillId) {
7734
7864
  join6(new URL(".", import.meta.url).pathname, "..", "..", "..", "..", "skills", skillId, "SKILL.md")
7735
7865
  ];
7736
7866
  for (const candidate of candidates) {
7737
- if (existsSync4(candidate)) {
7738
- const content = readFileSync6(candidate, "utf-8");
7867
+ if (existsSync5(candidate)) {
7868
+ const content = readFileSync7(candidate, "utf-8");
7739
7869
  const files = [{ relativePath: "SKILL.md", content }];
7740
7870
  builtInSkillCache.set(skillId, files);
7741
7871
  return files;
@@ -7906,6 +8036,12 @@ async function execFilePromiseLong(cmd, args, opts) {
7906
8036
  stdio: [opts?.stdin === "ignore" ? "ignore" : "pipe", "pipe", "pipe"],
7907
8037
  ...opts?.env ? { env: opts.env } : {}
7908
8038
  });
8039
+ if (opts?.onSpawn && typeof child.pid === "number") {
8040
+ try {
8041
+ opts.onSpawn(child.pid);
8042
+ } catch {
8043
+ }
8044
+ }
7909
8045
  let stdout = "";
7910
8046
  let stderr = "";
7911
8047
  child.stdout?.on("data", (d) => {
@@ -7920,6 +8056,12 @@ async function execFilePromiseLong(cmd, args, opts) {
7920
8056
  }, opts?.timeout ?? 12e4);
7921
8057
  child.on("close", (code) => {
7922
8058
  clearTimeout(timer);
8059
+ if (opts?.onExit && typeof child.pid === "number") {
8060
+ try {
8061
+ opts.onExit(child.pid);
8062
+ } catch {
8063
+ }
8064
+ }
7923
8065
  if (code !== 0) reject(new ChildProcessError(code, stdout, stderr));
7924
8066
  else resolve({ stdout, stderr });
7925
8067
  });
@@ -8688,14 +8830,14 @@ async function syncMemories(agent, configDir, log2) {
8688
8830
  }
8689
8831
  pendingFreshMemorySync.delete(agent.agent_id);
8690
8832
  }
8691
- if (existsSync4(memoryDir)) {
8833
+ if (existsSync5(memoryDir)) {
8692
8834
  const prevHashes = memoryFileHashes.get(agent.agent_id) ?? /* @__PURE__ */ new Map();
8693
8835
  const currentHashes = /* @__PURE__ */ new Map();
8694
8836
  const changedMemories = [];
8695
8837
  for (const file of readdirSync3(memoryDir)) {
8696
8838
  if (!file.endsWith(".md")) continue;
8697
8839
  try {
8698
- const raw = readFileSync6(join6(memoryDir, file), "utf-8");
8840
+ const raw = readFileSync7(join6(memoryDir, file), "utf-8");
8699
8841
  const fileHash = createHash3("sha256").update(raw).digest("hex").slice(0, 16);
8700
8842
  currentHashes.set(file, fileHash);
8701
8843
  if (prevHashes.get(file) === fileHash) continue;
@@ -8720,7 +8862,7 @@ async function syncMemories(agent, configDir, log2) {
8720
8862
  } catch (err) {
8721
8863
  for (const mem of changedMemories) {
8722
8864
  for (const [file] of currentHashes) {
8723
- const parsed = parseMemoryFile(readFileSync6(join6(memoryDir, file), "utf-8"), file.replace(/\.md$/, ""));
8865
+ const parsed = parseMemoryFile(readFileSync7(join6(memoryDir, file), "utf-8"), file.replace(/\.md$/, ""));
8724
8866
  if (parsed?.name === mem.name) currentHashes.delete(file);
8725
8867
  }
8726
8868
  }
@@ -8733,7 +8875,7 @@ async function syncMemories(agent, configDir, log2) {
8733
8875
  }
8734
8876
  }
8735
8877
  async function downloadMemories(agent, memoryDir, log2, { force }) {
8736
- 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() : [];
8737
8879
  const localListHash = createHash3("sha256").update(localFiles.join(",")).digest("hex").slice(0, 16);
8738
8880
  const prevLocalHash = lastLocalFileHash.get(agent.agent_id);
8739
8881
  const prevDownload = lastDownloadHash.get(agent.agent_id);
@@ -8748,7 +8890,7 @@ async function downloadMemories(agent, memoryDir, log2, { force }) {
8748
8890
  lastDownloadHash.set(agent.agent_id, responseHash);
8749
8891
  lastLocalFileHash.set(agent.agent_id, localListHash);
8750
8892
  if (dbMemories.memories?.length) {
8751
- mkdirSync3(memoryDir, { recursive: true });
8893
+ mkdirSync4(memoryDir, { recursive: true });
8752
8894
  let written = 0;
8753
8895
  let overwritten = 0;
8754
8896
  for (let i = 0; i < dbMemories.memories.length; i++) {
@@ -8764,10 +8906,10 @@ description: ${JSON.stringify(mem.content.slice(0, 200))}
8764
8906
 
8765
8907
  ${mem.content}
8766
8908
  `;
8767
- if (existsSync4(filePath)) {
8909
+ if (existsSync5(filePath)) {
8768
8910
  let existing = "";
8769
8911
  try {
8770
- existing = readFileSync6(filePath, "utf-8");
8912
+ existing = readFileSync7(filePath, "utf-8");
8771
8913
  } catch {
8772
8914
  }
8773
8915
  if (existing === desired) continue;
@@ -8791,7 +8933,7 @@ ${mem.content}
8791
8933
  }
8792
8934
  }
8793
8935
  async function cleanupAgentFiles(codeName, agentDir) {
8794
- if (existsSync4(agentDir)) {
8936
+ if (existsSync5(agentDir)) {
8795
8937
  try {
8796
8938
  rmSync2(agentDir, { recursive: true, force: true });
8797
8939
  log(`Removed provision directory for '${codeName}'`);
@@ -8947,9 +9089,24 @@ async function stopPolling(opts = {}) {
8947
9089
  process.exit(opts.forcedExitCode ?? 1);
8948
9090
  }, 15e3);
8949
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();
8950
9106
  stopCaffeinate();
8951
9107
  stopRealtimeChat();
8952
9108
  stopGatewayPool();
9109
+ log(formatDrainShutdownLine({ step: "stop-subsystems", elapsedMs: Date.now() - subsysStartedAt }));
8953
9110
  for (const codeName of [...scheduledRunsByCode.keys()]) {
8954
9111
  closeScheduledRunsForCode(codeName, "cancelled", "manager shutdown");
8955
9112
  }
@@ -8965,8 +9122,8 @@ function startManager(opts) {
8965
9122
  config = opts;
8966
9123
  try {
8967
9124
  const stateFile = getStateFile();
8968
- if (existsSync4(stateFile)) {
8969
- const raw = readFileSync6(stateFile, "utf-8");
9125
+ if (existsSync5(stateFile)) {
9126
+ const raw = readFileSync7(stateFile, "utf-8");
8970
9127
  const parsed = JSON.parse(raw);
8971
9128
  if (Array.isArray(parsed.agents)) {
8972
9129
  state4.agents = parsed.agents;
@@ -8986,8 +9143,77 @@ function startManager(opts) {
8986
9143
  );
8987
9144
  deployMcpAssets();
8988
9145
  reapOrphanChannelMcps({ log });
8989
- void ensureHostFrameworkBinaries();
8990
- 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
+ }
8991
9217
  }
8992
9218
  async function ensureHostFrameworkBinaries() {
8993
9219
  const apiKey = getApiKey();
@@ -9045,17 +9271,17 @@ function restartRunningChannelMcps(basenames) {
9045
9271
  }
9046
9272
  function deployMcpAssets() {
9047
9273
  const targetDir = join6(homedir4(), ".augmented", "_mcp");
9048
- mkdirSync3(targetDir, { recursive: true });
9049
- const moduleDir = dirname2(fileURLToPath(import.meta.url));
9274
+ mkdirSync4(targetDir, { recursive: true });
9275
+ const moduleDir = dirname3(fileURLToPath(import.meta.url));
9050
9276
  let mcpSourceDir = "";
9051
9277
  let dir = moduleDir;
9052
9278
  for (let i = 0; i < 6; i++) {
9053
9279
  const candidate = join6(dir, "dist", "mcp");
9054
- if (existsSync4(join6(candidate, "index.js"))) {
9280
+ if (existsSync5(join6(candidate, "index.js"))) {
9055
9281
  mcpSourceDir = candidate;
9056
9282
  break;
9057
9283
  }
9058
- const parent = dirname2(dir);
9284
+ const parent = dirname3(dir);
9059
9285
  if (parent === dir) break;
9060
9286
  dir = parent;
9061
9287
  }
@@ -9066,8 +9292,8 @@ function deployMcpAssets() {
9066
9292
  const changedBasenames = [];
9067
9293
  const fileHash = (p) => {
9068
9294
  try {
9069
- if (!existsSync4(p)) return null;
9070
- return createHash3("sha256").update(readFileSync6(p)).digest("hex");
9295
+ if (!existsSync5(p)) return null;
9296
+ return createHash3("sha256").update(readFileSync7(p)).digest("hex");
9071
9297
  } catch {
9072
9298
  return null;
9073
9299
  }
@@ -9080,7 +9306,7 @@ function deployMcpAssets() {
9080
9306
  for (const file of ["index.js", "slack-channel.js", "direct-chat-channel.js", "telegram-channel.js"]) {
9081
9307
  const src = join6(mcpSourceDir, file);
9082
9308
  const dst = join6(targetDir, file);
9083
- if (!existsSync4(src)) continue;
9309
+ if (!existsSync5(src)) continue;
9084
9310
  const before = fileHash(dst);
9085
9311
  try {
9086
9312
  copyFileSync(src, dst);
@@ -9100,13 +9326,13 @@ function deployMcpAssets() {
9100
9326
  const localMcpPath = join6(targetDir, "index.js");
9101
9327
  try {
9102
9328
  const agentsDir = join6(homedir4(), ".augmented", "agents");
9103
- if (existsSync4(agentsDir)) {
9329
+ if (existsSync5(agentsDir)) {
9104
9330
  for (const entry of readdirSync3(agentsDir, { withFileTypes: true })) {
9105
9331
  if (!entry.isDirectory()) continue;
9106
9332
  for (const subdir of ["provision", "project"]) {
9107
9333
  const mcpJsonPath = join6(agentsDir, entry.name, subdir, ".mcp.json");
9108
9334
  try {
9109
- const raw = readFileSync6(mcpJsonPath, "utf-8");
9335
+ const raw = readFileSync7(mcpJsonPath, "utf-8");
9110
9336
  if (!raw.includes("@integrity-labs/augmented-mcp")) continue;
9111
9337
  const mcpConfig = JSON.parse(raw);
9112
9338
  const augServer = mcpConfig.mcpServers?.["augmented"];