@integrity-labs/agt-cli 0.27.51 → 0.27.52

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-Y4KVZGPE.js";
18
+ } from "../chunk-IMKH4X56.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 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";
80
+ import { readFileSync as readFileSync8, 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 dirname3 } from "path";
83
+ import { join as join7, dirname as dirname3 } from "path";
84
84
  import { homedir as homedir4 } from "os";
85
85
  import { fileURLToPath } from "url";
86
86
 
@@ -412,7 +412,13 @@ function reapMissingMcpSessions(args) {
412
412
  graceMs = DEFAULT_COLD_START_GRACE_MS,
413
413
  rotationGraceMs = DEFAULT_ROTATION_GRACE_MS,
414
414
  onGiveUp,
415
- onRestart
415
+ onRestart,
416
+ // ENG-5932: defaults make quarantine a strict no-op (classify everything
417
+ // essential, mode off) so legacy callers/tests are unaffected.
418
+ classifyKey = () => "essential",
419
+ quarantineDwellMs = 0,
420
+ quarantineMode = "off",
421
+ onQuarantine
416
422
  } = args;
417
423
  const now = args.now ?? Date.now;
418
424
  const runPs = args.runPs ?? (() => execFileSync2("ps", ["-eo", "pid,ppid,args"], { encoding: "utf-8", timeout: 5e3 }));
@@ -466,6 +472,8 @@ function reapMissingMcpSessions(args) {
466
472
  state5.lastSeenLiveAt = now();
467
473
  state5.lastAttemptedSessionStartedAt = null;
468
474
  state5.gaveUpLogged = false;
475
+ state5.firstMissingAt = null;
476
+ state5.quarantineLogged = false;
469
477
  }
470
478
  }
471
479
  if (missing.length === 0) {
@@ -476,16 +484,22 @@ function reapMissingMcpSessions(args) {
476
484
  rotationGraced: rotationGraced.length > 0 ? rotationGraced : void 0
477
485
  };
478
486
  }
487
+ const nowMs = now();
479
488
  const givenUp = [];
480
489
  const active = [];
490
+ const quarantined = [];
491
+ const wouldQuarantine = [];
481
492
  for (const key of missing) {
482
493
  const sk = stateKey(codeName, key);
483
494
  const state5 = presenceReaperState.get(sk) ?? {
484
495
  attempts: 0,
485
496
  lastSeenLiveAt: null,
486
497
  lastAttemptedSessionStartedAt: null,
487
- gaveUpLogged: false
498
+ gaveUpLogged: false,
499
+ firstMissingAt: null,
500
+ quarantineLogged: false
488
501
  };
502
+ if (state5.firstMissingAt === null) state5.firstMissingAt = nowMs;
489
503
  if (state5.lastAttemptedSessionStartedAt !== sessionStartedAt) {
490
504
  state5.attempts += 1;
491
505
  state5.lastAttemptedSessionStartedAt = sessionStartedAt;
@@ -508,6 +522,32 @@ function reapMissingMcpSessions(args) {
508
522
  }
509
523
  }
510
524
  }
525
+ if (quarantineMode !== "off" && !state5.quarantineLogged && classifyKey(key) === "optional") {
526
+ const dwellElapsed = quarantineDwellMs <= 0 || state5.firstMissingAt !== null && nowMs - state5.firstMissingAt >= quarantineDwellMs;
527
+ if (dwellElapsed) {
528
+ state5.quarantineLogged = true;
529
+ if (quarantineMode === "enforce") {
530
+ quarantined.push(key);
531
+ log2(
532
+ `[mcp-presence-reaper] QUARANTINE '${codeName}:${key}' \u2014 optional channel dead past restart budget + ${quarantineDwellMs}ms dwell; dropping from provisioned set so the dead channel stops restarting the whole session (ENG-5932)`
533
+ );
534
+ if (onQuarantine) {
535
+ try {
536
+ onQuarantine(codeName, key);
537
+ } catch (err) {
538
+ log2(
539
+ `[mcp-presence-reaper] onQuarantine callback threw for '${codeName}:${key}' (suppressed; reaper continues): ${err.message}`
540
+ );
541
+ }
542
+ }
543
+ } else {
544
+ wouldQuarantine.push(key);
545
+ log2(
546
+ `[mcp-presence-reaper] SHADOW would-quarantine '${codeName}:${key}' \u2014 optional channel dead past restart budget + ${quarantineDwellMs}ms dwell; no action taken (ENG-5932 shadow mode)`
547
+ );
548
+ }
549
+ }
550
+ }
511
551
  } else {
512
552
  active.push(key);
513
553
  }
@@ -518,7 +558,9 @@ function reapMissingMcpSessions(args) {
518
558
  restarted: false,
519
559
  reason: "all-keys-over-cap",
520
560
  givenUp,
521
- rotationGraced: rotationGraced.length > 0 ? rotationGraced : void 0
561
+ rotationGraced: rotationGraced.length > 0 ? rotationGraced : void 0,
562
+ quarantined: quarantined.length > 0 ? quarantined : void 0,
563
+ wouldQuarantine: wouldQuarantine.length > 0 ? wouldQuarantine : void 0
522
564
  };
523
565
  }
524
566
  const givenUpSuffix = givenUp.length > 0 ? ` (skipping over-cap: [${givenUp.join(", ")}])` : "";
@@ -546,7 +588,9 @@ function reapMissingMcpSessions(args) {
546
588
  missing,
547
589
  restarted: true,
548
590
  givenUp: givenUp.length > 0 ? givenUp : void 0,
549
- rotationGraced: rotationGraced.length > 0 ? rotationGraced : void 0
591
+ rotationGraced: rotationGraced.length > 0 ? rotationGraced : void 0,
592
+ quarantined: quarantined.length > 0 ? quarantined : void 0,
593
+ wouldQuarantine: wouldQuarantine.length > 0 ? wouldQuarantine : void 0
550
594
  };
551
595
  }
552
596
 
@@ -609,6 +653,151 @@ function decideSenderPolicyRestart(input) {
609
653
  return { restart: true, firstPoll: false, changed: true };
610
654
  }
611
655
 
656
+ // src/lib/channel-quarantine.ts
657
+ import { readFileSync } from "fs";
658
+ import { join } from "path";
659
+
660
+ // src/lib/atomic-write.ts
661
+ import { closeSync, fsyncSync, openSync, writeSync, renameSync, mkdirSync } from "fs";
662
+ import { dirname } from "path";
663
+ function atomicWriteFileSync(path, data) {
664
+ const dirPath = dirname(path);
665
+ const tmpPath = `${path}.tmp.${process.pid}.${Math.random().toString(36).slice(2, 8)}`;
666
+ try {
667
+ mkdirSync(dirPath, { recursive: true });
668
+ } catch {
669
+ }
670
+ const fd = openSync(tmpPath, "w", 420);
671
+ try {
672
+ writeSync(fd, data);
673
+ try {
674
+ fsyncSync(fd);
675
+ } catch {
676
+ }
677
+ } finally {
678
+ closeSync(fd);
679
+ }
680
+ renameSync(tmpPath, path);
681
+ try {
682
+ const dirFd = openSync(dirPath, "r");
683
+ try {
684
+ fsyncSync(dirFd);
685
+ } finally {
686
+ closeSync(dirFd);
687
+ }
688
+ } catch {
689
+ }
690
+ }
691
+
692
+ // src/lib/channel-quarantine.ts
693
+ var ESSENTIAL_CHANNEL_KEYS = /* @__PURE__ */ new Set(["direct-chat", "augmented"]);
694
+ var OPTIONAL_CHANNEL_KEYS = /* @__PURE__ */ new Set([
695
+ "telegram",
696
+ "slack",
697
+ "msteams",
698
+ "discord",
699
+ "whatsapp",
700
+ "signal",
701
+ "imessage",
702
+ "matrix",
703
+ "irc",
704
+ "nostr"
705
+ ]);
706
+ function readDeclaredCriticality(entry) {
707
+ if (!entry || typeof entry !== "object") return void 0;
708
+ const meta = entry._augmented;
709
+ if (!meta || typeof meta !== "object") return void 0;
710
+ const value = meta.criticality;
711
+ return value === "essential" || value === "optional" ? value : void 0;
712
+ }
713
+ function classifyChannelCriticality(serverKey, entry) {
714
+ if (ESSENTIAL_CHANNEL_KEYS.has(serverKey)) return "essential";
715
+ const declared = readDeclaredCriticality(entry);
716
+ if (declared) return declared;
717
+ if (OPTIONAL_CHANNEL_KEYS.has(serverKey)) return "optional";
718
+ return "essential";
719
+ }
720
+ function defaultQuarantinePath(configDir) {
721
+ return join(configDir, "channel-quarantine.json");
722
+ }
723
+ var ChannelQuarantineStore = class {
724
+ path;
725
+ cache = null;
726
+ constructor(path) {
727
+ this.path = path;
728
+ }
729
+ load() {
730
+ if (this.cache) return this.cache;
731
+ try {
732
+ const raw = readFileSync(this.path, "utf-8");
733
+ const parsed = JSON.parse(raw);
734
+ this.cache = isQuarantineFile(parsed) ? parsed : {};
735
+ } catch {
736
+ this.cache = {};
737
+ }
738
+ return this.cache;
739
+ }
740
+ persist() {
741
+ atomicWriteFileSync(this.path, JSON.stringify(this.cache ?? {}, null, 2));
742
+ }
743
+ /** Is `serverKey` currently quarantined for `codeName`? */
744
+ isQuarantined(codeName, serverKey) {
745
+ return this.load()[codeName]?.[serverKey] !== void 0;
746
+ }
747
+ /** The set of quarantined server keys for `codeName` (empty Set if none). */
748
+ getQuarantinedKeys(codeName) {
749
+ const forAgent = this.load()[codeName];
750
+ return new Set(forAgent ? Object.keys(forAgent) : []);
751
+ }
752
+ /**
753
+ * Mark `serverKey` quarantined for `codeName`. Idempotent — re-quarantining
754
+ * an already-quarantined key preserves the original `quarantinedAt` (so the
755
+ * "how long has this been quarantined" escalation clock in Slice 3 isn't
756
+ * reset by provisioning churn). Returns true iff this call newly quarantined
757
+ * the key (false if it was already quarantined).
758
+ */
759
+ quarantine(codeName, serverKey, reason, now) {
760
+ const file = this.load();
761
+ const forAgent = file[codeName] ??= {};
762
+ if (forAgent[serverKey]) return false;
763
+ forAgent[serverKey] = { quarantinedAt: now, reason };
764
+ this.persist();
765
+ return true;
766
+ }
767
+ /**
768
+ * Clear the quarantine for `serverKey` (explicit re-enable). Returns true iff
769
+ * a marker was actually removed. NOTE: re-enable is an EXPLICIT verb (Slice 2
770
+ * exposes it via the API/console) — provisioning's idempotent `.mcp.json`
771
+ * rewrites must NEVER call this, or a quarantined channel would re-arm into a
772
+ * fresh flap laundered through the config loop.
773
+ */
774
+ clearQuarantine(codeName, serverKey) {
775
+ const file = this.load();
776
+ const forAgent = file[codeName];
777
+ if (!forAgent || !forAgent[serverKey]) return false;
778
+ delete forAgent[serverKey];
779
+ if (Object.keys(forAgent).length === 0) delete file[codeName];
780
+ this.persist();
781
+ return true;
782
+ }
783
+ /** Test seam: drop the in-memory cache so the next read re-hits disk. */
784
+ __invalidateCacheForTests() {
785
+ this.cache = null;
786
+ }
787
+ };
788
+ function isQuarantineFile(value) {
789
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
790
+ for (const forAgent of Object.values(value)) {
791
+ if (!forAgent || typeof forAgent !== "object" || Array.isArray(forAgent)) return false;
792
+ for (const entry of Object.values(forAgent)) {
793
+ if (!entry || typeof entry !== "object") return false;
794
+ const e = entry;
795
+ if (typeof e.quarantinedAt !== "number" || typeof e.reason !== "string") return false;
796
+ }
797
+ }
798
+ return true;
799
+ }
800
+
612
801
  // src/lib/restart-breaker.ts
