@integrity-labs/agt-cli 0.23.1 → 0.24.0

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.
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  SUPERVISOR_RESTART_EXIT_CODE,
3
3
  api,
4
+ estimateActiveTasksTokens,
4
5
  exchangeApiKey,
5
6
  extractCommandNotFound,
6
7
  getApiKey,
@@ -11,7 +12,7 @@ import {
11
12
  provisionOrientHook,
12
13
  provisionStopHook,
13
14
  requireHost
14
- } from "../chunk-Q4QD3TAC.js";
15
+ } from "../chunk-WZTRMJM4.js";
15
16
  import {
16
17
  findTaskByTemplate,
17
18
  getProjectDir as getProjectDir2,
@@ -46,7 +47,7 @@ import {
46
47
  stopAllSessionsAndWait,
47
48
  stopPersistentSession,
48
49
  takeZombieDetection
49
- } from "../chunk-TCCBS3PD.js";
50
+ } from "../chunk-ZKQGDH3T.js";
50
51
  import {
51
52
  KANBAN_CHECK_COMMAND,
52
53
  appendDmFooter,
@@ -62,15 +63,15 @@ import {
62
63
  resolveChannels,
63
64
  resolveDmTarget,
64
65
  wrapScheduledTaskPrompt
65
- } from "../chunk-2TOCO5D2.js";
66
+ } from "../chunk-HSIESZMZ.js";
66
67
 
67
68
  // src/lib/manager-worker.ts
68
69
  import { createHash as createHash2 } from "crypto";
69
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, appendFileSync, mkdirSync as mkdirSync2, chmodSync, existsSync as existsSync3, rmSync as rmSync2, readdirSync as readdirSync2, statSync, unlinkSync, copyFileSync } from "fs";
70
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, appendFileSync, mkdirSync as mkdirSync2, chmodSync, existsSync as existsSync4, rmSync as rmSync2, readdirSync as readdirSync2, statSync, unlinkSync, copyFileSync } from "fs";
70
71
  import https from "https";
71
72
  import { execFileSync as syncExecFile } from "child_process";
72
- import { join as join4, dirname } from "path";
73
- import { homedir as homedir3 } from "os";
73
+ import { join as join5, dirname } from "path";
74
+ import { homedir as homedir4 } from "os";
74
75
  import { fileURLToPath } from "url";
75
76
 
76
77
  // src/lib/mcp-config-drift.ts
@@ -434,12 +435,12 @@ function reapMissingMcpSessions(args) {
434
435
  if (!missingRaw.includes(key)) liveKeys.add(key);
435
436
  }