613
802
  function reaperRestartBreakerReason(activeKeys) {
614
803
  return activeKeys.length >= 2 ? "mcp-presence-reaper" : void 0;
@@ -942,8 +1131,8 @@ function runCliProbe(binary, args, opts = {}) {
942
1131
  }
943
1132
 
944
1133
  // src/lib/self-update-coalesce.ts
945
- import { readFileSync, writeFileSync, mkdirSync } from "fs";
946
- import { dirname } from "path";
1134
+ import { readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
1135
+ import { dirname as dirname2 } from "path";
947
1136
  var DEFAULT_SELF_UPDATE_COALESCE_MS = 30 * 60 * 1e3;
948
1137
  function resolveCoalesceWindowMs(env = process.env) {
949
1138
  const raw = env.AGT_SELF_UPDATE_COALESCE_MS;
@@ -952,7 +1141,7 @@ function resolveCoalesceWindowMs(env = process.env) {
952
1141
  if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_SELF_UPDATE_COALESCE_MS;
953
1142
  return Math.trunc(parsed);
954
1143
  }
955
- function readLastSelfUpdateAppliedMs(markerPath, now = Date.now(), read = (p) => readFileSync(p, "utf-8")) {
1144
+ function readLastSelfUpdateAppliedMs(markerPath, now = Date.now(), read = (p) => readFileSync2(p, "utf-8")) {
956
1145
  let raw;
957
1146
  try {
958
1147
  raw = read(markerPath);
@@ -965,7 +1154,7 @@ function readLastSelfUpdateAppliedMs(markerPath, now = Date.now(), read = (p) =>
965
1154
  return v;
966
1155
  }
967
1156
  function stampLastSelfUpdateApplied(markerPath, now = Date.now(), write = (p, v) => {
968
- mkdirSync(dirname(p), { recursive: true });
1157
+ mkdirSync2(dirname2(p), { recursive: true });
969
1158
  writeFileSync(p, v);
970
1159
  }) {
971
1160
  try {
@@ -988,44 +1177,12 @@ function formatCoalesceDeferLogLine(opts) {
988
1177
  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
1178
  }
990
1179
 
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
1180
  // src/lib/claude-pid-tracker.ts
1024
- import { existsSync, readFileSync as readFileSync2 } from "fs";
1181
+ import { existsSync, readFileSync as readFileSync3 } from "fs";
1025
1182
  function readPidFile(path) {
1026
1183
  if (!existsSync(path)) return { version: 1, spawns: [] };
1027
1184
  try {
1028
- const raw = JSON.parse(readFileSync2(path, "utf-8"));
1185
+ const raw = JSON.parse(readFileSync3(path, "utf-8"));
1029
1186
  if (raw.version !== 1 || !Array.isArray(raw.spawns)) return { version: 1, spawns: [] };
1030
1187
  const spawns = raw.spawns.filter(
1031
1188
  (s) => !!s && typeof s.pid === "number" && Number.isFinite(s.pid) && s.pid > 0
@@ -1133,8 +1290,8 @@ async function maybeReportUsageBanner(args) {
1133
1290
  }
1134
1291
 
1135
1292
  // src/lib/token-usage-monitor.ts
1136
- import { readdirSync, readFileSync as readFileSync3, statSync } from "fs";
1137
- import { join } from "path";
1293
+ import { readdirSync, readFileSync as readFileSync4, statSync } from "fs";
1294
+ import { join as join2 } from "path";
1138
1295
  var MIN_CHECK_INTERVAL_MS2 = 6e4;
1139
1296
  var TRANSCRIPT_MTIME_WINDOW_MS = 2 * 24 * 60 * 60 * 1e3;
1140
1297
  var MAX_ENTRIES_PER_POST = 200;
@@ -1163,7 +1320,7 @@ async function maybeReportTokenUsage(args) {
1163
1320
  if (!name.endsWith(".jsonl")) continue;
1164
1321
  const sessionId = name.slice(0, -".jsonl".length);
1165
1322
  if (!sessionId) continue;
1166
- const path = join(dir, name);
1323
+ const path = join2(dir, name);
1167
1324
  let st;
1168
1325
  try {
1169
1326
  st = statSync(path);
@@ -1179,7 +1336,7 @@ async function maybeReportTokenUsage(args) {
1179
1336
  }
1180
1337
  let content;
1181
1338
  try {
1182
- content = readFileSync3(path, "utf-8");
1339
+ content = readFileSync4(path, "utf-8");
1183
1340
  } catch (err) {
1184
1341
  log2(`[token-usage] read failed for '${codeName}/${name}': ${err.message}`);
1185
1342
  continue;
@@ -1260,11 +1417,11 @@ async function maybeReportTokenUsage(args) {
1260
1417
  }
1261
1418
 
1262
1419
  // src/lib/activity-cache-monitor.ts
1263
- import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
1420
+ import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
1264
1421
  import { homedir } from "os";
1265
- import { join as join2 } from "path";
1422
+ import { join as join3 } from "path";
1266
1423
  var MIN_CHECK_INTERVAL_MS3 = 6e4;
1267
- var STATS_CACHE_PATH = join2(homedir(), ".claude", "stats-cache.json");
1424
+ var STATS_CACHE_PATH = join3(homedir(), ".claude", "stats-cache.json");
1268
1425
  var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
1269
1426
  var state3 = { lastObservedDate: null, lastCheckedAt: 0 };
1270
1427
  function selectNewDailyRows(raw, lastObservedDate) {
@@ -1312,7 +1469,7 @@ async function maybeReportActivityCache(args) {
1312
1469
  }
1313
1470
  let raw;
1314
1471
  try {
1315
- raw = readFileSync4(STATS_CACHE_PATH, "utf-8");
1472
+ raw = readFileSync5(STATS_CACHE_PATH, "utf-8");
1316
1473
  } catch (err) {
1317
1474
  log2(`[activity-cache] readFileSync failed: ${err.message}`);
1318
1475
  return;
@@ -1762,7 +1919,7 @@ var GatewayClientPool = class extends EventEmitter {
1762
1919
  // src/lib/claude-auth-detect.ts
1763
1920
  import { readFile, readdir } from "fs/promises";
1764
1921
  import { homedir as homedir2, platform } from "os";
1765
- import { join as join3 } from "path";
1922
+ import { join as join4 } from "path";
1766
1923
  import { execFile as execFile2 } from "child_process";
1767
1924
  import { promisify } from "util";
1768
1925
  var execFileAsync = promisify(execFile2);
@@ -1777,8 +1934,8 @@ async function detectClaudeAuth() {
1777
1934
  }
1778
1935
  async function findClaudeCredentialsPaths() {
1779
1936
  const candidates = [
1780
- join3(homedir2(), ".claude", ".credentials.json"),
1781
- join3(homedir2(), ".claude", "credentials.json")
1937
+ join4(homedir2(), ".claude", ".credentials.json"),
1938
+ join4(homedir2(), ".claude", "credentials.json")
1782
1939
  ];
1783
1940
  const isLinuxRoot = platform() === "linux" && typeof process.getuid === "function" && process.getuid() === 0;
1784
1941
  if (isLinuxRoot) {
@@ -1786,8 +1943,8 @@ async function findClaudeCredentialsPaths() {
1786
1943
  const entries = await readdir("/home", { withFileTypes: true });
1787
1944
  for (const entry of entries) {
1788
1945
  if (!entry.isDirectory()) continue;
1789
- candidates.push(join3("/home", entry.name, ".claude", ".credentials.json"));
1790
- candidates.push(join3("/home", entry.name, ".claude", "credentials.json"));
1946
+ candidates.push(join4("/home", entry.name, ".claude", ".credentials.json"));
1947
+ candidates.push(join4("/home", entry.name, ".claude", "credentials.json"));
1791
1948
  }
1792
1949
  } catch {
1793
1950
  }
@@ -1879,18 +2036,18 @@ function normalize(value) {
1879
2036
  }
1880
2037
 
1881
2038
  // src/lib/channel-hash-cache.ts
1882
- import { existsSync as existsSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
1883
- import { join as join4 } from "path";
2039
+ import { existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
2040
+ import { join as join5 } from "path";
1884
2041
  var CACHE_FILENAME = "channel-hash-cache.json";
1885
2042
  function getChannelHashCacheFile(configDir) {
1886
- return join4(configDir, CACHE_FILENAME);
2043
+ return join5(configDir, CACHE_FILENAME);
1887
2044
  }
1888
2045
  function loadChannelHashCache(target, configDir) {
1889
2046
  const path = getChannelHashCacheFile(configDir);
1890
2047
  if (!existsSync3(path)) return;
1891
2048
  let parsed;
1892
2049
  try {
1893
- parsed = JSON.parse(readFileSync5(path, "utf-8"));
2050
+ parsed = JSON.parse(readFileSync6(path, "utf-8"));
1894
2051
  } catch {
1895
2052
  return;
1896
2053
  }
@@ -2435,15 +2592,15 @@ function clearAgentState(agentId, codeName) {
2435
2592
  }
2436
2593
 
2437
2594
  // src/lib/restart-flags.ts
2438
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync6, renameSync as renameSync2, rmSync, writeFileSync as writeFileSync3 } from "fs";
2595
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync7, renameSync as renameSync2, rmSync, writeFileSync as writeFileSync3 } from "fs";
2439
2596
  import { homedir as homedir3 } from "os";
2440
- import { join as join5 } from "path";
2597
+ import { join as join6 } from "path";
2441
2598
  import { randomUUID } from "crypto";
2442
2599
  function restartFlagsDir() {
2443
- return join5(homedir3(), ".augmented", "restart-flags");
2600
+ return join6(homedir3(), ".augmented", "restart-flags");
2444
2601
  }
2445
2602
  function flagPath(codeName) {
2446
- return join5(restartFlagsDir(), `${codeName}.flag`);
2603
+ return join6(restartFlagsDir(), `${codeName}.flag`);
2447
2604
  }
2448
2605
  function readRestartFlags() {
2449
2606
  const dir = restartFlagsDir();
@@ -2452,7 +2609,7 @@ function readRestartFlags() {
2452
2609
  for (const entry of readdirSync2(dir)) {
2453
2610
  if (!entry.endsWith(".flag")) continue;
2454
2611
  try {
2455
- const raw = readFileSync6(join5(dir, entry), "utf8");
2612
+ const raw = readFileSync7(join6(dir, entry), "utf8");
2456
2613
  const parsed = JSON.parse(raw);
2457
2614
  if (typeof parsed.codeName !== "string" || parsed.codeName.length === 0) {
2458
2615
  parsed.codeName = entry.replace(/\.flag$/, "");
@@ -2981,8 +3138,8 @@ function applyRestartAcks(args) {
2981
3138
  var GATEWAY_PORT_BASE = 18800;
2982
3139
  var GATEWAY_PORT_STEP = 10;
2983
3140
  var GATEWAY_PORT_MAX = 18899;
2984
- var AUGMENTED_DIR = join6(process.env["HOME"] ?? "/tmp", ".augmented");
2985
- var GATEWAY_PORTS_FILE = join6(AUGMENTED_DIR, "gateway-ports.json");
3141
+ var AUGMENTED_DIR = join7(process.env["HOME"] ?? "/tmp", ".augmented");
3142
+ var GATEWAY_PORTS_FILE = join7(AUGMENTED_DIR, "gateway-ports.json");
2986
3143
  var CHANNEL_SWEEP_INTERVAL_MS = (() => {
2987
3144
  const raw = parseInt(process.env["AGT_CHANNEL_SWEEP_INTERVAL_MS"] ?? "", 10);
2988
3145
  if (!Number.isFinite(raw)) return 5 * 60 * 1e3;
@@ -3151,7 +3308,7 @@ var runningMcpHashes = /* @__PURE__ */ new Map();
3151
3308
  var runningMcpServerKeys = /* @__PURE__ */ new Map();
3152
3309
  function projectMcpHash(_codeName, projectDir) {
3153
3310
  try {
3154
- const raw = readFileSync7(join6(projectDir, ".mcp.json"), "utf-8");
3311
+ const raw = readFileSync8(join7(projectDir, ".mcp.json"), "utf-8");
3155
3312
  return createHash3("sha256").update(canonicalJson(JSON.parse(raw))).digest("hex");
3156
3313
  } catch {
3157
3314
  return null;
@@ -3159,7 +3316,7 @@ function projectMcpHash(_codeName, projectDir) {
3159
3316
  }
3160
3317
  function projectMcpKeys(_codeName, projectDir) {
3161
3318
  try {
3162
- const raw = readFileSync7(join6(projectDir, ".mcp.json"), "utf-8");
3319
+ const raw = readFileSync8(join7(projectDir, ".mcp.json"), "utf-8");
3163
3320
  const parsed = JSON.parse(raw);
3164
3321
  const servers = parsed.mcpServers;
3165
3322
  if (!servers || typeof servers !== "object") return /* @__PURE__ */ new Set();
@@ -3170,7 +3327,7 @@ function projectMcpKeys(_codeName, projectDir) {
3170
3327
  }
3171
3328
  function readMcpHttpServerConfig(projectDir, serverKey) {
3172
3329
  try {
3173
- const raw = readFileSync7(join6(projectDir, ".mcp.json"), "utf-8");
3330
+ const raw = readFileSync8(join7(projectDir, ".mcp.json"), "utf-8");
3174
3331
  const servers = JSON.parse(raw).mcpServers ?? {};
3175
3332
  const entry = servers[serverKey];
3176
3333
  if (entry && typeof entry.url === "string" && (entry.type === "http" || entry.type === void 0)) {
@@ -3259,11 +3416,22 @@ function checkMcpConfigDriftAndScheduleRestart(codeName, projectDir) {
3259
3416
  const previousKeys = runningMcpServerKeys.get(codeName);
3260
3417
  const currentKeys = projectMcpKeys(codeName, projectDir);
3261
3418
  const decision = decideMcpRestartOnDrift(previousKeys, currentKeys);
3419
+ let quarantineOnlyDrift = false;
3420
+ if (decision.restart && !decision.membershipUnknown && currentKeys) {
3421
+ const quarantined = channelQuarantineStore().getQuarantinedKeys(codeName);
3422
+ const added = decision.addedOrRemoved.filter((k) => currentKeys.has(k));
3423
+ const removed = decision.addedOrRemoved.filter((k) => !currentKeys.has(k));
3424
+ quarantineOnlyDrift = added.length === 0 && removed.length > 0 && removed.every((k) => quarantined.has(k));
3425
+ }
3262
3426
  if (decision.membershipUnknown) {
3263
3427
  clearPresenceReaperState(codeName);
3264
3428
  log(
3265
3429
  `[hot-reload] .mcp.json key membership unavailable for '${codeName}' \u2014 clearing full presence-reaper state and restarting (ENG-5285/ENG-5537)`
3266
3430
  );
3431
+ } else if (quarantineOnlyDrift) {
3432
+ log(
3433
+ `[channel-quarantine] .mcp.json drift for '${codeName}' is quarantined-channel removal only [${decision.addedOrRemoved.join(", ")}] \u2014 adopting new baseline WITHOUT restart (0-restart-on-removal, ENG-5932)`
3434
+ );
3267
3435
  } else if (decision.restart) {
3268
3436
  clearPresenceReaperStateForKeys(codeName, new Set(decision.addedOrRemoved));
3269
3437
  log(
@@ -3274,7 +3442,7 @@ function checkMcpConfigDriftAndScheduleRestart(codeName, projectDir) {
3274
3442
  `[hot-reload] .mcp.json value-only drift for '${codeName}' \u2014 preserving presence-reaper state and NOT restarting (ENG-5285/ENG-5537)`
3275
3443
  );
3276
3444
  }
3277
- if (decision.restart) {
3445
+ if (decision.restart && !quarantineOnlyDrift) {
3278
3446
  scheduleSessionRestart(codeName, 0, ".mcp.json content change (ENG-4897)");
3279
3447
  runningMcpHashes.delete(codeName);
3280
3448
  runningMcpServerKeys.delete(codeName);
@@ -3344,7 +3512,7 @@ var cachedFrameworkVersion = null;
3344
3512
  var lastVersionCheckAt = 0;
3345
3513
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
3346
3514
  var lastResponsivenessProbeAt = 0;
3347
- var agtCliVersion = true ? "0.27.51" : "dev";
3515
+ var agtCliVersion = true ? "0.27.52" : "dev";
3348
3516
  function resolveBrewPath(execFileSync4) {
3349
3517
  try {
3350
3518
  const out = execFileSync4("which", ["brew"], { timeout: 5e3 }).toString().trim();
@@ -3521,7 +3689,7 @@ function ensureClaudeManagedSettings(path = claudeManagedSettingsPath()) {
3521
3689
  try {
3522
3690
  let settings = {};
3523
3691
  if (existsSync5(path)) {
3524
- const raw = readFileSync7(path, "utf-8").trim();
3692
+ const raw = readFileSync8(path, "utf-8").trim();
3525
3693
  if (raw) {
3526
3694
  let parsed;
3527
3695
  try {
@@ -3593,7 +3761,7 @@ async function ensureFrameworkBinary(frameworkId) {
3593
3761
  var CLAUDE_CODE_UPGRADE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
3594
3762
  var claudeCodeUpgradeInFlight = false;
3595
3763
  function claudeCodeUpgradeMarkerPath() {
3596
- return join6(homedir4(), ".augmented", ".last-claude-code-upgrade-check");
3764
+ return join7(homedir4(), ".augmented", ".last-claude-code-upgrade-check");
3597
3765
  }
3598
3766
  function stampClaudeCodeUpgradeMarker() {
3599
3767
  try {
@@ -3603,7 +3771,7 @@ function stampClaudeCodeUpgradeMarker() {
3603
3771
  }
3604
3772
  function claudeCodeUpgradeThrottled() {
3605
3773
  try {
3606
- const lastCheck = parseInt(readFileSync7(claudeCodeUpgradeMarkerPath(), "utf-8").trim(), 10);
3774
+ const lastCheck = parseInt(readFileSync8(claudeCodeUpgradeMarkerPath(), "utf-8").trim(), 10);
3607
3775
  if (!Number.isFinite(lastCheck)) return false;
3608
3776
  return Date.now() - lastCheck < CLAUDE_CODE_UPGRADE_CHECK_INTERVAL_MS;
3609
3777
  } catch {
@@ -3653,7 +3821,7 @@ ${r.stderr}`;
3653
3821
  }
3654
3822
  var UPDATE_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
3655
3823
  function selfUpdateAppliedMarkerPath() {
3656
- return join6(homedir4(), ".augmented", ".last-self-update-applied");
3824
+ return join7(homedir4(), ".augmented", ".last-self-update-applied");
3657
3825
  }
3658
3826
  var selfUpdateUpToDateLogged = false;
3659
3827
  var restartAfterUpgrade = false;
@@ -3676,7 +3844,7 @@ async function checkAndUpdateCli() {
3676
3844
  const isNpmGlobal = !isBrewFormula && resolvedPath.includes("node_modules");
3677
3845
  if (!isBrewFormula && !isNpmGlobal) return;
3678
3846
  const { readFileSync: readF, writeFileSync: writeF } = await import("fs");
3679
- const markerPath = join6(homedir4(), ".augmented", ".last-update-check");
3847
+ const markerPath = join7(homedir4(), ".augmented", ".last-update-check");
3680
3848
  try {
3681
3849
  const lastCheck = parseInt(readF(markerPath, "utf-8").trim(), 10);
3682
3850
  if (Date.now() - lastCheck < UPDATE_CHECK_INTERVAL_MS) return;
@@ -3897,9 +4065,9 @@ async function applyClaudeAuthToEnv(childEnv, label) {
3897
4065
  throw new Error("claude_auth_mode=api_key but /host/exchange returned no decrypted key");
3898
4066
  }
3899
4067
  childEnv.ANTHROPIC_API_KEY = exchange.anthropicApiKey;
3900
- const claudeDir = join6(homedir4(), ".claude");
4068
+ const claudeDir = join7(homedir4(), ".claude");
3901
4069
  for (const filename of [".credentials.json", "credentials.json"]) {
3902
- const p = join6(claudeDir, filename);
4070
+ const p = join7(claudeDir, filename);
3903
4071
  if (existsSync5(p)) {
3904
4072
  try {
3905
4073
  rmSync2(p, { force: true });
@@ -3914,7 +4082,7 @@ async function applyClaudeAuthToEnv(childEnv, label) {
3914
4082
  }
3915
4083
  function loadGatewayPorts() {
3916
4084
  try {
3917
- return JSON.parse(readFileSync7(GATEWAY_PORTS_FILE, "utf-8"));
4085
+ return JSON.parse(readFileSync8(GATEWAY_PORTS_FILE, "utf-8"));
3918
4086
  } catch {
3919
4087
  return {};
3920
4088
  }
@@ -3944,10 +4112,10 @@ function freePort(codeName) {
3944
4112
  }
3945
4113
  }
3946
4114
  function getStateFile() {
3947
- return join6(config?.configDir ?? join6(process.env["HOME"] ?? "/tmp", ".augmented"), "manager-state.json");
4115
+ return join7(config?.configDir ?? join7(process.env["HOME"] ?? "/tmp", ".augmented"), "manager-state.json");
3948
4116
  }
3949
4117
  function channelHashCacheDir() {
3950
- return config?.configDir ?? join6(process.env["HOME"] ?? "/tmp", ".augmented");
4118
+ return config?.configDir ?? join7(process.env["HOME"] ?? "/tmp", ".augmented");
3951
4119
  }
3952
4120
  function loadChannelHashCache2() {
3953
4121
  loadChannelHashCache(agentState.knownChannelConfigHashes, channelHashCacheDir());
@@ -3955,6 +4123,30 @@ function loadChannelHashCache2() {
3955
4123
  function saveChannelHashCache2() {
3956
4124
  saveChannelHashCache(agentState.knownChannelConfigHashes, channelHashCacheDir());
3957
4125
  }
4126
+ var _channelQuarantineStore = null;
4127
+ function channelQuarantineStore() {
4128
+ if (!_channelQuarantineStore) {
4129
+ const dir = config?.configDir ?? join7(process.env["HOME"] ?? "/tmp", ".augmented");
4130
+ _channelQuarantineStore = new ChannelQuarantineStore(defaultQuarantinePath(dir));
4131
+ }
4132
+ return _channelQuarantineStore;
4133
+ }
4134
+ function channelQuarantineMode() {
4135
+ const v = process.env["AGT_CHANNEL_QUARANTINE_MODE"]?.toLowerCase();
4136
+ if (v === "off") return "off";
4137
+ if (v === "enforce") return "enforce";
4138
+ return "shadow";
4139
+ }
4140
+ function channelQuarantineDwellMs() {
4141
+ const raw = parseInt(process.env["AGT_CHANNEL_QUARANTINE_DWELL_MS"] ?? "", 10);
4142
+ return Number.isFinite(raw) && raw >= 0 ? raw : 18e4;
4143
+ }
4144
+ function setWithout(source, remove) {
4145
+ if (remove.size === 0) return new Set(source);
4146
+ const out = /* @__PURE__ */ new Set();
4147
+ for (const v of source) if (!remove.has(v)) out.add(v);
4148
+ return out;
4149
+ }
3958
4150
  function send(msg) {
3959
4151
  if (msg.type === "state-update") {
3960
4152
  try {
@@ -3989,7 +4181,7 @@ function log(msg) {
3989
4181
  `;
3990
4182
  if (!managerLogPath) {
3991
4183
  try {
3992
- managerLogPath = join6(homedir4(), ".augmented", "manager.log");
4184
+ managerLogPath = join7(homedir4(), ".augmented", "manager.log");
3993
4185
  mkdirSync4(dirname3(managerLogPath), { recursive: true });
3994
4186
  if (existsSync5(managerLogPath)) {
3995
4187
  chmodSync(managerLogPath, 384);
@@ -4019,7 +4211,7 @@ function sha256(content) {
4019
4211
  }
4020
4212
  function hashFile(filePath) {
4021
4213
  try {
4022
- const content = readFileSync7(filePath, "utf-8");
4214
+ const content = readFileSync8(filePath, "utf-8");
4023
4215
  return sha256(content);
4024
4216
  } catch {
4025
4217
  return null;
@@ -4044,12 +4236,12 @@ function parseSkillFrontmatter(content) {
4044
4236
  }
4045
4237
  async function refreshSkillsIndexInClaudeMd(configDir, codeName, log2) {
4046
4238
  const { readdirSync: readdirSync4, readFileSync: rfs, existsSync: ex, writeFileSync: writeFileSync5 } = await import("fs");
4047
- const skillsDir = join6(configDir, codeName, "project", ".claude", "skills");
4048
- const claudeMdPath = join6(configDir, codeName, "project", "CLAUDE.md");
4239
+ const skillsDir = join7(configDir, codeName, "project", ".claude", "skills");
4240
+ const claudeMdPath = join7(configDir, codeName, "project", "CLAUDE.md");
4049
4241
  if (!ex(skillsDir) || !ex(claudeMdPath)) return;
4050
4242
  const entries = [];
4051
4243
  for (const dir of readdirSync4(skillsDir).sort()) {
4052
- const skillFile = join6(skillsDir, dir, "SKILL.md");
4244
+ const skillFile = join7(skillsDir, dir, "SKILL.md");
4053
4245
  if (!ex(skillFile)) continue;
4054
4246
  try {
4055
4247
  const { name, description } = parseSkillFrontmatter(rfs(skillFile, "utf-8"));
@@ -4097,10 +4289,10 @@ ${SKILLS_INDEX_END}`;
4097
4289
  }
4098
4290
  async function migrateToProfiles() {
4099
4291
  const homeDir = process.env["HOME"] ?? "/tmp";
4100
- const sharedConfigPath = join6(homeDir, ".openclaw", "openclaw.json");
4292
+ const sharedConfigPath = join7(homeDir, ".openclaw", "openclaw.json");
4101
4293
  let sharedConfig;
4102
4294
  try {
4103
- sharedConfig = JSON.parse(readFileSync7(sharedConfigPath, "utf-8"));
4295
+ sharedConfig = JSON.parse(readFileSync8(sharedConfigPath, "utf-8"));
4104
4296
  } catch {
4105
4297
  return;
4106
4298
  }
@@ -4113,19 +4305,19 @@ async function migrateToProfiles() {
4113
4305
  const codeName = agentEntry["id"];
4114
4306
  if (!codeName) continue;
4115
4307
  if (codeName === "main") continue;
4116
- const profileDir = join6(homeDir, `.openclaw-${codeName}`);
4117
- if (existsSync5(join6(profileDir, "openclaw.json"))) continue;
4308
+ const profileDir = join7(homeDir, `.openclaw-${codeName}`);
4309
+ if (existsSync5(join7(profileDir, "openclaw.json"))) continue;
4118
4310
  log(`Migrating agent '${codeName}' to per-agent profile`);
4119
4311
  if (adapter.seedProfileConfig) {
4120
4312
  adapter.seedProfileConfig(codeName);
4121
4313
  }
4122
- const sharedAuthDir = join6(homeDir, ".openclaw", "agents", codeName, "agent");
4123
- const profileAuthDir = join6(profileDir, "agents", codeName, "agent");
4124
- const authFile = join6(sharedAuthDir, "auth-profiles.json");
4314
+ const sharedAuthDir = join7(homeDir, ".openclaw", "agents", codeName, "agent");
4315
+ const profileAuthDir = join7(profileDir, "agents", codeName, "agent");
4316
+ const authFile = join7(sharedAuthDir, "auth-profiles.json");
4125
4317
  if (existsSync5(authFile)) {
4126
4318
  mkdirSync4(profileAuthDir, { recursive: true });
4127
- const authContent = readFileSync7(authFile, "utf-8");
4128
- writeFileSync4(join6(profileAuthDir, "auth-profiles.json"), authContent);
4319
+ const authContent = readFileSync8(authFile, "utf-8");
4320
+ writeFileSync4(join7(profileAuthDir, "auth-profiles.json"), authContent);
4129
4321
  }
4130
4322
  allocatePort(codeName);
4131
4323
  migrated++;
@@ -4163,7 +4355,7 @@ function readGatewayToken(codeName) {
4163
4355
  }
4164
4356
  const homeDir = process.env["HOME"] ?? "/tmp";
4165
4357
  try {
4166
- const cfg = JSON.parse(readFileSync7(join6(homeDir, `.openclaw-${codeName}`, "openclaw.json"), "utf-8"));
4358
+ const cfg = JSON.parse(readFileSync8(join7(homeDir, `.openclaw-${codeName}`, "openclaw.json"), "utf-8"));
4167
4359
  return cfg?.gateway?.auth?.token;
4168
4360
  } catch {
4169
4361
  return void 0;
@@ -4172,10 +4364,10 @@ function readGatewayToken(codeName) {
4172
4364
  var GATEWAY_HUNG_TIMEOUT_MS = 5 * 6e4;
4173
4365
  function isGatewayHung(codeName) {
4174
4366
  const homeDir = process.env["HOME"] ?? "/tmp";
4175
- const jobsPath = join6(homeDir, `.openclaw-${codeName}`, "cron", "jobs.json");
4367
+ const jobsPath = join7(homeDir, `.openclaw-${codeName}`, "cron", "jobs.json");
4176
4368
  if (!existsSync5(jobsPath)) return false;
4177
4369
  try {
4178
- const data = JSON.parse(readFileSync7(jobsPath, "utf-8"));
4370
+ const data = JSON.parse(readFileSync8(jobsPath, "utf-8"));
4179
4371
  const jobs = data.jobs ?? data;
4180
4372
  if (!Array.isArray(jobs)) return false;
4181
4373
  const now = Date.now();
@@ -4208,15 +4400,15 @@ async function ensureGatewayRunning(codeName, adapter) {
4208
4400
  }
4209
4401
  await new Promise((r) => setTimeout(r, 2e3));
4210
4402
  const homeDir = process.env["HOME"] ?? "/tmp";
4211
- const cronJobsPath = join6(homeDir, `.openclaw-${codeName}`, "cron", "jobs.json");
4403
+ const cronJobsPath = join7(homeDir, `.openclaw-${codeName}`, "cron", "jobs.json");
4212
4404
  clearStaleCronRunState(cronJobsPath);
4213
4405
  } else {
4214
4406
  if (status.port) {
4215
4407
  try {
4216
4408
  const homeDir = process.env["HOME"] ?? "/tmp";
4217
- const configPath = join6(homeDir, `.openclaw-${codeName}`, "openclaw.json");
4409
+ const configPath = join7(homeDir, `.openclaw-${codeName}`, "openclaw.json");
4218
4410
  if (existsSync5(configPath)) {
4219
- const cfg = JSON.parse(readFileSync7(configPath, "utf-8"));
4411
+ const cfg = JSON.parse(readFileSync8(configPath, "utf-8"));
4220
4412
  if (cfg.gateway?.port !== status.port) {
4221
4413
  if (!cfg.gateway) cfg.gateway = {};
4222
4414
  cfg.gateway.port = status.port;
@@ -4240,9 +4432,9 @@ async function ensureGatewayRunning(codeName, adapter) {
4240
4432
  gatewaysStartedThisCycle.add(codeName);
4241
4433
  try {
4242
4434
  const homeDir = process.env["HOME"] ?? "/tmp";
4243
- const configPath = join6(homeDir, `.openclaw-${codeName}`, "openclaw.json");
4435
+ const configPath = join7(homeDir, `.openclaw-${codeName}`, "openclaw.json");
4244
4436
  if (existsSync5(configPath)) {
4245
- const cfg = JSON.parse(readFileSync7(configPath, "utf-8"));
4437
+ const cfg = JSON.parse(readFileSync8(configPath, "utf-8"));
4246
4438
  if (!cfg.gateway) cfg.gateway = {};
4247
4439
  cfg.gateway.port = port;
4248
4440
  writeFileSync4(configPath, JSON.stringify(cfg, null, 2));
@@ -4608,7 +4800,7 @@ async function pollCycle() {
4608
4800
  }
4609
4801
  killAgentChannelProcesses(prev.codeName, { log });
4610
4802
  freePort(prev.codeName);
4611
- const agentDir = join6(adapter.getAgentDir(prev.codeName), "provision");
4803
+ const agentDir = join7(adapter.getAgentDir(prev.codeName), "provision");
4612
4804
  await cleanupAgentFiles(prev.codeName, agentDir);
4613
4805
  clearAgentCaches(prev.agentId, prev.codeName);
4614
4806
  }
@@ -4759,7 +4951,7 @@ async function processAgent(agent, agentStates) {
4759
4951
  }
4760
4952
  const now = (/* @__PURE__ */ new Date()).toISOString();
4761
4953
  const adapter = resolveAgentFramework(agent.code_name);
4762
- let agentDir = join6(adapter.getAgentDir(agent.code_name), "provision");
4954
+ let agentDir = join7(adapter.getAgentDir(agent.code_name), "provision");
4763
4955
  if (agent.status === "draft" || agent.status === "paused") {
4764
4956
  if (previousKnownStatus !== agent.status) {
4765
4957
  log(`Agent '${agent.code_name}' is ${agent.status}, skipping provisioning`);
@@ -4928,7 +5120,7 @@ async function processAgent(agent, agentStates) {
4928
5120
  const frameworkId = refreshData.agent.framework ?? "openclaw";
4929
5121
  agentFrameworkCache.set(agent.code_name, frameworkId);
4930
5122
  const frameworkAdapter = getFramework(frameworkId);
4931
- agentDir = join6(frameworkAdapter.getAgentDir(agent.code_name), "provision");
5123
+ agentDir = join7(frameworkAdapter.getAgentDir(agent.code_name), "provision");
4932
5124
  cacheAgentDeliveryMetadata(agent.code_name, refreshData);
4933
5125
  if (frameworkAdapter.seedProfileConfig) {
4934
5126
  frameworkAdapter.seedProfileConfig(agent.code_name);
@@ -4937,7 +5129,11 @@ async function processAgent(agent, agentStates) {
4937
5129
  const toolsVersion = refreshData.tools.version;
4938
5130
  const known = agentState.knownVersions.get(agent.agent_id);
4939
5131
  let lastProvisionAt = state4.agents.find((a) => a.agentId === agent.agent_id)?.lastProvisionAt ?? null;
4940
- const currentChannelIds = launchableChannelIds(refreshData.channel_configs);
5132
+ const quarantinedChannels = channelQuarantineStore().getQuarantinedKeys(agent.code_name);
5133
+ const currentChannelIds = setWithout(
5134
+ launchableChannelIds(refreshData.channel_configs),
5135
+ quarantinedChannels
5136
+ );
4941
5137
  const previousChannelIds = agentState.knownChannels.get(agent.agent_id);
4942
5138
  const channelsChanged = !previousChannelIds || currentChannelIds.size !== previousChannelIds.size || [...currentChannelIds].some((ch) => !previousChannelIds.has(ch)) || [...previousChannelIds].some((ch) => !currentChannelIds.has(ch));
4943
5139
  let channelConfigConverged = true;
@@ -4959,7 +5155,7 @@ async function processAgent(agent, agentStates) {
4959
5155
  const changedFiles = [];
4960
5156
  mkdirSync4(agentDir, { recursive: true });
4961
5157
  for (const artifact of artifacts) {
4962
- const filePath = join6(agentDir, artifact.relativePath);
5158
+ const filePath = join7(agentDir, artifact.relativePath);
4963
5159
  let existingHash;
4964
5160
  let newHash;
4965
5161
  let writeContent = artifact.content;
@@ -4978,8 +5174,8 @@ async function processAgent(agent, agentStates) {
4978
5174
  };
4979
5175
  newHash = sha256(stripDynamicSections(artifact.content));
4980
5176
  try {
4981
- const projectClaudeMd = join6(config.configDir, agent.code_name, "project", "CLAUDE.md");
4982
- const existing = readFileSync7(projectClaudeMd, "utf-8");
5177
+ const projectClaudeMd = join7(config.configDir, agent.code_name, "project", "CLAUDE.md");
5178
+ const existing = readFileSync8(projectClaudeMd, "utf-8");
4983
5179
  existingHash = sha256(stripDynamicSections(existing));
4984
5180
  } catch {
4985
5181
  existingHash = null;
@@ -4997,7 +5193,7 @@ async function processAgent(agent, agentStates) {
4997
5193
  const generatorKeys = Object.keys(generatorServers);
4998
5194
  let existingRaw = "";
4999
5195
  try {
5000
- existingRaw = readFileSync7(filePath, "utf-8");
5196
+ existingRaw = readFileSync8(filePath, "utf-8");
5001
5197
  } catch {
5002
5198
  }
5003
5199
  const existingServers = parseMcp(existingRaw);
@@ -5019,22 +5215,22 @@ async function processAgent(agent, agentStates) {
5019
5215
  }
5020
5216
  }
5021
5217
  if (changedFiles.length > 0) {
5022
- const isFirst = !existsSync5(join6(agentDir, "CHARTER.md"));
5218
+ const isFirst = !existsSync5(join7(agentDir, "CHARTER.md"));
5023
5219
  const verb = isFirst ? "Provisioning" : "Updating";
5024
5220
  const fileNames = changedFiles.map((f) => f.relativePath).join(", ");
5025
5221
  log(`${verb} '${agent.code_name}': ${fileNames}`);
5026
5222
  for (const file of changedFiles) {
5027
- const filePath = join6(agentDir, file.relativePath);
5223
+ const filePath = join7(agentDir, file.relativePath);
5028
5224
  mkdirSync4(dirname3(filePath), { recursive: true });
5029
5225
  writeFileSync4(filePath, file.content);
5030
5226
  }
5031
5227
  try {
5032
- const provSkillsDir = join6(agentDir, ".claude", "skills");
5228
+ const provSkillsDir = join7(agentDir, ".claude", "skills");
5033
5229
  if (existsSync5(provSkillsDir)) {
5034
5230
  for (const folder of readdirSync3(provSkillsDir)) {
5035
5231
  if (folder.startsWith("knowledge-")) {
5036
5232
  try {
5037
- rmSync2(join6(provSkillsDir, folder), { recursive: true });
5233
+ rmSync2(join7(provSkillsDir, folder), { recursive: true });
5038
5234
  } catch {
5039
5235
  }
5040
5236
  }
@@ -5047,7 +5243,7 @@ async function processAgent(agent, agentStates) {
5047
5243
  const trackedFiles2 = frameworkAdapter.driftTrackedFiles();
5048
5244
  const hashes = /* @__PURE__ */ new Map();
5049
5245
  for (const file of trackedFiles2) {
5050
- const h = hashFile(join6(agentDir, file));
5246
+ const h = hashFile(join7(agentDir, file));
5051
5247
  if (h) hashes.set(file, h);
5052
5248
  }
5053
5249
  agentState.writtenHashes.set(agent.agent_id, hashes);
@@ -5115,7 +5311,7 @@ async function processAgent(agent, agentStates) {
5115
5311
  if (written && existsSync5(agentDir)) {
5116
5312
  const driftedFiles = [];
5117
5313
  for (const [file, expectedHash] of written) {
5118
- const localHash = hashFile(join6(agentDir, file));
5314
+ const localHash = hashFile(join7(agentDir, file));
5119
5315
  if (localHash && localHash !== expectedHash) {
5120
5316
  driftedFiles.push(file);
5121
5317
  }
@@ -5126,7 +5322,7 @@ async function processAgent(agent, agentStates) {
5126
5322
  try {
5127
5323
  const localHashes = {};
5128
5324
  for (const file of driftedFiles) {
5129
- localHashes[file] = hashFile(join6(agentDir, file));
5325
+ localHashes[file] = hashFile(join7(agentDir, file));
5130
5326
  }
5131
5327
  await api.post("/host/drift", {
5132
5328
  agent_id: agent.agent_id,
@@ -5164,6 +5360,14 @@ async function processAgent(agent, agentStates) {
5164
5360
  if (refreshData.channel_configs && frameworkAdapter.writeChannelCredentials) {
5165
5361
  if (agent.status === "active") {
5166
5362
  for (const [channelId, entry] of Object.entries(refreshData.channel_configs)) {
5363
+ if (quarantinedChannels.has(channelId)) {
5364
+ try {
5365
+ frameworkAdapter.removeChannelCredentials?.(agent.code_name, channelId);
5366
+ } catch (err) {
5367
+ log(`[channel-quarantine] failed to drop quarantined channel '${agent.code_name}/${channelId}' from .mcp.json: ${err.message}`);
5368
+ }
5369
+ continue;
5370
+ }
5167
5371
  if ((entry.status === "active" || entry.status === "pending") && entry.config) {
5168
5372
  if (!activeChannels.has(channelId)) {
5169
5373
  activeChannels.set(channelId, /* @__PURE__ */ new Set());
@@ -5270,7 +5474,14 @@ async function processAgent(agent, agentStates) {
5270
5474
  framework: agentFrameworkCache.get(agent.code_name) ?? "openclaw",
5271
5475
  sessionHealthy: isSessionHealthy(agent.code_name)
5272
5476
  }) : { restart: false, added: [], removed: [] };
5273
- if (restartDecision.restart) {
5477
+ const quarantineOnlyRemoval = restartDecision.restart && restartDecision.added.length === 0 && restartDecision.removed.length > 0 && restartDecision.removed.every((c) => quarantinedChannels.has(c));
5478
+ const channelSetRestartScheduled = restartDecision.restart && !quarantineOnlyRemoval;
5479
+ if (quarantineOnlyRemoval) {
5480
+ log(
5481
+ `[channel-quarantine] suppressing channel-set-change restart for '${agent.code_name}' \u2014 only delta is quarantined-channel removal [${restartDecision.removed.join(", ")}] (0-restart-on-removal, ENG-5932)`
5482
+ );
5483
+ }
5484
+ if (channelSetRestartScheduled) {
5274
5485
  const reasonParts = [];
5275
5486
  if (restartDecision.added.length > 0) reasonParts.push(`added=${restartDecision.added.join(",")}`);
5276
5487
  if (restartDecision.removed.length > 0) reasonParts.push(`removed=${restartDecision.removed.join(",")}`);
@@ -5295,7 +5506,7 @@ async function processAgent(agent, agentStates) {
5295
5506
  sessionMode: refreshData.agent.session_mode,
5296
5507
  framework: agentFrameworkCache.get(agent.code_name) ?? "openclaw",
5297
5508
  sessionHealthy: isSessionHealthy(agent.code_name),
5298
- channelSetRestartAlreadyScheduled: restartDecision.restart
5509
+ channelSetRestartAlreadyScheduled: channelSetRestartScheduled
5299
5510
  }) : { restart: false, firstPoll: prevSenderPolicyHash === void 0, changed: false };
5300
5511
  if (senderPolicyDecision.restart) {
5301
5512
  log(
@@ -5328,18 +5539,18 @@ async function processAgent(agent, agentStates) {
5328
5539
  if (agentSessionMode === "persistent" && (agentFrameworkCache.get(agent.code_name) ?? "openclaw") === "claude-code") {
5329
5540
  try {
5330
5541
  const agentProvisionDir = agentDir;
5331
- const projectDir = join6(homedir4(), ".augmented", agent.code_name, "project");
5542
+ const projectDir = join7(homedir4(), ".augmented", agent.code_name, "project");
5332
5543
  mkdirSync4(agentProvisionDir, { recursive: true });
5333
5544
  mkdirSync4(projectDir, { recursive: true });
5334
- const provisionMcpPath = join6(agentProvisionDir, ".mcp.json");
5335
- const projectMcpPath = join6(projectDir, ".mcp.json");
5545
+ const provisionMcpPath = join7(agentProvisionDir, ".mcp.json");
5546
+ const projectMcpPath = join7(projectDir, ".mcp.json");
5336
5547
  let mcpConfig = { mcpServers: {} };
5337
5548
  try {
5338
- mcpConfig = JSON.parse(readFileSync7(provisionMcpPath, "utf-8"));
5549
+ mcpConfig = JSON.parse(readFileSync8(provisionMcpPath, "utf-8"));
5339
5550
  if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
5340
5551
  } catch {
5341
5552
  }
5342
- const localDirectChatChannel = join6(homedir4(), ".augmented", "_mcp", "direct-chat-channel.js");
5553
+ const localDirectChatChannel = join7(homedir4(), ".augmented", "_mcp", "direct-chat-channel.js");
5343
5554
  const directChatTeamSettings = refreshData.team?.settings;
5344
5555
  const directChatTz = (() => {
5345
5556
  const tz = directChatTeamSettings?.["timezone"];
@@ -5366,7 +5577,7 @@ async function processAgent(agent, agentStates) {
5366
5577
  log(`Channel credentials written for '${agent.code_name}/direct-chat'`);
5367
5578
  }
5368
5579
  }
5369
- const staleChannelsPath = join6(projectDir, ".mcp-channels.json");
5580
+ const staleChannelsPath = join7(projectDir, ".mcp-channels.json");
5370
5581
  if (existsSync5(staleChannelsPath)) {
5371
5582
  try {
5372
5583
  rmSync2(staleChannelsPath, { force: true });
@@ -5431,7 +5642,7 @@ async function processAgent(agent, agentStates) {
5431
5642
  }
5432
5643
  if (process.env.AGT_CONNECTIVITY_PROBE_ENABLED === "true") {
5433
5644
  try {
5434
- const probeProjectDir = join6(homedir4(), ".augmented", agent.code_name, "project");
5645
+ const probeProjectDir = join7(homedir4(), ".augmented", agent.code_name, "project");
5435
5646
  await runAgentConnectivityProbes(agent, integrations, probeProjectDir);
5436
5647
  } catch (err) {
5437
5648
  log(`Connectivity probe failed for '${agent.code_name}': ${err.message}`);
@@ -5441,11 +5652,11 @@ async function processAgent(agent, agentStates) {
5441
5652
  const intHash = computeIntegrationsHash(integrations);
5442
5653
  const prevIntHash = agentState.knownIntegrationHashes.get(agent.agent_id);
5443
5654
  if (intHash !== prevIntHash) {
5444
- const projectDir = join6(homedir4(), ".augmented", agent.code_name, "project");
5445
- const envIntPath = join6(projectDir, ".env.integrations");
5655
+ const projectDir = join7(homedir4(), ".augmented", agent.code_name, "project");
5656
+ const envIntPath = join7(projectDir, ".env.integrations");
5446
5657
  let preWriteEnv;
5447
5658
  try {
5448
- preWriteEnv = readFileSync7(envIntPath, "utf-8");
5659
+ preWriteEnv = readFileSync8(envIntPath, "utf-8");
5449
5660
  } catch {
5450
5661
  preWriteEnv = void 0;
5451
5662
  }
@@ -5457,9 +5668,9 @@ async function processAgent(agent, agentStates) {
5457
5668
  let rotationHandled = true;
5458
5669
  if (fw === "claude-code" && isSessionHealthy(agent.code_name)) {
5459
5670
  try {
5460
- const projectMcpPath = join6(projectDir, ".mcp.json");
5461
- const postWriteEnv = readFileSync7(envIntPath, "utf-8");
5462
- const mcpContent = readFileSync7(projectMcpPath, "utf-8");
5671
+ const projectMcpPath = join7(projectDir, ".mcp.json");
5672
+ const postWriteEnv = readFileSync8(envIntPath, "utf-8");
5673
+ const mcpContent = readFileSync8(projectMcpPath, "utf-8");
5463
5674
  const changedVars = diffEnvIntegrations(preWriteEnv, postWriteEnv);
5464
5675
  const mcpJsonForReap = JSON.parse(mcpContent);
5465
5676
  const affectedServerKeys = findMcpServersUsingVars(mcpJsonForReap, changedVars);
@@ -5527,8 +5738,8 @@ async function processAgent(agent, agentStates) {
5527
5738
  const mcpPath = frameworkAdapter.getMcpPath(agent.code_name);
5528
5739
  if (mcpPath) {
5529
5740
  try {
5530
- const { readFileSync: readFileSync8 } = await import("fs");
5531
- const mcpConfig = JSON.parse(readFileSync8(mcpPath, "utf-8"));
5741
+ const { readFileSync: readFileSync9 } = await import("fs");
5742
+ const mcpConfig = JSON.parse(readFileSync9(mcpPath, "utf-8"));
5532
5743
  if (mcpConfig.mcpServers) {
5533
5744
  const managedPrefixes = [
5534
5745
  "composio_",
@@ -5624,7 +5835,7 @@ async function processAgent(agent, agentStates) {
5624
5835
  if (agent.status === "active") {
5625
5836
  if (frameworkAdapter.installPlugin) {
5626
5837
  try {
5627
- const pluginPath = join6(process.cwd(), "packages", "openclaw-plugin-augmented", "src", "index.ts");
5838
+ const pluginPath = join7(process.cwd(), "packages", "openclaw-plugin-augmented", "src", "index.ts");
5628
5839
  if (existsSync5(pluginPath)) {
5629
5840
  frameworkAdapter.installPlugin(agent.code_name, "augmented", pluginPath, {
5630
5841
  agtHost: requireHost(),
@@ -5695,14 +5906,14 @@ async function processAgent(agent, agentStates) {
5695
5906
  const frameworkId2 = frameworkAdapter.id;
5696
5907
  const candidateSkillDirs = [
5697
5908
  // Claude Code — framework runtime tree
5698
- join6(homedir5(), ".augmented", agent.code_name, "skills"),
5909
+ join7(homedir5(), ".augmented", agent.code_name, "skills"),
5699
5910
  // Claude Code — project tree
5700
- join6(homedir5(), ".augmented", agent.code_name, "project", ".claude", "skills"),
5911
+ join7(homedir5(), ".augmented", agent.code_name, "project", ".claude", "skills"),
5701
5912
  // OpenClaw — framework runtime tree
5702
- join6(homedir5(), `.openclaw-${agent.code_name}`, "skills"),
5913
+ join7(homedir5(), `.openclaw-${agent.code_name}`, "skills"),
5703
5914
  // Defensive: legacy provision-side path, not currently an
5704
5915
  // install target but cheap to sweep.
5705
- join6(agentDir, ".claude", "skills")
5916
+ join7(agentDir, ".claude", "skills")
5706
5917
  ];
5707
5918
  const existingDirs = candidateSkillDirs.filter((d) => existsSync5(d));
5708
5919
  const discoveredEntries = /* @__PURE__ */ new Set();
@@ -5718,7 +5929,7 @@ async function processAgent(agent, agentStates) {
5718
5929
  }
5719
5930
  const removeSkillFolder = (entry, reason) => {
5720
5931
  for (const dir of existingDirs) {
5721
- const p = join6(dir, entry);
5932
+ const p = join7(dir, entry);
5722
5933
  if (existsSync5(p)) {
5723
5934
  rmSync3(p, { recursive: true, force: true });
5724
5935
  }
@@ -5880,8 +6091,8 @@ async function processAgent(agent, agentStates) {
5880
6091
  const sess = getSessionState(agent.code_name);
5881
6092
  let mcpJsonParsed = null;
5882
6093
  try {
5883
- const mcpPath = join6(getProjectDir(agent.code_name), ".mcp.json");
5884
- mcpJsonParsed = JSON.parse(readFileSync7(mcpPath, "utf-8"));
6094
+ const mcpPath = join7(getProjectDir(agent.code_name), ".mcp.json");
6095
+ mcpJsonParsed = JSON.parse(readFileSync8(mcpPath, "utf-8"));
5885
6096
  } catch {
5886
6097
  }
5887
6098
  reapMissingMcpSessions({
@@ -5952,6 +6163,37 @@ async function processAgent(agent, agentStates) {
5952
6163
  `[mcp-presence-reaper] failed to record restart event for '${codeName}' (ENG-5286): ${err.message} \u2014 local restart still proceeded; CloudWatch metric will under-count this event`
5953
6164
  );
5954
6165
  });
6166
+ },
6167
+ // ENG-5932: classify each declared key for the quarantine path. Reads a
6168
+ // server-declared criticality field off the .mcp.json entry when
6169
+ // present (Slice 2 will stamp it); otherwise fails closed to ESSENTIAL
6170
+ // for everything except the known optional channel set. Tool servers
6171
+ // (composio_*/xero) classify ESSENTIAL here and stay on the existing
6172
+ // managed-toolkit onGiveUp path above.
6173
+ classifyKey: (serverKey) => classifyChannelCriticality(
6174
+ serverKey,
6175
+ mcpJsonParsed?.mcpServers?.[serverKey]
6176
+ ),
6177
+ quarantineDwellMs: channelQuarantineDwellMs(),
6178
+ quarantineMode: channelQuarantineMode(),
6179
+ // ENG-5932: enforce-mode quarantine. Persist the local marker; the next
6180
+ // provisioning poll then omits the channel from .mcp.json + the launch
6181
+ // flags, and the channel-set-change / .mcp.json-drift restart paths are
6182
+ // taught to NOT bounce the session for a quarantine-driven removal.
6183
+ // Local-authoritative: no API call — with the control plane down, the
6184
+ // flap still stops.
6185
+ onQuarantine: (codeName, serverKey) => {
6186
+ const newly = channelQuarantineStore().quarantine(
6187
+ codeName,
6188
+ serverKey,
6189
+ `mcp-presence-reaper: optional channel dead past restart budget + dwell`,
6190
+ Date.now()
6191
+ );
6192
+ if (newly) {
6193
+ log(
6194
+ `[channel-quarantine] persisted quarantine marker for '${codeName}:${serverKey}' \u2014 will be dropped from .mcp.json on next provisioning poll (ENG-5932)`
6195
+ );
6196
+ }
5955
6197
  }
5956
6198
  });
5957
6199
  } catch (err) {
@@ -6035,10 +6277,10 @@ async function processAgent(agent, agentStates) {
6035
6277
  lastWorkTriggerAt.set(agent.code_name, triggerTs);
6036
6278
  if (agentFw === "openclaw" && gatewayRunning && gatewayPort) {
6037
6279
  const homeDir = process.env["HOME"] ?? "/tmp";
6038
- const jobsPath = join6(homeDir, `.openclaw-${agent.code_name}`, "cron", "jobs.json");
6280
+ const jobsPath = join7(homeDir, `.openclaw-${agent.code_name}`, "cron", "jobs.json");
6039
6281
  if (existsSync5(jobsPath)) {
6040
6282
  try {
6041
- const jobsData = JSON.parse(readFileSync7(jobsPath, "utf-8"));
6283
+ const jobsData = JSON.parse(readFileSync8(jobsPath, "utf-8"));
6042
6284
  const kanbanJob = (jobsData.jobs ?? []).find(
6043
6285
  (j) => typeof j.name === "string" && j.name.includes("kanban-work")
6044
6286
  );
@@ -6175,7 +6417,7 @@ In progress for ${age} minutes \u2014 auto-failed`).catch(() => {
6175
6417
  if (trackedFiles.length > 0 && existsSync5(agentDir)) {
6176
6418
  const hashes = /* @__PURE__ */ new Map();
6177
6419
  for (const file of trackedFiles) {
6178
- const h = hashFile(join6(agentDir, file));
6420
+ const h = hashFile(join7(agentDir, file));
6179
6421
  if (h) hashes.set(file, h);
6180
6422
  }
6181
6423
  agentState.writtenHashes.set(agent.agent_id, hashes);
@@ -6209,19 +6451,19 @@ function cleanupStaleSessions(codeName) {
6209
6451
  lastCleanupAt.set(codeName, Date.now());
6210
6452
  const homeDir = process.env["HOME"] ?? "/tmp";
6211
6453
  for (const agentDir of ["main", codeName]) {
6212
- const sessionsDir = join6(homeDir, `.openclaw-${codeName}`, "agents", agentDir, "sessions");
6454
+ const sessionsDir = join7(homeDir, `.openclaw-${codeName}`, "agents", agentDir, "sessions");
6213
6455
  cleanupCronSessions(sessionsDir, CRON_SESSION_KEEP_COUNT);
6214
6456
  }
6215
- const cronRunsDir = join6(homeDir, `.openclaw-${codeName}`, "cron", "runs");
6457
+ const cronRunsDir = join7(homeDir, `.openclaw-${codeName}`, "cron", "runs");
6216
6458
  cleanupOldFiles(cronRunsDir, CRON_RUN_RETENTION_DAYS, ".jsonl");
6217
- const cronJobsPath = join6(homeDir, `.openclaw-${codeName}`, "cron", "jobs.json");
6459
+ const cronJobsPath = join7(homeDir, `.openclaw-${codeName}`, "cron", "jobs.json");
6218
6460
  clearStaleCronRunState(cronJobsPath);
6219
6461
  }
6220
6462
  function cleanupCronSessions(sessionsDir, keepCount) {
6221
- const indexPath = join6(sessionsDir, "sessions.json");
6463
+ const indexPath = join7(sessionsDir, "sessions.json");
6222
6464
  if (!existsSync5(indexPath)) return;
6223
6465
  try {
6224
- const raw = readFileSync7(indexPath, "utf-8");
6466
+ const raw = readFileSync8(indexPath, "utf-8");
6225
6467
  const index = JSON.parse(raw);
6226
6468
  const cronRunKeys = Object.keys(index).filter((k) => k.includes(":cron:") && k.includes(":run:")).map((k) => ({
6227
6469
  key: k,
@@ -6234,7 +6476,7 @@ function cleanupCronSessions(sessionsDir, keepCount) {
6234
6476
  for (const entry of toDelete) {
6235
6477
  delete index[entry.key];
6236
6478
  if (entry.sessionId) {
6237
- const sessionFile = join6(sessionsDir, `${entry.sessionId}.jsonl`);
6479
+ const sessionFile = join7(sessionsDir, `${entry.sessionId}.jsonl`);
6238
6480
  try {
6239
6481
  if (existsSync5(sessionFile)) {
6240
6482
  unlinkSync(sessionFile);
@@ -6256,7 +6498,7 @@ function cleanupCronSessions(sessionsDir, keepCount) {
6256
6498
  delete index[parentKey];
6257
6499
  if (parentSessionId) {
6258
6500
  try {
6259
- const f = join6(sessionsDir, `${parentSessionId}.jsonl`);
6501
+ const f = join7(sessionsDir, `${parentSessionId}.jsonl`);
6260
6502
  if (existsSync5(f)) {
6261
6503
  unlinkSync(f);
6262
6504
  deletedFiles++;
@@ -6277,7 +6519,7 @@ var STALE_RUN_TIMEOUT_MS = 5 * 6e4;
6277
6519
  function clearStaleCronRunState(jobsPath) {
6278
6520
  if (!existsSync5(jobsPath)) return;
6279
6521
  try {
6280
- const raw = readFileSync7(jobsPath, "utf-8");
6522
+ const raw = readFileSync8(jobsPath, "utf-8");
6281
6523
  const data = JSON.parse(raw);
6282
6524
  const jobs = data.jobs ?? data;
6283
6525
  if (!Array.isArray(jobs)) return;
@@ -6314,7 +6556,7 @@ function cleanupOldFiles(dir, maxAgeDays, ext) {
6314
6556
  try {
6315
6557
  for (const f of readdirSync3(dir)) {
6316
6558
  if (!f.endsWith(ext)) continue;
6317
- const fullPath = join6(dir, f);
6559
+ const fullPath = join7(dir, f);
6318
6560
  try {
6319
6561
  const st = statSync2(fullPath);
6320
6562
  if (st.mtimeMs < cutoff) {
@@ -6334,7 +6576,7 @@ var inFlightClaudeTasks = /* @__PURE__ */ new Set();
6334
6576
  var claudeTaskConcurrency = /* @__PURE__ */ new Map();
6335
6577
  var MAX_CLAUDE_CONCURRENCY = 2;
6336
6578
  function claudePidFilePath() {
6337
- return join6(homedir4(), ".augmented", "manager-claude-pids.json");
6579
+ return join7(homedir4(), ".augmented", "manager-claude-pids.json");
6338
6580
  }
6339
6581
  var inFlightClaudePids = /* @__PURE__ */ new Map();
6340
6582
  function registerClaudeSpawn(record) {
@@ -6642,7 +6884,7 @@ async function fireScheduledTaskViaKanban(codeName, agentId, task, prompt) {
6642
6884
  }
6643
6885
  async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
6644
6886
  const projectDir = getProjectDir2(codeName);
6645
- const mcpConfigPath = join6(projectDir, ".mcp.json");
6887
+ const mcpConfigPath = join7(projectDir, ".mcp.json");
6646
6888
  let runId = null;
6647
6889
  let kanbanItemId = null;
6648
6890
  let taskResult;
@@ -6650,11 +6892,11 @@ async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
6650
6892
  const priorRuns = await fetchPriorScheduledRuns(agentId, task.taskId);
6651
6893
  prompt = wrapScheduledTaskPrompt(prompt, { priorRuns });
6652
6894
  try {
6653
- const claudeMdPath = join6(projectDir, "CLAUDE.md");
6895
+ const claudeMdPath = join7(projectDir, "CLAUDE.md");
6654
6896
  const serverNames = [];
6655
6897
  if (existsSync5(mcpConfigPath)) {
6656
6898
  try {
6657
- const d = JSON.parse(readFileSync7(mcpConfigPath, "utf-8"));
6899
+ const d = JSON.parse(readFileSync8(mcpConfigPath, "utf-8"));
6658
6900
  if (d.mcpServers) serverNames.push(...Object.keys(d.mcpServers));
6659
6901
  } catch {
6660
6902
  }
@@ -6677,10 +6919,10 @@ async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
6677
6919
  claudeArgs.push("--system-prompt-file", claudeMdPath);
6678
6920
  }
6679
6921
  const childEnv = { ...process.env };
6680
- const envIntPath = join6(projectDir, ".env.integrations");
6922
+ const envIntPath = join7(projectDir, ".env.integrations");
6681
6923
  if (existsSync5(envIntPath)) {
6682
6924
  try {
6683
- for (const line of readFileSync7(envIntPath, "utf-8").split("\n")) {
6925
+ for (const line of readFileSync8(envIntPath, "utf-8").split("\n")) {
6684
6926
  if (!line || line.startsWith("#") || !line.includes("=")) continue;
6685
6927
  const eqIdx = line.indexOf("=");
6686
6928
  childEnv[line.slice(0, eqIdx)] = line.slice(eqIdx + 1);
@@ -6849,8 +7091,8 @@ var claudeAuthTupleBySession = /* @__PURE__ */ new Map();
6849
7091
  async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
6850
7092
  const codeName = agent.code_name;
6851
7093
  const projectDir = getProjectDir(codeName);
6852
- const mcpConfigPath = join6(projectDir, ".mcp.json");
6853
- const claudeMdPath = join6(projectDir, "CLAUDE.md");
7094
+ const mcpConfigPath = join7(projectDir, ".mcp.json");
7095
+ const claudeMdPath = join7(projectDir, "CLAUDE.md");
6854
7096
  if (restartBreaker.isTripped(codeName)) {
6855
7097
  const trip = restartBreaker.getTrip(codeName);
6856
7098
  return {
@@ -6869,7 +7111,9 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
6869
7111
  const channels = [];
6870
7112
  const devChannels = [];
6871
7113
  if (channelConfigs) {
7114
+ const quarantinedChannels = channelQuarantineStore().getQuarantinedKeys(codeName);
6872
7115
  const isChannelEnabled = (id) => {
7116
+ if (quarantinedChannels.has(id)) return false;
6873
7117
  const entry = channelConfigs[id];
6874
7118
  return !!entry?.config && (entry.status === "active" || entry.status === "pending");
6875
7119
  };
@@ -7471,11 +7715,11 @@ ${escapeXml(msg.content)}
7471
7715
  if (fw === "claude-code") {
7472
7716
  const { getProjectDir: ccProjectDir } = await import("../claude-scheduler-EM24LTGV.js");
7473
7717
  const projDir = ccProjectDir(agent.codeName);
7474
- const mcpConfigPath = join6(projDir, ".mcp.json");
7718
+ const mcpConfigPath = join7(projDir, ".mcp.json");
7475
7719
  const serverNames = [];
7476
7720
  if (existsSync5(mcpConfigPath)) {
7477
7721
  try {
7478
- const d = JSON.parse(readFileSync7(mcpConfigPath, "utf-8"));
7722
+ const d = JSON.parse(readFileSync8(mcpConfigPath, "utf-8"));
7479
7723
  if (d.mcpServers) serverNames.push(...Object.keys(d.mcpServers));
7480
7724
  } catch {
7481
7725
  }
@@ -7494,15 +7738,15 @@ ${escapeXml(msg.content)}
7494
7738
  "--allowedTools",
7495
7739
  allowedTools
7496
7740
  ];
7497
- const chatClaudeMd = join6(projDir, "CLAUDE.md");
7741
+ const chatClaudeMd = join7(projDir, "CLAUDE.md");
7498
7742
  if (existsSync5(chatClaudeMd)) {
7499
7743
  chatArgs.push("--system-prompt-file", chatClaudeMd);
7500
7744
  }
7501
- const envIntPath = join6(projDir, ".env.integrations");
7745
+ const envIntPath = join7(projDir, ".env.integrations");
7502
7746
  const childEnv = { ...process.env };
7503
7747
  if (existsSync5(envIntPath)) {
7504
7748
  try {
7505
- for (const line of readFileSync7(envIntPath, "utf-8").split("\n")) {
7749
+ for (const line of readFileSync8(envIntPath, "utf-8").split("\n")) {
7506
7750
  if (!line || line.startsWith("#") || !line.includes("=")) continue;
7507
7751
  const eqIdx = line.indexOf("=");
7508
7752
  childEnv[line.slice(0, eqIdx)] = line.slice(eqIdx + 1);
@@ -7875,12 +8119,12 @@ function getBuiltInSkillContent(skillId) {
7875
8119
  if (builtInSkillCache.has(skillId)) return builtInSkillCache.get(skillId);
7876
8120
  try {
7877
8121
  const candidates = [
7878
- join6(process.cwd(), "skills", skillId, "SKILL.md"),
7879
- join6(new URL(".", import.meta.url).pathname, "..", "..", "..", "..", "skills", skillId, "SKILL.md")
8122
+ join7(process.cwd(), "skills", skillId, "SKILL.md"),
8123
+ join7(new URL(".", import.meta.url).pathname, "..", "..", "..", "..", "skills", skillId, "SKILL.md")
7880
8124
  ];
7881
8125
  for (const candidate of candidates) {
7882
8126
  if (existsSync5(candidate)) {
7883
- const content = readFileSync7(candidate, "utf-8");
8127
+ const content = readFileSync8(candidate, "utf-8");
7884
8128
  const files = [{ relativePath: "SKILL.md", content }];
7885
8129
  builtInSkillCache.set(skillId, files);
7886
8130
  return files;
@@ -8833,8 +9077,8 @@ function parseMemoryFile(raw, fallbackName) {
8833
9077
  };
8834
9078
  }
8835
9079
  async function syncMemories(agent, configDir, log2) {
8836
- const projectDir = join6(configDir, agent.code_name, "project");
8837
- const memoryDir = join6(projectDir, "memory");
9080
+ const projectDir = join7(configDir, agent.code_name, "project");
9081
+ const memoryDir = join7(projectDir, "memory");
8838
9082
  const isFreshSync = pendingFreshMemorySync.has(agent.agent_id);
8839
9083
  if (isFreshSync) {
8840
9084
  log2(`[memory-sync] Fresh-sync requested for '${agent.code_name}' \u2014 pulling DB first`);
@@ -8852,7 +9096,7 @@ async function syncMemories(agent, configDir, log2) {
8852
9096
  for (const file of readdirSync3(memoryDir)) {
8853
9097
  if (!file.endsWith(".md")) continue;
8854
9098
  try {
8855
- const raw = readFileSync7(join6(memoryDir, file), "utf-8");
9099
+ const raw = readFileSync8(join7(memoryDir, file), "utf-8");
8856
9100
  const fileHash = createHash3("sha256").update(raw).digest("hex").slice(0, 16);
8857
9101
  currentHashes.set(file, fileHash);
8858
9102
  if (prevHashes.get(file) === fileHash) continue;
@@ -8877,7 +9121,7 @@ async function syncMemories(agent, configDir, log2) {
8877
9121
  } catch (err) {
8878
9122
  for (const mem of changedMemories) {
8879
9123
  for (const [file] of currentHashes) {
8880
- const parsed = parseMemoryFile(readFileSync7(join6(memoryDir, file), "utf-8"), file.replace(/\.md$/, ""));
9124
+ const parsed = parseMemoryFile(readFileSync8(join7(memoryDir, file), "utf-8"), file.replace(/\.md$/, ""));
8881
9125
  if (parsed?.name === mem.name) currentHashes.delete(file);
8882
9126
  }
8883
9127
  }
@@ -8912,7 +9156,7 @@ async function downloadMemories(agent, memoryDir, log2, { force }) {
8912
9156
  const mem = dbMemories.memories[i];
8913
9157
  const rawSlug = mem.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "").slice(0, 60);
8914
9158
  const slug = rawSlug || `memory-${i}`;
8915
- const filePath = join6(memoryDir, `${slug}.md`);
9159
+ const filePath = join7(memoryDir, `${slug}.md`);
8916
9160
  const desired = `---
8917
9161
  name: ${JSON.stringify(mem.name)}
8918
9162
  type: ${mem.type}
@@ -8924,7 +9168,7 @@ ${mem.content}
8924
9168
  if (existsSync5(filePath)) {
8925
9169
  let existing = "";
8926
9170
  try {
8927
- existing = readFileSync7(filePath, "utf-8");
9171
+ existing = readFileSync8(filePath, "utf-8");
8928
9172
  } catch {
8929
9173
  }
8930
9174
  if (existing === desired) continue;
@@ -9138,7 +9382,7 @@ function startManager(opts) {
9138
9382
  try {
9139
9383
  const stateFile = getStateFile();
9140
9384
  if (existsSync5(stateFile)) {
9141
- const raw = readFileSync7(stateFile, "utf-8");
9385
+ const raw = readFileSync8(stateFile, "utf-8");
9142
9386
  const parsed = JSON.parse(raw);
9143
9387
  if (Array.isArray(parsed.agents)) {
9144
9388
  state4.agents = parsed.agents;
@@ -9154,7 +9398,7 @@ function startManager(opts) {
9154
9398
  log(`[startup] state rehydration failed (continuing with empty state): ${err.message}`);
9155
9399
  }
9156
9400
  log(
9157
- `[startup] worker pid=${process.pid} ppid=${process.ppid} node=${process.version} log=${join6(homedir4(), ".augmented", "manager.log")}`
9401
+ `[startup] worker pid=${process.pid} ppid=${process.ppid} node=${process.version} log=${join7(homedir4(), ".augmented", "manager.log")}`
9158
9402
  );
9159
9403
  deployMcpAssets();
9160
9404
  reapOrphanChannelMcps({ log });
@@ -9175,7 +9419,7 @@ async function reapOrphanedClaudePids() {
9175
9419
  const looksLikeClaude = (pid) => {
9176
9420
  if (process.platform !== "linux") return true;
9177
9421
  try {
9178
- const comm = readFileSync7(`/proc/${pid}/comm`, "utf-8").trim().toLowerCase();
9422
+ const comm = readFileSync8(`/proc/${pid}/comm`, "utf-8").trim().toLowerCase();
9179
9423
  return comm.includes("claude");
9180
9424
  } catch {
9181
9425
  return false;
@@ -9285,14 +9529,14 @@ function restartRunningChannelMcps(basenames) {
9285
9529
  }
9286
9530
  }
9287
9531
  function deployMcpAssets() {
9288
- const targetDir = join6(homedir4(), ".augmented", "_mcp");
9532
+ const targetDir = join7(homedir4(), ".augmented", "_mcp");
9289
9533
  mkdirSync4(targetDir, { recursive: true });
9290
9534
  const moduleDir = dirname3(fileURLToPath(import.meta.url));
9291
9535
  let mcpSourceDir = "";
9292
9536
  let dir = moduleDir;
9293
9537
  for (let i = 0; i < 6; i++) {
9294
- const candidate = join6(dir, "dist", "mcp");
9295
- if (existsSync5(join6(candidate, "index.js"))) {
9538
+ const candidate = join7(dir, "dist", "mcp");
9539
+ if (existsSync5(join7(candidate, "index.js"))) {
9296
9540
  mcpSourceDir = candidate;
9297
9541
  break;
9298
9542
  }
@@ -9308,7 +9552,7 @@ function deployMcpAssets() {
9308
9552
  const fileHash = (p) => {
9309
9553
  try {
9310
9554
  if (!existsSync5(p)) return null;
9311
- return createHash3("sha256").update(readFileSync7(p)).digest("hex");
9555
+ return createHash3("sha256").update(readFileSync8(p)).digest("hex");
9312
9556
  } catch {
9313
9557
  return null;
9314
9558
  }
@@ -9319,8 +9563,8 @@ function deployMcpAssets() {
9319
9563
  "telegram-channel.js"
9320
9564
  ]);
9321
9565
  for (const file of ["index.js", "slack-channel.js", "direct-chat-channel.js", "telegram-channel.js"]) {
9322
- const src = join6(mcpSourceDir, file);
9323
- const dst = join6(targetDir, file);
9566
+ const src = join7(mcpSourceDir, file);
9567
+ const dst = join7(targetDir, file);
9324
9568
  if (!existsSync5(src)) continue;
9325
9569
  const before = fileHash(dst);
9326
9570
  try {
@@ -9338,16 +9582,16 @@ function deployMcpAssets() {
9338
9582
  log(`[manager] Bundle(s) updated: ${changedBasenames.join(", ")} \u2014 signalling running instances to restart`);
9339
9583
  restartRunningChannelMcps(changedBasenames);
9340
9584
  }
9341
- const localMcpPath = join6(targetDir, "index.js");
9585
+ const localMcpPath = join7(targetDir, "index.js");
9342
9586
  try {
9343
- const agentsDir = join6(homedir4(), ".augmented", "agents");
9587
+ const agentsDir = join7(homedir4(), ".augmented", "agents");
9344
9588
  if (existsSync5(agentsDir)) {
9345
9589
  for (const entry of readdirSync3(agentsDir, { withFileTypes: true })) {
9346
9590
  if (!entry.isDirectory()) continue;
9347
9591
  for (const subdir of ["provision", "project"]) {
9348
- const mcpJsonPath = join6(agentsDir, entry.name, subdir, ".mcp.json");
9592
+ const mcpJsonPath = join7(agentsDir, entry.name, subdir, ".mcp.json");
9349
9593
  try {
9350
- const raw = readFileSync7(mcpJsonPath, "utf-8");
9594
+ const raw = readFileSync8(mcpJsonPath, "utf-8");
9351
9595
  if (!raw.includes("@integrity-labs/augmented-mcp")) continue;
9352
9596
  const mcpConfig = JSON.parse(raw);
9353
9597
  const augServer = mcpConfig.mcpServers?.["augmented"];