436
437
  for (const key of liveKeys) {
437
- const state3 = presenceReaperState.get(stateKey(codeName, key));
438
- if (state3) {
439
- state3.attempts = 0;
440
- state3.lastSeenLiveAt = now();
441
- state3.lastAttemptedSessionStartedAt = null;
442
- state3.gaveUpLogged = false;
438
+ const state4 = presenceReaperState.get(stateKey(codeName, key));
439
+ if (state4) {
440
+ state4.attempts = 0;
441
+ state4.lastSeenLiveAt = now();
442
+ state4.lastAttemptedSessionStartedAt = null;
443
+ state4.gaveUpLogged = false;
443
444
  }
444
445
  }
445
446
  if (missing.length === 0) {
@@ -454,24 +455,24 @@ function reapMissingMcpSessions(args) {
454
455
  const active = [];
455
456
  for (const key of missing) {
456
457
  const sk = stateKey(codeName, key);
457
- const state3 = presenceReaperState.get(sk) ?? {
458
+ const state4 = presenceReaperState.get(sk) ?? {
458
459
  attempts: 0,
459
460
  lastSeenLiveAt: null,
460
461
  lastAttemptedSessionStartedAt: null,
461
462
  gaveUpLogged: false
462
463
  };
463
- if (state3.lastAttemptedSessionStartedAt !== sessionStartedAt) {
464
- state3.attempts += 1;
465
- state3.lastAttemptedSessionStartedAt = sessionStartedAt;
464
+ if (state4.lastAttemptedSessionStartedAt !== sessionStartedAt) {
465
+ state4.attempts += 1;
466
+ state4.lastAttemptedSessionStartedAt = sessionStartedAt;
466
467
  }
467
- presenceReaperState.set(sk, state3);
468
- if (state3.attempts > MAX_PRESENCE_RESTART_ATTEMPTS) {
468
+ presenceReaperState.set(sk, state4);
469
+ if (state4.attempts > MAX_PRESENCE_RESTART_ATTEMPTS) {
469
470
  givenUp.push(key);
470
- if (!state3.gaveUpLogged) {
471
+ if (!state4.gaveUpLogged) {
471
472
  log2(
472
473
  `[mcp-presence-reaper] giving up on '${codeName}:${key}' after ${MAX_PRESENCE_RESTART_ATTEMPTS} consecutive failed restarts \u2014 declared but never recovers (likely orphaned config; see ENG-5279)`
473
474
  );
474
- state3.gaveUpLogged = true;
475
+ state4.gaveUpLogged = true;
475
476
  if (onGiveUp) {
476
477
  try {
477
478
  onGiveUp(codeName, key);
@@ -552,6 +553,108 @@ function decideChannelRestart(input) {
552
553
  return { restart: true, added, removed };
553
554
  }
554
555
 
556
+ // src/lib/restart-breaker.ts
557
+ var DEFAULT_MAX = 2;
558
+ var DEFAULT_WINDOW_MS = 6e5;
559
+ function readEnvNumber(name, fallback) {
560
+ const raw = process.env[name];
561
+ if (!raw) return fallback;
562
+ const parsed = Number(raw);
563
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
564
+ }
565
+ var RestartBreaker = class {
566
+ max;
567
+ windowMs;
568
+ now;
569
+ events = /* @__PURE__ */ new Map();
570
+ trips = /* @__PURE__ */ new Map();
571
+ constructor(opts = {}) {
572
+ this.max = opts.max ?? readEnvNumber("AGT_RESTART_BREAKER_MAX", DEFAULT_MAX);
573
+ this.windowMs = opts.windowMs ?? readEnvNumber("AGT_RESTART_BREAKER_WINDOW_MS", DEFAULT_WINDOW_MS);
574
+ this.now = opts.now ?? Date.now;
575
+ if (!Number.isFinite(this.max) || this.max < 1) {
576
+ throw new Error("restart-breaker max must be a finite number >= 1");
577
+ }
578
+ if (!Number.isFinite(this.windowMs) || this.windowMs < 1e3) {
579
+ throw new Error("restart-breaker windowMs must be a finite number >= 1000");
580
+ }
581
+ }
582
+ /** True if this agent's breaker is currently tripped (manager must skip spawn). */
583
+ isTripped(codeName) {
584
+ return this.trips.has(codeName);
585
+ }
586
+ getTrip(codeName) {
587
+ return this.trips.get(codeName);
588
+ }
589
+ /**
590
+ * Record a restart event. If recording this event puts the count
591
+ * inside the window above `max`, the breaker trips and the call site
592
+ * should NOT respawn.
593
+ *
594
+ * Idempotent on already-tripped breakers: returns the existing trip
595
+ * without double-counting events. Callers may still record the reason
596
+ * via the decision-log for forensics.
597
+ */
598
+ record(codeName, reason) {
599
+ const existing = this.trips.get(codeName);
600
+ if (existing) {
601
+ return { tripped: false, trip: existing, windowCount: existing.eventsAtTrip.length };
602
+ }
603
+ const at = this.now();
604
+ const cutoff = at - this.windowMs;
605
+ const prior = (this.events.get(codeName) ?? []).filter((e) => e.at >= cutoff);
606
+ prior.push({ reason, at });
607
+ this.events.set(codeName, prior);
608
+ if (prior.length > this.max) {
609
+ const trip = {
610
+ trippedAt: at,
611
+ eventsAtTrip: [...prior],
612
+ statusMessage: formatStatusMessage(prior, this.windowMs)
613
+ };
614
+ this.trips.set(codeName, trip);
615
+ this.events.delete(codeName);
616
+ return { tripped: true, trip, windowCount: prior.length };
617
+ }
618
+ return { tripped: false, windowCount: prior.length };
619
+ }
620
+ /** Operator-initiated reset: drops the trip + the events log for this agent. */
621
+ clear(codeName) {
622
+ this.trips.delete(codeName);
623
+ this.events.delete(codeName);
624
+ }
625
+ /** Snapshot tripped agents for `manager-state.json`. */
626
+ serialize() {
627
+ return Object.fromEntries(this.trips.entries());
628
+ }
629
+ /**
630
+ * Rehydrate trip state from `manager-state.json`. Called once at
631
+ * worker startup. Window history is intentionally NOT persisted — only
632
+ * tripped state. A tripped breaker survives manager restart; an
633
+ * un-tripped one starts a fresh window in the new worker.
634
+ */
635
+ hydrate(saved) {
636
+ if (!saved) return;
637
+ for (const [codeName, trip] of Object.entries(saved)) {
638
+ if (trip && typeof trip.trippedAt === "number" && Array.isArray(trip.eventsAtTrip)) {
639
+ this.trips.set(codeName, trip);
640
+ }
641
+ }
642
+ }
643
+ /** Test helper — current in-window event count for `codeName`. */
644
+ windowCount(codeName) {
645
+ const cutoff = this.now() - this.windowMs;
646
+ return (this.events.get(codeName) ?? []).filter((e) => e.at >= cutoff).length;
647
+ }
648
+ };
649
+ function formatStatusMessage(events, windowMs) {
650
+ const last = events[events.length - 1];
651
+ const windowLabel = windowMs < 6e4 ? `${Math.round(windowMs / 1e3)}s` : `${(windowMs / 6e4).toFixed(1).replace(/\.0$/, "")}min`;
652
+ const reasonCounts = /* @__PURE__ */ new Map();
653
+ for (const e of events) reasonCounts.set(e.reason, (reasonCounts.get(e.reason) ?? 0) + 1);
654
+ const breakdown = Array.from(reasonCounts.entries()).map(([r, n]) => `${r}=${n}`).join(", ");
655
+ return `Circuit breaker tripped: ${events.length} restarts in ${windowLabel} (${breakdown}); most recent=${last.reason} at ${new Date(last.at).toISOString()}`;
656
+ }
657
+
555
658
  // src/lib/usage-banner-monitor.ts
556
659
  var MIN_CHECK_INTERVAL_MS = 6e4;
557
660
  var PANE_TAIL_LINES_FOR_BANNER = 200;
@@ -607,6 +710,79 @@ async function maybeReportUsageBanner(args) {
607
710
  }
608
711
  }
609
712
 
713
+ // src/lib/activity-cache-monitor.ts
714
+ import { existsSync, readFileSync } from "fs";
715
+ import { homedir } from "os";
716
+ import { join } from "path";
717
+ var MIN_CHECK_INTERVAL_MS2 = 6e4;
718
+ var STATS_CACHE_PATH = join(homedir(), ".claude", "stats-cache.json");
719
+ var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
720
+ var state2 = { lastObservedDate: null, lastCheckedAt: 0 };
721
+ function selectNewDailyRows(raw, lastObservedDate) {
722
+ let parsed;
723
+ try {
724
+ parsed = JSON.parse(raw);
725
+ } catch {
726
+ return [];
727
+ }
728
+ const days = Array.isArray(parsed.dailyActivity) ? parsed.dailyActivity : [];
729
+ const nonNegInt = (v) => {
730
+ if (typeof v !== "number" || !Number.isFinite(v)) return null;
731
+ const n = Math.trunc(v);
732
+ return n >= 0 ? n : null;
733
+ };
734
+ const result = [];
735
+ for (const d of days) {
736
+ if (typeof d.date !== "string" || !ISO_DATE_RE.test(d.date)) continue;
737
+ if (lastObservedDate !== null && d.date <= lastObservedDate) continue;
738
+ const m = nonNegInt(d.messageCount);
739
+ const s = nonNegInt(d.sessionCount);
740
+ const t = nonNegInt(d.toolCallCount);
741
+ if (m === null || s === null || t === null) continue;
742
+ const breakdownForDay = parsed.dailyModelTokens?.[d.date];
743
+ const model_breakdown = breakdownForDay && typeof breakdownForDay === "object" && !Array.isArray(breakdownForDay) ? breakdownForDay : null;
744
+ result.push({
745
+ date: d.date,
746
+ message_count: m,
747
+ session_count: s,
748
+ tool_call_count: t,
749
+ model_breakdown
750
+ });
751
+ }
752
+ result.sort((a, b) => a.date.localeCompare(b.date));
753
+ return result;
754
+ }
755
+ async function maybeReportActivityCache(args) {
756
+ const { api: api2, log: log2 } = args;
757
+ const now = args.now ?? /* @__PURE__ */ new Date();
758
+ const nowMs = now.getTime();
759
+ if (nowMs - state2.lastCheckedAt < MIN_CHECK_INTERVAL_MS2) return;
760
+ state2.lastCheckedAt = nowMs;
761
+ if (!existsSync(STATS_CACHE_PATH)) {
762
+ return;
763
+ }
764
+ let raw;
765
+ try {
766
+ raw = readFileSync(STATS_CACHE_PATH, "utf-8");
767
+ } catch (err) {
768
+ log2(`[activity-cache] readFileSync failed: ${err.message}`);
769
+ return;
770
+ }
771
+ const rows = selectNewDailyRows(raw, state2.lastObservedDate);
772
+ if (rows.length === 0) return;
773
+ for (const row of rows) {
774
+ try {
775
+ await api2.post("/host/activity-observations", row);
776
+ state2.lastObservedDate = row.date;
777
+ } catch (err) {
778
+ log2(
779
+ `[activity-cache] POST /host/activity-observations failed for date=${row.date}: ${err.message}`
780
+ );
781
+ return;
782
+ }
783
+ }
784
+ }
785
+
610
786
  // src/lib/poll-backoff.ts
611
787
  var POLL_BACKOFF_BASE_MS = 1e3;
612
788
  var POLL_BACKOFF_MAX_MS = 6e4;
@@ -1036,8 +1212,8 @@ var GatewayClientPool = class extends EventEmitter {
1036
1212
 
1037
1213
  // src/lib/claude-auth-detect.ts
1038
1214
  import { readFile, readdir } from "fs/promises";
1039
- import { homedir, platform } from "os";
1040
- import { join } from "path";
1215
+ import { homedir as homedir2, platform } from "os";
1216
+ import { join as join2 } from "path";
1041
1217
  import { execFile } from "child_process";
1042
1218
  import { promisify } from "util";
1043
1219
  var execFileAsync = promisify(execFile);
@@ -1052,8 +1228,8 @@ async function detectClaudeAuth() {
1052
1228
  }
1053
1229
  async function findClaudeCredentialsPaths() {
1054
1230
  const candidates = [
1055
- join(homedir(), ".claude", ".credentials.json"),
1056
- join(homedir(), ".claude", "credentials.json")
1231
+ join2(homedir2(), ".claude", ".credentials.json"),
1232
+ join2(homedir2(), ".claude", "credentials.json")
1057
1233
  ];
1058
1234
  const isLinuxRoot = platform() === "linux" && typeof process.getuid === "function" && process.getuid() === 0;
1059
1235
  if (isLinuxRoot) {
@@ -1061,8 +1237,8 @@ async function findClaudeCredentialsPaths() {
1061
1237
  const entries = await readdir("/home", { withFileTypes: true });
1062
1238
  for (const entry of entries) {
1063
1239
  if (!entry.isDirectory()) continue;
1064
- candidates.push(join("/home", entry.name, ".claude", ".credentials.json"));
1065
- candidates.push(join("/home", entry.name, ".claude", "credentials.json"));
1240
+ candidates.push(join2("/home", entry.name, ".claude", ".credentials.json"));
1241
+ candidates.push(join2("/home", entry.name, ".claude", "credentials.json"));
1066
1242
  }
1067
1243
  } catch {
1068
1244
  }
@@ -1154,18 +1330,18 @@ function normalize(value) {
1154
1330
  }
1155
1331
 
1156
1332
  // src/lib/channel-hash-cache.ts
1157
- import { existsSync, readFileSync, writeFileSync } from "fs";
1158
- import { join as join2 } from "path";
1333
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
1334
+ import { join as join3 } from "path";
1159
1335
  var CACHE_FILENAME = "channel-hash-cache.json";
1160
1336
  function getChannelHashCacheFile(configDir) {
1161
- return join2(configDir, CACHE_FILENAME);
1337
+ return join3(configDir, CACHE_FILENAME);
1162
1338
  }
1163
1339
  function loadChannelHashCache(target, configDir) {
1164
1340
  const path = getChannelHashCacheFile(configDir);
1165
- if (!existsSync(path)) return;
1341
+ if (!existsSync2(path)) return;
1166
1342
  let parsed;
1167
1343
  try {
1168
- parsed = JSON.parse(readFileSync(path, "utf-8"));
1344
+ parsed = JSON.parse(readFileSync2(path, "utf-8"));
1169
1345
  } catch {
1170
1346
  return;
1171
1347
  }
@@ -1638,24 +1814,24 @@ function withScheduleLinkFooter(opts) {
1638
1814
  }
1639
1815
 
1640
1816
  // src/lib/restart-flags.ts
1641
- import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
1642
- import { homedir as homedir2 } from "os";
1643
- import { join as join3 } from "path";
1817
+ import { existsSync as existsSync3, mkdirSync, readdirSync, readFileSync as readFileSync3, renameSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
1818
+ import { homedir as homedir3 } from "os";
1819
+ import { join as join4 } from "path";
1644
1820
  import { randomUUID } from "crypto";
1645
1821
  function restartFlagsDir() {
1646
- return join3(homedir2(), ".augmented", "restart-flags");
1822
+ return join4(homedir3(), ".augmented", "restart-flags");
1647
1823
  }
1648
1824
  function flagPath(codeName) {
1649
- return join3(restartFlagsDir(), `${codeName}.flag`);
1825
+ return join4(restartFlagsDir(), `${codeName}.flag`);
1650
1826
  }
1651
1827
  function readRestartFlags() {
1652
1828
  const dir = restartFlagsDir();
1653
- if (!existsSync2(dir)) return [];
1829
+ if (!existsSync3(dir)) return [];
1654
1830
  const out = [];
1655
1831
  for (const entry of readdirSync(dir)) {
1656
1832
  if (!entry.endsWith(".flag")) continue;
1657
1833
  try {
1658
- const raw = readFileSync2(join3(dir, entry), "utf8");
1834
+ const raw = readFileSync3(join4(dir, entry), "utf8");
1659
1835
  const parsed = JSON.parse(raw);
1660
1836
  if (typeof parsed.codeName !== "string" || parsed.codeName.length === 0) {
1661
1837
  parsed.codeName = entry.replace(/\.flag$/, "");
@@ -1673,7 +1849,7 @@ function readRestartFlags() {
1673
1849
  }
1674
1850
  function deleteRestartFlag(codeName) {
1675
1851
  const path = flagPath(codeName);
1676
- if (existsSync2(path)) {
1852
+ if (existsSync3(path)) {
1677
1853
  rmSync(path, { force: true });
1678
1854
  }
1679
1855
  }
@@ -2171,8 +2347,8 @@ function applyRestartAcks(args) {
2171
2347
  var GATEWAY_PORT_BASE = 18800;
2172
2348
  var GATEWAY_PORT_STEP = 10;
2173
2349
  var GATEWAY_PORT_MAX = 18899;
2174
- var AUGMENTED_DIR = join4(process.env["HOME"] ?? "/tmp", ".augmented");
2175
- var GATEWAY_PORTS_FILE = join4(AUGMENTED_DIR, "gateway-ports.json");
2350
+ var AUGMENTED_DIR = join5(process.env["HOME"] ?? "/tmp", ".augmented");
2351
+ var GATEWAY_PORTS_FILE = join5(AUGMENTED_DIR, "gateway-ports.json");
2176
2352
  var CHANNEL_SWEEP_INTERVAL_MS = (() => {
2177
2353
  const raw = parseInt(process.env["AGT_CHANNEL_SWEEP_INTERVAL_MS"] ?? "", 10);
2178
2354
  if (!Number.isFinite(raw)) return 5 * 60 * 1e3;
@@ -2279,14 +2455,45 @@ var KNOWN_SAFE_TAIL_SIGNATURES = /* @__PURE__ */ new Set(["session_id_in_use"]);
2279
2455
  function shouldSkipRevokedCleanup(previousKnownStatus) {
2280
2456
  return previousKnownStatus === "revoked";
2281
2457
  }
2282
- function hasRevokedResiduals(state3) {
2283
- return state3.gatewayRunning || state3.portAllocated || state3.provisionDirExists;
2458
+ function hasRevokedResiduals(state4) {
2459
+ return state4.gatewayRunning || state4.portAllocated || state4.provisionDirExists;
2284
2460
  }
2285
2461
  var knownVersions = /* @__PURE__ */ new Map();
2286
2462
  var knownStatuses = /* @__PURE__ */ new Map();
2287
2463
  var knownChannels = /* @__PURE__ */ new Map();
2288
2464
  var pendingSessionRestarts = /* @__PURE__ */ new Map();
2289
- function scheduleSessionRestart(codeName, delayMs, reason) {
2465
+ var restartBreaker = new RestartBreaker();
2466
+ var reportedTrips = /* @__PURE__ */ new Map();
2467
+ function recordRestartForBreaker(codeName, reason) {
2468
+ const result = restartBreaker.record(codeName, reason);
2469
+ if (!result.tripped || !result.trip) return;
2470
+ const trip = result.trip;
2471
+ log(
2472
+ `[persistent-session-decision] agent=${codeName} decision=circuit-tripped detail=${JSON.stringify(trip.statusMessage)} (ENG-5441)`
2473
+ );
2474
+ maybeReportCurrentTrip(codeName);
2475
+ }
2476
+ async function reportCircuitTripped(codeName, trip) {
2477
+ if (reportedTrips.get(codeName) === trip.trippedAt) return;
2478
+ const agentId = codeNameToAgentId.get(codeName);
2479
+ if (!agentId) throw new Error(`no agent_id known yet for '${codeName}' \u2014 will retry next poll`);
2480
+ await api.post("/host/circuit-breaker/trip", {
2481
+ agent_id: agentId,
2482
+ status_message: trip.statusMessage,
2483
+ tripped_at: new Date(trip.trippedAt).toISOString(),
2484
+ events: trip.eventsAtTrip.map((e) => ({ reason: e.reason, at: new Date(e.at).toISOString() }))
2485
+ });
2486
+ reportedTrips.set(codeName, trip.trippedAt);
2487
+ }
2488
+ function maybeReportCurrentTrip(codeName) {
2489
+ const trip = restartBreaker.getTrip(codeName);
2490
+ if (!trip) return;
2491
+ if (reportedTrips.get(codeName) === trip.trippedAt) return;
2492
+ reportCircuitTripped(codeName, trip).catch((err) => {
2493
+ log(`[circuit-breaker] Failed to report trip for '${codeName}' to API: ${err.message}`);
2494
+ });
2495
+ }
2496
+ function scheduleSessionRestart(codeName, delayMs, reason, breakerReason = "hot-reload-mcp") {
2290
2497
  const existing = pendingSessionRestarts.get(codeName);
2291
2498
  if (existing) {
2292
2499
  clearTimeout(existing);
@@ -2296,6 +2503,7 @@ function scheduleSessionRestart(codeName, delayMs, reason) {
2296
2503
  pendingSessionRestarts.delete(codeName);
2297
2504
  stopPersistentSession(codeName, log);
2298
2505
  runningMcpHashes.delete(codeName);
2506
+ recordRestartForBreaker(codeName, breakerReason);
2299
2507
  log(`[hot-reload] Session stopped for '${codeName}' \u2014 will respawn with ${reason}`);
2300
2508
  }, delayMs);
2301
2509
  timer.unref?.();
@@ -2314,7 +2522,7 @@ var runningMcpHashes = /* @__PURE__ */ new Map();
2314
2522
  var runningMcpServerKeys = /* @__PURE__ */ new Map();
2315
2523
  function projectMcpHash(_codeName, projectDir) {
2316
2524
  try {
2317
- const raw = readFileSync3(join4(projectDir, ".mcp.json"), "utf-8");
2525
+ const raw = readFileSync4(join5(projectDir, ".mcp.json"), "utf-8");
2318
2526
  return createHash2("sha256").update(canonicalJson(JSON.parse(raw))).digest("hex");
2319
2527
  } catch {
2320
2528
  return null;
@@ -2322,7 +2530,7 @@ function projectMcpHash(_codeName, projectDir) {
2322
2530
  }
2323
2531
  function projectMcpKeys(_codeName, projectDir) {
2324
2532
  try {
2325
- const raw = readFileSync3(join4(projectDir, ".mcp.json"), "utf-8");
2533
+ const raw = readFileSync4(join5(projectDir, ".mcp.json"), "utf-8");
2326
2534
  const parsed = JSON.parse(raw);
2327
2535
  const servers = parsed.mcpServers;
2328
2536
  if (!servers || typeof servers !== "object") return /* @__PURE__ */ new Set();
@@ -2331,11 +2539,14 @@ function projectMcpKeys(_codeName, projectDir) {
2331
2539
  return null;
2332
2540
  }
2333
2541
  }
2334
- function stopPersistentSessionAndForgetMcpBaseline(codeName) {
2542
+ function stopPersistentSessionAndForgetMcpBaseline(codeName, breakerReason) {
2335
2543
  cancelPendingSessionRestart(codeName);
2336
2544
  stopPersistentSession(codeName, log);
2337
2545
  runningMcpHashes.delete(codeName);
2338
2546
  runningMcpServerKeys.delete(codeName);
2547
+ if (breakerReason) {
2548
+ recordRestartForBreaker(codeName, breakerReason);
2549
+ }
2339
2550
  }
2340
2551
  function checkMcpConfigDriftAndScheduleRestart(codeName, projectDir) {
2341
2552
  const currentHash = projectMcpHash(codeName, projectDir);
@@ -2416,7 +2627,7 @@ var codeNameToAgentId = /* @__PURE__ */ new Map();
2416
2627
  var agentChannelTokens = /* @__PURE__ */ new Map();
2417
2628
  var activeChannels = /* @__PURE__ */ new Map();
2418
2629
  var gatewaysStartedThisCycle = /* @__PURE__ */ new Set();
2419
- var state2 = {
2630
+ var state3 = {
2420
2631
  pid: process.pid,
2421
2632
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2422
2633
  lastPollAt: null,
@@ -2450,6 +2661,7 @@ function clearAgentCaches(agentId, codeName) {
2450
2661
  lastHarvestAt.delete(codeName);
2451
2662
  claudeSchedulerStates.delete(codeName);
2452
2663
  claudeTaskConcurrency.delete(codeName);
2664
+ lastMcpFailedBannerCount.delete(codeName);
2453
2665
  memoryFileHashes.delete(agentId);
2454
2666
  lastDownloadHash.delete(agentId);
2455
2667
  lastLocalFileHash.delete(agentId);
@@ -2472,7 +2684,7 @@ var cachedFrameworkVersion = null;
2472
2684
  var lastVersionCheckAt = 0;
2473
2685
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
2474
2686
  var lastResponsivenessProbeAt = 0;
2475
- var agtCliVersion = true ? "0.23.1" : "dev";
2687
+ var agtCliVersion = true ? "0.24.0" : "dev";
2476
2688
  function resolveBrewPath(execFileSync4) {
2477
2689
  try {
2478
2690
  const out = execFileSync4("which", ["brew"], { timeout: 5e3 }).toString().trim();
@@ -2485,7 +2697,7 @@ function resolveBrewPath(execFileSync4) {
2485
2697
  "/usr/local/bin/brew"
2486
2698
  ];
2487
2699
  for (const path of fallbacks) {
2488
- if (existsSync3(path)) return path;
2700
+ if (existsSync4(path)) return path;
2489
2701
  }
2490
2702
  return null;
2491
2703
  }
@@ -2495,7 +2707,7 @@ function claudeBinaryInstalled(execFileSync4) {
2495
2707
  "/opt/homebrew/bin/claude",
2496
2708
  "/usr/local/bin/claude"
2497
2709
  ];
2498
- if (canonical.some((path) => existsSync3(path))) return true;
2710
+ if (canonical.some((path) => existsSync4(path))) return true;
2499
2711
  try {
2500
2712
  execFileSync4("which", ["claude"], { timeout: 5e3 });
2501
2713
  return true;
@@ -2677,7 +2889,7 @@ async function ensureFrameworkBinary(frameworkId) {
2677
2889
  if (!process.env.PATH?.split(":").includes(brewBinDir)) {
2678
2890
  process.env.PATH = `${brewBinDir}:${process.env.PATH ?? ""}`;
2679
2891
  }
2680
- if (existsSync3("/home/linuxbrew/.linuxbrew/bin/claude")) {
2892
+ if (existsSync4("/home/linuxbrew/.linuxbrew/bin/claude")) {
2681
2893
  log("Claude Code installed successfully");
2682
2894
  } else {
2683
2895
  log("Claude Code install completed but binary not found at expected path \u2014 check brew logs");
@@ -2689,7 +2901,7 @@ async function ensureFrameworkBinary(frameworkId) {
2689
2901
  var CLAUDE_CODE_UPGRADE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
2690
2902
  var claudeCodeUpgradeInFlight = false;
2691
2903
  function claudeCodeUpgradeMarkerPath() {
2692
- return join4(homedir3(), ".augmented", ".last-claude-code-upgrade-check");
2904
+ return join5(homedir4(), ".augmented", ".last-claude-code-upgrade-check");
2693
2905
  }
2694
2906
  function stampClaudeCodeUpgradeMarker() {
2695
2907
  try {
@@ -2699,7 +2911,7 @@ function stampClaudeCodeUpgradeMarker() {
2699
2911
  }
2700
2912
  function claudeCodeUpgradeThrottled() {
2701
2913
  try {
2702
- const lastCheck = parseInt(readFileSync3(claudeCodeUpgradeMarkerPath(), "utf-8").trim(), 10);
2914
+ const lastCheck = parseInt(readFileSync4(claudeCodeUpgradeMarkerPath(), "utf-8").trim(), 10);
2703
2915
  if (!Number.isFinite(lastCheck)) return false;
2704
2916
  return Date.now() - lastCheck < CLAUDE_CODE_UPGRADE_CHECK_INTERVAL_MS;
2705
2917
  } catch {
@@ -2769,7 +2981,7 @@ async function checkAndUpdateCli() {
2769
2981
  const isNpmGlobal = !isBrewFormula && resolvedPath.includes("node_modules");
2770
2982
  if (!isBrewFormula && !isNpmGlobal) return;
2771
2983
  const { readFileSync: readF, writeFileSync: writeF } = await import("fs");
2772
- const markerPath = join4(homedir3(), ".augmented", ".last-update-check");
2984
+ const markerPath = join5(homedir4(), ".augmented", ".last-update-check");
2773
2985
  try {
2774
2986
  const lastCheck = parseInt(readF(markerPath, "utf-8").trim(), 10);
2775
2987
  if (Date.now() - lastCheck < UPDATE_CHECK_INTERVAL_MS) return;
@@ -2834,17 +3046,18 @@ async function checkAndUpdateCliViaBrew() {
2834
3046
  async function checkAndUpdateCliViaNpm() {
2835
3047
  const { execFileSync: execFileSync4 } = await import("child_process");
2836
3048
  if (agtCliVersion === "dev") return;
3049
+ const channel = process.env.AGT_CLI_RELEASE_CHANNEL || "latest";
2837
3050
  let latest;
2838
3051
  try {
2839
3052
  const res = await fetch(
2840
- "https://registry.npmjs.org/@integrity-labs/agt-cli/latest",
3053
+ `https://registry.npmjs.org/@integrity-labs/agt-cli/${encodeURIComponent(channel)}`,
2841
3054
  {
2842
3055
  signal: AbortSignal.timeout(1e4),
2843
3056
  headers: { Accept: "application/json" }
2844
3057
  }
2845
3058
  );
2846
3059
  if (!res.ok) {
2847
- log(`[self-update] npm registry returned ${res.status}`);
3060
+ log(`[self-update] npm registry returned ${res.status} (channel=${channel})`);
2848
3061
  return;
2849
3062
  }
2850
3063
  const body = await res.json();
@@ -2857,14 +3070,15 @@ async function checkAndUpdateCliViaNpm() {
2857
3070
  log(`[self-update] npm registry fetch failed: ${err.message}`);
2858
3071
  return;
2859
3072
  }
2860
- if (!isNewerSemver(agtCliVersion, latest)) {
3073
+ const shouldUpdate = channel === "latest" ? isNewerSemver(agtCliVersion, latest) : latest !== agtCliVersion;
3074
+ if (!shouldUpdate) {
2861
3075
  if (!selfUpdateUpToDateLogged) {
2862
- log(`[self-update] agt CLI is up to date (npm, ${agtCliVersion})`);
3076
+ log(`[self-update] agt CLI is up to date (npm, channel=${channel}, ${agtCliVersion})`);
2863
3077
  selfUpdateUpToDateLogged = true;
2864
3078
  }
2865
3079
  return;
2866
3080
  }
2867
- log(`[self-update] agt CLI update available: ${agtCliVersion} \u2192 ${latest}. Upgrading via npm...`);
3081
+ log(`[self-update] agt CLI update available: ${agtCliVersion} \u2192 ${latest} (channel=${channel}). Upgrading via npm...`);
2868
3082
  const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
2869
3083
  const cmd = isRoot ? "npm" : "sudo";
2870
3084
  const args = isRoot ? [
@@ -2931,10 +3145,10 @@ async function applyClaudeAuthToEnv(childEnv, label) {
2931
3145
  throw new Error("claude_auth_mode=api_key but /host/exchange returned no decrypted key");
2932
3146
  }
2933
3147
  childEnv.ANTHROPIC_API_KEY = exchange.anthropicApiKey;
2934
- const claudeDir = join4(homedir3(), ".claude");
3148
+ const claudeDir = join5(homedir4(), ".claude");
2935
3149
  for (const filename of [".credentials.json", "credentials.json"]) {
2936
- const p = join4(claudeDir, filename);
2937
- if (existsSync3(p)) {
3150
+ const p = join5(claudeDir, filename);
3151
+ if (existsSync4(p)) {
2938
3152
  try {
2939
3153
  rmSync2(p, { force: true });
2940
3154
  log(`[${label}] Removed ${p} (api_key mode \u2014 preventing OAuth fallback)`);
@@ -2948,7 +3162,7 @@ async function applyClaudeAuthToEnv(childEnv, label) {
2948
3162
  }
2949
3163
  function loadGatewayPorts() {
2950
3164
  try {
2951
- return JSON.parse(readFileSync3(GATEWAY_PORTS_FILE, "utf-8"));
3165
+ return JSON.parse(readFileSync4(GATEWAY_PORTS_FILE, "utf-8"));
2952
3166
  } catch {
2953
3167
  return {};
2954
3168
  }
@@ -2978,10 +3192,10 @@ function freePort(codeName) {
2978
3192
  }
2979
3193
  }
2980
3194
  function getStateFile() {
2981
- return join4(config?.configDir ?? join4(process.env["HOME"] ?? "/tmp", ".augmented"), "manager-state.json");
3195
+ return join5(config?.configDir ?? join5(process.env["HOME"] ?? "/tmp", ".augmented"), "manager-state.json");
2982
3196
  }
2983
3197
  function channelHashCacheDir() {
2984
- return config?.configDir ?? join4(process.env["HOME"] ?? "/tmp", ".augmented");
3198
+ return config?.configDir ?? join5(process.env["HOME"] ?? "/tmp", ".augmented");
2985
3199
  }
2986
3200
  function loadChannelHashCache2() {
2987
3201
  loadChannelHashCache(knownChannelConfigHashes, channelHashCacheDir());
@@ -3023,9 +3237,9 @@ function log(msg) {
3023
3237
  `;
3024
3238
  if (!managerLogPath) {
3025
3239
  try {
3026
- managerLogPath = join4(homedir3(), ".augmented", "manager.log");
3240
+ managerLogPath = join5(homedir4(), ".augmented", "manager.log");
3027
3241
  mkdirSync2(dirname(managerLogPath), { recursive: true });
3028
- if (existsSync3(managerLogPath)) {
3242
+ if (existsSync4(managerLogPath)) {
3029
3243
  chmodSync(managerLogPath, 384);
3030
3244
  }
3031
3245
  } catch {
@@ -3053,7 +3267,7 @@ function sha256(content) {
3053
3267
  }
3054
3268
  function hashFile(filePath) {
3055
3269
  try {
3056
- const content = readFileSync3(filePath, "utf-8");
3270
+ const content = readFileSync4(filePath, "utf-8");
3057
3271
  return sha256(content);
3058
3272
  } catch {
3059
3273
  return null;
@@ -3078,12 +3292,12 @@ function parseSkillFrontmatter(content) {
3078
3292
  }
3079
3293
  async function refreshSkillsIndexInClaudeMd(configDir, codeName, log2) {
3080
3294
  const { readdirSync: readdirSync3, readFileSync: rfs, existsSync: ex, writeFileSync: writeFileSync4 } = await import("fs");
3081
- const skillsDir = join4(configDir, codeName, "project", ".claude", "skills");
3082
- const claudeMdPath = join4(configDir, codeName, "project", "CLAUDE.md");
3295
+ const skillsDir = join5(configDir, codeName, "project", ".claude", "skills");
3296
+ const claudeMdPath = join5(configDir, codeName, "project", "CLAUDE.md");
3083
3297
  if (!ex(skillsDir) || !ex(claudeMdPath)) return;
3084
3298
  const entries = [];
3085
3299
  for (const dir of readdirSync3(skillsDir).sort()) {
3086
- const skillFile = join4(skillsDir, dir, "SKILL.md");
3300
+ const skillFile = join5(skillsDir, dir, "SKILL.md");
3087
3301
  if (!ex(skillFile)) continue;
3088
3302
  try {
3089
3303
  const { name, description } = parseSkillFrontmatter(rfs(skillFile, "utf-8"));
@@ -3131,10 +3345,10 @@ ${SKILLS_INDEX_END}`;
3131
3345
  }
3132
3346
  async function migrateToProfiles() {
3133
3347
  const homeDir = process.env["HOME"] ?? "/tmp";
3134
- const sharedConfigPath = join4(homeDir, ".openclaw", "openclaw.json");
3348
+ const sharedConfigPath = join5(homeDir, ".openclaw", "openclaw.json");
3135
3349
  let sharedConfig;
3136
3350
  try {
3137
- sharedConfig = JSON.parse(readFileSync3(sharedConfigPath, "utf-8"));
3351
+ sharedConfig = JSON.parse(readFileSync4(sharedConfigPath, "utf-8"));
3138
3352
  } catch {
3139
3353
  return;
3140
3354
  }
@@ -3147,19 +3361,19 @@ async function migrateToProfiles() {
3147
3361
  const codeName = agentEntry["id"];
3148
3362
  if (!codeName) continue;
3149
3363
  if (codeName === "main") continue;
3150
- const profileDir = join4(homeDir, `.openclaw-${codeName}`);
3151
- if (existsSync3(join4(profileDir, "openclaw.json"))) continue;
3364
+ const profileDir = join5(homeDir, `.openclaw-${codeName}`);
3365
+ if (existsSync4(join5(profileDir, "openclaw.json"))) continue;
3152
3366
  log(`Migrating agent '${codeName}' to per-agent profile`);
3153
3367
  if (adapter.seedProfileConfig) {
3154
3368
  adapter.seedProfileConfig(codeName);
3155
3369
  }
3156
- const sharedAuthDir = join4(homeDir, ".openclaw", "agents", codeName, "agent");
3157
- const profileAuthDir = join4(profileDir, "agents", codeName, "agent");
3158
- const authFile = join4(sharedAuthDir, "auth-profiles.json");
3159
- if (existsSync3(authFile)) {
3370
+ const sharedAuthDir = join5(homeDir, ".openclaw", "agents", codeName, "agent");
3371
+ const profileAuthDir = join5(profileDir, "agents", codeName, "agent");
3372
+ const authFile = join5(sharedAuthDir, "auth-profiles.json");
3373
+ if (existsSync4(authFile)) {
3160
3374
  mkdirSync2(profileAuthDir, { recursive: true });
3161
- const authContent = readFileSync3(authFile, "utf-8");
3162
- writeFileSync3(join4(profileAuthDir, "auth-profiles.json"), authContent);
3375
+ const authContent = readFileSync4(authFile, "utf-8");
3376
+ writeFileSync3(join5(profileAuthDir, "auth-profiles.json"), authContent);
3163
3377
  }
3164
3378
  allocatePort(codeName);
3165
3379
  migrated++;
@@ -3197,7 +3411,7 @@ function readGatewayToken(codeName) {
3197
3411
  }
3198
3412
  const homeDir = process.env["HOME"] ?? "/tmp";
3199
3413
  try {
3200
- const cfg = JSON.parse(readFileSync3(join4(homeDir, `.openclaw-${codeName}`, "openclaw.json"), "utf-8"));
3414
+ const cfg = JSON.parse(readFileSync4(join5(homeDir, `.openclaw-${codeName}`, "openclaw.json"), "utf-8"));
3201
3415
  return cfg?.gateway?.auth?.token;
3202
3416
  } catch {
3203
3417
  return void 0;
@@ -3206,18 +3420,18 @@ function readGatewayToken(codeName) {
3206
3420
  var GATEWAY_HUNG_TIMEOUT_MS = 5 * 6e4;
3207
3421
  function isGatewayHung(codeName) {
3208
3422
  const homeDir = process.env["HOME"] ?? "/tmp";
3209
- const jobsPath = join4(homeDir, `.openclaw-${codeName}`, "cron", "jobs.json");
3210
- if (!existsSync3(jobsPath)) return false;
3423
+ const jobsPath = join5(homeDir, `.openclaw-${codeName}`, "cron", "jobs.json");
3424
+ if (!existsSync4(jobsPath)) return false;
3211
3425
  try {
3212
- const data = JSON.parse(readFileSync3(jobsPath, "utf-8"));
3426
+ const data = JSON.parse(readFileSync4(jobsPath, "utf-8"));
3213
3427
  const jobs = data.jobs ?? data;
3214
3428
  if (!Array.isArray(jobs)) return false;
3215
3429
  const now = Date.now();
3216
3430
  for (const job of jobs) {
3217
- const state3 = job.state;
3218
- if (!state3) continue;
3219
- const runStartedAt = state3.runStartedAtMs;
3220
- const isRunning = state3.status === "running" || state3.running === true;
3431
+ const state4 = job.state;
3432
+ if (!state4) continue;
3433
+ const runStartedAt = state4.runStartedAtMs;
3434
+ const isRunning = state4.status === "running" || state4.running === true;
3221
3435
  if (isRunning && runStartedAt && now - runStartedAt > GATEWAY_HUNG_TIMEOUT_MS) {
3222
3436
  return true;
3223
3437
  }
@@ -3242,15 +3456,15 @@ async function ensureGatewayRunning(codeName, adapter) {
3242
3456
  }
3243
3457
  await new Promise((r) => setTimeout(r, 2e3));
3244
3458
  const homeDir = process.env["HOME"] ?? "/tmp";
3245
- const cronJobsPath = join4(homeDir, `.openclaw-${codeName}`, "cron", "jobs.json");
3459
+ const cronJobsPath = join5(homeDir, `.openclaw-${codeName}`, "cron", "jobs.json");
3246
3460
  clearStaleCronRunState(cronJobsPath);
3247
3461
  } else {
3248
3462
  if (status.port) {
3249
3463
  try {
3250
3464
  const homeDir = process.env["HOME"] ?? "/tmp";
3251
- const configPath = join4(homeDir, `.openclaw-${codeName}`, "openclaw.json");
3252
- if (existsSync3(configPath)) {
3253
- const cfg = JSON.parse(readFileSync3(configPath, "utf-8"));
3465
+ const configPath = join5(homeDir, `.openclaw-${codeName}`, "openclaw.json");
3466
+ if (existsSync4(configPath)) {
3467
+ const cfg = JSON.parse(readFileSync4(configPath, "utf-8"));
3254
3468
  if (cfg.gateway?.port !== status.port) {
3255
3469
  if (!cfg.gateway) cfg.gateway = {};
3256
3470
  cfg.gateway.port = status.port;
@@ -3274,9 +3488,9 @@ async function ensureGatewayRunning(codeName, adapter) {
3274
3488
  gatewaysStartedThisCycle.add(codeName);
3275
3489
  try {
3276
3490
  const homeDir = process.env["HOME"] ?? "/tmp";
3277
- const configPath = join4(homeDir, `.openclaw-${codeName}`, "openclaw.json");
3278
- if (existsSync3(configPath)) {
3279
- const cfg = JSON.parse(readFileSync3(configPath, "utf-8"));
3491
+ const configPath = join5(homeDir, `.openclaw-${codeName}`, "openclaw.json");
3492
+ if (existsSync4(configPath)) {
3493
+ const cfg = JSON.parse(readFileSync4(configPath, "utf-8"));
3280
3494
  if (!cfg.gateway) cfg.gateway = {};
3281
3495
  cfg.gateway.port = port;
3282
3496
  writeFileSync3(configPath, JSON.stringify(cfg, null, 2));
@@ -3368,7 +3582,7 @@ async function pollCycle() {
3368
3582
  log,
3369
3583
  resolveFramework: (codeName) => agentFrameworkCache.get(codeName) ?? null,
3370
3584
  stopSession: (codeName) => {
3371
- stopPersistentSessionAndForgetMcpBaseline(codeName);
3585
+ stopPersistentSessionAndForgetMcpBaseline(codeName, "channel-set-change");
3372
3586
  persistentSessionAgents.delete(codeName);
3373
3587
  claudeAuthTupleBySession.delete(codeName);
3374
3588
  },
@@ -3418,7 +3632,7 @@ async function pollCycle() {
3418
3632
  const now = Date.now();
3419
3633
  if (now - lastVersionCheckAt > VERSION_CHECK_INTERVAL_MS) {
3420
3634
  try {
3421
- const firstAgent = state2.agents[0];
3635
+ const firstAgent = state3.agents[0];
3422
3636
  const versionAdapter = firstAgent ? resolveAgentFramework(firstAgent.codeName) : getFramework("openclaw");
3423
3637
  if (versionAdapter.getVersion) {
3424
3638
  cachedFrameworkVersion = await versionAdapter.getVersion();
@@ -3429,7 +3643,7 @@ async function pollCycle() {
3429
3643
  }
3430
3644
  try {
3431
3645
  const { detectHostSecurity } = await import("../host-security-6PDFG7F5.js");
3432
- const { collectDiagnostics } = await import("../persistent-session-XEA4SPAN.js");
3646
+ const { collectDiagnostics } = await import("../persistent-session-L4YIJJML.js");
3433
3647
  const diagCodeNames = [...persistentSessionAgents];
3434
3648
  const agentDiagnostics = diagCodeNames.length > 0 ? collectDiagnostics(diagCodeNames) : void 0;
3435
3649
  let tailscaleHostname;
@@ -3480,7 +3694,7 @@ async function pollCycle() {
3480
3694
  const {
3481
3695
  collectResponsivenessProbes,
3482
3696
  getResponsivenessIntervalMs
3483
- } = await import("../responsiveness-probe-VWPOCABI.js");
3697
+ } = await import("../responsiveness-probe-YRLESO54.js");
3484
3698
  const probeIntervalMs = getResponsivenessIntervalMs();
3485
3699
  if (now - lastResponsivenessProbeAt > probeIntervalMs) {
3486
3700
  const probeCodeNames = [...persistentSessionAgents];
@@ -3497,13 +3711,50 @@ async function pollCycle() {
3497
3711
  } catch (err) {
3498
3712
  log(`[responsiveness-probe] collection failed: ${err.message}`);
3499
3713
  }
3714
+ try {
3715
+ const { scrapeMcpFailedBannerCount } = await import("../pane-mcp-banner-scraper-JA437JIB.js");
3716
+ const observations = [];
3717
+ const pendingCacheCommits = [];
3718
+ for (const codeName of persistentSessionAgents) {
3719
+ const agentId = codeNameToAgentId.get(codeName);
3720
+ if (!agentId) continue;
3721
+ const tail = readPaneLogTail(codeName, 40);
3722
+ if (!tail) continue;
3723
+ const observed = scrapeMcpFailedBannerCount(tail);
3724
+ const count = observed ?? 0;
3725
+ const last = lastMcpFailedBannerCount.get(codeName);
3726
+ if (last === count) continue;
3727
+ observations.push({ agent_id: agentId, count });
3728
+ pendingCacheCommits.push({ codeName, count });
3729
+ }
3730
+ if (observations.length > 0) {
3731
+ void api.post(
3732
+ "/host/mcp-banner-observation",
3733
+ { observations }
3734
+ ).then((resp) => {
3735
+ const accepted = new Set(
3736
+ (resp?.results ?? []).filter((r) => r.ok).map((r) => r.agent_id)
3737
+ );
3738
+ for (const { codeName, count } of pendingCacheCommits) {
3739
+ const agentId = codeNameToAgentId.get(codeName);
3740
+ if (agentId && accepted.has(agentId)) {
3741
+ lastMcpFailedBannerCount.set(codeName, count);
3742
+ }
3743
+ }
3744
+ }).catch((err) => {
3745
+ log(`[mcp-banner] post failed: ${err.message}`);
3746
+ });
3747
+ }
3748
+ } catch (err) {
3749
+ log(`[mcp-banner] collection failed: ${err.message}`);
3750
+ }
3500
3751
  const data = await api.post("/host/agents", { host_id: hostId });
3501
3752
  const agents = data.agents ?? [];
3502
3753
  const restartAcks = /* @__PURE__ */ new Map();
3503
3754
  for (const agent of agents) {
3504
3755
  const requested = agent.restart_requested_at ?? null;
3505
3756
  if (!requested) continue;
3506
- const prev = state2.agents.find((a) => a.agentId === agent.agent_id);
3757
+ const prev = state3.agents.find((a) => a.agentId === agent.agent_id);
3507
3758
  const lastProcessed = prev?.lastRestartProcessedAt ?? null;
3508
3759
  if (lastProcessed && Date.parse(lastProcessed) >= Date.parse(requested)) continue;
3509
3760
  log(`[restart] Dashboard requested restart for '${agent.code_name}' at ${requested}`);
@@ -3526,7 +3777,7 @@ async function pollCycle() {
3526
3777
  await processAgent(agent, agentStates);
3527
3778
  } catch (err) {
3528
3779
  log(`Error processing agent '${agent.code_name}': ${err.message}`);
3529
- const existing = state2.agents.find((a) => a.agentId === agent.agent_id);
3780
+ const existing = state3.agents.find((a) => a.agentId === agent.agent_id);
3530
3781
  if (existing) {
3531
3782
  agentStates.push(existing);
3532
3783
  } else {
@@ -3549,14 +3800,15 @@ async function pollCycle() {
3549
3800
  }
3550
3801
  }
3551
3802
  }
3803
+ void maybeReportActivityCache({ api, log });
3552
3804
  const restartAckStateChanged = applyRestartAcks({
3553
3805
  agentStates,
3554
- priorAgents: state2.agents,
3806
+ priorAgents: state3.agents,
3555
3807
  restartAcks
3556
3808
  });
3557
3809
  if (restartAckStateChanged) {
3558
3810
  try {
3559
- const ackedState = { ...state2, agents: agentStates };
3811
+ const ackedState = { ...state3, agents: agentStates };
3560
3812
  writeFileSync3(getStateFile(), JSON.stringify(ackedState, null, 2));
3561
3813
  } catch (err) {
3562
3814
  log(`[restart] failed to persist ack immediately: ${err.message}`);
@@ -3574,7 +3826,7 @@ async function pollCycle() {
3574
3826
  } catch {
3575
3827
  }
3576
3828
  const currentIds = new Set(agents.map((a) => a.agent_id));
3577
- for (const prev of state2.agents) {
3829
+ for (const prev of state3.agents) {
3578
3830
  if (!currentIds.has(prev.agentId)) {
3579
3831
  log(`Agent '${prev.codeName}' removed from host (deleted or unassigned)`);
3580
3832
  const adapter = resolveAgentFramework(prev.codeName);
@@ -3587,7 +3839,7 @@ async function pollCycle() {
3587
3839
  }
3588
3840
  killAgentChannelProcesses(prev.codeName, { log });
3589
3841
  freePort(prev.codeName);
3590
- const agentDir = join4(adapter.getAgentDir(prev.codeName), "provision");
3842
+ const agentDir = join5(adapter.getAgentDir(prev.codeName), "provision");
3591
3843
  await cleanupAgentFiles(prev.codeName, agentDir);
3592
3844
  clearAgentCaches(prev.agentId, prev.codeName);
3593
3845
  }
@@ -3674,19 +3926,23 @@ async function pollCycle() {
3674
3926
  }
3675
3927
  } catch {
3676
3928
  }
3677
- state2 = {
3678
- ...state2,
3929
+ state3 = {
3930
+ ...state3,
3679
3931
  lastPollAt: (/* @__PURE__ */ new Date()).toISOString(),
3680
- pollCount: state2.pollCount + 1,
3681
- agents: agentStates
3932
+ pollCount: state3.pollCount + 1,
3933
+ agents: agentStates,
3934
+ // ENG-5441: serialise trip state on every poll so manager restarts
3935
+ // never silently clear a tripped breaker. Cheap — only tripped
3936
+ // entries are stored, and the typical state is zero entries.
3937
+ circuitBreakerTrips: restartBreaker.serialize()
3682
3938
  };
3683
3939
  if (consecutivePollFailures > 0) {
3684
3940
  log(`[poll-backoff] recovered after ${consecutivePollFailures} failure(s), resuming normal interval`);
3685
3941
  consecutivePollFailures = 0;
3686
3942
  }
3687
- send({ type: "state-update", state: state2 });
3943
+ send({ type: "state-update", state: state3 });
3688
3944
  } catch (err) {
3689
- state2.errorCount++;
3945
+ state3.errorCount++;
3690
3946
  const message = err.message;
3691
3947
  log(`Poll error: ${message}`);
3692
3948
  send({ type: "error", message });
@@ -3713,6 +3969,7 @@ async function processAgent(agent, agentStates) {
3713
3969
  const previousKnownStatus = knownStatuses.get(agent.agent_id);
3714
3970
  agentDisplayNames.set(agent.code_name, agent.display_name);
3715
3971
  codeNameToAgentId.set(agent.code_name, agent.agent_id);
3972
+ maybeReportCurrentTrip(agent.code_name);
3716
3973
  if (agent.framework) {
3717
3974
  agentFrameworkCache.set(agent.code_name, agent.framework);
3718
3975
  }
@@ -3726,7 +3983,7 @@ async function processAgent(agent, agentStates) {
3726
3983
  }
3727
3984
  const now = (/* @__PURE__ */ new Date()).toISOString();
3728
3985
  const adapter = resolveAgentFramework(agent.code_name);
3729
- let agentDir = join4(adapter.getAgentDir(agent.code_name), "provision");
3986
+ let agentDir = join5(adapter.getAgentDir(agent.code_name), "provision");
3730
3987
  if (agent.status === "draft" || agent.status === "paused") {
3731
3988
  if (previousKnownStatus !== agent.status) {
3732
3989
  log(`Agent '${agent.code_name}' is ${agent.status}, skipping provisioning`);
@@ -3765,7 +4022,7 @@ async function processAgent(agent, agentStates) {
3765
4022
  const residuals = {
3766
4023
  gatewayRunning: gatewayLiveness.running,
3767
4024
  portAllocated: Object.prototype.hasOwnProperty.call(ports, agent.code_name),
3768
- provisionDirExists: existsSync3(agentDir)
4025
+ provisionDirExists: existsSync4(agentDir)
3769
4026
  };
3770
4027
  if (!hasRevokedResiduals(residuals)) {
3771
4028
  agentStates.push({
@@ -3825,6 +4082,11 @@ async function processAgent(agent, agentStates) {
3825
4082
  if (previousStatus && previousStatus !== agent.status) {
3826
4083
  log(`Agent '${agent.code_name}' status changed: ${previousStatus} \u2192 ${agent.status}`);
3827
4084
  knownVersions.delete(agent.agent_id);
4085
+ if (previousStatus === "paused" && agent.status === "active" && restartBreaker.isTripped(agent.code_name)) {
4086
+ restartBreaker.clear(agent.code_name);
4087
+ reportedTrips.delete(agent.code_name);
4088
+ log(`[circuit-breaker] Cleared trip for '${agent.code_name}' on operator resume (paused \u2192 active)`);
4089
+ }
3828
4090
  let channelCacheMutated = false;
3829
4091
  for (const key of knownChannelConfigHashes.keys()) {
3830
4092
  if (key.startsWith(`${agent.agent_id}:`)) {
@@ -3842,7 +4104,7 @@ async function processAgent(agent, agentStates) {
3842
4104
  });
3843
4105
  } catch (err) {
3844
4106
  log(`Refresh failed for '${agent.code_name}': ${err.message}`);
3845
- const existing = state2.agents.find((a) => a.agentId === agent.agent_id);
4107
+ const existing = state3.agents.find((a) => a.agentId === agent.agent_id);
3846
4108
  agentStates.push(existing ?? {
3847
4109
  agentId: agent.agent_id,
3848
4110
  codeName: agent.code_name,
@@ -3890,7 +4152,7 @@ async function processAgent(agent, agentStates) {
3890
4152
  const frameworkId = refreshData.agent.framework ?? "openclaw";
3891
4153
  agentFrameworkCache.set(agent.code_name, frameworkId);
3892
4154
  const frameworkAdapter = getFramework(frameworkId);
3893
- agentDir = join4(frameworkAdapter.getAgentDir(agent.code_name), "provision");
4155
+ agentDir = join5(frameworkAdapter.getAgentDir(agent.code_name), "provision");
3894
4156
  cacheAgentDeliveryMetadata(agent.code_name, refreshData);
3895
4157
  if (frameworkAdapter.seedProfileConfig) {
3896
4158
  frameworkAdapter.seedProfileConfig(agent.code_name);
@@ -3898,7 +4160,7 @@ async function processAgent(agent, agentStates) {
3898
4160
  const charterVersion = refreshData.charter.version;
3899
4161
  const toolsVersion = refreshData.tools.version;
3900
4162
  const known = knownVersions.get(agent.agent_id);
3901
- let lastProvisionAt = state2.agents.find((a) => a.agentId === agent.agent_id)?.lastProvisionAt ?? null;
4163
+ let lastProvisionAt = state3.agents.find((a) => a.agentId === agent.agent_id)?.lastProvisionAt ?? null;
3902
4164
  const currentChannelIds = launchableChannelIds(refreshData.channel_configs);
3903
4165
  const previousChannelIds = knownChannels.get(agent.agent_id);
3904
4166
  const channelsChanged = !previousChannelIds || currentChannelIds.size !== previousChannelIds.size || [...currentChannelIds].some((ch) => !previousChannelIds.has(ch)) || [...previousChannelIds].some((ch) => !currentChannelIds.has(ch));
@@ -3921,7 +4183,7 @@ async function processAgent(agent, agentStates) {
3921
4183
  const changedFiles = [];
3922
4184
  mkdirSync2(agentDir, { recursive: true });
3923
4185
  for (const artifact of artifacts) {
3924
- const filePath = join4(agentDir, artifact.relativePath);
4186
+ const filePath = join5(agentDir, artifact.relativePath);
3925
4187
  let existingHash;
3926
4188
  let newHash;
3927
4189
  let writeContent = artifact.content;
@@ -3933,8 +4195,8 @@ async function processAgent(agent, agentStates) {
3933
4195
  };
3934
4196
  newHash = sha256(stripDynamicSections(artifact.content));
3935
4197
  try {
3936
- const projectClaudeMd = join4(config.configDir, agent.code_name, "project", "CLAUDE.md");
3937
- const existing = readFileSync3(projectClaudeMd, "utf-8");
4198
+ const projectClaudeMd = join5(config.configDir, agent.code_name, "project", "CLAUDE.md");
4199
+ const existing = readFileSync4(projectClaudeMd, "utf-8");
3938
4200
  existingHash = sha256(stripDynamicSections(existing));
3939
4201
  } catch {
3940
4202
  existingHash = null;
@@ -3952,7 +4214,7 @@ async function processAgent(agent, agentStates) {
3952
4214
  const generatorKeys = Object.keys(generatorServers);
3953
4215
  let existingRaw = "";
3954
4216
  try {
3955
- existingRaw = readFileSync3(filePath, "utf-8");
4217
+ existingRaw = readFileSync4(filePath, "utf-8");
3956
4218
  } catch {
3957
4219
  }
3958
4220
  const existingServers = parseMcp(existingRaw);
@@ -3974,22 +4236,22 @@ async function processAgent(agent, agentStates) {
3974
4236
  }
3975
4237
  }
3976
4238
  if (changedFiles.length > 0) {
3977
- const isFirst = !existsSync3(join4(agentDir, "CHARTER.md"));
4239
+ const isFirst = !existsSync4(join5(agentDir, "CHARTER.md"));
3978
4240
  const verb = isFirst ? "Provisioning" : "Updating";
3979
4241
  const fileNames = changedFiles.map((f) => f.relativePath).join(", ");
3980
4242
  log(`${verb} '${agent.code_name}': ${fileNames}`);
3981
4243
  for (const file of changedFiles) {
3982
- const filePath = join4(agentDir, file.relativePath);
4244
+ const filePath = join5(agentDir, file.relativePath);
3983
4245
  mkdirSync2(dirname(filePath), { recursive: true });
3984
4246
  writeFileSync3(filePath, file.content);
3985
4247
  }
3986
4248
  try {
3987
- const provSkillsDir = join4(agentDir, ".claude", "skills");
3988
- if (existsSync3(provSkillsDir)) {
4249
+ const provSkillsDir = join5(agentDir, ".claude", "skills");
4250
+ if (existsSync4(provSkillsDir)) {
3989
4251
  for (const folder of readdirSync2(provSkillsDir)) {
3990
4252
  if (folder.startsWith("knowledge-")) {
3991
4253
  try {
3992
- rmSync2(join4(provSkillsDir, folder), { recursive: true });
4254
+ rmSync2(join5(provSkillsDir, folder), { recursive: true });
3993
4255
  } catch {
3994
4256
  }
3995
4257
  }
@@ -4002,7 +4264,7 @@ async function processAgent(agent, agentStates) {
4002
4264
  const trackedFiles2 = frameworkAdapter.driftTrackedFiles();
4003
4265
  const hashes = /* @__PURE__ */ new Map();
4004
4266
  for (const file of trackedFiles2) {
4005
- const h = hashFile(join4(agentDir, file));
4267
+ const h = hashFile(join5(agentDir, file));
4006
4268
  if (h) hashes.set(file, h);
4007
4269
  }
4008
4270
  writtenHashes.set(agent.agent_id, hashes);
@@ -4046,10 +4308,10 @@ async function processAgent(agent, agentStates) {
4046
4308
  }
4047
4309
  let lastDriftCheckAt = now;
4048
4310
  const written = writtenHashes.get(agent.agent_id);
4049
- if (written && existsSync3(agentDir)) {
4311
+ if (written && existsSync4(agentDir)) {
4050
4312
  const driftedFiles = [];
4051
4313
  for (const [file, expectedHash] of written) {
4052
- const localHash = hashFile(join4(agentDir, file));
4314
+ const localHash = hashFile(join5(agentDir, file));
4053
4315
  if (localHash && localHash !== expectedHash) {
4054
4316
  driftedFiles.push(file);
4055
4317
  }
@@ -4060,7 +4322,7 @@ async function processAgent(agent, agentStates) {
4060
4322
  try {
4061
4323
  const localHashes = {};
4062
4324
  for (const file of driftedFiles) {
4063
- localHashes[file] = hashFile(join4(agentDir, file));
4325
+ localHashes[file] = hashFile(join5(agentDir, file));
4064
4326
  }
4065
4327
  await api.post("/host/drift", {
4066
4328
  agent_id: agent.agent_id,
@@ -4218,24 +4480,24 @@ async function processAgent(agent, agentStates) {
4218
4480
  if (agentSessionMode === "persistent" && (agentFrameworkCache.get(agent.code_name) ?? "openclaw") === "claude-code") {
4219
4481
  try {
4220
4482
  const agentProvisionDir = agentDir;
4221
- const projectDir = join4(homedir3(), ".augmented", agent.code_name, "project");
4483
+ const projectDir = join5(homedir4(), ".augmented", agent.code_name, "project");
4222
4484
  mkdirSync2(agentProvisionDir, { recursive: true });
4223
4485
  mkdirSync2(projectDir, { recursive: true });
4224
- const provisionMcpPath = join4(agentProvisionDir, ".mcp.json");
4225
- const projectMcpPath = join4(projectDir, ".mcp.json");
4486
+ const provisionMcpPath = join5(agentProvisionDir, ".mcp.json");
4487
+ const projectMcpPath = join5(projectDir, ".mcp.json");
4226
4488
  let mcpConfig = { mcpServers: {} };
4227
4489
  try {
4228
- mcpConfig = JSON.parse(readFileSync3(provisionMcpPath, "utf-8"));
4490
+ mcpConfig = JSON.parse(readFileSync4(provisionMcpPath, "utf-8"));
4229
4491
  if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
4230
4492
  } catch {
4231
4493
  }
4232
- const localDirectChatChannel = join4(homedir3(), ".augmented", "_mcp", "direct-chat-channel.js");
4494
+ const localDirectChatChannel = join5(homedir4(), ".augmented", "_mcp", "direct-chat-channel.js");
4233
4495
  const directChatTeamSettings = refreshData.team?.settings;
4234
4496
  const directChatTz = (() => {
4235
4497
  const tz = directChatTeamSettings?.["timezone"];
4236
4498
  return typeof tz === "string" && tz.trim() !== "" ? tz.trim() : void 0;
4237
4499
  })();
4238
- if (existsSync3(localDirectChatChannel) && !mcpConfig.mcpServers["direct-chat"]) {
4500
+ if (existsSync4(localDirectChatChannel) && !mcpConfig.mcpServers["direct-chat"]) {
4239
4501
  mcpConfig.mcpServers["direct-chat"] = {
4240
4502
  command: "node",
4241
4503
  args: [localDirectChatChannel],
@@ -4251,8 +4513,8 @@ async function processAgent(agent, agentStates) {
4251
4513
  writeFileSync3(projectMcpPath, serialized);
4252
4514
  log(`Channel credentials written for '${agent.code_name}/direct-chat'`);
4253
4515
  }
4254
- const staleChannelsPath = join4(projectDir, ".mcp-channels.json");
4255
- if (existsSync3(staleChannelsPath)) {
4516
+ const staleChannelsPath = join5(projectDir, ".mcp-channels.json");
4517
+ if (existsSync4(staleChannelsPath)) {
4256
4518
  try {
4257
4519
  rmSync2(staleChannelsPath, { force: true });
4258
4520
  } catch {
@@ -4262,7 +4524,7 @@ async function processAgent(agent, agentStates) {
4262
4524
  log(`Failed to provision direct-chat channel for '${agent.code_name}': ${err.message}`);
4263
4525
  }
4264
4526
  }
4265
- let lastSecretsProvisionAt = state2.agents.find((a) => a.agentId === agent.agent_id)?.lastSecretsProvisionAt ?? null;
4527
+ let lastSecretsProvisionAt = state3.agents.find((a) => a.agentId === agent.agent_id)?.lastSecretsProvisionAt ?? null;
4266
4528
  let secretsHash = knownSecretsHashes.get(agent.agent_id) ?? null;
4267
4529
  try {
4268
4530
  const secretsData = await api.post("/host/secrets", { agent_id: agent.agent_id });
@@ -4318,11 +4580,11 @@ async function processAgent(agent, agentStates) {
4318
4580
  const intHash = computeIntegrationsHash(integrations);
4319
4581
  const prevIntHash = knownIntegrationHashes.get(agent.agent_id);
4320
4582
  if (intHash !== prevIntHash) {
4321
- const projectDir = join4(homedir3(), ".augmented", agent.code_name, "project");
4322
- const envIntPath = join4(projectDir, ".env.integrations");
4583
+ const projectDir = join5(homedir4(), ".augmented", agent.code_name, "project");
4584
+ const envIntPath = join5(projectDir, ".env.integrations");
4323
4585
  let preWriteEnv;
4324
4586
  try {
4325
- preWriteEnv = readFileSync3(envIntPath, "utf-8");
4587
+ preWriteEnv = readFileSync4(envIntPath, "utf-8");
4326
4588
  } catch {
4327
4589
  preWriteEnv = void 0;
4328
4590
  }
@@ -4334,9 +4596,9 @@ async function processAgent(agent, agentStates) {
4334
4596
  let rotationHandled = true;
4335
4597
  if (fw === "claude-code" && isSessionHealthy(agent.code_name)) {
4336
4598
  try {
4337
- const projectMcpPath = join4(projectDir, ".mcp.json");
4338
- const postWriteEnv = readFileSync3(envIntPath, "utf-8");
4339
- const mcpContent = readFileSync3(projectMcpPath, "utf-8");
4599
+ const projectMcpPath = join5(projectDir, ".mcp.json");
4600
+ const postWriteEnv = readFileSync4(envIntPath, "utf-8");
4601
+ const mcpContent = readFileSync4(projectMcpPath, "utf-8");
4340
4602
  const changedVars = diffEnvIntegrations(preWriteEnv, postWriteEnv);
4341
4603
  const mcpJsonForReap = JSON.parse(mcpContent);
4342
4604
  const affectedServerKeys = findMcpServersUsingVars(mcpJsonForReap, changedVars);
@@ -4402,8 +4664,8 @@ async function processAgent(agent, agentStates) {
4402
4664
  const mcpPath = frameworkAdapter.getMcpPath(agent.code_name);
4403
4665
  if (mcpPath) {
4404
4666
  try {
4405
- const { readFileSync: readFileSync4 } = await import("fs");
4406
- const mcpConfig = JSON.parse(readFileSync4(mcpPath, "utf-8"));
4667
+ const { readFileSync: readFileSync5 } = await import("fs");
4668
+ const mcpConfig = JSON.parse(readFileSync5(mcpPath, "utf-8"));
4407
4669
  if (mcpConfig.mcpServers) {
4408
4670
  const managedPrefixes = [
4409
4671
  "composio_",
@@ -4492,8 +4754,8 @@ async function processAgent(agent, agentStates) {
4492
4754
  if (agent.status === "active") {
4493
4755
  if (frameworkAdapter.installPlugin) {
4494
4756
  try {
4495
- const pluginPath = join4(process.cwd(), "packages", "openclaw-plugin-augmented", "src", "index.ts");
4496
- if (existsSync3(pluginPath)) {
4757
+ const pluginPath = join5(process.cwd(), "packages", "openclaw-plugin-augmented", "src", "index.ts");
4758
+ if (existsSync4(pluginPath)) {
4497
4759
  frameworkAdapter.installPlugin(agent.code_name, "augmented", pluginPath, {
4498
4760
  agtHost: requireHost(),
4499
4761
  agtApiKey: getApiKey() ?? void 0,
@@ -4559,20 +4821,20 @@ async function processAgent(agent, agentStates) {
4559
4821
  }
4560
4822
  try {
4561
4823
  const { readdirSync: readdirSync3, rmSync: rmSync3 } = await import("fs");
4562
- const { homedir: homedir4 } = await import("os");
4824
+ const { homedir: homedir5 } = await import("os");
4563
4825
  const frameworkId2 = frameworkAdapter.id;
4564
4826
  const candidateSkillDirs = [
4565
4827
  // Claude Code — framework runtime tree
4566
- join4(homedir4(), ".augmented", agent.code_name, "skills"),
4828
+ join5(homedir5(), ".augmented", agent.code_name, "skills"),
4567
4829
  // Claude Code — project tree
4568
- join4(homedir4(), ".augmented", agent.code_name, "project", ".claude", "skills"),
4830
+ join5(homedir5(), ".augmented", agent.code_name, "project", ".claude", "skills"),
4569
4831
  // OpenClaw — framework runtime tree
4570
- join4(homedir4(), `.openclaw-${agent.code_name}`, "skills"),
4832
+ join5(homedir5(), `.openclaw-${agent.code_name}`, "skills"),
4571
4833
  // Defensive: legacy provision-side path, not currently an
4572
4834
  // install target but cheap to sweep.
4573
- join4(agentDir, ".claude", "skills")
4835
+ join5(agentDir, ".claude", "skills")
4574
4836
  ];
4575
- const existingDirs = candidateSkillDirs.filter((d) => existsSync3(d));
4837
+ const existingDirs = candidateSkillDirs.filter((d) => existsSync4(d));
4576
4838
  const discoveredEntries = /* @__PURE__ */ new Set();
4577
4839
  for (const dir of existingDirs) {
4578
4840
  try {
@@ -4586,8 +4848,8 @@ async function processAgent(agent, agentStates) {
4586
4848
  }
4587
4849
  const removeSkillFolder = (entry, reason) => {
4588
4850
  for (const dir of existingDirs) {
4589
- const p = join4(dir, entry);
4590
- if (existsSync3(p)) {
4851
+ const p = join5(dir, entry);
4852
+ if (existsSync4(p)) {
4591
4853
  rmSync3(p, { recursive: true, force: true });
4592
4854
  }
4593
4855
  }
@@ -4747,8 +5009,8 @@ async function processAgent(agent, agentStates) {
4747
5009
  const sess = getSessionState(agent.code_name);
4748
5010
  let mcpJsonParsed = null;
4749
5011
  try {
4750
- const mcpPath = join4(getProjectDir(agent.code_name), ".mcp.json");
4751
- mcpJsonParsed = JSON.parse(readFileSync3(mcpPath, "utf-8"));
5012
+ const mcpPath = join5(getProjectDir(agent.code_name), ".mcp.json");
5013
+ mcpJsonParsed = JSON.parse(readFileSync4(mcpPath, "utf-8"));
4752
5014
  } catch {
4753
5015
  }
4754
5016
  reapMissingMcpSessions({
@@ -4756,7 +5018,12 @@ async function processAgent(agent, agentStates) {
4756
5018
  codeName: agent.code_name,
4757
5019
  mcpJson: mcpJsonParsed,
4758
5020
  sessionStartedAt: sess?.startedAt ?? null,
4759
- stopSession: (codeName) => stopPersistentSessionAndForgetMcpBaseline(codeName),
5021
+ // ENG-5441: mcp-presence-reaper-driven restart counts against
5022
+ // the breaker. Each declared MCP has its own ENG-5279 give-up
5023
+ // cap (~3 cycles per server), but a thrashing collection of
5024
+ // MCPs can still rotate restarts of the whole session. The
5025
+ // breaker catches the rotating case the per-MCP cap can't see.
5026
+ stopSession: (codeName) => stopPersistentSessionAndForgetMcpBaseline(codeName, "mcp-presence-reaper"),
4760
5027
  // ENG-5292: when the reaper gives up on a managed MCP (cap from
4761
5028
  // ENG-5279 + state-preservation from ENG-5285 both said "this
4762
5029
  // MCP keeps failing after 3 restart cycles"), mark the matching
@@ -4892,10 +5159,10 @@ async function processAgent(agent, agentStates) {
4892
5159
  lastWorkTriggerAt.set(agent.code_name, triggerTs);
4893
5160
  if (agentFw === "openclaw" && gatewayRunning && gatewayPort) {
4894
5161
  const homeDir = process.env["HOME"] ?? "/tmp";
4895
- const jobsPath = join4(homeDir, `.openclaw-${agent.code_name}`, "cron", "jobs.json");
4896
- if (existsSync3(jobsPath)) {
5162
+ const jobsPath = join5(homeDir, `.openclaw-${agent.code_name}`, "cron", "jobs.json");
5163
+ if (existsSync4(jobsPath)) {
4897
5164
  try {
4898
- const jobsData = JSON.parse(readFileSync3(jobsPath, "utf-8"));
5165
+ const jobsData = JSON.parse(readFileSync4(jobsPath, "utf-8"));
4899
5166
  const kanbanJob = (jobsData.jobs ?? []).find(
4900
5167
  (j) => typeof j.name === "string" && j.name.includes("kanban-work")
4901
5168
  );
@@ -5036,10 +5303,10 @@ In progress for ${age} minutes \u2014 auto-failed`).catch(() => {
5036
5303
  }
5037
5304
  }
5038
5305
  const trackedFiles = frameworkAdapter.driftTrackedFiles();
5039
- if (trackedFiles.length > 0 && existsSync3(agentDir)) {
5306
+ if (trackedFiles.length > 0 && existsSync4(agentDir)) {
5040
5307
  const hashes = /* @__PURE__ */ new Map();
5041
5308
  for (const file of trackedFiles) {
5042
- const h = hashFile(join4(agentDir, file));
5309
+ const h = hashFile(join5(agentDir, file));
5043
5310
  if (h) hashes.set(file, h);
5044
5311
  }
5045
5312
  writtenHashes.set(agent.agent_id, hashes);
@@ -5073,19 +5340,19 @@ function cleanupStaleSessions(codeName) {
5073
5340
  lastCleanupAt.set(codeName, Date.now());
5074
5341
  const homeDir = process.env["HOME"] ?? "/tmp";
5075
5342
  for (const agentDir of ["main", codeName]) {
5076
- const sessionsDir = join4(homeDir, `.openclaw-${codeName}`, "agents", agentDir, "sessions");
5343
+ const sessionsDir = join5(homeDir, `.openclaw-${codeName}`, "agents", agentDir, "sessions");
5077
5344
  cleanupCronSessions(sessionsDir, CRON_SESSION_KEEP_COUNT);
5078
5345
  }
5079
- const cronRunsDir = join4(homeDir, `.openclaw-${codeName}`, "cron", "runs");
5346
+ const cronRunsDir = join5(homeDir, `.openclaw-${codeName}`, "cron", "runs");
5080
5347
  cleanupOldFiles(cronRunsDir, CRON_RUN_RETENTION_DAYS, ".jsonl");
5081
- const cronJobsPath = join4(homeDir, `.openclaw-${codeName}`, "cron", "jobs.json");
5348
+ const cronJobsPath = join5(homeDir, `.openclaw-${codeName}`, "cron", "jobs.json");
5082
5349
  clearStaleCronRunState(cronJobsPath);
5083
5350
  }
5084
5351
  function cleanupCronSessions(sessionsDir, keepCount) {
5085
- const indexPath = join4(sessionsDir, "sessions.json");
5086
- if (!existsSync3(indexPath)) return;
5352
+ const indexPath = join5(sessionsDir, "sessions.json");
5353
+ if (!existsSync4(indexPath)) return;
5087
5354
  try {
5088
- const raw = readFileSync3(indexPath, "utf-8");
5355
+ const raw = readFileSync4(indexPath, "utf-8");
5089
5356
  const index = JSON.parse(raw);
5090
5357
  const cronRunKeys = Object.keys(index).filter((k) => k.includes(":cron:") && k.includes(":run:")).map((k) => ({
5091
5358
  key: k,
@@ -5098,9 +5365,9 @@ function cleanupCronSessions(sessionsDir, keepCount) {
5098
5365
  for (const entry of toDelete) {
5099
5366
  delete index[entry.key];
5100
5367
  if (entry.sessionId) {
5101
- const sessionFile = join4(sessionsDir, `${entry.sessionId}.jsonl`);
5368
+ const sessionFile = join5(sessionsDir, `${entry.sessionId}.jsonl`);
5102
5369
  try {
5103
- if (existsSync3(sessionFile)) {
5370
+ if (existsSync4(sessionFile)) {
5104
5371
  unlinkSync(sessionFile);
5105
5372
  deletedFiles++;
5106
5373
  }
@@ -5120,8 +5387,8 @@ function cleanupCronSessions(sessionsDir, keepCount) {
5120
5387
  delete index[parentKey];
5121
5388
  if (parentSessionId) {
5122
5389
  try {
5123
- const f = join4(sessionsDir, `${parentSessionId}.jsonl`);
5124
- if (existsSync3(f)) {
5390
+ const f = join5(sessionsDir, `${parentSessionId}.jsonl`);
5391
+ if (existsSync4(f)) {
5125
5392
  unlinkSync(f);
5126
5393
  deletedFiles++;
5127
5394
  }
@@ -5139,28 +5406,28 @@ function cleanupCronSessions(sessionsDir, keepCount) {
5139
5406
  }
5140
5407
  var STALE_RUN_TIMEOUT_MS = 5 * 6e4;
5141
5408
  function clearStaleCronRunState(jobsPath) {
5142
- if (!existsSync3(jobsPath)) return;
5409
+ if (!existsSync4(jobsPath)) return;
5143
5410
  try {
5144
- const raw = readFileSync3(jobsPath, "utf-8");
5411
+ const raw = readFileSync4(jobsPath, "utf-8");
5145
5412
  const data = JSON.parse(raw);
5146
5413
  const jobs = data.jobs ?? data;
5147
5414
  if (!Array.isArray(jobs)) return;
5148
5415
  let changed = false;
5149
5416
  const now = Date.now();
5150
5417
  for (const job of jobs) {
5151
- const state3 = job.state;
5152
- if (!state3) continue;
5153
- const runStartedAt = state3.runningAtMs ?? state3.runStartedAtMs;
5154
- const isRunning = state3.running === true || state3.status === "running";
5418
+ const state4 = job.state;
5419
+ if (!state4) continue;
5420
+ const runStartedAt = state4.runningAtMs ?? state4.runStartedAtMs;
5421
+ const isRunning = state4.running === true || state4.status === "running";
5155
5422
  if (isRunning && runStartedAt && now - runStartedAt > STALE_RUN_TIMEOUT_MS) {
5156
- state3.running = false;
5157
- delete state3.status;
5158
- delete state3.runStartedAtMs;
5159
- delete state3.currentRunId;
5423
+ state4.running = false;
5424
+ delete state4.status;
5425
+ delete state4.runStartedAtMs;
5426
+ delete state4.currentRunId;
5160
5427
  changed = true;
5161
5428
  log(`Cleared stale running state for cron job '${job.name}' (stuck for ${Math.round((now - runStartedAt) / 6e4)}min)`);
5162
5429
  } else if (isRunning && !runStartedAt) {
5163
- state3.running = false;
5430
+ state4.running = false;
5164
5431
  changed = true;
5165
5432
  log(`Cleared stale running state for cron job '${job.name}' (no start timestamp)`);
5166
5433
  }
@@ -5172,13 +5439,13 @@ function clearStaleCronRunState(jobsPath) {
5172
5439
  }
5173
5440
  }
5174
5441
  function cleanupOldFiles(dir, maxAgeDays, ext) {
5175
- if (!existsSync3(dir)) return;
5442
+ if (!existsSync4(dir)) return;
5176
5443
  const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3;
5177
5444
  let removed = 0;
5178
5445
  try {
5179
5446
  for (const f of readdirSync2(dir)) {
5180
5447
  if (!f.endsWith(ext)) continue;
5181
- const fullPath = join4(dir, f);
5448
+ const fullPath = join5(dir, f);
5182
5449
  try {
5183
5450
  const st = statSync(fullPath);
5184
5451
  if (st.mtimeMs < cutoff) {
@@ -5224,16 +5491,16 @@ async function syncAndCheckClaudeScheduler(agent, tasks, boardItems, refreshData
5224
5491
  enabled: t.enabled ?? true,
5225
5492
  triggered_at: t.triggered_at ?? null
5226
5493
  }));
5227
- const state4 = syncTasksToScheduler(codeName, agent.agent_id, taskInputs);
5228
- claudeSchedulerStates.set(codeName, state4);
5494
+ const state5 = syncTasksToScheduler(codeName, agent.agent_id, taskInputs);
5495
+ claudeSchedulerStates.set(codeName, state5);
5229
5496
  knownTasksHashes.set(agent.agent_id, combinedHash);
5230
5497
  log(`[claude-scheduler] Tasks synced for '${codeName}' (${taskInputs.length} task(s))`);
5231
5498
  }
5232
5499
  if (!claudeSchedulerStates.has(codeName)) {
5233
5500
  claudeSchedulerStates.set(codeName, loadSchedulerState(codeName));
5234
5501
  }
5235
- const state3 = claudeSchedulerStates.get(codeName);
5236
- const ready = getReadyTasks(state3, inFlightClaudeTasks);
5502
+ const state4 = claudeSchedulerStates.get(codeName);
5503
+ const ready = getReadyTasks(state4, inFlightClaudeTasks);
5237
5504
  if (ready.length === 0) return;
5238
5505
  for (const task of ready) {
5239
5506
  if ((claudeTaskConcurrency.get(codeName) ?? 0) >= MAX_CLAUDE_CONCURRENCY) break;
@@ -5322,7 +5589,7 @@ async function fetchPriorScheduledRuns(agentId, taskId) {
5322
5589
  }
5323
5590
  async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
5324
5591
  const projectDir = getProjectDir2(codeName);
5325
- const mcpConfigPath = join4(projectDir, ".mcp.json");
5592
+ const mcpConfigPath = join5(projectDir, ".mcp.json");
5326
5593
  let runId = null;
5327
5594
  let kanbanItemId = null;
5328
5595
  let taskResult;
@@ -5330,11 +5597,11 @@ async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
5330
5597
  const priorRuns = await fetchPriorScheduledRuns(agentId, task.taskId);
5331
5598
  prompt = wrapScheduledTaskPrompt(prompt, { priorRuns });
5332
5599
  try {
5333
- const claudeMdPath = join4(projectDir, "CLAUDE.md");
5600
+ const claudeMdPath = join5(projectDir, "CLAUDE.md");
5334
5601
  const serverNames = [];
5335
- if (existsSync3(mcpConfigPath)) {
5602
+ if (existsSync4(mcpConfigPath)) {
5336
5603
  try {
5337
- const d = JSON.parse(readFileSync3(mcpConfigPath, "utf-8"));
5604
+ const d = JSON.parse(readFileSync4(mcpConfigPath, "utf-8"));
5338
5605
  if (d.mcpServers) serverNames.push(...Object.keys(d.mcpServers));
5339
5606
  } catch {
5340
5607
  }
@@ -5353,14 +5620,14 @@ async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
5353
5620
  "--allowedTools",
5354
5621
  allowedTools
5355
5622
  ];
5356
- if (existsSync3(claudeMdPath)) {
5623
+ if (existsSync4(claudeMdPath)) {
5357
5624
  claudeArgs.push("--system-prompt-file", claudeMdPath);
5358
5625
  }
5359
5626
  const childEnv = { ...process.env };
5360
- const envIntPath = join4(projectDir, ".env.integrations");
5361
- if (existsSync3(envIntPath)) {
5627
+ const envIntPath = join5(projectDir, ".env.integrations");
5628
+ if (existsSync4(envIntPath)) {
5362
5629
  try {
5363
- for (const line of readFileSync3(envIntPath, "utf-8").split("\n")) {
5630
+ for (const line of readFileSync4(envIntPath, "utf-8").split("\n")) {
5364
5631
  if (!line || line.startsWith("#") || !line.includes("=")) continue;
5365
5632
  const eqIdx = line.indexOf("=");
5366
5633
  childEnv[line.slice(0, eqIdx)] = line.slice(eqIdx + 1);
@@ -5514,8 +5781,8 @@ async function processClaudeTaskResult(codeName, agentId, templateId, rawOutput,
5514
5781
  }
5515
5782
  }
5516
5783
  function fireClaudeWorkTrigger(codeName, agentId, boardItems) {
5517
- const state3 = claudeSchedulerStates.get(codeName) ?? loadSchedulerState(codeName);
5518
- const kanbanTask = findTaskByTemplate(state3, "kanban-work");
5784
+ const state4 = claudeSchedulerStates.get(codeName) ?? loadSchedulerState(codeName);
5785
+ const kanbanTask = findTaskByTemplate(state4, "kanban-work");
5519
5786
  if (!kanbanTask) {
5520
5787
  log(`[claude-scheduler] Work trigger: no kanban-work task found for '${codeName}'`);
5521
5788
  return;
@@ -5538,13 +5805,23 @@ function fireClaudeWorkTrigger(codeName, agentId, boardItems) {
5538
5805
  });
5539
5806
  }
5540
5807
  var persistentSessionAgents = /* @__PURE__ */ new Set();
5808
+ var lastMcpFailedBannerCount = /* @__PURE__ */ new Map();
5541
5809
  var persistentSessionStuckTracker = new PersistentSessionStuckTracker();
5542
5810
  var claudeAuthTupleBySession = /* @__PURE__ */ new Map();
5543
5811
  async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
5544
5812
  const codeName = agent.code_name;
5545
5813
  const projectDir = getProjectDir(codeName);
5546
- const mcpConfigPath = join4(projectDir, ".mcp.json");
5547
- const claudeMdPath = join4(projectDir, "CLAUDE.md");
5814
+ const mcpConfigPath = join5(projectDir, ".mcp.json");
5815
+ const claudeMdPath = join5(projectDir, "CLAUDE.md");
5816
+ if (restartBreaker.isTripped(codeName)) {
5817
+ const trip = restartBreaker.getTrip(codeName);
5818
+ return {
5819
+ decision: "circuit-tripped",
5820
+ spawnAttempted: false,
5821
+ sessionHealthyAfter: isSessionHealthy(codeName),
5822
+ detail: trip.statusMessage
5823
+ };
5824
+ }
5548
5825
  const teamSettingsForTz = refreshData.team?.settings;
5549
5826
  const agentTimezone = (() => {
5550
5827
  const tz = teamSettingsForTz?.["timezone"];
@@ -5627,7 +5904,7 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
5627
5904
  const recordedAuthTuple = claudeAuthTupleBySession.get(codeName);
5628
5905
  if (recordedAuthTuple && recordedAuthTuple !== currentAuthTuple && isSessionHealthy(codeName)) {
5629
5906
  log(`[persistent-session] Auth config changed for '${codeName}' (${recordedAuthTuple} \u2192 ${currentAuthTuple}) \u2014 restarting session`);
5630
- stopPersistentSessionAndForgetMcpBaseline(codeName);
5907
+ stopPersistentSessionAndForgetMcpBaseline(codeName, "auth-tuple-change");
5631
5908
  persistentSessionAgents.delete(codeName);
5632
5909
  restartTrigger = "auth-tuple";
5633
5910
  }
@@ -5639,7 +5916,7 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
5639
5916
  log(
5640
5917
  `[persistent-session] Day rollover for '${codeName}' (yesterday=${current.date}) \u2014 agent idle, restarting to mint fresh session`
5641
5918
  );
5642
- stopPersistentSessionAndForgetMcpBaseline(codeName);
5919
+ stopPersistentSessionAndForgetMcpBaseline(codeName, "day-rollover");
5643
5920
  persistentSessionAgents.delete(codeName);
5644
5921
  restartTrigger = "day-rollover";
5645
5922
  } else {
@@ -5650,6 +5927,15 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
5650
5927
  }
5651
5928
  }
5652
5929
  }
5930
+ if (restartBreaker.isTripped(codeName)) {
5931
+ const trip = restartBreaker.getTrip(codeName);
5932
+ return {
5933
+ decision: "circuit-tripped",
5934
+ spawnAttempted: false,
5935
+ sessionHealthyAfter: isSessionHealthy(codeName),
5936
+ detail: trip.statusMessage
5937
+ };
5938
+ }
5653
5939
  if (!isSessionHealthy(codeName)) {
5654
5940
  if (persistentSessionAgents.has(codeName)) {
5655
5941
  const ctx = getLastFailureContext(codeName);
@@ -5763,9 +6049,9 @@ ${truncateForLog(ctx.tail)}` : `; pane_tail_hash=sha256:${createHash2("sha256").
5763
6049
  } else if (!claudeSchedulerStates.has(codeName)) {
5764
6050
  claudeSchedulerStates.set(codeName, loadSchedulerState(codeName));
5765
6051
  }
5766
- const state3 = claudeSchedulerStates.get(codeName);
5767
- if (state3) {
5768
- const ready = getReadyTasks(state3, inFlightClaudeTasks);
6052
+ const state4 = claudeSchedulerStates.get(codeName);
6053
+ if (state4) {
6054
+ const ready = getReadyTasks(state4, inFlightClaudeTasks);
5769
6055
  if (ready.length > 0) {
5770
6056
  log(`[persistent-session] ${ready.length} ready task(s) for '${codeName}': ${ready.map((t) => `${t.name}(next=${t.nextFireAt ? new Date(t.nextFireAt).toISOString() : "null"})`).join(", ")}`);
5771
6057
  }
@@ -6144,11 +6430,11 @@ ${escapeXml(msg.content)}
6144
6430
  if (fw === "claude-code") {
6145
6431
  const { getProjectDir: ccProjectDir } = await import("../claude-scheduler-XHJS2MZV.js");
6146
6432
  const projDir = ccProjectDir(agent.codeName);
6147
- const mcpConfigPath = join4(projDir, ".mcp.json");
6433
+ const mcpConfigPath = join5(projDir, ".mcp.json");
6148
6434
  const serverNames = [];
6149
- if (existsSync3(mcpConfigPath)) {
6435
+ if (existsSync4(mcpConfigPath)) {
6150
6436
  try {
6151
- const d = JSON.parse(readFileSync3(mcpConfigPath, "utf-8"));
6437
+ const d = JSON.parse(readFileSync4(mcpConfigPath, "utf-8"));
6152
6438
  if (d.mcpServers) serverNames.push(...Object.keys(d.mcpServers));
6153
6439
  } catch {
6154
6440
  }
@@ -6167,15 +6453,15 @@ ${escapeXml(msg.content)}
6167
6453
  "--allowedTools",
6168
6454
  allowedTools
6169
6455
  ];
6170
- const chatClaudeMd = join4(projDir, "CLAUDE.md");
6171
- if (existsSync3(chatClaudeMd)) {
6456
+ const chatClaudeMd = join5(projDir, "CLAUDE.md");
6457
+ if (existsSync4(chatClaudeMd)) {
6172
6458
  chatArgs.push("--system-prompt-file", chatClaudeMd);
6173
6459
  }
6174
- const envIntPath = join4(projDir, ".env.integrations");
6460
+ const envIntPath = join5(projDir, ".env.integrations");
6175
6461
  const childEnv = { ...process.env };
6176
- if (existsSync3(envIntPath)) {
6462
+ if (existsSync4(envIntPath)) {
6177
6463
  try {
6178
- for (const line of readFileSync3(envIntPath, "utf-8").split("\n")) {
6464
+ for (const line of readFileSync4(envIntPath, "utf-8").split("\n")) {
6179
6465
  if (!line || line.startsWith("#") || !line.includes("=")) continue;
6180
6466
  const eqIdx = line.indexOf("=");
6181
6467
  childEnv[line.slice(0, eqIdx)] = line.slice(eqIdx + 1);
@@ -6518,12 +6804,12 @@ function getBuiltInSkillContent(skillId) {
6518
6804
  if (builtInSkillCache.has(skillId)) return builtInSkillCache.get(skillId);
6519
6805
  try {
6520
6806
  const candidates = [
6521
- join4(process.cwd(), "skills", skillId, "SKILL.md"),
6522
- join4(new URL(".", import.meta.url).pathname, "..", "..", "..", "..", "skills", skillId, "SKILL.md")
6807
+ join5(process.cwd(), "skills", skillId, "SKILL.md"),
6808
+ join5(new URL(".", import.meta.url).pathname, "..", "..", "..", "..", "skills", skillId, "SKILL.md")
6523
6809
  ];
6524
6810
  for (const candidate of candidates) {
6525
- if (existsSync3(candidate)) {
6526
- const content = readFileSync3(candidate, "utf-8");
6811
+ if (existsSync4(candidate)) {
6812
+ const content = readFileSync4(candidate, "utf-8");
6527
6813
  const files = [{ relativePath: "SKILL.md", content }];
6528
6814
  builtInSkillCache.set(skillId, files);
6529
6815
  return files;
@@ -7136,7 +7422,7 @@ async function processClaudePairSessions(agents) {
7136
7422
  killPairSession,
7137
7423
  pairTmuxSession,
7138
7424
  finalizeClaudePairOnboarding
7139
- } = await import("../claude-pair-runtime-KUWBZW33.js");
7425
+ } = await import("../claude-pair-runtime-XCFWTH6T.js");
7140
7426
  for (const pairId of pendingResp.cancelled_pair_ids ?? []) {
7141
7427
  log(`[claude-pair] sweeping orphan tmux session for pair ${pairId.slice(0, 8)}`);
7142
7428
  const killed = await killPairSession(pairTmuxSession(pairId));
@@ -7357,6 +7643,14 @@ function generateArtifacts(agent, refreshData, adapter) {
7357
7643
  };
7358
7644
  }
7359
7645
  }
7646
+ const activeTasksInjectEnabled = process.env["AGT_ACTIVE_TASKS_INJECT"] === "1";
7647
+ const activeTasksFromRefresh = activeTasksInjectEnabled ? refreshData.active_tasks ?? [] : void 0;
7648
+ if (activeTasksInjectEnabled) {
7649
+ const tokens = estimateActiveTasksTokens(activeTasksFromRefresh);
7650
+ log(
7651
+ `[provision/active-tasks] agent=${agent.code_name} count=${activeTasksFromRefresh?.length ?? 0} estimated_tokens=${tokens}`
7652
+ );
7653
+ }
7360
7654
  const provisionInput = {
7361
7655
  agent: refreshData.agent,
7362
7656
  charterFrontmatter,
@@ -7367,6 +7661,7 @@ function generateArtifacts(agent, refreshData, adapter) {
7367
7661
  deploymentTarget: "local_docker",
7368
7662
  gatewayPort: 9e3,
7369
7663
  team: refreshData.team ?? void 0,
7664
+ activeTasks: activeTasksFromRefresh,
7370
7665
  // ENG-5009: org name for the identity-preamble. Only present when
7371
7666
  // /host/refresh ships the field — falls through cleanly for older
7372
7667
  // API payloads that don't carry it.
@@ -7422,8 +7717,8 @@ function parseMemoryFile(raw, fallbackName) {
7422
7717
  };
7423
7718
  }
7424
7719
  async function syncMemories(agent, configDir, log2) {
7425
- const projectDir = join4(configDir, agent.code_name, "project");
7426
- const memoryDir = join4(projectDir, "memory");
7720
+ const projectDir = join5(configDir, agent.code_name, "project");
7721
+ const memoryDir = join5(projectDir, "memory");
7427
7722
  const isFreshSync = pendingFreshMemorySync.has(agent.agent_id);
7428
7723
  if (isFreshSync) {
7429
7724
  log2(`[memory-sync] Fresh-sync requested for '${agent.code_name}' \u2014 pulling DB first`);
@@ -7434,14 +7729,14 @@ async function syncMemories(agent, configDir, log2) {
7434
7729
  }
7435
7730
  pendingFreshMemorySync.delete(agent.agent_id);
7436
7731
  }
7437
- if (existsSync3(memoryDir)) {
7732
+ if (existsSync4(memoryDir)) {
7438
7733
  const prevHashes = memoryFileHashes.get(agent.agent_id) ?? /* @__PURE__ */ new Map();
7439
7734
  const currentHashes = /* @__PURE__ */ new Map();
7440
7735
  const changedMemories = [];
7441
7736
  for (const file of readdirSync2(memoryDir)) {
7442
7737
  if (!file.endsWith(".md")) continue;
7443
7738
  try {
7444
- const raw = readFileSync3(join4(memoryDir, file), "utf-8");
7739
+ const raw = readFileSync4(join5(memoryDir, file), "utf-8");
7445
7740
  const fileHash = createHash2("sha256").update(raw).digest("hex").slice(0, 16);
7446
7741
  currentHashes.set(file, fileHash);
7447
7742
  if (prevHashes.get(file) === fileHash) continue;
@@ -7466,7 +7761,7 @@ async function syncMemories(agent, configDir, log2) {
7466
7761
  } catch (err) {
7467
7762
  for (const mem of changedMemories) {
7468
7763
  for (const [file] of currentHashes) {
7469
- const parsed = parseMemoryFile(readFileSync3(join4(memoryDir, file), "utf-8"), file.replace(/\.md$/, ""));
7764
+ const parsed = parseMemoryFile(readFileSync4(join5(memoryDir, file), "utf-8"), file.replace(/\.md$/, ""));
7470
7765
  if (parsed?.name === mem.name) currentHashes.delete(file);
7471
7766
  }
7472
7767
  }
@@ -7479,7 +7774,7 @@ async function syncMemories(agent, configDir, log2) {
7479
7774
  }
7480
7775
  }
7481
7776
  async function downloadMemories(agent, memoryDir, log2, { force }) {
7482
- const localFiles = existsSync3(memoryDir) ? readdirSync2(memoryDir).filter((f) => f.endsWith(".md")).sort() : [];
7777
+ const localFiles = existsSync4(memoryDir) ? readdirSync2(memoryDir).filter((f) => f.endsWith(".md")).sort() : [];
7483
7778
  const localListHash = createHash2("sha256").update(localFiles.join(",")).digest("hex").slice(0, 16);
7484
7779
  const prevLocalHash = lastLocalFileHash.get(agent.agent_id);
7485
7780
  const prevDownload = lastDownloadHash.get(agent.agent_id);
@@ -7501,7 +7796,7 @@ async function downloadMemories(agent, memoryDir, log2, { force }) {
7501
7796
  const mem = dbMemories.memories[i];
7502
7797
  const rawSlug = mem.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "").slice(0, 60);
7503
7798
  const slug = rawSlug || `memory-${i}`;
7504
- const filePath = join4(memoryDir, `${slug}.md`);
7799
+ const filePath = join5(memoryDir, `${slug}.md`);
7505
7800
  const desired = `---
7506
7801
  name: ${JSON.stringify(mem.name)}
7507
7802
  type: ${mem.type}
@@ -7510,10 +7805,10 @@ description: ${JSON.stringify(mem.content.slice(0, 200))}
7510
7805
 
7511
7806
  ${mem.content}
7512
7807
  `;
7513
- if (existsSync3(filePath)) {
7808
+ if (existsSync4(filePath)) {
7514
7809
  let existing = "";
7515
7810
  try {
7516
- existing = readFileSync3(filePath, "utf-8");
7811
+ existing = readFileSync4(filePath, "utf-8");
7517
7812
  } catch {
7518
7813
  }
7519
7814
  if (existing === desired) continue;
@@ -7537,7 +7832,7 @@ ${mem.content}
7537
7832
  }
7538
7833
  }
7539
7834
  async function cleanupAgentFiles(codeName, agentDir) {
7540
- if (existsSync3(agentDir)) {
7835
+ if (existsSync4(agentDir)) {
7541
7836
  try {
7542
7837
  rmSync2(agentDir, { recursive: true, force: true });
7543
7838
  log(`Removed provision directory for '${codeName}'`);
@@ -7708,19 +8003,24 @@ function startManager(opts) {
7708
8003
  config = opts;
7709
8004
  try {
7710
8005
  const stateFile = getStateFile();
7711
- if (existsSync3(stateFile)) {
7712
- const raw = readFileSync3(stateFile, "utf-8");
8006
+ if (existsSync4(stateFile)) {
8007
+ const raw = readFileSync4(stateFile, "utf-8");
7713
8008
  const parsed = JSON.parse(raw);
7714
8009
  if (Array.isArray(parsed.agents)) {
7715
- state2.agents = parsed.agents;
7716
- log(`[startup] rehydrated ${state2.agents.length} agent state(s) from ${stateFile}`);
8010
+ state3.agents = parsed.agents;
8011
+ log(`[startup] rehydrated ${state3.agents.length} agent state(s) from ${stateFile}`);
8012
+ }
8013
+ if (parsed.circuitBreakerTrips && typeof parsed.circuitBreakerTrips === "object") {
8014
+ restartBreaker.hydrate(parsed.circuitBreakerTrips);
8015
+ const n = Object.keys(parsed.circuitBreakerTrips).length;
8016
+ if (n > 0) log(`[startup] rehydrated ${n} circuit-breaker trip(s) \u2014 agents will remain paused until manually resumed`);
7717
8017
  }
7718
8018
  }
7719
8019
  } catch (err) {
7720
8020
  log(`[startup] state rehydration failed (continuing with empty state): ${err.message}`);
7721
8021
  }
7722
8022
  log(
7723
- `[startup] worker pid=${process.pid} ppid=${process.ppid} node=${process.version} log=${join4(homedir3(), ".augmented", "manager.log")}`
8023
+ `[startup] worker pid=${process.pid} ppid=${process.ppid} node=${process.version} log=${join5(homedir4(), ".augmented", "manager.log")}`
7724
8024
  );
7725
8025
  warnOnConflictingKanbanModes();
7726
8026
  deployMcpAssets();
@@ -7783,14 +8083,14 @@ function restartRunningChannelMcps(basenames) {
7783
8083
  }
7784
8084
  }
7785
8085
  function deployMcpAssets() {
7786
- const targetDir = join4(homedir3(), ".augmented", "_mcp");
8086
+ const targetDir = join5(homedir4(), ".augmented", "_mcp");
7787
8087
  mkdirSync2(targetDir, { recursive: true });
7788
8088
  const moduleDir = dirname(fileURLToPath(import.meta.url));
7789
8089
  let mcpSourceDir = "";
7790
8090
  let dir = moduleDir;
7791
8091
  for (let i = 0; i < 6; i++) {
7792
- const candidate = join4(dir, "dist", "mcp");
7793
- if (existsSync3(join4(candidate, "index.js"))) {
8092
+ const candidate = join5(dir, "dist", "mcp");
8093
+ if (existsSync4(join5(candidate, "index.js"))) {
7794
8094
  mcpSourceDir = candidate;
7795
8095
  break;
7796
8096
  }
@@ -7805,8 +8105,8 @@ function deployMcpAssets() {
7805
8105
  const changedBasenames = [];
7806
8106
  const fileHash = (p) => {
7807
8107
  try {
7808
- if (!existsSync3(p)) return null;
7809
- return createHash2("sha256").update(readFileSync3(p)).digest("hex");
8108
+ if (!existsSync4(p)) return null;
8109
+ return createHash2("sha256").update(readFileSync4(p)).digest("hex");
7810
8110
  } catch {
7811
8111
  return null;
7812
8112
  }
@@ -7817,9 +8117,9 @@ function deployMcpAssets() {
7817
8117
  "telegram-channel.js"
7818
8118
  ]);
7819
8119
  for (const file of ["index.js", "slack-channel.js", "direct-chat-channel.js", "telegram-channel.js"]) {
7820
- const src = join4(mcpSourceDir, file);
7821
- const dst = join4(targetDir, file);
7822
- if (!existsSync3(src)) continue;
8120
+ const src = join5(mcpSourceDir, file);
8121
+ const dst = join5(targetDir, file);
8122
+ if (!existsSync4(src)) continue;
7823
8123
  const before = fileHash(dst);
7824
8124
  try {
7825
8125
  copyFileSync(src, dst);
@@ -7836,16 +8136,16 @@ function deployMcpAssets() {
7836
8136
  log(`[manager] Bundle(s) updated: ${changedBasenames.join(", ")} \u2014 signalling running instances to restart`);
7837
8137
  restartRunningChannelMcps(changedBasenames);
7838
8138
  }
7839
- const localMcpPath = join4(targetDir, "index.js");
8139
+ const localMcpPath = join5(targetDir, "index.js");
7840
8140
  try {
7841
- const agentsDir = join4(homedir3(), ".augmented", "agents");
7842
- if (existsSync3(agentsDir)) {
8141
+ const agentsDir = join5(homedir4(), ".augmented", "agents");
8142
+ if (existsSync4(agentsDir)) {
7843
8143
  for (const entry of readdirSync2(agentsDir, { withFileTypes: true })) {
7844
8144
  if (!entry.isDirectory()) continue;
7845
8145
  for (const subdir of ["provision", "project"]) {
7846
- const mcpJsonPath = join4(agentsDir, entry.name, subdir, ".mcp.json");
8146
+ const mcpJsonPath = join5(agentsDir, entry.name, subdir, ".mcp.json");
7847
8147
  try {
7848
- const raw = readFileSync3(mcpJsonPath, "utf-8");
8148
+ const raw = readFileSync4(mcpJsonPath, "utf-8");
7849
8149
  if (!raw.includes("@integrity-labs/augmented-mcp")) continue;
7850
8150
  const mcpConfig = JSON.parse(raw);
7851
8151
  const augServer = mcpConfig.mcpServers?.["augmented"];