@integrity-labs/agt-cli 0.27.8 → 0.27.9-test.12

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.
Files changed (28) hide show
  1. package/dist/assets/impersonate-statusline.sh +7 -2
  2. package/dist/bin/agt.js +946 -129
  3. package/dist/bin/agt.js.map +1 -1
  4. package/dist/{chunk-YSBGIXJG.js → chunk-I2OTQJH3.js} +361 -26
  5. package/dist/chunk-I2OTQJH3.js.map +1 -0
  6. package/dist/{chunk-GN4XPQWJ.js → chunk-MQJ5DMPT.js} +535 -94
  7. package/dist/chunk-MQJ5DMPT.js.map +1 -0
  8. package/dist/{chunk-IB655E5U.js → chunk-SXBZDUKN.js} +1086 -285
  9. package/dist/chunk-SXBZDUKN.js.map +1 -0
  10. package/dist/{claude-pair-runtime-ZBQKBBMT.js → claude-pair-runtime-F6USL3EW.js} +2 -2
  11. package/dist/lib/manager-worker.js +2071 -517
  12. package/dist/lib/manager-worker.js.map +1 -1
  13. package/dist/mcp/direct-chat-channel.js +49 -455
  14. package/dist/mcp/index.js +232 -137
  15. package/dist/mcp/slack-channel.js +1379 -856
  16. package/dist/mcp/teams-channel.js +16018 -0
  17. package/dist/mcp/telegram-channel.js +1264 -618
  18. package/dist/{persistent-session-ICYFLUAM.js → persistent-session-OL5EDYB4.js} +5 -3
  19. package/dist/responsiveness-probe-B6LJJRUD.js +74 -0
  20. package/dist/responsiveness-probe-B6LJJRUD.js.map +1 -0
  21. package/package.json +2 -2
  22. package/dist/chunk-GN4XPQWJ.js.map +0 -1
  23. package/dist/chunk-IB655E5U.js.map +0 -1
  24. package/dist/chunk-YSBGIXJG.js.map +0 -1
  25. package/dist/responsiveness-probe-WZNQ2762.js +0 -33
  26. package/dist/responsiveness-probe-WZNQ2762.js.map +0 -1
  27. /package/dist/{claude-pair-runtime-ZBQKBBMT.js.map → claude-pair-runtime-F6USL3EW.js.map} +0 -0
  28. /package/dist/{persistent-session-ICYFLUAM.js.map → persistent-session-OL5EDYB4.js.map} +0 -0
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  ApiError,
3
+ CHANNEL_SECRET_ENV_KEYS,
3
4
  INTEGRATIONS_SECTION_END,
4
5
  INTEGRATIONS_SECTION_START,
5
6
  SUPERVISOR_RESTART_EXIT_CODE,
@@ -14,8 +15,9 @@ import {
14
15
  provisionIsolationHook,
15
16
  provisionOrientHook,
16
17
  provisionStopHook,
17
- requireHost
18
- } from "../chunk-IB655E5U.js";
18
+ requireHost,
19
+ safeWriteJsonAtomic
20
+ } from "../chunk-SXBZDUKN.js";
19
21
  import {
20
22
  getProjectDir as getProjectDir2,
21
23
  getReadyTasks,
@@ -25,6 +27,9 @@ import {
25
27
  } from "../chunk-HR5T2RQF.js";
26
28
  import {
27
29
  buildAllowedTools,
30
+ checkChannelInputs,
31
+ creditWatchdogGiveUpCount,
32
+ formatMissingVar,
28
33
  getLastFailureContext,
29
34
  getProjectDir,
30
35
  getSessionState,
@@ -34,8 +39,11 @@ import {
34
39
  isAgentPromptReady,
35
40
  isSessionHealthy,
36
41
  isStaleForToday,
42
+ paneLogPath,
43
+ parseEnvIntegrations,
37
44
  peekCurrentSession,
38
45
  prepareForRespawn,
46
+ probeMcpEnvSubstitution,
39
47
  readPaneLogTail,
40
48
  resetRestartCount,
41
49
  resolveClaudeBinary,
@@ -45,10 +53,12 @@ import {
45
53
  startPersistentSession,
46
54
  stopAllSessionsAndWait,
47
55
  stopPersistentSession,
56
+ takeWatchdogGiveUpCount,
48
57
  takeZombieDetection
49
- } from "../chunk-GN4XPQWJ.js";
58
+ } from "../chunk-MQJ5DMPT.js";
50
59
  import {
51
60
  KANBAN_CHECK_COMMAND,
61
+ SUPPRESS_SENTINEL,
52
62
  appendDmFooter,
53
63
  attributeTranscriptUsageByRun,
54
64
  classifyActor,
@@ -69,7 +79,7 @@ import {
69
79
  resolveConnectivityProbe,
70
80
  resolveDmTarget,
71
81
  wrapScheduledTaskPrompt
72
- } from "../chunk-YSBGIXJG.js";
82
+ } from "../chunk-I2OTQJH3.js";
73
83
  import {
74
84
  parsePsRows,
75
85
  reapOrphanChannelMcps
@@ -77,10 +87,10 @@ import {
77
87
 
78
88
  // src/lib/manager-worker.ts
79
89
  import { createHash as createHash3 } from "crypto";
80
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, appendFileSync, mkdirSync as mkdirSync2, chmodSync, existsSync as existsSync4, rmSync as rmSync2, readdirSync as readdirSync3, statSync as statSync2, unlinkSync, copyFileSync } from "fs";
90
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync4, appendFileSync, mkdirSync as mkdirSync4, chmodSync, existsSync as existsSync5, rmSync as rmSync2, readdirSync as readdirSync4, statSync as statSync3, unlinkSync, copyFileSync } from "fs";
81
91
  import https from "https";
82
92
  import { execFileSync as syncExecFile } from "child_process";
83
- import { join as join6, dirname } from "path";
93
+ import { join as join8, dirname as dirname3 } from "path";
84
94
  import { homedir as homedir4 } from "os";
85
95
  import { fileURLToPath } from "url";
86
96
 
@@ -106,6 +116,12 @@ function managedMcpStructureHash(entries) {
106
116
  const basis = entries.slice().sort((a, b) => a.serverId.localeCompare(b.serverId)).map((e) => `${e.serverId}|${Object.keys(e.headers ?? {}).sort().join(",")}`).join("\n");
107
117
  return createHash("sha256").update(basis).digest("hex").slice(0, 16);
108
118
  }
119
+ function channelSecretValueHash(envEntries, channelSecretKeys) {
120
+ if (!envEntries) return null;
121
+ const basis = channelSecretKeys.slice().sort().filter((k) => Object.prototype.hasOwnProperty.call(envEntries, k)).map((k) => `${k}=${envEntries[k]}`).join("\n");
122
+ if (basis.length === 0) return null;
123
+ return createHash("sha256").update(basis).digest("hex").slice(0, 16);
124
+ }
109
125
 
110
126
  // src/lib/integration-hash.ts
111
127
  import { createHash as createHash2 } from "crypto";
@@ -412,7 +428,13 @@ function reapMissingMcpSessions(args) {
412
428
  graceMs = DEFAULT_COLD_START_GRACE_MS,
413
429
  rotationGraceMs = DEFAULT_ROTATION_GRACE_MS,
414
430
  onGiveUp,
415
- onRestart
431
+ onRestart,
432
+ // ENG-5932: defaults make quarantine a strict no-op (classify everything
433
+ // essential, mode off) so legacy callers/tests are unaffected.
434
+ classifyKey = () => "essential",
435
+ quarantineDwellMs = 0,
436
+ quarantineMode = "off",
437
+ onQuarantine
416
438
  } = args;
417
439
  const now = args.now ?? Date.now;
418
440
  const runPs = args.runPs ?? (() => execFileSync2("ps", ["-eo", "pid,ppid,args"], { encoding: "utf-8", timeout: 5e3 }));
@@ -460,12 +482,14 @@ function reapMissingMcpSessions(args) {
460
482
  if (!missingRaw.includes(key)) liveKeys.add(key);
461
483
  }
462
484
  for (const key of liveKeys) {
463
- const state5 = presenceReaperState.get(stateKey(codeName, key));
464
- if (state5) {
465
- state5.attempts = 0;
466
- state5.lastSeenLiveAt = now();
467
- state5.lastAttemptedSessionStartedAt = null;
468
- state5.gaveUpLogged = false;
485
+ const state6 = presenceReaperState.get(stateKey(codeName, key));
486
+ if (state6) {
487
+ state6.attempts = 0;
488
+ state6.lastSeenLiveAt = now();
489
+ state6.lastAttemptedSessionStartedAt = null;
490
+ state6.gaveUpLogged = false;
491
+ state6.firstMissingAt = null;
492
+ state6.quarantineLogged = false;
469
493
  }
470
494
  }
471
495
  if (missing.length === 0) {
@@ -476,28 +500,34 @@ function reapMissingMcpSessions(args) {
476
500
  rotationGraced: rotationGraced.length > 0 ? rotationGraced : void 0
477
501
  };
478
502
  }
503
+ const nowMs = now();
479
504
  const givenUp = [];
480
505
  const active = [];
506
+ const quarantined = [];
507
+ const wouldQuarantine = [];
481
508
  for (const key of missing) {
482
509
  const sk = stateKey(codeName, key);
483
- const state5 = presenceReaperState.get(sk) ?? {
510
+ const state6 = presenceReaperState.get(sk) ?? {
484
511
  attempts: 0,
485
512
  lastSeenLiveAt: null,
486
513
  lastAttemptedSessionStartedAt: null,
487
- gaveUpLogged: false
514
+ gaveUpLogged: false,
515
+ firstMissingAt: null,
516
+ quarantineLogged: false
488
517
  };
489
- if (state5.lastAttemptedSessionStartedAt !== sessionStartedAt) {
490
- state5.attempts += 1;
491
- state5.lastAttemptedSessionStartedAt = sessionStartedAt;
518
+ if (state6.firstMissingAt === null) state6.firstMissingAt = nowMs;
519
+ if (state6.lastAttemptedSessionStartedAt !== sessionStartedAt) {
520
+ state6.attempts += 1;
521
+ state6.lastAttemptedSessionStartedAt = sessionStartedAt;
492
522
  }
493
- presenceReaperState.set(sk, state5);
494
- if (state5.attempts > MAX_PRESENCE_RESTART_ATTEMPTS) {
523
+ presenceReaperState.set(sk, state6);
524
+ if (state6.attempts > MAX_PRESENCE_RESTART_ATTEMPTS) {
495
525
  givenUp.push(key);
496
- if (!state5.gaveUpLogged) {
526
+ if (!state6.gaveUpLogged) {
497
527
  log2(
498
528
  `[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)`
499
529
  );
500
- state5.gaveUpLogged = true;
530
+ state6.gaveUpLogged = true;
501
531
  if (onGiveUp) {
502
532
  try {
503
533
  onGiveUp(codeName, key);
@@ -508,6 +538,32 @@ function reapMissingMcpSessions(args) {
508
538
  }
509
539
  }
510
540
  }
541
+ if (quarantineMode !== "off" && !state6.quarantineLogged && classifyKey(key) === "optional") {
542
+ const dwellElapsed = quarantineDwellMs <= 0 || state6.firstMissingAt !== null && nowMs - state6.firstMissingAt >= quarantineDwellMs;
543
+ if (dwellElapsed) {
544
+ state6.quarantineLogged = true;
545
+ if (quarantineMode === "enforce") {
546
+ quarantined.push(key);
547
+ log2(
548
+ `[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)`
549
+ );
550
+ if (onQuarantine) {
551
+ try {
552
+ onQuarantine(codeName, key);
553
+ } catch (err) {
554
+ log2(
555
+ `[mcp-presence-reaper] onQuarantine callback threw for '${codeName}:${key}' (suppressed; reaper continues): ${err.message}`
556
+ );
557
+ }
558
+ }
559
+ } else {
560
+ wouldQuarantine.push(key);
561
+ log2(
562
+ `[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)`
563
+ );
564
+ }
565
+ }
566
+ }
511
567
  } else {
512
568
  active.push(key);
513
569
  }
@@ -518,7 +574,9 @@ function reapMissingMcpSessions(args) {
518
574
  restarted: false,
519
575
  reason: "all-keys-over-cap",
520
576
  givenUp,
521
- rotationGraced: rotationGraced.length > 0 ? rotationGraced : void 0
577
+ rotationGraced: rotationGraced.length > 0 ? rotationGraced : void 0,
578
+ quarantined: quarantined.length > 0 ? quarantined : void 0,
579
+ wouldQuarantine: wouldQuarantine.length > 0 ? wouldQuarantine : void 0
522
580
  };
523
581
  }
524
582
  const givenUpSuffix = givenUp.length > 0 ? ` (skipping over-cap: [${givenUp.join(", ")}])` : "";
@@ -546,7 +604,9 @@ function reapMissingMcpSessions(args) {
546
604
  missing,
547
605
  restarted: true,
548
606
  givenUp: givenUp.length > 0 ? givenUp : void 0,
549
- rotationGraced: rotationGraced.length > 0 ? rotationGraced : void 0
607
+ rotationGraced: rotationGraced.length > 0 ? rotationGraced : void 0,
608
+ quarantined: quarantined.length > 0 ? quarantined : void 0,
609
+ wouldQuarantine: wouldQuarantine.length > 0 ? wouldQuarantine : void 0
550
610
  };
551
611
  }
552
612
 
@@ -578,6 +638,228 @@ function decideChannelRestart(input) {
578
638
  return { restart: true, added, removed };
579
639
  }
580
640
 
641
+ // src/lib/channel-launch-flags.ts
642
+ var DEV_CHANNEL_SERVER_IDS = ["slack", "telegram", "msteams"];
643
+ function resolveChannelLaunchFlags(channelConfigs, quarantinedChannels) {
644
+ const devChannels = [];
645
+ const pluginChannels = [];
646
+ if (!channelConfigs) return { devChannels, pluginChannels };
647
+ const isChannelEnabled = (id) => {
648
+ if (quarantinedChannels.has(id)) return false;
649
+ const entry = channelConfigs[id];
650
+ return !!entry?.config && (entry.status === "active" || entry.status === "pending");
651
+ };
652
+ for (const id of DEV_CHANNEL_SERVER_IDS) {
653
+ if (isChannelEnabled(id)) devChannels.push(`server:${id}`);
654
+ }
655
+ if (isChannelEnabled("discord")) {
656
+ pluginChannels.push("plugin:discord@claude-plugins-official");
657
+ }
658
+ return { devChannels, pluginChannels };
659
+ }
660
+
661
+ // src/lib/sender-policy-restart-decision.ts
662
+ function decideSenderPolicyRestart(input) {
663
+ const {
664
+ previousHash,
665
+ currentHash,
666
+ sessionMode,
667
+ framework,
668
+ sessionHealthy,
669
+ channelSetRestartAlreadyScheduled
670
+ } = input;
671
+ if (previousHash === void 0) {
672
+ return { restart: false, firstPoll: true, changed: false };
673
+ }
674
+ if (previousHash === currentHash) {
675
+ return { restart: false, firstPoll: false, changed: false };
676
+ }
677
+ if (sessionMode !== "persistent") {
678
+ return { restart: false, firstPoll: false, changed: true };
679
+ }
680
+ if (framework !== "claude-code") {
681
+ return { restart: false, firstPoll: false, changed: true };
682
+ }
683
+ if (!sessionHealthy) {
684
+ return { restart: false, firstPoll: false, changed: true };
685
+ }
686
+ if (channelSetRestartAlreadyScheduled) {
687
+ return { restart: false, firstPoll: false, changed: true };
688
+ }
689
+ return { restart: true, firstPoll: false, changed: true };
690
+ }
691
+
692
+ // src/lib/msteams-behaviour-restart.ts
693
+ function extractMsTeamsBehaviourSubset(config2) {
694
+ const adaptiveCardsEnabled = config2?.["adaptive_cards_enabled"] === true;
695
+ return {
696
+ thread_auto_follow: config2?.["thread_auto_follow"] ?? "off",
697
+ channel_response_mode: config2?.["channel_response_mode"] ?? "mention_only",
698
+ adaptive_cards_enabled: adaptiveCardsEnabled,
699
+ // The adapter only emits MSTEAMS_ADAPTIVE_CARDS_ASK_USER_ENABLED when the
700
+ // parent flag is also on — mirror that so toggling ask_user while
701
+ // Adaptive Cards is off (a no-op for the env) doesn't restart.
702
+ adaptive_cards_ask_user_enabled: adaptiveCardsEnabled && config2?.["adaptive_cards_ask_user_enabled"] === true
703
+ };
704
+ }
705
+
706
+ // src/lib/slack-behaviour-restart.ts
707
+ function extractSlackBehaviourSubset(config2) {
708
+ const rawAllowed = config2?.["allowed_users"];
709
+ return {
710
+ thread_auto_follow: config2?.["thread_auto_follow"] ?? "off",
711
+ channel_response_mode: config2?.["channel_response_mode"] ?? "mention_only",
712
+ // Mirror the adapter's defensive filter exactly — a malformed entry the
713
+ // adapter would drop must not count as an env change.
714
+ allowed_users: Array.isArray(rawAllowed) ? rawAllowed.filter((v) => typeof v === "string" && v.trim().length > 0).map((v) => v.trim()) : []
715
+ };
716
+ }
717
+
718
+ // src/lib/channel-quarantine.ts
719
+ import { readFileSync } from "fs";
720
+ import { join } from "path";
721
+
722
+ // src/lib/atomic-write.ts
723
+ import { closeSync, fsyncSync, openSync, writeSync, renameSync, mkdirSync } from "fs";
724
+ import { dirname } from "path";
725
+ function atomicWriteFileSync(path, data) {
726
+ const dirPath = dirname(path);
727
+ const tmpPath = `${path}.tmp.${process.pid}.${Math.random().toString(36).slice(2, 8)}`;
728
+ try {
729
+ mkdirSync(dirPath, { recursive: true });
730
+ } catch {
731
+ }
732
+ const fd = openSync(tmpPath, "w", 420);
733
+ try {
734
+ writeSync(fd, data);
735
+ try {
736
+ fsyncSync(fd);
737
+ } catch {
738
+ }
739
+ } finally {
740
+ closeSync(fd);
741
+ }
742
+ renameSync(tmpPath, path);
743
+ try {
744
+ const dirFd = openSync(dirPath, "r");
745
+ try {
746
+ fsyncSync(dirFd);
747
+ } finally {
748
+ closeSync(dirFd);
749
+ }
750
+ } catch {
751
+ }
752
+ }
753
+
754
+ // src/lib/channel-quarantine.ts
755
+ var ESSENTIAL_CHANNEL_KEYS = /* @__PURE__ */ new Set(["direct-chat", "augmented"]);
756
+ var OPTIONAL_CHANNEL_KEYS = /* @__PURE__ */ new Set([
757
+ "telegram",
758
+ "slack",
759
+ "msteams",
760
+ "discord",
761
+ "whatsapp",
762
+ "signal",
763
+ "imessage",
764
+ "matrix",
765
+ "irc",
766
+ "nostr"
767
+ ]);
768
+ function readDeclaredCriticality(entry) {
769
+ if (!entry || typeof entry !== "object") return void 0;
770
+ const meta = entry._augmented;
771
+ if (!meta || typeof meta !== "object") return void 0;
772
+ const value = meta.criticality;
773
+ return value === "essential" || value === "optional" ? value : void 0;
774
+ }
775
+ function classifyChannelCriticality(serverKey, entry) {
776
+ if (ESSENTIAL_CHANNEL_KEYS.has(serverKey)) return "essential";
777
+ const declared = readDeclaredCriticality(entry);
778
+ if (declared) return declared;
779
+ if (OPTIONAL_CHANNEL_KEYS.has(serverKey)) return "optional";
780
+ return "essential";
781
+ }
782
+ function defaultQuarantinePath(configDir) {
783
+ return join(configDir, "channel-quarantine.json");
784
+ }
785
+ var ChannelQuarantineStore = class {
786
+ path;
787
+ cache = null;
788
+ constructor(path) {
789
+ this.path = path;
790
+ }
791
+ load() {
792
+ if (this.cache) return this.cache;
793
+ try {
794
+ const raw = readFileSync(this.path, "utf-8");
795
+ const parsed = JSON.parse(raw);
796
+ this.cache = isQuarantineFile(parsed) ? parsed : {};
797
+ } catch {
798
+ this.cache = {};
799
+ }
800
+ return this.cache;
801
+ }
802
+ persist() {
803
+ atomicWriteFileSync(this.path, JSON.stringify(this.cache ?? {}, null, 2));
804
+ }
805
+ /** Is `serverKey` currently quarantined for `codeName`? */
806
+ isQuarantined(codeName, serverKey) {
807
+ return this.load()[codeName]?.[serverKey] !== void 0;
808
+ }
809
+ /** The set of quarantined server keys for `codeName` (empty Set if none). */
810
+ getQuarantinedKeys(codeName) {
811
+ const forAgent = this.load()[codeName];
812
+ return new Set(forAgent ? Object.keys(forAgent) : []);
813
+ }
814
+ /**
815
+ * Mark `serverKey` quarantined for `codeName`. Idempotent — re-quarantining
816
+ * an already-quarantined key preserves the original `quarantinedAt` (so the
817
+ * "how long has this been quarantined" escalation clock in Slice 3 isn't
818
+ * reset by provisioning churn). Returns true iff this call newly quarantined
819
+ * the key (false if it was already quarantined).
820
+ */
821
+ quarantine(codeName, serverKey, reason, now) {
822
+ const file = this.load();
823
+ const forAgent = file[codeName] ??= {};
824
+ if (forAgent[serverKey]) return false;
825
+ forAgent[serverKey] = { quarantinedAt: now, reason };
826
+ this.persist();
827
+ return true;
828
+ }
829
+ /**
830
+ * Clear the quarantine for `serverKey` (explicit re-enable). Returns true iff
831
+ * a marker was actually removed. NOTE: re-enable is an EXPLICIT verb (Slice 2
832
+ * exposes it via the API/console) — provisioning's idempotent `.mcp.json`
833
+ * rewrites must NEVER call this, or a quarantined channel would re-arm into a
834
+ * fresh flap laundered through the config loop.
835
+ */
836
+ clearQuarantine(codeName, serverKey) {
837
+ const file = this.load();
838
+ const forAgent = file[codeName];
839
+ if (!forAgent || !forAgent[serverKey]) return false;
840
+ delete forAgent[serverKey];
841
+ if (Object.keys(forAgent).length === 0) delete file[codeName];
842
+ this.persist();
843
+ return true;
844
+ }
845
+ /** Test seam: drop the in-memory cache so the next read re-hits disk. */
846
+ __invalidateCacheForTests() {
847
+ this.cache = null;
848
+ }
849
+ };
850
+ function isQuarantineFile(value) {
851
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
852
+ for (const forAgent of Object.values(value)) {
853
+ if (!forAgent || typeof forAgent !== "object" || Array.isArray(forAgent)) return false;
854
+ for (const entry of Object.values(forAgent)) {
855
+ if (!entry || typeof entry !== "object") return false;
856
+ const e = entry;
857
+ if (typeof e.quarantinedAt !== "number" || typeof e.reason !== "string") return false;
858
+ }
859
+ }
860
+ return true;
861
+ }
862
+
581
863
  // src/lib/restart-breaker.ts
582
864
  function reaperRestartBreakerReason(activeKeys) {
583
865
  return activeKeys.length >= 2 ? "mcp-presence-reaper" : void 0;
@@ -910,6 +1192,300 @@ function runCliProbe(binary, args, opts = {}) {
910
1192
  });
911
1193
  }
912
1194
 
1195
+ // src/lib/self-update-coalesce.ts
1196
+ import { readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
1197
+ import { dirname as dirname2 } from "path";
1198
+ var DEFAULT_SELF_UPDATE_COALESCE_MS = 30 * 60 * 1e3;
1199
+ function resolveCoalesceWindowMs(env = process.env) {
1200
+ const raw = env.AGT_SELF_UPDATE_COALESCE_MS;
1201
+ if (raw == null || raw === "") return DEFAULT_SELF_UPDATE_COALESCE_MS;
1202
+ const parsed = Number(raw);
1203
+ if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_SELF_UPDATE_COALESCE_MS;
1204
+ return Math.trunc(parsed);
1205
+ }
1206
+ function readLastSelfUpdateAppliedMs(markerPath, now = Date.now(), read = (p) => readFileSync2(p, "utf-8")) {
1207
+ let raw;
1208
+ try {
1209
+ raw = read(markerPath);
1210
+ } catch {
1211
+ return null;
1212
+ }
1213
+ const v = parseInt(raw.trim(), 10);
1214
+ if (!Number.isFinite(v) || v <= 0) return null;
1215
+ if (v > now) return null;
1216
+ return v;
1217
+ }
1218
+ function stampLastSelfUpdateApplied(markerPath, now = Date.now(), write = (p, v) => {
1219
+ mkdirSync2(dirname2(p), { recursive: true });
1220
+ writeFileSync(p, v);
1221
+ }) {
1222
+ try {
1223
+ write(markerPath, String(now));
1224
+ } catch {
1225
+ }
1226
+ }
1227
+ function decideSelfUpdateCoalesce(inputs) {
1228
+ const { windowMs, lastAppliedMs, now } = inputs;
1229
+ if (windowMs <= 0) return { proceed: true, remainingMs: 0, lastAppliedMs: null };
1230
+ if (lastAppliedMs == null) return { proceed: true, remainingMs: 0, lastAppliedMs: null };
1231
+ const elapsed = now - lastAppliedMs;
1232
+ if (elapsed < 0) return { proceed: true, remainingMs: 0, lastAppliedMs: null };
1233
+ if (elapsed >= windowMs) return { proceed: true, remainingMs: 0, lastAppliedMs: null };
1234
+ return { proceed: false, remainingMs: windowMs - elapsed, lastAppliedMs };
1235
+ }
1236
+ function formatCoalesceDeferLogLine(opts) {
1237
+ const remainingSec = Math.ceil(opts.remainingMs / 1e3);
1238
+ const windowMin = Math.round(opts.windowMs / 6e4);
1239
+ 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.`;
1240
+ }
1241
+
1242
+ // src/lib/maintenance-window.ts
1243
+ var DEFAULT_WINDOW_START = "01:00";
1244
+ var DEFAULT_WINDOW_END = "02:00";
1245
+ function isMaintenanceWindowDisabled(env = process.env) {
1246
+ return env["AGT_MAINTENANCE_WINDOW_DISABLE"] === "1";
1247
+ }
1248
+ var HHMM_RE = /^([01]\d|2[0-3]):([0-5]\d)$/;
1249
+ function normalizeHhmm(value) {
1250
+ if (typeof value !== "string") return null;
1251
+ const trimmed = value.trim();
1252
+ return HHMM_RE.test(trimmed) ? trimmed : null;
1253
+ }
1254
+ function resolveWindow(input) {
1255
+ const tzRaw = typeof input.timezone === "string" ? input.timezone.trim() : "";
1256
+ return {
1257
+ start: normalizeHhmm(input.start) ?? DEFAULT_WINDOW_START,
1258
+ end: normalizeHhmm(input.end) ?? DEFAULT_WINDOW_END,
1259
+ // Blank/whitespace → UTC. An explicit 'UTC' is also UTC. (Unlike
1260
+ // getTeamTimezone we keep 'UTC' here because this is the *evaluation* tz,
1261
+ // not an inherit-or-not decision.)
1262
+ timezone: tzRaw.length > 0 ? tzRaw : "UTC"
1263
+ };
1264
+ }
1265
+ function hhmmToMinutes(hhmm) {
1266
+ const [h, m] = hhmm.split(":");
1267
+ return Number(h) * 60 + Number(m);
1268
+ }
1269
+ function localHhmm(now, timezone) {
1270
+ try {
1271
+ const parts = new Intl.DateTimeFormat("en-GB", {
1272
+ timeZone: timezone,
1273
+ hour: "2-digit",
1274
+ minute: "2-digit",
1275
+ hour12: false
1276
+ }).formatToParts(now);
1277
+ const hh = parts.find((p) => p.type === "hour")?.value ?? "00";
1278
+ const mm = parts.find((p) => p.type === "minute")?.value ?? "00";
1279
+ const h = hh === "24" ? "00" : hh;
1280
+ return `${h}:${mm}`;
1281
+ } catch {
1282
+ const h = String(now.getUTCHours()).padStart(2, "0");
1283
+ const m = String(now.getUTCMinutes()).padStart(2, "0");
1284
+ return `${h}:${m}`;
1285
+ }
1286
+ }
1287
+ function isWithinMaintenanceWindow(input, now) {
1288
+ const w = resolveWindow(input);
1289
+ const start = hhmmToMinutes(w.start);
1290
+ const end = hhmmToMinutes(w.end);
1291
+ if (start === end) return false;
1292
+ const cur = hhmmToMinutes(localHhmm(now, w.timezone));
1293
+ if (start < end) return cur >= start && cur < end;
1294
+ return cur >= start || cur < end;
1295
+ }
1296
+ function minutesUntilWindowOpen(input, now) {
1297
+ if (isWithinMaintenanceWindow(input, now)) return 0;
1298
+ const w = resolveWindow(input);
1299
+ const start = hhmmToMinutes(w.start);
1300
+ const cur = hhmmToMinutes(localHhmm(now, w.timezone));
1301
+ return cur <= start ? start - cur : 1440 - cur + start;
1302
+ }
1303
+ function formatWindowDeferLogLine(input, now) {
1304
+ const w = resolveWindow(input);
1305
+ const mins = minutesUntilWindowOpen(input, now);
1306
+ return `next maintenance window ${w.start} ${w.timezone} (~${mins} min)`;
1307
+ }
1308
+
1309
+ // src/lib/update-window-gate.ts
1310
+ function decideMaintenanceWindowGate(opts) {
1311
+ if (isMaintenanceWindowDisabled(opts.env)) return "proceed";
1312
+ if (!opts.window) return "proceed";
1313
+ return isWithinMaintenanceWindow(opts.window, opts.now) ? "proceed" : "defer";
1314
+ }
1315
+ function isUrgentUpgrade(opts) {
1316
+ if (!opts.urgentTarget) return false;
1317
+ if (opts.installed === "dev") return false;
1318
+ return opts.isNewer(opts.installed, opts.urgentTarget);
1319
+ }
1320
+ var RESTART_IDLE_THRESHOLD_SECONDS = 120;
1321
+ var RESTART_INBOUND_QUIET_SECONDS = 300;
1322
+ var GATEABLE_RESTART_REASONS = /* @__PURE__ */ new Set([
1323
+ "model-change",
1324
+ "channel-set-change",
1325
+ "sender-policy-change",
1326
+ "channel-behaviour-change",
1327
+ "hot-reload-mcp",
1328
+ "mcp-presence-reaper"
1329
+ ]);
1330
+ function isGateableRestartReason(reason) {
1331
+ return reason != null && GATEABLE_RESTART_REASONS.has(reason);
1332
+ }
1333
+ function decideRestartGate(opts) {
1334
+ if (isMaintenanceWindowDisabled(opts.env)) return "proceed";
1335
+ if (opts.window && !isWithinMaintenanceWindow(opts.window, opts.now)) {
1336
+ return "defer-window";
1337
+ }
1338
+ const paneThreshold = opts.idleThresholdSeconds ?? RESTART_IDLE_THRESHOLD_SECONDS;
1339
+ const inboundThreshold = opts.inboundQuietSeconds ?? RESTART_INBOUND_QUIET_SECONDS;
1340
+ const paneBusy = opts.paneLogAgeSeconds !== null && opts.paneLogAgeSeconds < paneThreshold;
1341
+ const inboundBusy = opts.inboundAgeSeconds !== null && opts.inboundAgeSeconds < inboundThreshold;
1342
+ if (paneBusy || inboundBusy) return "defer-idle";
1343
+ return "proceed";
1344
+ }
1345
+
1346
+ // src/lib/claude-pid-tracker.ts
1347
+ import { existsSync, readFileSync as readFileSync3 } from "fs";
1348
+ function readPidFile(path) {
1349
+ if (!existsSync(path)) return { version: 1, spawns: [] };
1350
+ try {
1351
+ const raw = JSON.parse(readFileSync3(path, "utf-8"));
1352
+ if (raw.version !== 1 || !Array.isArray(raw.spawns)) return { version: 1, spawns: [] };
1353
+ const spawns = raw.spawns.filter(
1354
+ (s) => !!s && typeof s.pid === "number" && Number.isFinite(s.pid) && s.pid > 0
1355
+ );
1356
+ return { version: 1, spawns };
1357
+ } catch {
1358
+ return { version: 1, spawns: [] };
1359
+ }
1360
+ }
1361
+ function writePidFile(path, file) {
1362
+ atomicWriteFileSync(path, JSON.stringify(file, null, 2));
1363
+ }
1364
+ function addSpawn(path, record) {
1365
+ const file = readPidFile(path);
1366
+ const filtered = file.spawns.filter((s) => s.pid !== record.pid);
1367
+ writePidFile(path, { version: 1, spawns: [...filtered, record] });
1368
+ }
1369
+ function removeSpawn(path, pid) {
1370
+ const file = readPidFile(path);
1371
+ const next = file.spawns.filter((s) => s.pid !== pid);
1372
+ if (next.length !== file.spawns.length) writePidFile(path, { version: 1, spawns: next });
1373
+ }
1374
+ function decideReaperActions(file, isAlive, looksLikeClaude = () => true) {
1375
+ const toKill = [];
1376
+ const alreadyDead = [];
1377
+ const pidReusedSkipped = [];
1378
+ for (const s of file.spawns) {
1379
+ if (!isAlive(s.pid)) {
1380
+ alreadyDead.push(s);
1381
+ continue;
1382
+ }
1383
+ if (!looksLikeClaude(s.pid)) {
1384
+ pidReusedSkipped.push(s);
1385
+ continue;
1386
+ }
1387
+ toKill.push(s);
1388
+ }
1389
+ const toResetKanban = toKill.filter(
1390
+ (s) => s.kind === "kanban-work" && typeof s.kanban_card_id === "string" && s.kanban_card_id.length > 0
1391
+ );
1392
+ return { toKill, alreadyDead, pidReusedSkipped, toResetKanban };
1393
+ }
1394
+ function formatDrainShutdownLine(opts) {
1395
+ const killed = opts.killedPids && opts.killedPids.length > 0 ? ` killed_pids=${opts.killedPids.join(",")}` : "";
1396
+ const note = opts.note ? ` note="${opts.note.replace(/"/g, '\\"')}"` : "";
1397
+ return `[drain] step=${opts.step} elapsed_ms=${opts.elapsedMs}${killed}${note}`;
1398
+ }
1399
+ function formatReaperBootLine(opts) {
1400
+ return `[drain] step=boot-reaper total=${opts.totalRecorded} killed=${opts.killed} already_dead=${opts.alreadyDead} pid_reused_skipped=${opts.pidReusedSkipped} kanban_reset=${opts.kanbanReset}`;
1401
+ }
1402
+
1403
+ // src/lib/direct-chat-spawn-gate.ts
1404
+ var DEFAULT_DIRECT_CHAT_HOST_CONCURRENCY = 2;
1405
+ var DEFAULT_DIRECT_CHAT_PER_AGENT_CONCURRENCY = 1;
1406
+ var DEFAULT_DIRECT_CHAT_MAX_AGE_MS = 30 * 6e4;
1407
+ function directChatHostConcurrency() {
1408
+ const raw = parseInt(process.env["AGT_DIRECT_CHAT_MAX_CONCURRENCY"] ?? "", 10);
1409
+ return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_DIRECT_CHAT_HOST_CONCURRENCY;
1410
+ }
1411
+ function directChatMaxAgeMs() {
1412
+ const raw = parseInt(process.env["AGT_DIRECT_CHAT_MAX_AGE_MS"] ?? "", 10);
1413
+ return Number.isFinite(raw) && raw >= 0 ? raw : DEFAULT_DIRECT_CHAT_MAX_AGE_MS;
1414
+ }
1415
+ function isDirectChatMessageExpired(createdAt, nowMs, maxAgeMs) {
1416
+ if (!maxAgeMs || maxAgeMs <= 0) return false;
1417
+ if (!createdAt) return false;
1418
+ const created = Date.parse(createdAt);
1419
+ if (!Number.isFinite(created)) return false;
1420
+ return nowMs - created > maxAgeMs;
1421
+ }
1422
+ var DirectChatSpawnGate = class {
1423
+ hostMax;
1424
+ perAgentMax;
1425
+ host = 0;
1426
+ perAgent = /* @__PURE__ */ new Map();
1427
+ queue = [];
1428
+ constructor(config2) {
1429
+ this.hostMax = Math.max(1, config2?.hostMaxConcurrency ?? DEFAULT_DIRECT_CHAT_HOST_CONCURRENCY);
1430
+ this.perAgentMax = Math.max(
1431
+ 1,
1432
+ config2?.perAgentMaxConcurrency ?? DEFAULT_DIRECT_CHAT_PER_AGENT_CONCURRENCY
1433
+ );
1434
+ }
1435
+ /** Live host-wide spawn count. */
1436
+ get hostInFlight() {
1437
+ return this.host;
1438
+ }
1439
+ /** Live spawn count for a single agent. */
1440
+ agentInFlight(codeName) {
1441
+ return this.perAgent.get(codeName) ?? 0;
1442
+ }
1443
+ /** Number of acquirers currently waiting for a slot. */
1444
+ get queuedCount() {
1445
+ return this.queue.length;
1446
+ }
1447
+ canGrant(codeName) {
1448
+ return this.host < this.hostMax && this.agentInFlight(codeName) < this.perAgentMax;
1449
+ }
1450
+ grant(codeName) {
1451
+ this.host += 1;
1452
+ this.perAgent.set(codeName, this.agentInFlight(codeName) + 1);
1453
+ let released = false;
1454
+ return () => {
1455
+ if (released) return;
1456
+ released = true;
1457
+ this.host -= 1;
1458
+ const remaining = this.agentInFlight(codeName) - 1;
1459
+ if (remaining <= 0) this.perAgent.delete(codeName);
1460
+ else this.perAgent.set(codeName, remaining);
1461
+ this.drain();
1462
+ };
1463
+ }
1464
+ /** Acquire a spawn slot for `codeName`. Resolves with a single-use release. */
1465
+ acquire(codeName) {
1466
+ if (this.canGrant(codeName)) {
1467
+ return Promise.resolve(this.grant(codeName));
1468
+ }
1469
+ return new Promise((resolve) => {
1470
+ this.queue.push({ codeName, resolve });
1471
+ });
1472
+ }
1473
+ /** Grant as many queued waiters as free slots allow, in FIFO order. */
1474
+ drain() {
1475
+ let i = 0;
1476
+ while (i < this.queue.length && this.host < this.hostMax) {
1477
+ const waiter = this.queue[i];
1478
+ if (!waiter) break;
1479
+ if (this.canGrant(waiter.codeName)) {
1480
+ this.queue.splice(i, 1);
1481
+ waiter.resolve(this.grant(waiter.codeName));
1482
+ } else {
1483
+ i += 1;
1484
+ }
1485
+ }
1486
+ }
1487
+ };
1488
+
913
1489
  // src/lib/usage-banner-monitor.ts
914
1490
  var MIN_CHECK_INTERVAL_MS = 6e4;
915
1491
  var PANE_TAIL_LINES_FOR_BANNER = 200;
@@ -966,8 +1542,8 @@ async function maybeReportUsageBanner(args) {
966
1542
  }
967
1543
 
968
1544
  // src/lib/token-usage-monitor.ts
969
- import { readdirSync, readFileSync, statSync } from "fs";
970
- import { join } from "path";
1545
+ import { readdirSync, readFileSync as readFileSync4, statSync } from "fs";
1546
+ import { join as join2 } from "path";
971
1547
  var MIN_CHECK_INTERVAL_MS2 = 6e4;
972
1548
  var TRANSCRIPT_MTIME_WINDOW_MS = 2 * 24 * 60 * 60 * 1e3;
973
1549
  var MAX_ENTRIES_PER_POST = 200;
@@ -996,7 +1572,7 @@ async function maybeReportTokenUsage(args) {
996
1572
  if (!name.endsWith(".jsonl")) continue;
997
1573
  const sessionId = name.slice(0, -".jsonl".length);
998
1574
  if (!sessionId) continue;
999
- const path = join(dir, name);
1575
+ const path = join2(dir, name);
1000
1576
  let st;
1001
1577
  try {
1002
1578
  st = statSync(path);
@@ -1012,7 +1588,7 @@ async function maybeReportTokenUsage(args) {
1012
1588
  }
1013
1589
  let content;
1014
1590
  try {
1015
- content = readFileSync(path, "utf-8");
1591
+ content = readFileSync4(path, "utf-8");
1016
1592
  } catch (err) {
1017
1593
  log2(`[token-usage] read failed for '${codeName}/${name}': ${err.message}`);
1018
1594
  continue;
@@ -1092,14 +1668,272 @@ async function maybeReportTokenUsage(args) {
1092
1668
  state2.set(codeName, next);
1093
1669
  }
1094
1670
 
1671
+ // src/lib/conversation-evaluator.ts
1672
+ import { readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
1673
+ import { join as join3 } from "path";
1674
+ var MIN_CHECK_INTERVAL_MS3 = 5 * 6e4;
1675
+ var TRANSCRIPT_MTIME_WINDOW_MS2 = 3 * 24 * 60 * 60 * 1e3;
1676
+ var WINDOW_PAD_MS = 5 * 6e4;
1677
+ var MAX_TURN_CHARS = 1500;
1678
+ var MAX_TRANSCRIPT_CHARS = 6e3;
1679
+ var DEFAULT_CLAUDE_EVAL_MODEL = "claude-haiku-4-5-20251001";
1680
+ var DEFAULT_LOCAL_EVAL_URL = "http://localhost:11434/v1/chat/completions";
1681
+ var DEFAULT_LOCAL_EVAL_MODEL = "gemma4:12b";
1682
+ var EVAL_TIMEOUT_MS = 12e4;
1683
+ async function runLocalEvalChat(prompt, opts) {
1684
+ const endpoint = new URL(opts.url);
1685
+ const hostname = endpoint.hostname.toLowerCase();
1686
+ const isLoopback = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
1687
+ if (!isLoopback) {
1688
+ throw new Error(`local eval url must be loopback-only, got host '${endpoint.hostname}'`);
1689
+ }
1690
+ const doFetch = opts.fetchImpl ?? fetch;
1691
+ const res = await doFetch(opts.url, {
1692
+ method: "POST",
1693
+ headers: {
1694
+ "Content-Type": "application/json",
1695
+ ...opts.apiKey ? { Authorization: `Bearer ${opts.apiKey}` } : {}
1696
+ },
1697
+ body: JSON.stringify({
1698
+ model: opts.model,
1699
+ messages: [{ role: "user", content: prompt }],
1700
+ temperature: 0,
1701
+ max_tokens: 512,
1702
+ stream: false
1703
+ }),
1704
+ signal: AbortSignal.timeout(opts.timeoutMs ?? EVAL_TIMEOUT_MS)
1705
+ });
1706
+ if (!res.ok) {
1707
+ throw new Error(`local eval endpoint ${opts.url} returned ${res.status}`);
1708
+ }
1709
+ const data = await res.json();
1710
+ const text = data.choices?.[0]?.message?.content;
1711
+ if (typeof text !== "string" || !text.trim()) {
1712
+ throw new Error("local eval endpoint returned no message content");
1713
+ }
1714
+ return text;
1715
+ }
1716
+ var state3 = /* @__PURE__ */ new Map();
1717
+ function channelRefTokens(channelRef) {
1718
+ return channelRef.split(":").slice(1).filter((p) => p && p !== "dm");
1719
+ }
1720
+ function contentToText(content) {
1721
+ if (typeof content === "string") return content;
1722
+ if (Array.isArray(content)) {
1723
+ return content.map((b) => {
1724
+ if (typeof b === "string") return b;
1725
+ if (b && typeof b === "object" && b.type === "text") {
1726
+ return String(b.text ?? "");
1727
+ }
1728
+ return "";
1729
+ }).filter(Boolean).join(" ");
1730
+ }
1731
+ return "";
1732
+ }
1733
+ function parseTranscriptTurns(jsonl) {
1734
+ const turns = [];
1735
+ for (const line of jsonl.split("\n")) {
1736
+ const trimmed = line.trim();
1737
+ if (!trimmed) continue;
1738
+ let obj;
1739
+ try {
1740
+ obj = JSON.parse(trimmed);
1741
+ } catch {
1742
+ continue;
1743
+ }
1744
+ const role = obj.type === "user" || obj.type === "assistant" ? obj.type : void 0;
1745
+ if (!role) continue;
1746
+ const text = contentToText(obj.message?.content).trim();
1747
+ if (!text) continue;
1748
+ const ts = obj.timestamp ? Date.parse(obj.timestamp) : NaN;
1749
+ turns.push({ role, text, ts: Number.isFinite(ts) ? ts : 0 });
1750
+ }
1751
+ return turns;
1752
+ }
1753
+ function stripChannelTag(text) {
1754
+ return text.replace(/<channel\b[^>]*\/?>/i, "").replace(/<\/channel>/i, "").trim();
1755
+ }
1756
+ function reconstructConversation(allTurns, tokens, windowStartMs, windowEndMs) {
1757
+ if (tokens.length === 0) return [];
1758
+ const sorted = [...allTurns].sort((a, b) => a.ts - b.ts);
1759
+ const out = [];
1760
+ let inConversation = false;
1761
+ for (const turn of sorted) {
1762
+ if (turn.ts && (turn.ts < windowStartMs || turn.ts > windowEndMs)) continue;
1763
+ if (turn.role === "user") {
1764
+ inConversation = tokens.every((t) => turn.text.includes(t));
1765
+ if (inConversation) {
1766
+ out.push({ ...turn, text: stripChannelTag(turn.text) });
1767
+ }
1768
+ } else if (inConversation) {
1769
+ out.push(turn);
1770
+ }
1771
+ }
1772
+ return out;
1773
+ }
1774
+ function renderTranscript(turns) {
1775
+ const lines = [];
1776
+ let total = 0;
1777
+ for (const turn of turns) {
1778
+ const label = turn.role === "user" ? "User" : "Agent";
1779
+ const body = turn.text.length > MAX_TURN_CHARS ? `${turn.text.slice(0, MAX_TURN_CHARS)}\u2026` : turn.text;
1780
+ const line = `${label}: ${body}`;
1781
+ if (total + line.length > MAX_TRANSCRIPT_CHARS) break;
1782
+ lines.push(line);
1783
+ total += line.length;
1784
+ }
1785
+ return lines.join("\n\n");
1786
+ }
1787
+ function buildEvalPrompt(channel, transcript) {
1788
+ return `You are a strict QA evaluator scoring whether an AI agent successfully helped an end-user in a single ${channel} conversation. Judge ONLY the agent's effectiveness for the user \u2014 not tone, length, or policy.
1789
+
1790
+ Conversation transcript:
1791
+ """
1792
+ ${transcript}
1793
+ """
1794
+
1795
+ Score the agent's success:
1796
+ - score: integer 0-100 (0 = ignored/unhelpful/wrong, 100 = fully resolved the user's need)
1797
+ - verdict: "success" (need clearly met), "partial" (some help but incomplete/ambiguous), or "failure" (did not help / made it worse)
1798
+ - summary: ONE short sentence (max 140 chars) explaining the score. Do NOT quote sensitive user content.
1799
+
1800
+ Respond with ONLY a JSON object, no other text:
1801
+ {"score": 0-100, "verdict": "success|partial|failure", "summary": "..."}`;
1802
+ }
1803
+ function parseVerdict(raw) {
1804
+ const match = raw.match(/\{[\s\S]*\}/);
1805
+ if (!match) return null;
1806
+ let obj;
1807
+ try {
1808
+ obj = JSON.parse(match[0]);
1809
+ } catch {
1810
+ return null;
1811
+ }
1812
+ const score = typeof obj.score === "number" ? obj.score : Number(obj.score);
1813
+ if (!Number.isFinite(score) || score < 0 || score > 100) return null;
1814
+ if (obj.verdict !== "success" && obj.verdict !== "partial" && obj.verdict !== "failure") return null;
1815
+ const summary = typeof obj.summary === "string" ? obj.summary.slice(0, 140) : "";
1816
+ return { score: Math.round(score), verdict: obj.verdict, summary };
1817
+ }
1818
+ async function maybeEvaluateConversations(args) {
1819
+ const { api: api2, backend, codeName, agentId, log: log2 } = args;
1820
+ const now = args.now ?? /* @__PURE__ */ new Date();
1821
+ const nowMs = now.getTime();
1822
+ const existing = state3.get(codeName);
1823
+ if (existing && nowMs - existing.lastCheckedAt < MIN_CHECK_INTERVAL_MS3) {
1824
+ return;
1825
+ }
1826
+ state3.set(codeName, { lastCheckedAt: nowMs });
1827
+ let pending;
1828
+ try {
1829
+ const resp = await api2.get(
1830
+ `/host/conversations/pending-evaluation?agent_id=${encodeURIComponent(agentId)}`
1831
+ );
1832
+ pending = resp?.conversations ?? [];
1833
+ } catch (err) {
1834
+ log2(`[conversation-eval] ${codeName}: pending fetch failed: ${err.message}`);
1835
+ return;
1836
+ }
1837
+ if (pending.length === 0) return;
1838
+ const dir = args.transcriptDir ?? sessionTranscriptDir(getProjectDir(codeName));
1839
+ const allTurns = readRecentTurns(dir, nowMs);
1840
+ if (allTurns.length === 0) {
1841
+ for (const conv of pending) {
1842
+ await reportSkip(api2, agentId, conv.conversation_id, log2, codeName);
1843
+ }
1844
+ return;
1845
+ }
1846
+ for (const conv of pending) {
1847
+ const tokens = channelRefTokens(conv.channel_ref);
1848
+ const windowStart = Date.parse(conv.started_at) - WINDOW_PAD_MS;
1849
+ const windowEnd = Date.parse(conv.last_message_at) + WINDOW_PAD_MS;
1850
+ const turns = reconstructConversation(allTurns, tokens, windowStart, windowEnd);
1851
+ if (turns.length === 0) {
1852
+ await reportSkip(api2, agentId, conv.conversation_id, log2, codeName);
1853
+ continue;
1854
+ }
1855
+ const transcript = renderTranscript(turns);
1856
+ if (!transcript.trim()) {
1857
+ await reportSkip(api2, agentId, conv.conversation_id, log2, codeName);
1858
+ continue;
1859
+ }
1860
+ let verdict;
1861
+ try {
1862
+ const out = await backend.run(buildEvalPrompt(conv.channel, transcript));
1863
+ verdict = parseVerdict(out);
1864
+ } catch (err) {
1865
+ log2(`[conversation-eval] ${codeName}: scoring failed: ${err.message}`);
1866
+ continue;
1867
+ }
1868
+ if (!verdict) {
1869
+ log2(`[conversation-eval] ${codeName}: unparseable verdict for ${conv.conversation_id.slice(0, 8)}`);
1870
+ continue;
1871
+ }
1872
+ try {
1873
+ await api2.post("/host/conversations/evaluation", {
1874
+ agent_id: agentId,
1875
+ conversation_id: conv.conversation_id,
1876
+ score: verdict.score,
1877
+ verdict: verdict.verdict,
1878
+ summary: verdict.summary,
1879
+ model: backend.model
1880
+ });
1881
+ log2(
1882
+ `[conversation-eval] ${codeName}: ${conv.conversation_id.slice(0, 8)} \u2192 ${verdict.verdict} (${verdict.score})`
1883
+ );
1884
+ } catch (err) {
1885
+ log2(`[conversation-eval] ${codeName}: report failed: ${err.message}`);
1886
+ }
1887
+ }
1888
+ }
1889
+ async function reportSkip(api2, agentId, conversationId, log2, codeName) {
1890
+ try {
1891
+ await api2.post("/host/conversations/evaluation", {
1892
+ agent_id: agentId,
1893
+ conversation_id: conversationId,
1894
+ skipped: true
1895
+ });
1896
+ } catch (err) {
1897
+ log2(`[conversation-eval] ${codeName}: skip report failed: ${err.message}`);
1898
+ }
1899
+ }
1900
+ function readRecentTurns(dir, nowMs) {
1901
+ let entries;
1902
+ try {
1903
+ entries = readdirSync2(dir);
1904
+ } catch {
1905
+ return [];
1906
+ }
1907
+ const turns = [];
1908
+ for (const name of entries) {
1909
+ if (!name.endsWith(".jsonl")) continue;
1910
+ const full = join3(dir, name);
1911
+ let mtimeMs;
1912
+ try {
1913
+ mtimeMs = statSync2(full).mtimeMs;
1914
+ } catch {
1915
+ continue;
1916
+ }
1917
+ if (nowMs - mtimeMs > TRANSCRIPT_MTIME_WINDOW_MS2) continue;
1918
+ let content;
1919
+ try {
1920
+ content = readFileSync5(full, "utf8");
1921
+ } catch {
1922
+ continue;
1923
+ }
1924
+ turns.push(...parseTranscriptTurns(content));
1925
+ }
1926
+ return turns;
1927
+ }
1928
+
1095
1929
  // src/lib/activity-cache-monitor.ts
1096
- import { existsSync, readFileSync as readFileSync2 } from "fs";
1930
+ import { existsSync as existsSync2, readFileSync as readFileSync6 } from "fs";
1097
1931
  import { homedir } from "os";
1098
- import { join as join2 } from "path";
1099
- var MIN_CHECK_INTERVAL_MS3 = 6e4;
1100
- var STATS_CACHE_PATH = join2(homedir(), ".claude", "stats-cache.json");
1932
+ import { join as join4 } from "path";
1933
+ var MIN_CHECK_INTERVAL_MS4 = 6e4;
1934
+ var STATS_CACHE_PATH = join4(homedir(), ".claude", "stats-cache.json");
1101
1935
  var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
1102
- var state3 = { lastObservedDate: null, lastCheckedAt: 0 };
1936
+ var state4 = { lastObservedDate: null, lastCheckedAt: 0 };
1103
1937
  function selectNewDailyRows(raw, lastObservedDate) {
1104
1938
  let parsed;
1105
1939
  try {
@@ -1138,24 +1972,24 @@ async function maybeReportActivityCache(args) {
1138
1972
  const { api: api2, log: log2 } = args;
1139
1973
  const now = args.now ?? /* @__PURE__ */ new Date();
1140
1974
  const nowMs = now.getTime();
1141
- if (nowMs - state3.lastCheckedAt < MIN_CHECK_INTERVAL_MS3) return;
1142
- state3.lastCheckedAt = nowMs;
1143
- if (!existsSync(STATS_CACHE_PATH)) {
1975
+ if (nowMs - state4.lastCheckedAt < MIN_CHECK_INTERVAL_MS4) return;
1976
+ state4.lastCheckedAt = nowMs;
1977
+ if (!existsSync2(STATS_CACHE_PATH)) {
1144
1978
  return;
1145
1979
  }
1146
1980
  let raw;
1147
1981
  try {
1148
- raw = readFileSync2(STATS_CACHE_PATH, "utf-8");
1982
+ raw = readFileSync6(STATS_CACHE_PATH, "utf-8");
1149
1983
  } catch (err) {
1150
1984
  log2(`[activity-cache] readFileSync failed: ${err.message}`);
1151
1985
  return;
1152
1986
  }
1153
- const rows = selectNewDailyRows(raw, state3.lastObservedDate);
1987
+ const rows = selectNewDailyRows(raw, state4.lastObservedDate);
1154
1988
  if (rows.length === 0) return;
1155
1989
  for (const row of rows) {
1156
1990
  try {
1157
1991
  await api2.post("/host/activity-observations", row);
1158
- state3.lastObservedDate = row.date;
1992
+ state4.lastObservedDate = row.date;
1159
1993
  } catch (err) {
1160
1994
  log2(
1161
1995
  `[activity-cache] POST /host/activity-observations failed for date=${row.date}: ${err.message}`
@@ -1595,7 +2429,7 @@ var GatewayClientPool = class extends EventEmitter {
1595
2429
  // src/lib/claude-auth-detect.ts
1596
2430
  import { readFile, readdir } from "fs/promises";
1597
2431
  import { homedir as homedir2, platform } from "os";
1598
- import { join as join3 } from "path";
2432
+ import { join as join5 } from "path";
1599
2433
  import { execFile as execFile2 } from "child_process";
1600
2434
  import { promisify } from "util";
1601
2435
  var execFileAsync = promisify(execFile2);
@@ -1610,8 +2444,8 @@ async function detectClaudeAuth() {
1610
2444
  }
1611
2445
  async function findClaudeCredentialsPaths() {
1612
2446
  const candidates = [
1613
- join3(homedir2(), ".claude", ".credentials.json"),
1614
- join3(homedir2(), ".claude", "credentials.json")
2447
+ join5(homedir2(), ".claude", ".credentials.json"),
2448
+ join5(homedir2(), ".claude", "credentials.json")
1615
2449
  ];
1616
2450
  const isLinuxRoot = platform() === "linux" && typeof process.getuid === "function" && process.getuid() === 0;
1617
2451
  if (isLinuxRoot) {
@@ -1619,8 +2453,8 @@ async function findClaudeCredentialsPaths() {
1619
2453
  const entries = await readdir("/home", { withFileTypes: true });
1620
2454
  for (const entry of entries) {
1621
2455
  if (!entry.isDirectory()) continue;
1622
- candidates.push(join3("/home", entry.name, ".claude", ".credentials.json"));
1623
- candidates.push(join3("/home", entry.name, ".claude", "credentials.json"));
2456
+ candidates.push(join5("/home", entry.name, ".claude", ".credentials.json"));
2457
+ candidates.push(join5("/home", entry.name, ".claude", "credentials.json"));
1624
2458
  }
1625
2459
  } catch {
1626
2460
  }
@@ -1712,18 +2546,18 @@ function normalize(value) {
1712
2546
  }
1713
2547
 
1714
2548
  // src/lib/channel-hash-cache.ts
1715
- import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync } from "fs";
1716
- import { join as join4 } from "path";
2549
+ import { existsSync as existsSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
2550
+ import { join as join6 } from "path";
1717
2551
  var CACHE_FILENAME = "channel-hash-cache.json";
1718
2552
  function getChannelHashCacheFile(configDir) {
1719
- return join4(configDir, CACHE_FILENAME);
2553
+ return join6(configDir, CACHE_FILENAME);
1720
2554
  }
1721
2555
  function loadChannelHashCache(target, configDir) {
1722
2556
  const path = getChannelHashCacheFile(configDir);
1723
- if (!existsSync2(path)) return;
2557
+ if (!existsSync3(path)) return;
1724
2558
  let parsed;
1725
2559
  try {
1726
- parsed = JSON.parse(readFileSync3(path, "utf-8"));
2560
+ parsed = JSON.parse(readFileSync7(path, "utf-8"));
1727
2561
  } catch {
1728
2562
  return;
1729
2563
  }
@@ -1737,7 +2571,7 @@ function saveChannelHashCache(source, configDir) {
1737
2571
  const obj = {};
1738
2572
  for (const [key, value] of source) obj[key] = value;
1739
2573
  try {
1740
- writeFileSync(path, JSON.stringify(obj, null, 2));
2574
+ writeFileSync2(path, JSON.stringify(obj, null, 2));
1741
2575
  } catch {
1742
2576
  }
1743
2577
  }
@@ -1992,108 +2826,6 @@ function killAgentChannelProcesses(codeName, opts) {
1992
2826
  return pids;
1993
2827
  }
1994
2828
 
1995
- // src/lib/channel-input-watchdog.ts
1996
- var STUCK_THRESHOLD_MS = 5e3;
1997
- var ATTACHED_STUCK_THRESHOLD_MS = 15e3;
1998
- var INPUT_BOX_DIVIDER = /^[─━]{10,}/;
1999
- var PROMPT_PREFIX = "\u276F ";
2000
- function decide(pane, prev, now, config2 = {}) {
2001
- const threshold = config2.stuckThresholdMs ?? STUCK_THRESHOLD_MS;
2002
- const inputText = extractInputBoxText(pane);
2003
- if (!inputText) {
2004
- return { fire: false, next: void 0 };
2005
- }
2006
- if (isActivelyProcessing(pane)) {
2007
- return { fire: false, next: prev };
2008
- }
2009
- const hash = simpleHash(inputText);
2010
- if (!prev || prev.lastInputHash !== hash) {
2011
- return {
2012
- fire: false,
2013
- next: { lastInputHash: hash, firstSeenAt: now, resolved: false }
2014
- };
2015
- }
2016
- if (prev.resolved) return { fire: false, next: prev };
2017
- if (now - prev.firstSeenAt < threshold) return { fire: false, next: prev };
2018
- return {
2019
- fire: true,
2020
- next: { ...prev, resolved: true }
2021
- };
2022
- }
2023
- function extractInputBoxText(pane) {
2024
- const lines = pane.split("\n");
2025
- for (let i = 1; i < lines.length; i++) {
2026
- const line = lines[i] ?? "";
2027
- if (!line.startsWith(PROMPT_PREFIX)) continue;
2028
- let j = i - 1;
2029
- while (j >= 0 && (lines[j] ?? "").trim() === "") j--;
2030
- if (j < 0) continue;
2031
- if (!INPUT_BOX_DIVIDER.test((lines[j] ?? "").trim())) continue;
2032
- const text = line.slice(PROMPT_PREFIX.length).trim();
2033
- return text.length > 0 ? text : null;
2034
- }
2035
- return null;
2036
- }
2037
- function isActivelyProcessing(pane) {
2038
- const lines = pane.split("\n");
2039
- for (let i = lines.length - 1; i >= 0; i--) {
2040
- const line = (lines[i] ?? "").trim();
2041
- if (!line.startsWith("\u273B")) continue;
2042
- if (/\bfor\s+\d+s\s*$/.test(line)) return false;
2043
- if (/\b\w+ing[…\.]{0,3}\s*$/i.test(line)) return true;
2044
- return false;
2045
- }
2046
- return false;
2047
- }
2048
- function simpleHash(s) {
2049
- let h = 0;
2050
- for (let i = 0; i < s.length; i++) {
2051
- h = (h << 5) - h + s.charCodeAt(i) | 0;
2052
- }
2053
- return h.toString(16);
2054
- }
2055
- function checkChannelInputs(codeNames, io, config2 = {}, states = sharedStates) {
2056
- const live = new Set(codeNames);
2057
- for (const codeName of codeNames) {
2058
- try {
2059
- checkOne(codeName, io, config2, states);
2060
- } catch (err) {
2061
- io.log(`[channel-input-watchdog] '${codeName}': ${err.message}`);
2062
- }
2063
- }
2064
- for (const key of [...states.keys()]) {
2065
- if (!live.has(key)) states.delete(key);
2066
- }
2067
- }
2068
- function checkOne(codeName, io, config2, states) {
2069
- const pane = io.capturePane(codeName);
2070
- if (!pane) {
2071
- states.delete(codeName);
2072
- return;
2073
- }
2074
- const attached = io.isClientAttached(codeName);
2075
- const effectiveConfig = attached ? {
2076
- ...config2,
2077
- stuckThresholdMs: config2.attachedStuckThresholdMs ?? ATTACHED_STUCK_THRESHOLD_MS
2078
- } : config2;
2079
- const prev = states.get(codeName);
2080
- const { fire, next } = decide(pane, prev, io.now(), effectiveConfig);
2081
- if (next === void 0) {
2082
- states.delete(codeName);
2083
- } else {
2084
- states.set(codeName, next);
2085
- }
2086
- if (fire) {
2087
- const text = extractInputBoxText(pane) ?? "";
2088
- const hash = next?.lastInputHash ?? simpleHash(text);
2089
- io.log(
2090
- `[channel-input-watchdog] '${codeName}': stuck channel input \u2014 firing Enter (input_hash=${hash}, len=${text.length})`
2091
- );
2092
- io.sendEnter(codeName);
2093
- }
2094
- }
2095
- var sharedStates = /* @__PURE__ */ new Map();
2096
-
2097
2829
  // src/lib/delivery-hint.ts
2098
2830
  var DEFAULT_PROBABILITY = 0.1;
2099
2831
  function envSuffixFor(codeName) {
@@ -2210,6 +2942,34 @@ var agentState = {
2210
2942
  knownIntegrationHashes: /* @__PURE__ */ new Map(),
2211
2943
  knownManagedMcpHashes: /* @__PURE__ */ new Map(),
2212
2944
  knownManagedMcpStructure: /* @__PURE__ */ new Map(),
2945
+ /**
2946
+ * ENG-5858: hash of the effective sender_policy returned by
2947
+ * /host/refresh for this agent. Tracked separately from
2948
+ * `knownChannelConfigHashes` because the channel-cred hash is
2949
+ * per-channel (cleared/rewritten per channel) whereas sender_policy is
2950
+ * agent-wide. A change here triggers a session restart so the
2951
+ * slack-channel / teams-channel MCP children pick up the new env vars
2952
+ * (they read SLACK_SENDER_POLICY / MSTEAMS_SENDER_POLICY only at boot).
2953
+ */
2954
+ knownSenderPolicyHashes: /* @__PURE__ */ new Map(),
2955
+ /**
2956
+ * ENG-6024: hash of the Teams behaviour subset (thread_auto_follow,
2957
+ * channel_response_mode, adaptive_cards_enabled/_ask_user_enabled) from the
2958
+ * agent's msteams channel config. Same last-hop rationale as
2959
+ * `knownSenderPolicyHashes`: teams-channel reads the corresponding env vars
2960
+ * once at boot, and neither the channel-set restart nor the .mcp.json drift
2961
+ * watcher fires on a value-only change, so this dedicated trigger restarts
2962
+ * the session when an operator saves the webapp's Teams Advanced panel.
2963
+ */
2964
+ knownMsTeamsBehaviourHashes: /* @__PURE__ */ new Map(),
2965
+ /**
2966
+ * ENG-6035: hash of the Slack behaviour subset (allowed_users,
2967
+ * thread_auto_follow, channel_response_mode) from the agent's slack
2968
+ * channel config. Same last-hop rationale as the Teams map above —
2969
+ * slack-channel reads SLACK_ALLOWED_USERS (and friends) once at boot,
2970
+ * so an operator save needs this dedicated value-change trigger.
2971
+ */
2972
+ knownSlackBehaviourHashes: /* @__PURE__ */ new Map(),
2213
2973
  // ---------------------------------------------------------------------------
2214
2974
  // codeName-keyed
2215
2975
  // ---------------------------------------------------------------------------
@@ -2239,6 +2999,9 @@ function clearAgentState(agentId, codeName) {
2239
2999
  agentState.knownIntegrationHashes.delete(agentId);
2240
3000
  agentState.knownManagedMcpHashes.delete(agentId);
2241
3001
  agentState.knownManagedMcpStructure.delete(agentId);
3002
+ agentState.knownSenderPolicyHashes.delete(agentId);
3003
+ agentState.knownMsTeamsBehaviourHashes.delete(agentId);
3004
+ agentState.knownSlackBehaviourHashes.delete(agentId);
2242
3005
  agentState.agentDisplayNames.delete(codeName);
2243
3006
  agentState.codeNameToAgentId.delete(codeName);
2244
3007
  let channelCacheMutated = false;
@@ -2257,24 +3020,24 @@ function clearAgentState(agentId, codeName) {
2257
3020
  }
2258
3021
 
2259
3022
  // src/lib/restart-flags.ts
2260
- import { existsSync as existsSync3, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync4, renameSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
3023
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync8, renameSync as renameSync2, rmSync, writeFileSync as writeFileSync3 } from "fs";
2261
3024
  import { homedir as homedir3 } from "os";
2262
- import { join as join5 } from "path";
3025
+ import { join as join7 } from "path";
2263
3026
  import { randomUUID } from "crypto";
2264
3027
  function restartFlagsDir() {
2265
- return join5(homedir3(), ".augmented", "restart-flags");
3028
+ return join7(homedir3(), ".augmented", "restart-flags");
2266
3029
  }
2267
3030
  function flagPath(codeName) {
2268
- return join5(restartFlagsDir(), `${codeName}.flag`);
3031
+ return join7(restartFlagsDir(), `${codeName}.flag`);
2269
3032
  }
2270
3033
  function readRestartFlags() {
2271
3034
  const dir = restartFlagsDir();
2272
- if (!existsSync3(dir)) return [];
3035
+ if (!existsSync4(dir)) return [];
2273
3036
  const out = [];
2274
- for (const entry of readdirSync2(dir)) {
3037
+ for (const entry of readdirSync3(dir)) {
2275
3038
  if (!entry.endsWith(".flag")) continue;
2276
3039
  try {
2277
- const raw = readFileSync4(join5(dir, entry), "utf8");
3040
+ const raw = readFileSync8(join7(dir, entry), "utf8");
2278
3041
  const parsed = JSON.parse(raw);
2279
3042
  if (typeof parsed.codeName !== "string" || parsed.codeName.length === 0) {
2280
3043
  parsed.codeName = entry.replace(/\.flag$/, "");
@@ -2292,7 +3055,7 @@ function readRestartFlags() {
2292
3055
  }
2293
3056
  function deleteRestartFlag(codeName) {
2294
3057
  const path = flagPath(codeName);
2295
- if (existsSync3(path)) {
3058
+ if (existsSync4(path)) {
2296
3059
  rmSync(path, { force: true });
2297
3060
  }
2298
3061
  }
@@ -2666,14 +3429,32 @@ function startRealtimeIntegrationContext(config2) {
2666
3429
  if (agentIds.length === 0) return;
2667
3430
  const sb = ensureClient(config2);
2668
3431
  const filterStr = agentIds.length === 1 ? `agent_id=eq.${agentIds[0]}` : `agent_id=in.(${agentIds.join(",")})`;
2669
- integrationContextChannel = sb.channel("plugin-context-realtime").on("postgres_changes", {
3432
+ integrationContextChannel = sb.channel("agent-integration-context-realtime").on("postgres_changes", {
3433
+ event: "INSERT",
3434
+ schema: "public",
3435
+ table: "agent_integration_context",
3436
+ filter: filterStr
3437
+ }, (payload) => {
3438
+ const row = payload.new;
3439
+ log2(`[realtime] agent_integration_context INSERT for agent ${row.agent_id} (plugin ${row.plugin_id})`);
3440
+ onContextChange(row);
3441
+ }).on("postgres_changes", {
3442
+ event: "UPDATE",
3443
+ schema: "public",
3444
+ table: "agent_integration_context",
3445
+ filter: filterStr
3446
+ }, (payload) => {
3447
+ const row = payload.new;
3448
+ log2(`[realtime] agent_integration_context UPDATE for agent ${row.agent_id} (plugin ${row.plugin_id})`);
3449
+ onContextChange(row);
3450
+ }).on("postgres_changes", {
2670
3451
  event: "INSERT",
2671
3452
  schema: "public",
2672
3453
  table: "plugin_context",
2673
3454
  filter: filterStr
2674
3455
  }, (payload) => {
2675
3456
  const row = payload.new;
2676
- log2(`[realtime] plugin_context INSERT for agent ${row.agent_id} (plugin ${row.plugin_id})`);
3457
+ log2(`[realtime] plugin_context (legacy) INSERT for agent ${row.agent_id} (plugin ${row.plugin_id})`);
2677
3458
  onContextChange(row);
2678
3459
  }).on("postgres_changes", {
2679
3460
  event: "UPDATE",
@@ -2682,7 +3463,7 @@ function startRealtimeIntegrationContext(config2) {
2682
3463
  filter: filterStr
2683
3464
  }, (payload) => {
2684
3465
  const row = payload.new;
2685
- log2(`[realtime] plugin_context UPDATE for agent ${row.agent_id} (plugin ${row.plugin_id})`);
3466
+ log2(`[realtime] plugin_context (legacy) UPDATE for agent ${row.agent_id} (plugin ${row.plugin_id})`);
2686
3467
  onContextChange(row);
2687
3468
  }).subscribe((status) => {
2688
3469
  if (status === "SUBSCRIBED") {
@@ -2691,7 +3472,7 @@ function startRealtimeIntegrationContext(config2) {
2691
3472
  log2(`[realtime] Integration context channel: ${status}`);
2692
3473
  }
2693
3474
  });
2694
- log2(`[realtime] Subscribing to plugin_context for ${agentIds.length} agent(s)`);
3475
+ log2(`[realtime] Subscribing to agent_integration_context (+ legacy plugin_context) for ${agentIds.length} agent(s)`);
2695
3476
  }
2696
3477
  function stopRealtimeIntegrationContext() {
2697
3478
  if (!integrationContextChannel) return;
@@ -2803,8 +3584,8 @@ function applyRestartAcks(args) {
2803
3584
  var GATEWAY_PORT_BASE = 18800;
2804
3585
  var GATEWAY_PORT_STEP = 10;
2805
3586
  var GATEWAY_PORT_MAX = 18899;
2806
- var AUGMENTED_DIR = join6(process.env["HOME"] ?? "/tmp", ".augmented");
2807
- var GATEWAY_PORTS_FILE = join6(AUGMENTED_DIR, "gateway-ports.json");
3587
+ var AUGMENTED_DIR = join8(process.env["HOME"] ?? "/tmp", ".augmented");
3588
+ var GATEWAY_PORTS_FILE = join8(AUGMENTED_DIR, "gateway-ports.json");
2808
3589
  var CHANNEL_SWEEP_INTERVAL_MS = (() => {
2809
3590
  const raw = parseInt(process.env["AGT_CHANNEL_SWEEP_INTERVAL_MS"] ?? "", 10);
2810
3591
  if (!Number.isFinite(raw)) return 5 * 60 * 1e3;
@@ -2911,8 +3692,8 @@ var KNOWN_SAFE_TAIL_SIGNATURES = /* @__PURE__ */ new Set(["session_id_in_use"]);
2911
3692
  function shouldSkipRevokedCleanup(previousKnownStatus) {
2912
3693
  return previousKnownStatus === "revoked";
2913
3694
  }
2914
- function hasRevokedResiduals(state5) {
2915
- return state5.gatewayRunning || state5.portAllocated || state5.provisionDirExists;
3695
+ function hasRevokedResiduals(state6) {
3696
+ return state6.gatewayRunning || state6.portAllocated || state6.provisionDirExists;
2916
3697
  }
2917
3698
  var pendingSessionRestarts = /* @__PURE__ */ new Map();
2918
3699
  var restartBreaker = new RestartBreaker();
@@ -2954,6 +3735,14 @@ function scheduleSessionRestart(codeName, delayMs, reason, breakerReason = "hot-
2954
3735
  }
2955
3736
  const timer = setTimeout(() => {
2956
3737
  pendingSessionRestarts.delete(codeName);
3738
+ const gate = restartGateFor(codeName, breakerReason);
3739
+ if (gate !== "bypass" && gate !== "proceed") {
3740
+ log(
3741
+ `[maintenance-window] Deferring '${reason}' restart for '${codeName}' (${gate}) \u2014 re-checking in ${RESTART_DEFER_RECHECK_MS / 1e3}s`
3742
+ );
3743
+ scheduleSessionRestart(codeName, RESTART_DEFER_RECHECK_MS, reason, breakerReason);
3744
+ return;
3745
+ }
2957
3746
  stopPersistentSession(codeName, log);
2958
3747
  runningMcpHashes.delete(codeName);
2959
3748
  recordRestartForBreaker(codeName, breakerReason);
@@ -2969,11 +3758,40 @@ function cancelPendingSessionRestart(codeName) {
2969
3758
  pendingSessionRestarts.delete(codeName);
2970
3759
  log(`[hot-reload] Cancelled pending restart timer for '${codeName}' (another teardown path is handling it)`);
2971
3760
  }
3761
+ var RESTART_DEFER_RECHECK_MS = 6e4;
3762
+ var lastInboundMs = /* @__PURE__ */ new Map();
3763
+ function noteInbound(codeName) {
3764
+ lastInboundMs.set(codeName, Date.now());
3765
+ }
3766
+ function inboundAgeSecondsFor(codeName) {
3767
+ const ts = lastInboundMs.get(codeName);
3768
+ if (ts === void 0) return null;
3769
+ return Math.max(0, Math.floor((Date.now() - ts) / 1e3));
3770
+ }
3771
+ function paneLogAgeSecondsFor(codeName) {
3772
+ try {
3773
+ const mtimeMs = statSync3(paneLogPath(codeName)).mtimeMs;
3774
+ return Math.max(0, Math.floor((Date.now() - mtimeMs) / 1e3));
3775
+ } catch (err) {
3776
+ if (err?.code === "ENOENT") return null;
3777
+ return 0;
3778
+ }
3779
+ }
3780
+ function restartGateFor(codeName, breakerReason) {
3781
+ if (!isGateableRestartReason(breakerReason)) return "bypass";
3782
+ return decideRestartGate({
3783
+ window: cachedMaintenanceWindow,
3784
+ paneLogAgeSeconds: paneLogAgeSecondsFor(codeName),
3785
+ inboundAgeSeconds: inboundAgeSecondsFor(codeName),
3786
+ now: /* @__PURE__ */ new Date()
3787
+ });
3788
+ }
2972
3789
  var runningMcpHashes = /* @__PURE__ */ new Map();
2973
3790
  var runningMcpServerKeys = /* @__PURE__ */ new Map();
3791
+ var runningChannelSecretHashes = /* @__PURE__ */ new Map();
2974
3792
  function projectMcpHash(_codeName, projectDir) {
2975
3793
  try {
2976
- const raw = readFileSync5(join6(projectDir, ".mcp.json"), "utf-8");
3794
+ const raw = readFileSync9(join8(projectDir, ".mcp.json"), "utf-8");
2977
3795
  return createHash3("sha256").update(canonicalJson(JSON.parse(raw))).digest("hex");
2978
3796
  } catch {
2979
3797
  return null;
@@ -2981,7 +3799,7 @@ function projectMcpHash(_codeName, projectDir) {
2981
3799
  }
2982
3800
  function projectMcpKeys(_codeName, projectDir) {
2983
3801
  try {
2984
- const raw = readFileSync5(join6(projectDir, ".mcp.json"), "utf-8");
3802
+ const raw = readFileSync9(join8(projectDir, ".mcp.json"), "utf-8");
2985
3803
  const parsed = JSON.parse(raw);
2986
3804
  const servers = parsed.mcpServers;
2987
3805
  if (!servers || typeof servers !== "object") return /* @__PURE__ */ new Set();
@@ -2992,7 +3810,7 @@ function projectMcpKeys(_codeName, projectDir) {
2992
3810
  }
2993
3811
  function readMcpHttpServerConfig(projectDir, serverKey) {
2994
3812
  try {
2995
- const raw = readFileSync5(join6(projectDir, ".mcp.json"), "utf-8");
3813
+ const raw = readFileSync9(join8(projectDir, ".mcp.json"), "utf-8");
2996
3814
  const servers = JSON.parse(raw).mcpServers ?? {};
2997
3815
  const entry = servers[serverKey];
2998
3816
  if (entry && typeof entry.url === "string" && (entry.type === "http" || entry.type === void 0)) {
@@ -3047,10 +3865,16 @@ async function runAgentConnectivityProbes(agent, integrations, projectDir) {
3047
3865
  }
3048
3866
  }
3049
3867
  function stopPersistentSessionAndForgetMcpBaseline(codeName, breakerReason) {
3868
+ const gate = restartGateFor(codeName, breakerReason);
3869
+ if (gate !== "bypass" && gate !== "proceed") {
3870
+ log(`[maintenance-window] Deferring '${breakerReason}' restart for '${codeName}' (${gate})`);
3871
+ return;
3872
+ }
3050
3873
  cancelPendingSessionRestart(codeName);
3051
3874
  stopPersistentSession(codeName, log);
3052
3875
  runningMcpHashes.delete(codeName);
3053
3876
  runningMcpServerKeys.delete(codeName);
3877
+ runningChannelSecretHashes.delete(codeName);
3054
3878
  closeInjectedRunIfOpen(codeName, "cancelled", `session stopped (${breakerReason ?? "deprovision"})`);
3055
3879
  closeScheduledRunsForCode(codeName, "cancelled", `session stopped (${breakerReason ?? "deprovision"})`);
3056
3880
  if (breakerReason) {
@@ -3081,11 +3905,22 @@ function checkMcpConfigDriftAndScheduleRestart(codeName, projectDir) {
3081
3905
  const previousKeys = runningMcpServerKeys.get(codeName);
3082
3906
  const currentKeys = projectMcpKeys(codeName, projectDir);
3083
3907
  const decision = decideMcpRestartOnDrift(previousKeys, currentKeys);
3908
+ let quarantineOnlyDrift = false;
3909
+ if (decision.restart && !decision.membershipUnknown && currentKeys) {
3910
+ const quarantined = channelQuarantineStore().getQuarantinedKeys(codeName);
3911
+ const added = decision.addedOrRemoved.filter((k) => currentKeys.has(k));
3912
+ const removed = decision.addedOrRemoved.filter((k) => !currentKeys.has(k));
3913
+ quarantineOnlyDrift = added.length === 0 && removed.length > 0 && removed.every((k) => quarantined.has(k));
3914
+ }
3084
3915
  if (decision.membershipUnknown) {
3085
3916
  clearPresenceReaperState(codeName);
3086
3917
  log(
3087
3918
  `[hot-reload] .mcp.json key membership unavailable for '${codeName}' \u2014 clearing full presence-reaper state and restarting (ENG-5285/ENG-5537)`
3088
3919
  );
3920
+ } else if (quarantineOnlyDrift) {
3921
+ log(
3922
+ `[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)`
3923
+ );
3089
3924
  } else if (decision.restart) {
3090
3925
  clearPresenceReaperStateForKeys(codeName, new Set(decision.addedOrRemoved));
3091
3926
  log(
@@ -3096,7 +3931,7 @@ function checkMcpConfigDriftAndScheduleRestart(codeName, projectDir) {
3096
3931
  `[hot-reload] .mcp.json value-only drift for '${codeName}' \u2014 preserving presence-reaper state and NOT restarting (ENG-5285/ENG-5537)`
3097
3932
  );
3098
3933
  }
3099
- if (decision.restart) {
3934
+ if (decision.restart && !quarantineOnlyDrift) {
3100
3935
  scheduleSessionRestart(codeName, 0, ".mcp.json content change (ENG-4897)");
3101
3936
  runningMcpHashes.delete(codeName);
3102
3937
  runningMcpServerKeys.delete(codeName);
@@ -3108,6 +3943,35 @@ function checkMcpConfigDriftAndScheduleRestart(codeName, projectDir) {
3108
3943
  }
3109
3944
  }
3110
3945
  }
3946
+ function projectChannelSecretHash(projectDir) {
3947
+ try {
3948
+ const entries = parseEnvIntegrations(
3949
+ readFileSync9(join8(projectDir, ".env.integrations"), "utf-8")
3950
+ );
3951
+ return channelSecretValueHash(entries, CHANNEL_SECRET_ENV_KEYS);
3952
+ } catch {
3953
+ return null;
3954
+ }
3955
+ }
3956
+ function checkChannelSecretDriftAndScheduleRestart(codeName, projectDir) {
3957
+ const currentHash = projectChannelSecretHash(projectDir);
3958
+ const action = decideMcpDriftAction(currentHash, runningChannelSecretHashes.get(codeName));
3959
+ switch (action.kind) {
3960
+ case "no-config":
3961
+ case "no-drift":
3962
+ return;
3963
+ case "baseline":
3964
+ runningChannelSecretHashes.set(codeName, action.hash);
3965
+ return;
3966
+ case "drift":
3967
+ log(
3968
+ `[hot-reload] channel credential rotated for '${codeName}' (${action.previous} \u2192 ${action.current}) \u2014 respawning to load the new token (ENG-6062)`
3969
+ );
3970
+ scheduleSessionRestart(codeName, 0, "channel credential rotation (ENG-6062)");
3971
+ runningChannelSecretHashes.delete(codeName);
3972
+ return;
3973
+ }
3974
+ }
3111
3975
  var managedToolkitIdByAgentAndServerId = /* @__PURE__ */ new Map();
3112
3976
  function managedToolkitMapKey(agentId, serverId) {
3113
3977
  return `${agentId}\0${serverId}`;
@@ -3127,9 +3991,12 @@ var alertSlackWebhook = null;
3127
3991
  var alertedJobs = /* @__PURE__ */ new Set();
3128
3992
  var taskDisplayInfo = /* @__PURE__ */ new Map();
3129
3993
  var agentChannelTokens = /* @__PURE__ */ new Map();
3994
+ function __setAgentChannelTokensForTest(codeName, tokens) {
3995
+ agentChannelTokens.set(codeName, tokens);
3996
+ }
3130
3997
  var activeChannels = /* @__PURE__ */ new Map();
3131
3998
  var gatewaysStartedThisCycle = /* @__PURE__ */ new Set();
3132
- var state4 = {
3999
+ var state5 = {
3133
4000
  pid: process.pid,
3134
4001
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3135
4002
  lastPollAt: null,
@@ -3139,8 +4006,10 @@ var state4 = {
3139
4006
  };
3140
4007
  var registeredAgentsCache = /* @__PURE__ */ new Map();
3141
4008
  var agentFrameworkCache = /* @__PURE__ */ new Map();
4009
+ var migratedSecretStorage = /* @__PURE__ */ new Set();
3142
4010
  var frameworkBinaryChecked = /* @__PURE__ */ new Set();
3143
4011
  var agentRuntimeAuthenticated = false;
4012
+ var lastManagedSettingsStatus = null;
3144
4013
  function resolveAgentFramework(codeName) {
3145
4014
  const frameworkId = agentFrameworkCache.get(codeName) ?? "openclaw";
3146
4015
  return getFramework(frameworkId);
@@ -3163,10 +4032,11 @@ function clearAgentCaches(agentId, codeName) {
3163
4032
  if (channelCacheMutated) saveChannelHashCache2();
3164
4033
  }
3165
4034
  var cachedFrameworkVersion = null;
4035
+ var cachedMaintenanceWindow = null;
3166
4036
  var lastVersionCheckAt = 0;
3167
4037
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
3168
4038
  var lastResponsivenessProbeAt = 0;
3169
- var agtCliVersion = true ? "0.27.8" : "dev";
4039
+ var agtCliVersion = true ? "0.27.9-test.12" : "dev";
3170
4040
  function resolveBrewPath(execFileSync4) {
3171
4041
  try {
3172
4042
  const out = execFileSync4("which", ["brew"], { timeout: 5e3 }).toString().trim();
@@ -3179,7 +4049,7 @@ function resolveBrewPath(execFileSync4) {
3179
4049
  "/usr/local/bin/brew"
3180
4050
  ];
3181
4051
  for (const path of fallbacks) {
3182
- if (existsSync4(path)) return path;
4052
+ if (existsSync5(path)) return path;
3183
4053
  }
3184
4054
  return null;
3185
4055
  }
@@ -3189,7 +4059,7 @@ function claudeBinaryInstalled(execFileSync4) {
3189
4059
  "/opt/homebrew/bin/claude",
3190
4060
  "/usr/local/bin/claude"
3191
4061
  ];
3192
- if (canonical.some((path) => existsSync4(path))) return true;
4062
+ if (canonical.some((path) => existsSync5(path))) return true;
3193
4063
  try {
3194
4064
  execFileSync4("which", ["claude"], { timeout: 5e3 });
3195
4065
  return true;
@@ -3261,7 +4131,7 @@ async function ensureToolkitCli(toolkitSlug) {
3261
4131
  toolkitCliEnsured.add(toolkitSlug);
3262
4132
  return;
3263
4133
  }
3264
- brewBinDir = dirname(brewPath);
4134
+ brewBinDir = dirname3(brewPath);
3265
4135
  const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
3266
4136
  log(`[toolkit-install] ${toolkitSlug}: installing via brew (${pkg})\u2026`);
3267
4137
  if (isRoot) {
@@ -3342,36 +4212,38 @@ function claudeManagedSettingsPath() {
3342
4212
  function ensureClaudeManagedSettings(path = claudeManagedSettingsPath()) {
3343
4213
  try {
3344
4214
  let settings = {};
3345
- if (existsSync4(path)) {
3346
- const raw = readFileSync5(path, "utf-8").trim();
4215
+ if (existsSync5(path)) {
4216
+ const raw = readFileSync9(path, "utf-8").trim();
3347
4217
  if (raw) {
3348
4218
  let parsed;
3349
4219
  try {
3350
4220
  parsed = JSON.parse(raw);
3351
4221
  } catch (err) {
3352
4222
  log(`[managed-settings] ${path} is not valid JSON (${err.message}) \u2014 leaving as-is; Claude Code channels may be blocked`);
3353
- return;
4223
+ return "invalid_json";
3354
4224
  }
3355
4225
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3356
4226
  settings = parsed;
3357
4227
  }
3358
4228
  }
3359
4229
  }
3360
- if (settings.channelsEnabled === true) return;
4230
+ if (settings.channelsEnabled === true) return "ok";
3361
4231
  settings.channelsEnabled = true;
3362
- mkdirSync2(dirname(path), { recursive: true });
3363
- writeFileSync3(path, `${JSON.stringify(settings, null, 2)}
4232
+ mkdirSync4(dirname3(path), { recursive: true });
4233
+ writeFileSync4(path, `${JSON.stringify(settings, null, 2)}
3364
4234
  `);
3365
4235
  log(`[managed-settings] set channelsEnabled:true in ${path} (ENG-5786 \u2014 unblocks Claude Code channels)`);
4236
+ return "ok";
3366
4237
  } catch (err) {
3367
4238
  log(`[managed-settings] could not ensure ${path}: ${err.message} \u2014 Claude Code channels may be blocked; set channelsEnabled:true manually`);
4239
+ return "write_failed";
3368
4240
  }
3369
4241
  }
3370
4242
  async function ensureFrameworkBinary(frameworkId) {
3371
4243
  if (frameworkId !== "claude-code") return;
3372
4244
  if (frameworkBinaryChecked.has(frameworkId)) return;
3373
4245
  frameworkBinaryChecked.add(frameworkId);
3374
- ensureClaudeManagedSettings();
4246
+ lastManagedSettingsStatus = ensureClaudeManagedSettings();
3375
4247
  const { execFileSync: execFileSync4 } = await import("child_process");
3376
4248
  const brewPath = resolveBrewPath(execFileSync4);
3377
4249
  if (!brewPath) {
@@ -3399,11 +4271,11 @@ async function ensureFrameworkBinary(frameworkId) {
3399
4271
  log(`Claude Code install failed: ${err.message}`);
3400
4272
  return;
3401
4273
  }
3402
- const brewBinDir = dirname(brewPath);
4274
+ const brewBinDir = dirname3(brewPath);
3403
4275
  if (!process.env.PATH?.split(":").includes(brewBinDir)) {
3404
4276
  process.env.PATH = `${brewBinDir}:${process.env.PATH ?? ""}`;
3405
4277
  }
3406
- if (existsSync4("/home/linuxbrew/.linuxbrew/bin/claude")) {
4278
+ if (existsSync5("/home/linuxbrew/.linuxbrew/bin/claude")) {
3407
4279
  log("Claude Code installed successfully");
3408
4280
  } else {
3409
4281
  log("Claude Code install completed but binary not found at expected path \u2014 check brew logs");
@@ -3415,17 +4287,17 @@ async function ensureFrameworkBinary(frameworkId) {
3415
4287
  var CLAUDE_CODE_UPGRADE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
3416
4288
  var claudeCodeUpgradeInFlight = false;
3417
4289
  function claudeCodeUpgradeMarkerPath() {
3418
- return join6(homedir4(), ".augmented", ".last-claude-code-upgrade-check");
4290
+ return join8(homedir4(), ".augmented", ".last-claude-code-upgrade-check");
3419
4291
  }
3420
4292
  function stampClaudeCodeUpgradeMarker() {
3421
4293
  try {
3422
- writeFileSync3(claudeCodeUpgradeMarkerPath(), String(Date.now()));
4294
+ writeFileSync4(claudeCodeUpgradeMarkerPath(), String(Date.now()));
3423
4295
  } catch {
3424
4296
  }
3425
4297
  }
3426
4298
  function claudeCodeUpgradeThrottled() {
3427
4299
  try {
3428
- const lastCheck = parseInt(readFileSync5(claudeCodeUpgradeMarkerPath(), "utf-8").trim(), 10);
4300
+ const lastCheck = parseInt(readFileSync9(claudeCodeUpgradeMarkerPath(), "utf-8").trim(), 10);
3429
4301
  if (!Number.isFinite(lastCheck)) return false;
3430
4302
  return Date.now() - lastCheck < CLAUDE_CODE_UPGRADE_CHECK_INTERVAL_MS;
3431
4303
  } catch {
@@ -3435,6 +4307,9 @@ function claudeCodeUpgradeThrottled() {
3435
4307
  async function maybeUpgradeClaudeCode() {
3436
4308
  if (claudeCodeUpgradeInFlight) return;
3437
4309
  if (claudeCodeUpgradeThrottled()) return;
4310
+ if (decideMaintenanceWindowGate({ window: cachedMaintenanceWindow, now: /* @__PURE__ */ new Date() }) === "defer") {
4311
+ return;
4312
+ }
3438
4313
  claudeCodeUpgradeInFlight = true;
3439
4314
  stampClaudeCodeUpgradeMarker();
3440
4315
  const { execFileSync: execFileSync4 } = await import("child_process");
@@ -3474,6 +4349,9 @@ ${r.stderr}`;
3474
4349
  });
3475
4350
  }
3476
4351
  var UPDATE_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
4352
+ function selfUpdateAppliedMarkerPath() {
4353
+ return join8(homedir4(), ".augmented", ".last-self-update-applied");
4354
+ }
3477
4355
  var selfUpdateUpToDateLogged = false;
3478
4356
  var restartAfterUpgrade = false;
3479
4357
  var pendingUpgradeVersion = null;
@@ -3495,7 +4373,7 @@ async function checkAndUpdateCli() {
3495
4373
  const isNpmGlobal = !isBrewFormula && resolvedPath.includes("node_modules");
3496
4374
  if (!isBrewFormula && !isNpmGlobal) return;
3497
4375
  const { readFileSync: readF, writeFileSync: writeF } = await import("fs");
3498
- const markerPath = join6(homedir4(), ".augmented", ".last-update-check");
4376
+ const markerPath = join8(homedir4(), ".augmented", ".last-update-check");
3499
4377
  try {
3500
4378
  const lastCheck = parseInt(readF(markerPath, "utf-8").trim(), 10);
3501
4379
  if (Date.now() - lastCheck < UPDATE_CHECK_INTERVAL_MS) return;
@@ -3537,6 +4415,28 @@ async function checkAndUpdateCliViaBrew() {
3537
4415
  if (agtOutdated) {
3538
4416
  const installed = agtOutdated.installed_versions?.[0] ?? "unknown";
3539
4417
  const latest = agtOutdated.current_version ?? "unknown";
4418
+ if (decideMaintenanceWindowGate({ window: cachedMaintenanceWindow, now: /* @__PURE__ */ new Date() }) === "defer") {
4419
+ log(
4420
+ `[self-update] agt CLI ${installed} \u2192 ${latest} (brew) deferred \u2014 ` + formatWindowDeferLogLine(cachedMaintenanceWindow ?? {}, /* @__PURE__ */ new Date())
4421
+ );
4422
+ return;
4423
+ }
4424
+ const coalesceWindowMs = resolveCoalesceWindowMs();
4425
+ const coalesce = decideSelfUpdateCoalesce({
4426
+ windowMs: coalesceWindowMs,
4427
+ lastAppliedMs: readLastSelfUpdateAppliedMs(selfUpdateAppliedMarkerPath()),
4428
+ now: Date.now()
4429
+ });
4430
+ if (!coalesce.proceed) {
4431
+ log(formatCoalesceDeferLogLine({
4432
+ installed,
4433
+ latest,
4434
+ channelLabel: "brew",
4435
+ remainingMs: coalesce.remainingMs,
4436
+ windowMs: coalesceWindowMs
4437
+ }));
4438
+ return;
4439
+ }
3540
4440
  log(`[self-update] agt CLI update available: ${installed} \u2192 ${latest}. Upgrading via brew...`);
3541
4441
  try {
3542
4442
  execFileSync4(brewPath, ["upgrade", "integrity-labs/tap/agt"], {
@@ -3544,6 +4444,7 @@ async function checkAndUpdateCliViaBrew() {
3544
4444
  stdio: "pipe"
3545
4445
  });
3546
4446
  log(`[self-update] agt CLI upgraded to ${latest}. Scheduling manager restart so the new binary takes effect.`);
4447
+ stampLastSelfUpdateApplied(selfUpdateAppliedMarkerPath());
3547
4448
  restartAfterUpgrade = true;
3548
4449
  pendingUpgradeVersion = latest;
3549
4450
  } catch (err) {
@@ -3558,7 +4459,6 @@ async function checkAndUpdateCliViaBrew() {
3558
4459
  }
3559
4460
  }
3560
4461
  async function checkAndUpdateCliViaNpm() {
3561
- const { execFileSync: execFileSync4 } = await import("child_process");
3562
4462
  if (agtCliVersion === "dev") return;
3563
4463
  const channel = process.env.AGT_CLI_RELEASE_CHANNEL || "latest";
3564
4464
  let latest;
@@ -3584,6 +4484,18 @@ async function checkAndUpdateCliViaNpm() {
3584
4484
  log(`[self-update] npm registry fetch failed: ${err.message}`);
3585
4485
  return;
3586
4486
  }
4487
+ const urgentTarget = await fetchUrgentDistTagVersion();
4488
+ if (isUrgentUpgrade({
4489
+ urgentTarget,
4490
+ installed: agtCliVersion,
4491
+ isNewer: (installed, candidate) => candidate !== installed && !isOlderSemverTuple(installed, candidate)
4492
+ })) {
4493
+ log(
4494
+ `[self-update] URGENT agt CLI hotfix ${agtCliVersion} \u2192 ${urgentTarget} (overrides channel=${channel} + maintenance window). Upgrading via npm...`
4495
+ );
4496
+ await installAgtCliViaNpm(urgentTarget, "urgent");
4497
+ return;
4498
+ }
3587
4499
  let shouldUpdate;
3588
4500
  if (channel === "latest") {
3589
4501
  shouldUpdate = isNewerSemver(agtCliVersion, latest);
@@ -3604,27 +4516,63 @@ async function checkAndUpdateCliViaNpm() {
3604
4516
  }
3605
4517
  return;
3606
4518
  }
4519
+ if (decideMaintenanceWindowGate({ window: cachedMaintenanceWindow, now: /* @__PURE__ */ new Date() }) === "defer") {
4520
+ log(
4521
+ `[self-update] agt CLI ${agtCliVersion} \u2192 ${latest} (channel=${channel}) deferred \u2014 ` + formatWindowDeferLogLine(cachedMaintenanceWindow ?? {}, /* @__PURE__ */ new Date())
4522
+ );
4523
+ return;
4524
+ }
4525
+ const coalesceWindowMs = resolveCoalesceWindowMs();
4526
+ const coalesce = decideSelfUpdateCoalesce({
4527
+ windowMs: coalesceWindowMs,
4528
+ lastAppliedMs: readLastSelfUpdateAppliedMs(selfUpdateAppliedMarkerPath()),
4529
+ now: Date.now()
4530
+ });
4531
+ if (!coalesce.proceed) {
4532
+ log(formatCoalesceDeferLogLine({
4533
+ installed: agtCliVersion,
4534
+ latest,
4535
+ channelLabel: `npm, channel=${channel}`,
4536
+ remainingMs: coalesce.remainingMs,
4537
+ windowMs: coalesceWindowMs
4538
+ }));
4539
+ return;
4540
+ }
3607
4541
  log(`[self-update] agt CLI update available: ${agtCliVersion} \u2192 ${latest} (channel=${channel}). Upgrading via npm...`);
4542
+ await installAgtCliViaNpm(latest, `channel=${channel}`);
4543
+ }
4544
+ async function fetchUrgentDistTagVersion() {
4545
+ try {
4546
+ const res = await fetch("https://registry.npmjs.org/@integrity-labs/agt-cli/urgent", {
4547
+ signal: AbortSignal.timeout(1e4),
4548
+ headers: { Accept: "application/json" }
4549
+ });
4550
+ if (!res.ok) return null;
4551
+ const body = await res.json();
4552
+ return body.version ?? null;
4553
+ } catch {
4554
+ return null;
4555
+ }
4556
+ }
4557
+ async function installAgtCliViaNpm(version, reasonLabel) {
4558
+ const { execFileSync: execFileSync4 } = await import("child_process");
3608
4559
  const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
3609
4560
  const cmd = isRoot ? "npm" : "sudo";
3610
- const args = isRoot ? [
3611
- "install",
3612
- "-g",
3613
- `@integrity-labs/agt-cli@${latest}`,
3614
- "--registry=https://registry.npmjs.org"
3615
- ] : [
3616
- "-n",
3617
- "npm",
4561
+ const installArgs = [
3618
4562
  "install",
3619
4563
  "-g",
3620
- `@integrity-labs/agt-cli@${latest}`,
4564
+ `@integrity-labs/agt-cli@${version}`,
3621
4565
  "--registry=https://registry.npmjs.org"
3622
4566
  ];
4567
+ const args = isRoot ? installArgs : ["-n", "npm", ...installArgs];
3623
4568
  try {
3624
4569
  execFileSync4(cmd, args, { timeout: 18e4, stdio: "pipe" });
3625
- log(`[self-update] agt CLI upgraded to ${latest}. Scheduling manager restart so the new binary takes effect.`);
4570
+ log(
4571
+ `[self-update] agt CLI upgraded to ${version} (${reasonLabel}). Scheduling manager restart so the new binary takes effect.`
4572
+ );
4573
+ stampLastSelfUpdateApplied(selfUpdateAppliedMarkerPath());
3626
4574
  restartAfterUpgrade = true;
3627
- pendingUpgradeVersion = latest;
4575
+ pendingUpgradeVersion = version;
3628
4576
  } catch (err) {
3629
4577
  log(`[self-update] npm upgrade failed: ${err.message}`);
3630
4578
  }
@@ -3682,10 +4630,10 @@ async function applyClaudeAuthToEnv(childEnv, label) {
3682
4630
  throw new Error("claude_auth_mode=api_key but /host/exchange returned no decrypted key");
3683
4631
  }
3684
4632
  childEnv.ANTHROPIC_API_KEY = exchange.anthropicApiKey;
3685
- const claudeDir = join6(homedir4(), ".claude");
4633
+ const claudeDir = join8(homedir4(), ".claude");
3686
4634
  for (const filename of [".credentials.json", "credentials.json"]) {
3687
- const p = join6(claudeDir, filename);
3688
- if (existsSync4(p)) {
4635
+ const p = join8(claudeDir, filename);
4636
+ if (existsSync5(p)) {
3689
4637
  try {
3690
4638
  rmSync2(p, { force: true });
3691
4639
  log(`[${label}] Removed ${p} (api_key mode \u2014 preventing OAuth fallback)`);
@@ -3697,16 +4645,93 @@ async function applyClaudeAuthToEnv(childEnv, label) {
3697
4645
  delete childEnv.ANTHROPIC_API_KEY;
3698
4646
  }
3699
4647
  }
4648
+ var evalEmptyMcpConfigPath = null;
4649
+ function ensureEvalEmptyMcpConfig() {
4650
+ if (evalEmptyMcpConfigPath && existsSync5(evalEmptyMcpConfigPath)) return evalEmptyMcpConfigPath;
4651
+ const dir = join8(homedir4(), ".augmented");
4652
+ try {
4653
+ mkdirSync4(dir, { recursive: true });
4654
+ } catch {
4655
+ }
4656
+ const p = join8(dir, ".eval-empty-mcp.json");
4657
+ writeFileSync4(p, JSON.stringify({ mcpServers: {} }));
4658
+ evalEmptyMcpConfigPath = p;
4659
+ return p;
4660
+ }
4661
+ async function runEvalClaude(prompt, model) {
4662
+ const childEnv = { ...process.env };
4663
+ await applyClaudeAuthToEnv(childEnv, "conversation-eval");
4664
+ const emptyMcp = ensureEvalEmptyMcpConfig();
4665
+ const args = [
4666
+ "-p",
4667
+ prompt,
4668
+ "--model",
4669
+ model,
4670
+ "--output-format",
4671
+ "text",
4672
+ // Isolation: only the empty MCP config, ignore project/user config + no tools.
4673
+ "--mcp-config",
4674
+ emptyMcp,
4675
+ "--strict-mcp-config",
4676
+ "--permission-mode",
4677
+ "auto",
4678
+ "--allowedTools",
4679
+ ""
4680
+ ];
4681
+ const { stdout } = await execFilePromiseLong(resolveClaudeBinary(), args, {
4682
+ cwd: homedir4(),
4683
+ timeout: 12e4,
4684
+ stdin: "ignore",
4685
+ env: childEnv,
4686
+ onSpawn: (pid) => registerClaudeSpawn({ pid, started_at: Date.now(), kind: "other" }),
4687
+ onExit: (pid) => unregisterClaudeSpawn(pid)
4688
+ });
4689
+ return stdout;
4690
+ }
4691
+ var conversationEvalBackend = null;
4692
+ function resolveConversationEvalBackend() {
4693
+ if (conversationEvalBackend) return conversationEvalBackend;
4694
+ const kind = (process.env["AGT_CONV_EVAL_BACKEND"] ?? "claude-p").trim().toLowerCase();
4695
+ if (kind === "local") {
4696
+ const url = process.env["AGT_CONV_EVAL_LOCAL_URL"]?.trim() || DEFAULT_LOCAL_EVAL_URL;
4697
+ const model = process.env["AGT_CONV_EVAL_LOCAL_MODEL"]?.trim() || DEFAULT_LOCAL_EVAL_MODEL;
4698
+ const apiKey = process.env["AGT_CONV_EVAL_LOCAL_API_KEY"]?.trim() || void 0;
4699
+ const safeUrl = (() => {
4700
+ try {
4701
+ const parsed = new URL(url);
4702
+ return `${parsed.origin}${parsed.pathname}`;
4703
+ } catch {
4704
+ return "[invalid-url]";
4705
+ }
4706
+ })();
4707
+ log(`[conversation-eval] backend=local url=${safeUrl} model=${model}`);
4708
+ conversationEvalBackend = { model, run: (prompt) => runLocalEvalChat(prompt, { url, model, apiKey }) };
4709
+ return conversationEvalBackend;
4710
+ }
4711
+ if (kind === "" || kind === "claude-p") {
4712
+ const model = process.env["AGT_CONV_EVAL_CLAUDE_MODEL"]?.trim() || DEFAULT_CLAUDE_EVAL_MODEL;
4713
+ conversationEvalBackend = { model, run: (prompt) => runEvalClaude(prompt, model) };
4714
+ return conversationEvalBackend;
4715
+ }
4716
+ log(`[conversation-eval] invalid AGT_CONV_EVAL_BACKEND='${kind}' \u2014 expected 'claude-p' or 'local'; evaluation disabled`);
4717
+ conversationEvalBackend = {
4718
+ model: "invalid-conversation-eval-backend",
4719
+ run: async () => {
4720
+ throw new Error(`Unsupported AGT_CONV_EVAL_BACKEND='${kind}'. Expected 'claude-p' or 'local'.`);
4721
+ }
4722
+ };
4723
+ return conversationEvalBackend;
4724
+ }
3700
4725
  function loadGatewayPorts() {
3701
4726
  try {
3702
- return JSON.parse(readFileSync5(GATEWAY_PORTS_FILE, "utf-8"));
4727
+ return JSON.parse(readFileSync9(GATEWAY_PORTS_FILE, "utf-8"));
3703
4728
  } catch {
3704
4729
  return {};
3705
4730
  }
3706
4731
  }
3707
4732
  function saveGatewayPorts(ports) {
3708
- mkdirSync2(AUGMENTED_DIR, { recursive: true });
3709
- writeFileSync3(GATEWAY_PORTS_FILE, JSON.stringify(ports, null, 2));
4733
+ mkdirSync4(AUGMENTED_DIR, { recursive: true });
4734
+ writeFileSync4(GATEWAY_PORTS_FILE, JSON.stringify(ports, null, 2));
3710
4735
  }
3711
4736
  function allocatePort(codeName) {
3712
4737
  const ports = loadGatewayPorts();
@@ -3729,10 +4754,10 @@ function freePort(codeName) {
3729
4754
  }
3730
4755
  }
3731
4756
  function getStateFile() {
3732
- return join6(config?.configDir ?? join6(process.env["HOME"] ?? "/tmp", ".augmented"), "manager-state.json");
4757
+ return join8(config?.configDir ?? join8(process.env["HOME"] ?? "/tmp", ".augmented"), "manager-state.json");
3733
4758
  }
3734
4759
  function channelHashCacheDir() {
3735
- return config?.configDir ?? join6(process.env["HOME"] ?? "/tmp", ".augmented");
4760
+ return config?.configDir ?? join8(process.env["HOME"] ?? "/tmp", ".augmented");
3736
4761
  }
3737
4762
  function loadChannelHashCache2() {
3738
4763
  loadChannelHashCache(agentState.knownChannelConfigHashes, channelHashCacheDir());
@@ -3740,10 +4765,34 @@ function loadChannelHashCache2() {
3740
4765
  function saveChannelHashCache2() {
3741
4766
  saveChannelHashCache(agentState.knownChannelConfigHashes, channelHashCacheDir());
3742
4767
  }
4768
+ var _channelQuarantineStore = null;
4769
+ function channelQuarantineStore() {
4770
+ if (!_channelQuarantineStore) {
4771
+ const dir = config?.configDir ?? join8(process.env["HOME"] ?? "/tmp", ".augmented");
4772
+ _channelQuarantineStore = new ChannelQuarantineStore(defaultQuarantinePath(dir));
4773
+ }
4774
+ return _channelQuarantineStore;
4775
+ }
4776
+ function channelQuarantineMode() {
4777
+ const v = process.env["AGT_CHANNEL_QUARANTINE_MODE"]?.toLowerCase();
4778
+ if (v === "off") return "off";
4779
+ if (v === "enforce") return "enforce";
4780
+ return "shadow";
4781
+ }
4782
+ function channelQuarantineDwellMs() {
4783
+ const raw = parseInt(process.env["AGT_CHANNEL_QUARANTINE_DWELL_MS"] ?? "", 10);
4784
+ return Number.isFinite(raw) && raw >= 0 ? raw : 18e4;
4785
+ }
4786
+ function setWithout(source, remove) {
4787
+ if (remove.size === 0) return new Set(source);
4788
+ const out = /* @__PURE__ */ new Set();
4789
+ for (const v of source) if (!remove.has(v)) out.add(v);
4790
+ return out;
4791
+ }
3743
4792
  function send(msg) {
3744
4793
  if (msg.type === "state-update") {
3745
4794
  try {
3746
- writeFileSync3(getStateFile(), JSON.stringify(msg.state, null, 2));
4795
+ atomicWriteFileSync(getStateFile(), JSON.stringify(msg.state, null, 2));
3747
4796
  } catch {
3748
4797
  }
3749
4798
  }
@@ -3774,9 +4823,9 @@ function log(msg) {
3774
4823
  `;
3775
4824
  if (!managerLogPath) {
3776
4825
  try {
3777
- managerLogPath = join6(homedir4(), ".augmented", "manager.log");
3778
- mkdirSync2(dirname(managerLogPath), { recursive: true });
3779
- if (existsSync4(managerLogPath)) {
4826
+ managerLogPath = join8(homedir4(), ".augmented", "manager.log");
4827
+ mkdirSync4(dirname3(managerLogPath), { recursive: true });
4828
+ if (existsSync5(managerLogPath)) {
3780
4829
  chmodSync(managerLogPath, 384);
3781
4830
  }
3782
4831
  } catch {
@@ -3804,7 +4853,7 @@ function sha256(content) {
3804
4853
  }
3805
4854
  function hashFile(filePath) {
3806
4855
  try {
3807
- const content = readFileSync5(filePath, "utf-8");
4856
+ const content = readFileSync9(filePath, "utf-8");
3808
4857
  return sha256(content);
3809
4858
  } catch {
3810
4859
  return null;
@@ -3828,13 +4877,13 @@ function parseSkillFrontmatter(content) {
3828
4877
  return out;
3829
4878
  }
3830
4879
  async function refreshSkillsIndexInClaudeMd(configDir, codeName, log2) {
3831
- const { readdirSync: readdirSync4, readFileSync: rfs, existsSync: ex, writeFileSync: writeFileSync4 } = await import("fs");
3832
- const skillsDir = join6(configDir, codeName, "project", ".claude", "skills");
3833
- const claudeMdPath = join6(configDir, codeName, "project", "CLAUDE.md");
4880
+ const { readdirSync: readdirSync5, readFileSync: rfs, existsSync: ex, writeFileSync: writeFileSync5 } = await import("fs");
4881
+ const skillsDir = join8(configDir, codeName, "project", ".claude", "skills");
4882
+ const claudeMdPath = join8(configDir, codeName, "project", "CLAUDE.md");
3834
4883
  if (!ex(skillsDir) || !ex(claudeMdPath)) return;
3835
4884
  const entries = [];
3836
- for (const dir of readdirSync4(skillsDir).sort()) {
3837
- const skillFile = join6(skillsDir, dir, "SKILL.md");
4885
+ for (const dir of readdirSync5(skillsDir).sort()) {
4886
+ const skillFile = join8(skillsDir, dir, "SKILL.md");
3838
4887
  if (!ex(skillFile)) continue;
3839
4888
  try {
3840
4889
  const { name, description } = parseSkillFrontmatter(rfs(skillFile, "utf-8"));
@@ -3860,9 +4909,9 @@ Integration skills under \`.claude/skills/integration-*/SKILL.md\` are **read-on
3860
4909
 
3861
4910
  **Never edit \`.claude/skills/integration-*/SKILL.md\` files directly.** If you do, your edit will be silently overwritten on the next manager refresh, AND it won't propagate to other agents using the same integration.
3862
4911
 
3863
- To change an integration's behavior (add a rule, update a default, tell the integration not to do something), call the **\`plugin.improve\`** MCP tool with the user's request. The tool calls the platform API, which uses an LLM to translate the request into a structured update of the integration's typed context fields and/or freeform overrides text. You'll get a diff back to show the user; on confirmation, call the tool again with \`auto_apply: true\` to apply.
4912
+ To change an integration's behavior (add a rule, update a default, tell the integration not to do something), call the **\`plugin_improve\`** MCP tool with the user's request. The tool calls the platform API, which uses an LLM to translate the request into a structured update of the integration's typed context fields and/or freeform overrides text. You'll get a diff back to show the user; on confirmation, call the tool again with \`auto_apply: true\` to apply.
3864
4913
 
3865
- Examples of when to use \`plugin.improve\`:
4914
+ Examples of when to use \`plugin_improve\`:
3866
4915
  - *"Update the Coding integration so we always use trunk instead of main"*
3867
4916
  - *"Add a rule to the Coding integration: never merge directly to main, always open a PR"*
3868
4917
  - *"Tell the Knowledge Base integration not to return results below 70% relevance"*
@@ -3876,16 +4925,16 @@ ${SKILLS_INDEX_END}`;
3876
4925
  next = current.trimEnd() + "\n\n" + section + "\n";
3877
4926
  }
3878
4927
  if (next !== current) {
3879
- writeFileSync4(claudeMdPath, next, "utf-8");
4928
+ writeFileSync5(claudeMdPath, next, "utf-8");
3880
4929
  log2(`Refreshed skills index in CLAUDE.md for '${codeName}' (${entries.length} skills)`);
3881
4930
  }
3882
4931
  }
3883
4932
  async function migrateToProfiles() {
3884
4933
  const homeDir = process.env["HOME"] ?? "/tmp";
3885
- const sharedConfigPath = join6(homeDir, ".openclaw", "openclaw.json");
4934
+ const sharedConfigPath = join8(homeDir, ".openclaw", "openclaw.json");
3886
4935
  let sharedConfig;
3887
4936
  try {
3888
- sharedConfig = JSON.parse(readFileSync5(sharedConfigPath, "utf-8"));
4937
+ sharedConfig = JSON.parse(readFileSync9(sharedConfigPath, "utf-8"));
3889
4938
  } catch {
3890
4939
  return;
3891
4940
  }
@@ -3898,19 +4947,19 @@ async function migrateToProfiles() {
3898
4947
  const codeName = agentEntry["id"];
3899
4948
  if (!codeName) continue;
3900
4949
  if (codeName === "main") continue;
3901
- const profileDir = join6(homeDir, `.openclaw-${codeName}`);
3902
- if (existsSync4(join6(profileDir, "openclaw.json"))) continue;
4950
+ const profileDir = join8(homeDir, `.openclaw-${codeName}`);
4951
+ if (existsSync5(join8(profileDir, "openclaw.json"))) continue;
3903
4952
  log(`Migrating agent '${codeName}' to per-agent profile`);
3904
4953
  if (adapter.seedProfileConfig) {
3905
4954
  adapter.seedProfileConfig(codeName);
3906
4955
  }
3907
- const sharedAuthDir = join6(homeDir, ".openclaw", "agents", codeName, "agent");
3908
- const profileAuthDir = join6(profileDir, "agents", codeName, "agent");
3909
- const authFile = join6(sharedAuthDir, "auth-profiles.json");
3910
- if (existsSync4(authFile)) {
3911
- mkdirSync2(profileAuthDir, { recursive: true });
3912
- const authContent = readFileSync5(authFile, "utf-8");
3913
- writeFileSync3(join6(profileAuthDir, "auth-profiles.json"), authContent);
4956
+ const sharedAuthDir = join8(homeDir, ".openclaw", "agents", codeName, "agent");
4957
+ const profileAuthDir = join8(profileDir, "agents", codeName, "agent");
4958
+ const authFile = join8(sharedAuthDir, "auth-profiles.json");
4959
+ if (existsSync5(authFile)) {
4960
+ mkdirSync4(profileAuthDir, { recursive: true });
4961
+ const authContent = readFileSync9(authFile, "utf-8");
4962
+ writeFileSync4(join8(profileAuthDir, "auth-profiles.json"), authContent);
3914
4963
  }
3915
4964
  allocatePort(codeName);
3916
4965
  migrated++;
@@ -3948,7 +4997,7 @@ function readGatewayToken(codeName) {
3948
4997
  }
3949
4998
  const homeDir = process.env["HOME"] ?? "/tmp";
3950
4999
  try {
3951
- const cfg = JSON.parse(readFileSync5(join6(homeDir, `.openclaw-${codeName}`, "openclaw.json"), "utf-8"));
5000
+ const cfg = JSON.parse(readFileSync9(join8(homeDir, `.openclaw-${codeName}`, "openclaw.json"), "utf-8"));
3952
5001
  return cfg?.gateway?.auth?.token;
3953
5002
  } catch {
3954
5003
  return void 0;
@@ -3957,18 +5006,18 @@ function readGatewayToken(codeName) {
3957
5006
  var GATEWAY_HUNG_TIMEOUT_MS = 5 * 6e4;
3958
5007
  function isGatewayHung(codeName) {
3959
5008
  const homeDir = process.env["HOME"] ?? "/tmp";
3960
- const jobsPath = join6(homeDir, `.openclaw-${codeName}`, "cron", "jobs.json");
3961
- if (!existsSync4(jobsPath)) return false;
5009
+ const jobsPath = join8(homeDir, `.openclaw-${codeName}`, "cron", "jobs.json");
5010
+ if (!existsSync5(jobsPath)) return false;
3962
5011
  try {
3963
- const data = JSON.parse(readFileSync5(jobsPath, "utf-8"));
5012
+ const data = JSON.parse(readFileSync9(jobsPath, "utf-8"));
3964
5013
  const jobs = data.jobs ?? data;
3965
5014
  if (!Array.isArray(jobs)) return false;
3966
5015
  const now = Date.now();
3967
5016
  for (const job of jobs) {
3968
- const state5 = job.state;
3969
- if (!state5) continue;
3970
- const runStartedAt = state5.runStartedAtMs;
3971
- const isRunning = state5.status === "running" || state5.running === true;
5017
+ const state6 = job.state;
5018
+ if (!state6) continue;
5019
+ const runStartedAt = state6.runStartedAtMs;
5020
+ const isRunning = state6.status === "running" || state6.running === true;
3972
5021
  if (isRunning && runStartedAt && now - runStartedAt > GATEWAY_HUNG_TIMEOUT_MS) {
3973
5022
  return true;
3974
5023
  }
@@ -3993,19 +5042,19 @@ async function ensureGatewayRunning(codeName, adapter) {
3993
5042
  }
3994
5043
  await new Promise((r) => setTimeout(r, 2e3));
3995
5044
  const homeDir = process.env["HOME"] ?? "/tmp";
3996
- const cronJobsPath = join6(homeDir, `.openclaw-${codeName}`, "cron", "jobs.json");
5045
+ const cronJobsPath = join8(homeDir, `.openclaw-${codeName}`, "cron", "jobs.json");
3997
5046
  clearStaleCronRunState(cronJobsPath);
3998
5047
  } else {
3999
5048
  if (status.port) {
4000
5049
  try {
4001
5050
  const homeDir = process.env["HOME"] ?? "/tmp";
4002
- const configPath = join6(homeDir, `.openclaw-${codeName}`, "openclaw.json");
4003
- if (existsSync4(configPath)) {
4004
- const cfg = JSON.parse(readFileSync5(configPath, "utf-8"));
5051
+ const configPath = join8(homeDir, `.openclaw-${codeName}`, "openclaw.json");
5052
+ if (existsSync5(configPath)) {
5053
+ const cfg = JSON.parse(readFileSync9(configPath, "utf-8"));
4005
5054
  if (cfg.gateway?.port !== status.port) {
4006
5055
  if (!cfg.gateway) cfg.gateway = {};
4007
5056
  cfg.gateway.port = status.port;
4008
- writeFileSync3(configPath, JSON.stringify(cfg, null, 2));
5057
+ writeFileSync4(configPath, JSON.stringify(cfg, null, 2));
4009
5058
  }
4010
5059
  }
4011
5060
  } catch {
@@ -4025,12 +5074,12 @@ async function ensureGatewayRunning(codeName, adapter) {
4025
5074
  gatewaysStartedThisCycle.add(codeName);
4026
5075
  try {
4027
5076
  const homeDir = process.env["HOME"] ?? "/tmp";
4028
- const configPath = join6(homeDir, `.openclaw-${codeName}`, "openclaw.json");
4029
- if (existsSync4(configPath)) {
4030
- const cfg = JSON.parse(readFileSync5(configPath, "utf-8"));
5077
+ const configPath = join8(homeDir, `.openclaw-${codeName}`, "openclaw.json");
5078
+ if (existsSync5(configPath)) {
5079
+ const cfg = JSON.parse(readFileSync9(configPath, "utf-8"));
4031
5080
  if (!cfg.gateway) cfg.gateway = {};
4032
5081
  cfg.gateway.port = port;
4033
- writeFileSync3(configPath, JSON.stringify(cfg, null, 2));
5082
+ writeFileSync4(configPath, JSON.stringify(cfg, null, 2));
4034
5083
  }
4035
5084
  } catch {
4036
5085
  }
@@ -4119,7 +5168,7 @@ async function pollCycle() {
4119
5168
  log,
4120
5169
  resolveFramework: (codeName) => agentFrameworkCache.get(codeName) ?? null,
4121
5170
  stopSession: (codeName) => {
4122
- stopPersistentSessionAndForgetMcpBaseline(codeName, "channel-set-change");
5171
+ stopPersistentSessionAndForgetMcpBaseline(codeName, "channel-restart-flag");
4123
5172
  agentState.persistentSessionAgents.delete(codeName);
4124
5173
  claudeAuthTupleBySession.delete(codeName);
4125
5174
  },
@@ -4169,7 +5218,7 @@ async function pollCycle() {
4169
5218
  const now = Date.now();
4170
5219
  if (now - lastVersionCheckAt > VERSION_CHECK_INTERVAL_MS) {
4171
5220
  try {
4172
- const firstAgent = state4.agents[0];
5221
+ const firstAgent = state5.agents[0];
4173
5222
  const versionAdapter = firstAgent ? resolveAgentFramework(firstAgent.codeName) : getFramework("openclaw");
4174
5223
  if (versionAdapter.getVersion) {
4175
5224
  cachedFrameworkVersion = await versionAdapter.getVersion();
@@ -4180,7 +5229,7 @@ async function pollCycle() {
4180
5229
  }
4181
5230
  try {
4182
5231
  const { detectHostSecurity } = await import("../host-security-6PDFG7F5.js");
4183
- const { collectDiagnostics } = await import("../persistent-session-ICYFLUAM.js");
5232
+ const { collectDiagnostics } = await import("../persistent-session-OL5EDYB4.js");
4184
5233
  const diagCodeNames = [...agentState.persistentSessionAgents];
4185
5234
  const agentDiagnostics = diagCodeNames.length > 0 ? collectDiagnostics(diagCodeNames) : void 0;
4186
5235
  let tailscaleHostname;
@@ -4213,6 +5262,14 @@ async function pollCycle() {
4213
5262
  const errId = createHash3("sha256").update(errText).digest("hex").slice(0, 12);
4214
5263
  log(`Claude auth detection failed (error_id=${errId})`);
4215
5264
  }
5265
+ const hostHasClaudeCode = state5.agents.some(
5266
+ (a) => agentFrameworkCache.get(a.codeName) === "claude-code"
5267
+ );
5268
+ if (hostHasClaudeCode) {
5269
+ lastManagedSettingsStatus = ensureClaudeManagedSettings();
5270
+ } else {
5271
+ lastManagedSettingsStatus = null;
5272
+ }
4216
5273
  const hbResp = await api.post("/host/heartbeat", {
4217
5274
  host_id: hostId,
4218
5275
  framework_version: cachedFrameworkVersion ?? void 0,
@@ -4229,15 +5286,26 @@ async function pollCycle() {
4229
5286
  // polling fallback (healRealtimeSocketDivergence forces it). Surfaced
4230
5287
  // so a sustained socket-down window is observable rather than hidden
4231
5288
  // behind a fresh heartbeat.
4232
- realtime_socket_connected: isRealtimeSocketConnected()
5289
+ realtime_socket_connected: isRealtimeSocketConnected(),
5290
+ // ENG-6068: outcome of the channels managed-settings assertion.
5291
+ // 'invalid_json' / 'write_failed' mean CC may be silently dropping
5292
+ // ALL channel inbound — the API opens a host-scoped alert on it.
5293
+ // Omitted (undefined) on hosts that haven't run the assertion
5294
+ // (non-claude-code frameworks).
5295
+ channels_setting_status: lastManagedSettingsStatus ?? void 0
4233
5296
  });
5297
+ if (hbResp?.maintenance_window) {
5298
+ cachedMaintenanceWindow = hbResp.maintenance_window;
5299
+ }
4234
5300
  try {
4235
- const { maybeUpdateClaudeCode } = await import("../claude-code-updater-4E5T2X3Z.js");
4236
- await maybeUpdateClaudeCode({
4237
- desiredVersion: hbResp?.desired_claude_code_version ?? "latest",
4238
- currentVersion: cachedFrameworkVersion,
4239
- log
4240
- });
5301
+ if (decideMaintenanceWindowGate({ window: cachedMaintenanceWindow, now: /* @__PURE__ */ new Date() }) === "proceed") {
5302
+ const { maybeUpdateClaudeCode } = await import("../claude-code-updater-4E5T2X3Z.js");
5303
+ await maybeUpdateClaudeCode({
5304
+ desiredVersion: hbResp?.desired_claude_code_version ?? "latest",
5305
+ currentVersion: cachedFrameworkVersion,
5306
+ log
5307
+ });
5308
+ }
4241
5309
  } catch (err) {
4242
5310
  log(`Claude Code updater error: ${err.message}`);
4243
5311
  }
@@ -4248,15 +5316,23 @@ async function pollCycle() {
4248
5316
  const {
4249
5317
  collectResponsivenessProbes,
4250
5318
  getResponsivenessIntervalMs
4251
- } = await import("../responsiveness-probe-WZNQ2762.js");
5319
+ } = await import("../responsiveness-probe-B6LJJRUD.js");
4252
5320
  const probeIntervalMs = getResponsivenessIntervalMs();
4253
5321
  if (now - lastResponsivenessProbeAt > probeIntervalMs) {
4254
5322
  const probeCodeNames = [...agentState.persistentSessionAgents];
4255
5323
  if (probeCodeNames.length > 0) {
4256
- const probes = collectResponsivenessProbes(probeCodeNames);
5324
+ const drainedGiveUps = /* @__PURE__ */ new Map();
5325
+ const probes = collectResponsivenessProbes(probeCodeNames).map((p) => {
5326
+ const giveUps = takeWatchdogGiveUpCount(p.code_name);
5327
+ if (giveUps > 0) drainedGiveUps.set(p.code_name, giveUps);
5328
+ return { ...p, input_stuck_give_ups: giveUps };
5329
+ });
4257
5330
  if (probes.length > 0) {
4258
5331
  void api.post("/host/responsiveness-probe", { host_id: hostId, probes }).catch((err) => {
4259
- log(`[responsiveness-probe] post failed: ${err.message}`);
5332
+ for (const [codeName, count] of drainedGiveUps) {
5333
+ creditWatchdogGiveUpCount(codeName, count);
5334
+ }
5335
+ log(`[responsiveness-probe] post failed (give-up counts re-credited): ${err.message}`);
4260
5336
  });
4261
5337
  }
4262
5338
  }
@@ -4308,7 +5384,7 @@ async function pollCycle() {
4308
5384
  for (const agent of agents) {
4309
5385
  const requested = agent.restart_requested_at ?? null;
4310
5386
  if (!requested) continue;
4311
- const prev = state4.agents.find((a) => a.agentId === agent.agent_id);
5387
+ const prev = state5.agents.find((a) => a.agentId === agent.agent_id);
4312
5388
  const lastProcessed = prev?.lastRestartProcessedAt ?? null;
4313
5389
  if (lastProcessed && Date.parse(lastProcessed) >= Date.parse(requested)) continue;
4314
5390
  log(`[restart] Dashboard requested restart for '${agent.code_name}' at ${requested}`);
@@ -4331,7 +5407,7 @@ async function pollCycle() {
4331
5407
  await processAgent(agent, agentStates);
4332
5408
  } catch (err) {
4333
5409
  log(`Error processing agent '${agent.code_name}': ${err.message}`);
4334
- const existing = state4.agents.find((a) => a.agentId === agent.agent_id);
5410
+ const existing = state5.agents.find((a) => a.agentId === agent.agent_id);
4335
5411
  if (existing) {
4336
5412
  agentStates.push(existing);
4337
5413
  } else {
@@ -4357,13 +5433,13 @@ async function pollCycle() {
4357
5433
  void maybeReportActivityCache({ api, log });
4358
5434
  const restartAckStateChanged = applyRestartAcks({
4359
5435
  agentStates,
4360
- priorAgents: state4.agents,
5436
+ priorAgents: state5.agents,
4361
5437
  restartAcks
4362
5438
  });
4363
5439
  if (restartAckStateChanged) {
4364
5440
  try {
4365
- const ackedState = { ...state4, agents: agentStates };
4366
- writeFileSync3(getStateFile(), JSON.stringify(ackedState, null, 2));
5441
+ const ackedState = { ...state5, agents: agentStates };
5442
+ atomicWriteFileSync(getStateFile(), JSON.stringify(ackedState, null, 2));
4367
5443
  } catch (err) {
4368
5444
  log(`[restart] failed to persist ack immediately: ${err.message}`);
4369
5445
  }
@@ -4380,7 +5456,7 @@ async function pollCycle() {
4380
5456
  } catch {
4381
5457
  }
4382
5458
  const currentIds = new Set(agents.map((a) => a.agent_id));
4383
- for (const prev of state4.agents) {
5459
+ for (const prev of state5.agents) {
4384
5460
  if (!currentIds.has(prev.agentId)) {
4385
5461
  log(`Agent '${prev.codeName}' removed from host (deleted or unassigned)`);
4386
5462
  const adapter = resolveAgentFramework(prev.codeName);
@@ -4393,7 +5469,7 @@ async function pollCycle() {
4393
5469
  }
4394
5470
  killAgentChannelProcesses(prev.codeName, { log });
4395
5471
  freePort(prev.codeName);
4396
- const agentDir = join6(adapter.getAgentDir(prev.codeName), "provision");
5472
+ const agentDir = join8(adapter.getAgentDir(prev.codeName), "provision");
4397
5473
  await cleanupAgentFiles(prev.codeName, agentDir);
4398
5474
  clearAgentCaches(prev.agentId, prev.codeName);
4399
5475
  }
@@ -4451,8 +5527,48 @@ async function pollCycle() {
4451
5527
  } catch {
4452
5528
  }
4453
5529
  },
5530
+ // ENG-6017: dialog-dismissal keys — one send-keys call per key so a
5531
+ // multi-key sequence can't be wrapped into a single bracketed paste
5532
+ // (see defaultArmSender in persistent-session.ts). The inter-key
5533
+ // delay is a blocking sleep; dialog sequences are at most two keys
5534
+ // with a 300ms gap, well within poll-cycle tolerances.
5535
+ sendKeys: (codeName, keys, interKeyDelayMs) => {
5536
+ try {
5537
+ for (let i = 0; i < keys.length; i++) {
5538
+ if (i > 0 && interKeyDelayMs > 0) {
5539
+ const view = new Int32Array(new SharedArrayBuffer(4));
5540
+ Atomics.wait(view, 0, 0, interKeyDelayMs);
5541
+ }
5542
+ syncExecFile("tmux", ["send-keys", "-t", `agt-${codeName}`, keys[i]], {
5543
+ stdio: "ignore",
5544
+ timeout: 2e3
5545
+ });
5546
+ }
5547
+ } catch {
5548
+ }
5549
+ },
4454
5550
  log,
4455
- now: () => Date.now()
5551
+ now: () => Date.now(),
5552
+ // ENG-6058: persist the give-up for the agent's channel servers —
5553
+ // their periodic sweeps turn it into a throttled user-facing
5554
+ // "please resend" notice for every conversation with an undrained
5555
+ // pending-inbound marker. Best-effort: a write failure is logged by
5556
+ // the watchdog, never fails the poll cycle.
5557
+ signalGiveUp: (codeName) => {
5558
+ const dir = join8(homedir4(), ".augmented", codeName);
5559
+ if (!existsSync5(dir)) return;
5560
+ atomicWriteFileSync(
5561
+ join8(dir, "watchdog-give-up.json"),
5562
+ JSON.stringify({ gave_up_at: (/* @__PURE__ */ new Date()).toISOString() })
5563
+ );
5564
+ }
5565
+ }, {
5566
+ // ENG-6055: heal-keystroke escalation. 'disturb' (default) retries
5567
+ // with x→BSpace→Enter after the first bare Enter fails — a bare
5568
+ // retry repeats the exact keystroke the stuck TUI state eats
5569
+ // (kylie 2026-06-05). AGT_INPUT_HEAL_MODE=bare reverts to the
5570
+ // pre-ENG-6055 behaviour if a future CC/tmux pairing misbehaves.
5571
+ healMode: process.env.AGT_INPUT_HEAL_MODE === "bare" ? "bare" : "disturb"
4456
5572
  });
4457
5573
  }
4458
5574
  const lastHealthCheck = lastHarvestAt.get("__cron_health__") ?? 0;
@@ -4481,10 +5597,10 @@ async function pollCycle() {
4481
5597
  }
4482
5598
  } catch {
4483
5599
  }
4484
- state4 = {
4485
- ...state4,
5600
+ state5 = {
5601
+ ...state5,
4486
5602
  lastPollAt: (/* @__PURE__ */ new Date()).toISOString(),
4487
- pollCount: state4.pollCount + 1,
5603
+ pollCount: state5.pollCount + 1,
4488
5604
  agents: agentStates,
4489
5605
  // ENG-5441: serialise trip state on every poll so manager restarts
4490
5606
  // never silently clear a tripped breaker. Cheap — only tripped
@@ -4495,9 +5611,9 @@ async function pollCycle() {
4495
5611
  log(`[poll-backoff] recovered after ${consecutivePollFailures} failure(s), resuming normal interval`);
4496
5612
  consecutivePollFailures = 0;
4497
5613
  }
4498
- send({ type: "state-update", state: state4 });
5614
+ send({ type: "state-update", state: state5 });
4499
5615
  } catch (err) {
4500
- state4.errorCount++;
5616
+ state5.errorCount++;
4501
5617
  const message = err.message;
4502
5618
  log(`Poll error: ${message}`);
4503
5619
  send({ type: "error", message });
@@ -4541,10 +5657,17 @@ async function processAgent(agent, agentStates) {
4541
5657
  agentId: agent.agent_id,
4542
5658
  log
4543
5659
  });
5660
+ void maybeEvaluateConversations({
5661
+ api,
5662
+ backend: resolveConversationEvalBackend(),
5663
+ codeName: agent.code_name,
5664
+ agentId: agent.agent_id,
5665
+ log
5666
+ });
4544
5667
  }
4545
5668
  const now = (/* @__PURE__ */ new Date()).toISOString();
4546
5669
  const adapter = resolveAgentFramework(agent.code_name);
4547
- let agentDir = join6(adapter.getAgentDir(agent.code_name), "provision");
5670
+ let agentDir = join8(adapter.getAgentDir(agent.code_name), "provision");
4548
5671
  if (agent.status === "draft" || agent.status === "paused") {
4549
5672
  if (previousKnownStatus !== agent.status) {
4550
5673
  log(`Agent '${agent.code_name}' is ${agent.status}, skipping provisioning`);
@@ -4583,7 +5706,7 @@ async function processAgent(agent, agentStates) {
4583
5706
  const residuals = {
4584
5707
  gatewayRunning: gatewayLiveness.running,
4585
5708
  portAllocated: Object.prototype.hasOwnProperty.call(ports, agent.code_name),
4586
- provisionDirExists: existsSync4(agentDir)
5709
+ provisionDirExists: existsSync5(agentDir)
4587
5710
  };
4588
5711
  if (!hasRevokedResiduals(residuals)) {
4589
5712
  agentStates.push({
@@ -4665,7 +5788,7 @@ async function processAgent(agent, agentStates) {
4665
5788
  });
4666
5789
  } catch (err) {
4667
5790
  log(`Refresh failed for '${agent.code_name}': ${err.message}`);
4668
- const existing = state4.agents.find((a) => a.agentId === agent.agent_id);
5791
+ const existing = state5.agents.find((a) => a.agentId === agent.agent_id);
4669
5792
  agentStates.push(existing ?? {
4670
5793
  agentId: agent.agent_id,
4671
5794
  codeName: agent.code_name,
@@ -4713,16 +5836,28 @@ async function processAgent(agent, agentStates) {
4713
5836
  const frameworkId = refreshData.agent.framework ?? "openclaw";
4714
5837
  agentFrameworkCache.set(agent.code_name, frameworkId);
4715
5838
  const frameworkAdapter = getFramework(frameworkId);
4716
- agentDir = join6(frameworkAdapter.getAgentDir(agent.code_name), "provision");
5839
+ agentDir = join8(frameworkAdapter.getAgentDir(agent.code_name), "provision");
4717
5840
  cacheAgentDeliveryMetadata(agent.code_name, refreshData);
5841
+ if (frameworkAdapter.migrateSecretStorage && !migratedSecretStorage.has(agent.code_name)) {
5842
+ try {
5843
+ frameworkAdapter.migrateSecretStorage(agent.code_name);
5844
+ migratedSecretStorage.add(agent.code_name);
5845
+ } catch (err) {
5846
+ log(`[mcp-migrate] failed for '${agent.code_name}': ${err.message}`);
5847
+ }
5848
+ }
4718
5849
  if (frameworkAdapter.seedProfileConfig) {
4719
5850
  frameworkAdapter.seedProfileConfig(agent.code_name);
4720
5851
  }
4721
5852
  const charterVersion = refreshData.charter.version;
4722
5853
  const toolsVersion = refreshData.tools.version;
4723
5854
  const known = agentState.knownVersions.get(agent.agent_id);
4724
- let lastProvisionAt = state4.agents.find((a) => a.agentId === agent.agent_id)?.lastProvisionAt ?? null;
4725
- const currentChannelIds = launchableChannelIds(refreshData.channel_configs);
5855
+ let lastProvisionAt = state5.agents.find((a) => a.agentId === agent.agent_id)?.lastProvisionAt ?? null;
5856
+ const quarantinedChannels = channelQuarantineStore().getQuarantinedKeys(agent.code_name);
5857
+ const currentChannelIds = setWithout(
5858
+ launchableChannelIds(refreshData.channel_configs),
5859
+ quarantinedChannels
5860
+ );
4726
5861
  const previousChannelIds = agentState.knownChannels.get(agent.agent_id);
4727
5862
  const channelsChanged = !previousChannelIds || currentChannelIds.size !== previousChannelIds.size || [...currentChannelIds].some((ch) => !previousChannelIds.has(ch)) || [...previousChannelIds].some((ch) => !currentChannelIds.has(ch));
4728
5863
  let channelConfigConverged = true;
@@ -4742,9 +5877,9 @@ async function processAgent(agent, agentStates) {
4742
5877
  try {
4743
5878
  const artifacts = generateArtifacts(agent, refreshData, frameworkAdapter);
4744
5879
  const changedFiles = [];
4745
- mkdirSync2(agentDir, { recursive: true });
5880
+ mkdirSync4(agentDir, { recursive: true });
4746
5881
  for (const artifact of artifacts) {
4747
- const filePath = join6(agentDir, artifact.relativePath);
5882
+ const filePath = join8(agentDir, artifact.relativePath);
4748
5883
  let existingHash;
4749
5884
  let newHash;
4750
5885
  let writeContent = artifact.content;
@@ -4763,8 +5898,8 @@ async function processAgent(agent, agentStates) {
4763
5898
  };
4764
5899
  newHash = sha256(stripDynamicSections(artifact.content));
4765
5900
  try {
4766
- const projectClaudeMd = join6(config.configDir, agent.code_name, "project", "CLAUDE.md");
4767
- const existing = readFileSync5(projectClaudeMd, "utf-8");
5901
+ const projectClaudeMd = join8(config.configDir, agent.code_name, "project", "CLAUDE.md");
5902
+ const existing = readFileSync9(projectClaudeMd, "utf-8");
4768
5903
  existingHash = sha256(stripDynamicSections(existing));
4769
5904
  } catch {
4770
5905
  existingHash = null;
@@ -4782,7 +5917,7 @@ async function processAgent(agent, agentStates) {
4782
5917
  const generatorKeys = Object.keys(generatorServers);
4783
5918
  let existingRaw = "";
4784
5919
  try {
4785
- existingRaw = readFileSync5(filePath, "utf-8");
5920
+ existingRaw = readFileSync9(filePath, "utf-8");
4786
5921
  } catch {
4787
5922
  }
4788
5923
  const existingServers = parseMcp(existingRaw);
@@ -4804,22 +5939,26 @@ async function processAgent(agent, agentStates) {
4804
5939
  }
4805
5940
  }
4806
5941
  if (changedFiles.length > 0) {
4807
- const isFirst = !existsSync4(join6(agentDir, "CHARTER.md"));
5942
+ const isFirst = !existsSync5(join8(agentDir, "CHARTER.md"));
4808
5943
  const verb = isFirst ? "Provisioning" : "Updating";
4809
5944
  const fileNames = changedFiles.map((f) => f.relativePath).join(", ");
4810
5945
  log(`${verb} '${agent.code_name}': ${fileNames}`);
4811
5946
  for (const file of changedFiles) {
4812
- const filePath = join6(agentDir, file.relativePath);
4813
- mkdirSync2(dirname(filePath), { recursive: true });
4814
- writeFileSync3(filePath, file.content);
5947
+ const filePath = join8(agentDir, file.relativePath);
5948
+ mkdirSync4(dirname3(filePath), { recursive: true });
5949
+ if (file.relativePath === ".mcp.json") {
5950
+ safeWriteJsonAtomic(filePath, file.content, { mode: 384 });
5951
+ } else {
5952
+ writeFileSync4(filePath, file.content);
5953
+ }
4815
5954
  }
4816
5955
  try {
4817
- const provSkillsDir = join6(agentDir, ".claude", "skills");
4818
- if (existsSync4(provSkillsDir)) {
4819
- for (const folder of readdirSync3(provSkillsDir)) {
5956
+ const provSkillsDir = join8(agentDir, ".claude", "skills");
5957
+ if (existsSync5(provSkillsDir)) {
5958
+ for (const folder of readdirSync4(provSkillsDir)) {
4820
5959
  if (folder.startsWith("knowledge-")) {
4821
5960
  try {
4822
- rmSync2(join6(provSkillsDir, folder), { recursive: true });
5961
+ rmSync2(join8(provSkillsDir, folder), { recursive: true });
4823
5962
  } catch {
4824
5963
  }
4825
5964
  }
@@ -4832,7 +5971,7 @@ async function processAgent(agent, agentStates) {
4832
5971
  const trackedFiles2 = frameworkAdapter.driftTrackedFiles();
4833
5972
  const hashes = /* @__PURE__ */ new Map();
4834
5973
  for (const file of trackedFiles2) {
4835
- const h = hashFile(join6(agentDir, file));
5974
+ const h = hashFile(join8(agentDir, file));
4836
5975
  if (h) hashes.set(file, h);
4837
5976
  }
4838
5977
  agentState.writtenHashes.set(agent.agent_id, hashes);
@@ -4897,10 +6036,10 @@ async function processAgent(agent, agentStates) {
4897
6036
  }
4898
6037
  let lastDriftCheckAt = now;
4899
6038
  const written = agentState.writtenHashes.get(agent.agent_id);
4900
- if (written && existsSync4(agentDir)) {
6039
+ if (written && existsSync5(agentDir)) {
4901
6040
  const driftedFiles = [];
4902
6041
  for (const [file, expectedHash] of written) {
4903
- const localHash = hashFile(join6(agentDir, file));
6042
+ const localHash = hashFile(join8(agentDir, file));
4904
6043
  if (localHash && localHash !== expectedHash) {
4905
6044
  driftedFiles.push(file);
4906
6045
  }
@@ -4911,7 +6050,7 @@ async function processAgent(agent, agentStates) {
4911
6050
  try {
4912
6051
  const localHashes = {};
4913
6052
  for (const file of driftedFiles) {
4914
- localHashes[file] = hashFile(join6(agentDir, file));
6053
+ localHashes[file] = hashFile(join8(agentDir, file));
4915
6054
  }
4916
6055
  await api.post("/host/drift", {
4917
6056
  agent_id: agent.agent_id,
@@ -4949,6 +6088,14 @@ async function processAgent(agent, agentStates) {
4949
6088
  if (refreshData.channel_configs && frameworkAdapter.writeChannelCredentials) {
4950
6089
  if (agent.status === "active") {
4951
6090
  for (const [channelId, entry] of Object.entries(refreshData.channel_configs)) {
6091
+ if (quarantinedChannels.has(channelId)) {
6092
+ try {
6093
+ frameworkAdapter.removeChannelCredentials?.(agent.code_name, channelId);
6094
+ } catch (err) {
6095
+ log(`[channel-quarantine] failed to drop quarantined channel '${agent.code_name}/${channelId}' from .mcp.json: ${err.message}`);
6096
+ }
6097
+ continue;
6098
+ }
4952
6099
  if ((entry.status === "active" || entry.status === "pending") && entry.config) {
4953
6100
  if (!activeChannels.has(channelId)) {
4954
6101
  activeChannels.set(channelId, /* @__PURE__ */ new Set());
@@ -4975,14 +6122,16 @@ async function processAgent(agent, agentStates) {
4975
6122
  } : void 0;
4976
6123
  const peersForHash = channelId === "telegram" ? extractCharterTelegramPeers(refreshData.charter?.raw_content ?? "", gateContext) : channelId === "slack" ? extractCharterSlackPeers(refreshData.charter?.raw_content ?? "", gateContext) : null;
4977
6124
  const sessionModeForHash = refreshData.agent.session_mode;
4978
- const CHANNEL_WRITE_VERSION = 5;
6125
+ const senderPolicyForHash = refreshData.sender_policy ?? null;
6126
+ const CHANNEL_WRITE_VERSION = 9;
4979
6127
  const configHash = createHash3("sha256").update(
4980
6128
  canonicalJson({
4981
6129
  writeVersion: CHANNEL_WRITE_VERSION,
4982
6130
  config: entry.config,
4983
6131
  team: teamSettingsForHash,
4984
6132
  peers: peersForHash,
4985
- sessionMode: sessionModeForHash ?? null
6133
+ sessionMode: sessionModeForHash ?? null,
6134
+ senderPolicy: senderPolicyForHash
4986
6135
  })
4987
6136
  ).digest("hex");
4988
6137
  const cacheKey = `${agent.agent_id}:${channelId}`;
@@ -5008,6 +6157,7 @@ async function processAgent(agent, agentStates) {
5008
6157
  const telegramPeerDisabled = channelId === "telegram" ? peerDisabledMode === "all" : void 0;
5009
6158
  const telegramPeers = channelId === "telegram" ? extractCharterTelegramPeers(refreshData.charter?.raw_content ?? "", gateContext) : void 0;
5010
6159
  const slackPeers = channelId === "slack" ? extractCharterSlackPeers(refreshData.charter?.raw_content ?? "", gateContext) : void 0;
6160
+ const senderPolicyForCall = channelId === "slack" || channelId === "msteams" ? refreshData.sender_policy ?? void 0 : void 0;
5011
6161
  frameworkAdapter.writeChannelCredentials(
5012
6162
  agent.code_name,
5013
6163
  channelId,
@@ -5019,7 +6169,8 @@ async function processAgent(agent, agentStates) {
5019
6169
  peerDisabled,
5020
6170
  telegramPeers,
5021
6171
  slackPeers,
5022
- agentTimezone
6172
+ agentTimezone,
6173
+ senderPolicy: senderPolicyForCall
5023
6174
  }
5024
6175
  );
5025
6176
  agentState.knownChannelConfigHashes.set(cacheKey, configHash);
@@ -5051,7 +6202,14 @@ async function processAgent(agent, agentStates) {
5051
6202
  framework: agentFrameworkCache.get(agent.code_name) ?? "openclaw",
5052
6203
  sessionHealthy: isSessionHealthy(agent.code_name)
5053
6204
  }) : { restart: false, added: [], removed: [] };
5054
- if (restartDecision.restart) {
6205
+ const quarantineOnlyRemoval = restartDecision.restart && restartDecision.added.length === 0 && restartDecision.removed.length > 0 && restartDecision.removed.every((c) => quarantinedChannels.has(c));
6206
+ const channelSetRestartScheduled = restartDecision.restart && !quarantineOnlyRemoval;
6207
+ if (quarantineOnlyRemoval) {
6208
+ log(
6209
+ `[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)`
6210
+ );
6211
+ }
6212
+ if (channelSetRestartScheduled) {
5055
6213
  const reasonParts = [];
5056
6214
  if (restartDecision.added.length > 0) reasonParts.push(`added=${restartDecision.added.join(",")}`);
5057
6215
  if (restartDecision.removed.length > 0) reasonParts.push(`removed=${restartDecision.removed.join(",")}`);
@@ -5065,45 +6223,188 @@ async function processAgent(agent, agentStates) {
5065
6223
  }
5066
6224
  scheduleSessionRestart(agent.code_name, delay, "new channel set");
5067
6225
  }
6226
+ if (channelConfigConverged) {
6227
+ const hasSenderPolicyChannel = currentChannelIds.has("slack") || currentChannelIds.has("msteams");
6228
+ const senderPolicyForRestartHash = refreshData.sender_policy ?? null;
6229
+ const senderPolicyHash = createHash3("sha256").update(canonicalJson({ senderPolicy: senderPolicyForRestartHash })).digest("hex");
6230
+ const prevSenderPolicyHash = agentState.knownSenderPolicyHashes.get(agent.agent_id);
6231
+ const senderPolicyDecision = hasSenderPolicyChannel ? decideSenderPolicyRestart({
6232
+ previousHash: prevSenderPolicyHash,
6233
+ currentHash: senderPolicyHash,
6234
+ sessionMode: refreshData.agent.session_mode,
6235
+ framework: agentFrameworkCache.get(agent.code_name) ?? "openclaw",
6236
+ sessionHealthy: isSessionHealthy(agent.code_name),
6237
+ channelSetRestartAlreadyScheduled: channelSetRestartScheduled
6238
+ }) : { restart: false, firstPoll: prevSenderPolicyHash === void 0, changed: false };
6239
+ if (senderPolicyDecision.restart) {
6240
+ log(
6241
+ `[hot-reload] sender_policy changed for '${agent.code_name}' (${prevSenderPolicyHash?.slice(0, 8) ?? "first"} \u2192 ${senderPolicyHash.slice(0, 8)}) \u2014 restarting session`
6242
+ );
6243
+ const notice = "Your sender_policy has been updated. Restarting session shortly so the channel MCP servers pick up the new gate.";
6244
+ const delivered = await injectMessage(
6245
+ agent.code_name,
6246
+ "system",
6247
+ notice,
6248
+ { task_name: "sender-policy-update" },
6249
+ log
6250
+ ).catch(() => false);
6251
+ const delay = delivered ? 8e3 : 3e3;
6252
+ if (!delivered) {
6253
+ log(
6254
+ `[hot-reload] Inject notification unconfirmed for '${agent.code_name}' \u2014 proceeding with shorter delay`
6255
+ );
6256
+ }
6257
+ scheduleSessionRestart(
6258
+ agent.code_name,
6259
+ delay,
6260
+ "sender_policy change",
6261
+ "sender-policy-change"
6262
+ );
6263
+ }
6264
+ agentState.knownSenderPolicyHashes.set(agent.agent_id, senderPolicyHash);
6265
+ let msteamsBehaviourRestartScheduled = false;
6266
+ if (currentChannelIds.has("msteams")) {
6267
+ const msteamsEntry = refreshData.channel_configs?.["msteams"];
6268
+ const behaviourSubset = extractMsTeamsBehaviourSubset(
6269
+ msteamsEntry?.config
6270
+ );
6271
+ const behaviourHash = createHash3("sha256").update(canonicalJson(behaviourSubset)).digest("hex");
6272
+ const prevBehaviourHash = agentState.knownMsTeamsBehaviourHashes.get(agent.agent_id);
6273
+ const behaviourDecision = decideSenderPolicyRestart({
6274
+ previousHash: prevBehaviourHash,
6275
+ currentHash: behaviourHash,
6276
+ sessionMode: refreshData.agent.session_mode,
6277
+ framework: agentFrameworkCache.get(agent.code_name) ?? "openclaw",
6278
+ sessionHealthy: isSessionHealthy(agent.code_name),
6279
+ // A channel-set or sender-policy restart scheduled this tick will
6280
+ // respawn the MCP children with the freshly-written env for free.
6281
+ channelSetRestartAlreadyScheduled: channelSetRestartScheduled || senderPolicyDecision.restart
6282
+ });
6283
+ if (behaviourDecision.restart) {
6284
+ log(
6285
+ `[hot-reload] msteams behaviour settings changed for '${agent.code_name}' (${prevBehaviourHash?.slice(0, 8) ?? "first"} \u2192 ${behaviourHash.slice(0, 8)}) \u2014 restarting session`
6286
+ );
6287
+ const behaviourNotice = "Your Microsoft Teams channel behaviour settings were updated. Restarting session shortly so the teams-channel MCP picks up the new configuration.";
6288
+ const behaviourDelivered = await injectMessage(
6289
+ agent.code_name,
6290
+ "system",
6291
+ behaviourNotice,
6292
+ { task_name: "channel-update" },
6293
+ log
6294
+ ).catch(() => false);
6295
+ const behaviourDelay = behaviourDelivered ? 8e3 : 3e3;
6296
+ if (!behaviourDelivered) {
6297
+ log(
6298
+ `[hot-reload] Inject notification unconfirmed for '${agent.code_name}' \u2014 proceeding with shorter delay`
6299
+ );
6300
+ }
6301
+ scheduleSessionRestart(
6302
+ agent.code_name,
6303
+ behaviourDelay,
6304
+ "msteams behaviour change",
6305
+ "channel-behaviour-change"
6306
+ );
6307
+ msteamsBehaviourRestartScheduled = true;
6308
+ }
6309
+ agentState.knownMsTeamsBehaviourHashes.set(agent.agent_id, behaviourHash);
6310
+ } else {
6311
+ agentState.knownMsTeamsBehaviourHashes.delete(agent.agent_id);
6312
+ }
6313
+ if (currentChannelIds.has("slack")) {
6314
+ const slackEntry = refreshData.channel_configs?.["slack"];
6315
+ const slackBehaviourSubset = extractSlackBehaviourSubset(
6316
+ slackEntry?.config
6317
+ );
6318
+ const slackBehaviourHash = createHash3("sha256").update(canonicalJson(slackBehaviourSubset)).digest("hex");
6319
+ const prevSlackBehaviourHash = agentState.knownSlackBehaviourHashes.get(agent.agent_id);
6320
+ const slackBehaviourDecision = decideSenderPolicyRestart({
6321
+ previousHash: prevSlackBehaviourHash,
6322
+ currentHash: slackBehaviourHash,
6323
+ sessionMode: refreshData.agent.session_mode,
6324
+ framework: agentFrameworkCache.get(agent.code_name) ?? "openclaw",
6325
+ sessionHealthy: isSessionHealthy(agent.code_name),
6326
+ // A channel-set, sender-policy, or msteams-behaviour restart
6327
+ // scheduled this tick respawns the MCP children with the
6328
+ // freshly-written env for free.
6329
+ channelSetRestartAlreadyScheduled: channelSetRestartScheduled || senderPolicyDecision.restart || msteamsBehaviourRestartScheduled
6330
+ });
6331
+ if (slackBehaviourDecision.restart) {
6332
+ log(
6333
+ `[hot-reload] slack behaviour settings changed for '${agent.code_name}' (${prevSlackBehaviourHash?.slice(0, 8) ?? "first"} \u2192 ${slackBehaviourHash.slice(0, 8)}) \u2014 restarting session`
6334
+ );
6335
+ const slackBehaviourNotice = "Your Slack channel behaviour settings were updated. Restarting session shortly so the slack-channel MCP picks up the new configuration.";
6336
+ const slackBehaviourDelivered = await injectMessage(
6337
+ agent.code_name,
6338
+ "system",
6339
+ slackBehaviourNotice,
6340
+ { task_name: "channel-update" },
6341
+ log
6342
+ ).catch(() => false);
6343
+ const slackBehaviourDelay = slackBehaviourDelivered ? 8e3 : 3e3;
6344
+ if (!slackBehaviourDelivered) {
6345
+ log(
6346
+ `[hot-reload] Inject notification unconfirmed for '${agent.code_name}' \u2014 proceeding with shorter delay`
6347
+ );
6348
+ }
6349
+ scheduleSessionRestart(
6350
+ agent.code_name,
6351
+ slackBehaviourDelay,
6352
+ "slack behaviour change",
6353
+ "channel-behaviour-change"
6354
+ );
6355
+ }
6356
+ agentState.knownSlackBehaviourHashes.set(agent.agent_id, slackBehaviourHash);
6357
+ } else {
6358
+ agentState.knownSlackBehaviourHashes.delete(agent.agent_id);
6359
+ }
6360
+ }
5068
6361
  const agentSessionMode = refreshData.agent.session_mode;
5069
6362
  if (agentSessionMode === "persistent" && (agentFrameworkCache.get(agent.code_name) ?? "openclaw") === "claude-code") {
5070
6363
  try {
5071
6364
  const agentProvisionDir = agentDir;
5072
- const projectDir = join6(homedir4(), ".augmented", agent.code_name, "project");
5073
- mkdirSync2(agentProvisionDir, { recursive: true });
5074
- mkdirSync2(projectDir, { recursive: true });
5075
- const provisionMcpPath = join6(agentProvisionDir, ".mcp.json");
5076
- const projectMcpPath = join6(projectDir, ".mcp.json");
6365
+ const projectDir = join8(homedir4(), ".augmented", agent.code_name, "project");
6366
+ mkdirSync4(agentProvisionDir, { recursive: true });
6367
+ mkdirSync4(projectDir, { recursive: true });
6368
+ const provisionMcpPath = join8(agentProvisionDir, ".mcp.json");
6369
+ const projectMcpPath = join8(projectDir, ".mcp.json");
5077
6370
  let mcpConfig = { mcpServers: {} };
5078
6371
  try {
5079
- mcpConfig = JSON.parse(readFileSync5(provisionMcpPath, "utf-8"));
6372
+ mcpConfig = JSON.parse(readFileSync9(provisionMcpPath, "utf-8"));
5080
6373
  if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
5081
6374
  } catch {
5082
6375
  }
5083
- const localDirectChatChannel = join6(homedir4(), ".augmented", "_mcp", "direct-chat-channel.js");
6376
+ const localDirectChatChannel = join8(homedir4(), ".augmented", "_mcp", "direct-chat-channel.js");
5084
6377
  const directChatTeamSettings = refreshData.team?.settings;
5085
6378
  const directChatTz = (() => {
5086
6379
  const tz = directChatTeamSettings?.["timezone"];
5087
6380
  return typeof tz === "string" && tz.trim() !== "" ? tz.trim() : void 0;
5088
6381
  })();
5089
- if (existsSync4(localDirectChatChannel) && !mcpConfig.mcpServers["direct-chat"]) {
5090
- mcpConfig.mcpServers["direct-chat"] = {
6382
+ if (existsSync5(localDirectChatChannel)) {
6383
+ const directChatEnv = {
6384
+ AGT_HOST: requireHost(),
6385
+ // ENG-5901 Track D: templated — the manager exports the real
6386
+ // AGT_API_KEY to every spawn env (getApiKey()); Claude Code
6387
+ // substitutes at MCP-launch. No literal in .mcp.json.
6388
+ AGT_API_KEY: "${AGT_API_KEY}",
6389
+ AGT_AGENT_ID: agent.agent_id
6390
+ };
6391
+ if (directChatTz) directChatEnv["TZ"] = directChatTz;
6392
+ if (!frameworkAdapter.writeMcpServer) {
6393
+ throw new Error(
6394
+ `[direct-chat] Framework '${frameworkAdapter.id}' must implement writeMcpServer for direct-chat provisioning (agent=${agent.code_name}, cachedFramework=${agentFrameworkCache.get(agent.code_name) ?? "openclaw"})`
6395
+ );
6396
+ }
6397
+ frameworkAdapter.writeMcpServer(agent.code_name, "direct-chat", {
5091
6398
  command: "node",
5092
6399
  args: [localDirectChatChannel],
5093
- env: {
5094
- AGT_HOST: requireHost(),
5095
- AGT_API_KEY: getApiKey() ?? "",
5096
- AGT_AGENT_ID: agent.agent_id,
5097
- ...directChatTz ? { TZ: directChatTz } : {}
5098
- }
5099
- };
5100
- const serialized = JSON.stringify(mcpConfig, null, 2);
5101
- writeFileSync3(provisionMcpPath, serialized);
5102
- writeFileSync3(projectMcpPath, serialized);
5103
- log(`Channel credentials written for '${agent.code_name}/direct-chat'`);
6400
+ env: directChatEnv
6401
+ });
6402
+ if (!mcpConfig.mcpServers["direct-chat"]) {
6403
+ log(`Channel credentials written for '${agent.code_name}/direct-chat'`);
6404
+ }
5104
6405
  }
5105
- const staleChannelsPath = join6(projectDir, ".mcp-channels.json");
5106
- if (existsSync4(staleChannelsPath)) {
6406
+ const staleChannelsPath = join8(projectDir, ".mcp-channels.json");
6407
+ if (existsSync5(staleChannelsPath)) {
5107
6408
  try {
5108
6409
  rmSync2(staleChannelsPath, { force: true });
5109
6410
  } catch {
@@ -5113,7 +6414,7 @@ async function processAgent(agent, agentStates) {
5113
6414
  log(`Failed to provision direct-chat channel for '${agent.code_name}': ${err.message}`);
5114
6415
  }
5115
6416
  }
5116
- let lastSecretsProvisionAt = state4.agents.find((a) => a.agentId === agent.agent_id)?.lastSecretsProvisionAt ?? null;
6417
+ let lastSecretsProvisionAt = state5.agents.find((a) => a.agentId === agent.agent_id)?.lastSecretsProvisionAt ?? null;
5117
6418
  let secretsHash = agentState.knownSecretsHashes.get(agent.agent_id) ?? null;
5118
6419
  try {
5119
6420
  const secretsData = await api.post("/host/secrets", { agent_id: agent.agent_id });
@@ -5167,7 +6468,7 @@ async function processAgent(agent, agentStates) {
5167
6468
  }
5168
6469
  if (process.env.AGT_CONNECTIVITY_PROBE_ENABLED === "true") {
5169
6470
  try {
5170
- const probeProjectDir = join6(homedir4(), ".augmented", agent.code_name, "project");
6471
+ const probeProjectDir = join8(homedir4(), ".augmented", agent.code_name, "project");
5171
6472
  await runAgentConnectivityProbes(agent, integrations, probeProjectDir);
5172
6473
  } catch (err) {
5173
6474
  log(`Connectivity probe failed for '${agent.code_name}': ${err.message}`);
@@ -5177,11 +6478,11 @@ async function processAgent(agent, agentStates) {
5177
6478
  const intHash = computeIntegrationsHash(integrations);
5178
6479
  const prevIntHash = agentState.knownIntegrationHashes.get(agent.agent_id);
5179
6480
  if (intHash !== prevIntHash) {
5180
- const projectDir = join6(homedir4(), ".augmented", agent.code_name, "project");
5181
- const envIntPath = join6(projectDir, ".env.integrations");
6481
+ const projectDir = join8(homedir4(), ".augmented", agent.code_name, "project");
6482
+ const envIntPath = join8(projectDir, ".env.integrations");
5182
6483
  let preWriteEnv;
5183
6484
  try {
5184
- preWriteEnv = readFileSync5(envIntPath, "utf-8");
6485
+ preWriteEnv = readFileSync9(envIntPath, "utf-8");
5185
6486
  } catch {
5186
6487
  preWriteEnv = void 0;
5187
6488
  }
@@ -5193,9 +6494,9 @@ async function processAgent(agent, agentStates) {
5193
6494
  let rotationHandled = true;
5194
6495
  if (fw === "claude-code" && isSessionHealthy(agent.code_name)) {
5195
6496
  try {
5196
- const projectMcpPath = join6(projectDir, ".mcp.json");
5197
- const postWriteEnv = readFileSync5(envIntPath, "utf-8");
5198
- const mcpContent = readFileSync5(projectMcpPath, "utf-8");
6497
+ const projectMcpPath = join8(projectDir, ".mcp.json");
6498
+ const postWriteEnv = readFileSync9(envIntPath, "utf-8");
6499
+ const mcpContent = readFileSync9(projectMcpPath, "utf-8");
5199
6500
  const changedVars = diffEnvIntegrations(preWriteEnv, postWriteEnv);
5200
6501
  const mcpJsonForReap = JSON.parse(mcpContent);
5201
6502
  const affectedServerKeys = findMcpServersUsingVars(mcpJsonForReap, changedVars);
@@ -5263,8 +6564,8 @@ async function processAgent(agent, agentStates) {
5263
6564
  const mcpPath = frameworkAdapter.getMcpPath(agent.code_name);
5264
6565
  if (mcpPath) {
5265
6566
  try {
5266
- const { readFileSync: readFileSync6 } = await import("fs");
5267
- const mcpConfig = JSON.parse(readFileSync6(mcpPath, "utf-8"));
6567
+ const { readFileSync: readFileSync10 } = await import("fs");
6568
+ const mcpConfig = JSON.parse(readFileSync10(mcpPath, "utf-8"));
5268
6569
  if (mcpConfig.mcpServers) {
5269
6570
  const managedPrefixes = [
5270
6571
  "composio_",
@@ -5360,8 +6661,8 @@ async function processAgent(agent, agentStates) {
5360
6661
  if (agent.status === "active") {
5361
6662
  if (frameworkAdapter.installPlugin) {
5362
6663
  try {
5363
- const pluginPath = join6(process.cwd(), "packages", "openclaw-plugin-augmented", "src", "index.ts");
5364
- if (existsSync4(pluginPath)) {
6664
+ const pluginPath = join8(process.cwd(), "packages", "openclaw-plugin-augmented", "src", "index.ts");
6665
+ if (existsSync5(pluginPath)) {
5365
6666
  frameworkAdapter.installPlugin(agent.code_name, "augmented", pluginPath, {
5366
6667
  agtHost: requireHost(),
5367
6668
  agtApiKey: getApiKey() ?? void 0,
@@ -5426,25 +6727,25 @@ async function processAgent(agent, agentStates) {
5426
6727
  }
5427
6728
  }
5428
6729
  try {
5429
- const { readdirSync: readdirSync4, rmSync: rmSync3 } = await import("fs");
6730
+ const { readdirSync: readdirSync5, rmSync: rmSync3 } = await import("fs");
5430
6731
  const { homedir: homedir5 } = await import("os");
5431
6732
  const frameworkId2 = frameworkAdapter.id;
5432
6733
  const candidateSkillDirs = [
5433
6734
  // Claude Code — framework runtime tree
5434
- join6(homedir5(), ".augmented", agent.code_name, "skills"),
6735
+ join8(homedir5(), ".augmented", agent.code_name, "skills"),
5435
6736
  // Claude Code — project tree
5436
- join6(homedir5(), ".augmented", agent.code_name, "project", ".claude", "skills"),
6737
+ join8(homedir5(), ".augmented", agent.code_name, "project", ".claude", "skills"),
5437
6738
  // OpenClaw — framework runtime tree
5438
- join6(homedir5(), `.openclaw-${agent.code_name}`, "skills"),
6739
+ join8(homedir5(), `.openclaw-${agent.code_name}`, "skills"),
5439
6740
  // Defensive: legacy provision-side path, not currently an
5440
6741
  // install target but cheap to sweep.
5441
- join6(agentDir, ".claude", "skills")
6742
+ join8(agentDir, ".claude", "skills")
5442
6743
  ];
5443
- const existingDirs = candidateSkillDirs.filter((d) => existsSync4(d));
6744
+ const existingDirs = candidateSkillDirs.filter((d) => existsSync5(d));
5444
6745
  const discoveredEntries = /* @__PURE__ */ new Set();
5445
6746
  for (const dir of existingDirs) {
5446
6747
  try {
5447
- for (const entry of readdirSync4(dir)) {
6748
+ for (const entry of readdirSync5(dir)) {
5448
6749
  if (entry.startsWith("plugin-") || entry.startsWith("integration-")) {
5449
6750
  discoveredEntries.add(entry);
5450
6751
  }
@@ -5454,8 +6755,8 @@ async function processAgent(agent, agentStates) {
5454
6755
  }
5455
6756
  const removeSkillFolder = (entry, reason) => {
5456
6757
  for (const dir of existingDirs) {
5457
- const p = join6(dir, entry);
5458
- if (existsSync4(p)) {
6758
+ const p = join8(dir, entry);
6759
+ if (existsSync5(p)) {
5459
6760
  rmSync3(p, { recursive: true, force: true });
5460
6761
  }
5461
6762
  }
@@ -5612,12 +6913,19 @@ async function processAgent(agent, agentStates) {
5612
6913
  } catch (err) {
5613
6914
  log(`[hot-reload] .mcp.json drift check failed for '${agent.code_name}': ${err.message}`);
5614
6915
  }
6916
+ if (channelConfigConverged) {
6917
+ try {
6918
+ checkChannelSecretDriftAndScheduleRestart(agent.code_name, getProjectDir(agent.code_name));
6919
+ } catch (err) {
6920
+ log(`[hot-reload] channel-secret drift check failed for '${agent.code_name}': ${err.message}`);
6921
+ }
6922
+ }
5615
6923
  try {
5616
6924
  const sess = getSessionState(agent.code_name);
5617
6925
  let mcpJsonParsed = null;
5618
6926
  try {
5619
- const mcpPath = join6(getProjectDir(agent.code_name), ".mcp.json");
5620
- mcpJsonParsed = JSON.parse(readFileSync5(mcpPath, "utf-8"));
6927
+ const mcpPath = join8(getProjectDir(agent.code_name), ".mcp.json");
6928
+ mcpJsonParsed = JSON.parse(readFileSync9(mcpPath, "utf-8"));
5621
6929
  } catch {
5622
6930
  }
5623
6931
  reapMissingMcpSessions({
@@ -5688,6 +6996,37 @@ async function processAgent(agent, agentStates) {
5688
6996
  `[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`
5689
6997
  );
5690
6998
  });
6999
+ },
7000
+ // ENG-5932: classify each declared key for the quarantine path. Reads a
7001
+ // server-declared criticality field off the .mcp.json entry when
7002
+ // present (Slice 2 will stamp it); otherwise fails closed to ESSENTIAL
7003
+ // for everything except the known optional channel set. Tool servers
7004
+ // (composio_*/xero) classify ESSENTIAL here and stay on the existing
7005
+ // managed-toolkit onGiveUp path above.
7006
+ classifyKey: (serverKey) => classifyChannelCriticality(
7007
+ serverKey,
7008
+ mcpJsonParsed?.mcpServers?.[serverKey]
7009
+ ),
7010
+ quarantineDwellMs: channelQuarantineDwellMs(),
7011
+ quarantineMode: channelQuarantineMode(),
7012
+ // ENG-5932: enforce-mode quarantine. Persist the local marker; the next
7013
+ // provisioning poll then omits the channel from .mcp.json + the launch
7014
+ // flags, and the channel-set-change / .mcp.json-drift restart paths are
7015
+ // taught to NOT bounce the session for a quarantine-driven removal.
7016
+ // Local-authoritative: no API call — with the control plane down, the
7017
+ // flap still stops.
7018
+ onQuarantine: (codeName, serverKey) => {
7019
+ const newly = channelQuarantineStore().quarantine(
7020
+ codeName,
7021
+ serverKey,
7022
+ `mcp-presence-reaper: optional channel dead past restart budget + dwell`,
7023
+ Date.now()
7024
+ );
7025
+ if (newly) {
7026
+ log(
7027
+ `[channel-quarantine] persisted quarantine marker for '${codeName}:${serverKey}' \u2014 will be dropped from .mcp.json on next provisioning poll (ENG-5932)`
7028
+ );
7029
+ }
5691
7030
  }
5692
7031
  });
5693
7032
  } catch (err) {
@@ -5771,10 +7110,10 @@ async function processAgent(agent, agentStates) {
5771
7110
  lastWorkTriggerAt.set(agent.code_name, triggerTs);
5772
7111
  if (agentFw === "openclaw" && gatewayRunning && gatewayPort) {
5773
7112
  const homeDir = process.env["HOME"] ?? "/tmp";
5774
- const jobsPath = join6(homeDir, `.openclaw-${agent.code_name}`, "cron", "jobs.json");
5775
- if (existsSync4(jobsPath)) {
7113
+ const jobsPath = join8(homeDir, `.openclaw-${agent.code_name}`, "cron", "jobs.json");
7114
+ if (existsSync5(jobsPath)) {
5776
7115
  try {
5777
- const jobsData = JSON.parse(readFileSync5(jobsPath, "utf-8"));
7116
+ const jobsData = JSON.parse(readFileSync9(jobsPath, "utf-8"));
5778
7117
  const kanbanJob = (jobsData.jobs ?? []).find(
5779
7118
  (j) => typeof j.name === "string" && j.name.includes("kanban-work")
5780
7119
  );
@@ -5908,10 +7247,10 @@ In progress for ${age} minutes \u2014 auto-failed`).catch(() => {
5908
7247
  }
5909
7248
  }
5910
7249
  const trackedFiles = frameworkAdapter.driftTrackedFiles();
5911
- if (trackedFiles.length > 0 && existsSync4(agentDir)) {
7250
+ if (trackedFiles.length > 0 && existsSync5(agentDir)) {
5912
7251
  const hashes = /* @__PURE__ */ new Map();
5913
7252
  for (const file of trackedFiles) {
5914
- const h = hashFile(join6(agentDir, file));
7253
+ const h = hashFile(join8(agentDir, file));
5915
7254
  if (h) hashes.set(file, h);
5916
7255
  }
5917
7256
  agentState.writtenHashes.set(agent.agent_id, hashes);
@@ -5945,19 +7284,19 @@ function cleanupStaleSessions(codeName) {
5945
7284
  lastCleanupAt.set(codeName, Date.now());
5946
7285
  const homeDir = process.env["HOME"] ?? "/tmp";
5947
7286
  for (const agentDir of ["main", codeName]) {
5948
- const sessionsDir = join6(homeDir, `.openclaw-${codeName}`, "agents", agentDir, "sessions");
7287
+ const sessionsDir = join8(homeDir, `.openclaw-${codeName}`, "agents", agentDir, "sessions");
5949
7288
  cleanupCronSessions(sessionsDir, CRON_SESSION_KEEP_COUNT);
5950
7289
  }
5951
- const cronRunsDir = join6(homeDir, `.openclaw-${codeName}`, "cron", "runs");
7290
+ const cronRunsDir = join8(homeDir, `.openclaw-${codeName}`, "cron", "runs");
5952
7291
  cleanupOldFiles(cronRunsDir, CRON_RUN_RETENTION_DAYS, ".jsonl");
5953
- const cronJobsPath = join6(homeDir, `.openclaw-${codeName}`, "cron", "jobs.json");
7292
+ const cronJobsPath = join8(homeDir, `.openclaw-${codeName}`, "cron", "jobs.json");
5954
7293
  clearStaleCronRunState(cronJobsPath);
5955
7294
  }
5956
7295
  function cleanupCronSessions(sessionsDir, keepCount) {
5957
- const indexPath = join6(sessionsDir, "sessions.json");
5958
- if (!existsSync4(indexPath)) return;
7296
+ const indexPath = join8(sessionsDir, "sessions.json");
7297
+ if (!existsSync5(indexPath)) return;
5959
7298
  try {
5960
- const raw = readFileSync5(indexPath, "utf-8");
7299
+ const raw = readFileSync9(indexPath, "utf-8");
5961
7300
  const index = JSON.parse(raw);
5962
7301
  const cronRunKeys = Object.keys(index).filter((k) => k.includes(":cron:") && k.includes(":run:")).map((k) => ({
5963
7302
  key: k,
@@ -5970,9 +7309,9 @@ function cleanupCronSessions(sessionsDir, keepCount) {
5970
7309
  for (const entry of toDelete) {
5971
7310
  delete index[entry.key];
5972
7311
  if (entry.sessionId) {
5973
- const sessionFile = join6(sessionsDir, `${entry.sessionId}.jsonl`);
7312
+ const sessionFile = join8(sessionsDir, `${entry.sessionId}.jsonl`);
5974
7313
  try {
5975
- if (existsSync4(sessionFile)) {
7314
+ if (existsSync5(sessionFile)) {
5976
7315
  unlinkSync(sessionFile);
5977
7316
  deletedFiles++;
5978
7317
  }
@@ -5992,8 +7331,8 @@ function cleanupCronSessions(sessionsDir, keepCount) {
5992
7331
  delete index[parentKey];
5993
7332
  if (parentSessionId) {
5994
7333
  try {
5995
- const f = join6(sessionsDir, `${parentSessionId}.jsonl`);
5996
- if (existsSync4(f)) {
7334
+ const f = join8(sessionsDir, `${parentSessionId}.jsonl`);
7335
+ if (existsSync5(f)) {
5997
7336
  unlinkSync(f);
5998
7337
  deletedFiles++;
5999
7338
  }
@@ -6002,7 +7341,7 @@ function cleanupCronSessions(sessionsDir, keepCount) {
6002
7341
  }
6003
7342
  }
6004
7343
  }
6005
- writeFileSync3(indexPath, JSON.stringify(index));
7344
+ writeFileSync4(indexPath, JSON.stringify(index));
6006
7345
  if (toDelete.length > 0) {
6007
7346
  log(`Cleaned ${toDelete.length} cron session(s) and ${deletedFiles} file(s) from ${sessionsDir}`);
6008
7347
  }
@@ -6011,48 +7350,48 @@ function cleanupCronSessions(sessionsDir, keepCount) {
6011
7350
  }
6012
7351
  var STALE_RUN_TIMEOUT_MS = 5 * 6e4;
6013
7352
  function clearStaleCronRunState(jobsPath) {
6014
- if (!existsSync4(jobsPath)) return;
7353
+ if (!existsSync5(jobsPath)) return;
6015
7354
  try {
6016
- const raw = readFileSync5(jobsPath, "utf-8");
7355
+ const raw = readFileSync9(jobsPath, "utf-8");
6017
7356
  const data = JSON.parse(raw);
6018
7357
  const jobs = data.jobs ?? data;
6019
7358
  if (!Array.isArray(jobs)) return;
6020
7359
  let changed = false;
6021
7360
  const now = Date.now();
6022
7361
  for (const job of jobs) {
6023
- const state5 = job.state;
6024
- if (!state5) continue;
6025
- const runStartedAt = state5.runningAtMs ?? state5.runStartedAtMs;
6026
- const isRunning = state5.running === true || state5.status === "running";
7362
+ const state6 = job.state;
7363
+ if (!state6) continue;
7364
+ const runStartedAt = state6.runningAtMs ?? state6.runStartedAtMs;
7365
+ const isRunning = state6.running === true || state6.status === "running";
6027
7366
  if (isRunning && runStartedAt && now - runStartedAt > STALE_RUN_TIMEOUT_MS) {
6028
- state5.running = false;
6029
- delete state5.status;
6030
- delete state5.runStartedAtMs;
6031
- delete state5.currentRunId;
7367
+ state6.running = false;
7368
+ delete state6.status;
7369
+ delete state6.runStartedAtMs;
7370
+ delete state6.currentRunId;
6032
7371
  changed = true;
6033
7372
  log(`Cleared stale running state for cron job '${job.name}' (stuck for ${Math.round((now - runStartedAt) / 6e4)}min)`);
6034
7373
  } else if (isRunning && !runStartedAt) {
6035
- state5.running = false;
7374
+ state6.running = false;
6036
7375
  changed = true;
6037
7376
  log(`Cleared stale running state for cron job '${job.name}' (no start timestamp)`);
6038
7377
  }
6039
7378
  }
6040
7379
  if (changed) {
6041
- writeFileSync3(jobsPath, JSON.stringify(data, null, 2));
7380
+ writeFileSync4(jobsPath, JSON.stringify(data, null, 2));
6042
7381
  }
6043
7382
  } catch {
6044
7383
  }
6045
7384
  }
6046
7385
  function cleanupOldFiles(dir, maxAgeDays, ext) {
6047
- if (!existsSync4(dir)) return;
7386
+ if (!existsSync5(dir)) return;
6048
7387
  const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3;
6049
7388
  let removed = 0;
6050
7389
  try {
6051
- for (const f of readdirSync3(dir)) {
7390
+ for (const f of readdirSync4(dir)) {
6052
7391
  if (!f.endsWith(ext)) continue;
6053
- const fullPath = join6(dir, f);
7392
+ const fullPath = join8(dir, f);
6054
7393
  try {
6055
- const st = statSync2(fullPath);
7394
+ const st = statSync3(fullPath);
6056
7395
  if (st.mtimeMs < cutoff) {
6057
7396
  unlinkSync(fullPath);
6058
7397
  removed++;
@@ -6069,6 +7408,25 @@ function cleanupOldFiles(dir, maxAgeDays, ext) {
6069
7408
  var inFlightClaudeTasks = /* @__PURE__ */ new Set();
6070
7409
  var claudeTaskConcurrency = /* @__PURE__ */ new Map();
6071
7410
  var MAX_CLAUDE_CONCURRENCY = 2;
7411
+ function claudePidFilePath() {
7412
+ return join8(homedir4(), ".augmented", "manager-claude-pids.json");
7413
+ }
7414
+ var inFlightClaudePids = /* @__PURE__ */ new Map();
7415
+ function registerClaudeSpawn(record) {
7416
+ inFlightClaudePids.set(record.pid, record);
7417
+ try {
7418
+ addSpawn(claudePidFilePath(), record);
7419
+ } catch (err) {
7420
+ log(`[drain] pid-file write failed: ${err.message}`);
7421
+ }
7422
+ }
7423
+ function unregisterClaudeSpawn(pid) {
7424
+ inFlightClaudePids.delete(pid);
7425
+ try {
7426
+ removeSpawn(claudePidFilePath(), pid);
7427
+ } catch {
7428
+ }
7429
+ }
6072
7430
  var claudeSchedulerStates = /* @__PURE__ */ new Map();
6073
7431
  async function syncAndCheckClaudeScheduler(agent, tasks, boardItems, refreshData) {
6074
7432
  const codeName = agent.code_name;
@@ -6096,16 +7454,16 @@ async function syncAndCheckClaudeScheduler(agent, tasks, boardItems, refreshData
6096
7454
  enabled: t.enabled ?? true,
6097
7455
  triggered_at: t.triggered_at ?? null
6098
7456
  }));
6099
- const state6 = syncTasksToScheduler(codeName, agent.agent_id, taskInputs);
6100
- claudeSchedulerStates.set(codeName, state6);
7457
+ const state7 = syncTasksToScheduler(codeName, agent.agent_id, taskInputs);
7458
+ claudeSchedulerStates.set(codeName, state7);
6101
7459
  agentState.knownTasksHashes.set(agent.agent_id, combinedHash);
6102
7460
  log(`[claude-scheduler] Tasks synced for '${codeName}' (${taskInputs.length} task(s))`);
6103
7461
  }
6104
7462
  if (!claudeSchedulerStates.has(codeName)) {
6105
7463
  claudeSchedulerStates.set(codeName, loadSchedulerState(codeName));
6106
7464
  }
6107
- const state5 = claudeSchedulerStates.get(codeName);
6108
- const ready = getReadyTasks(state5, inFlightClaudeTasks);
7465
+ const state6 = claudeSchedulerStates.get(codeName);
7466
+ const ready = getReadyTasks(state6, inFlightClaudeTasks);
6109
7467
  if (ready.length === 0) return;
6110
7468
  for (const task of ready) {
6111
7469
  if ((claudeTaskConcurrency.get(codeName) ?? 0) >= MAX_CLAUDE_CONCURRENCY) break;
@@ -6122,6 +7480,12 @@ async function syncAndCheckClaudeScheduler(agent, tasks, boardItems, refreshData
6122
7480
  }
6123
7481
  inFlightClaudeTasks.add(task.taskId);
6124
7482
  claudeTaskConcurrency.set(codeName, (claudeTaskConcurrency.get(codeName) ?? 0) + 1);
7483
+ if (isScheduledViaKanbanEnabled() && isPlainScheduledTemplate(task.templateId)) {
7484
+ await fireScheduledTaskViaKanban(codeName, agent.agent_id, task, prompt);
7485
+ inFlightClaudeTasks.delete(task.taskId);
7486
+ claudeTaskConcurrency.set(codeName, Math.max(0, (claudeTaskConcurrency.get(codeName) ?? 1) - 1));
7487
+ continue;
7488
+ }
6125
7489
  log(`[claude-scheduler] Firing '${task.name}' for '${codeName}'`);
6126
7490
  executeAndProcessClaudeTask(codeName, agent.agent_id, task, prompt).finally(() => {
6127
7491
  inFlightClaudeTasks.delete(task.taskId);
@@ -6129,6 +7493,19 @@ async function syncAndCheckClaudeScheduler(agent, tasks, boardItems, refreshData
6129
7493
  });
6130
7494
  }
6131
7495
  }
7496
+ function deriveScheduledTaskNotify(task) {
7497
+ if (task.deliveryChannel !== "msteams") return {};
7498
+ const to = task.deliveryTo;
7499
+ let conversationId = null;
7500
+ if (typeof to === "string" && to.trim().length > 0) {
7501
+ conversationId = to.trim();
7502
+ } else if (to && typeof to === "object") {
7503
+ const cid = to.conversation_id;
7504
+ if (typeof cid === "string" && cid.trim().length > 0) conversationId = cid.trim();
7505
+ }
7506
+ if (!conversationId) return {};
7507
+ return { notify_channel: "msteams", notify_to: conversationId };
7508
+ }
6132
7509
  async function startRun(opts) {
6133
7510
  try {
6134
7511
  const res = await api.post(
@@ -6179,7 +7556,7 @@ async function fetchPriorScheduledRuns(agentId, taskId) {
6179
7556
  }
6180
7557
  function isScheduledViaKanbanEnabled() {
6181
7558
  const v = process.env["AGT_SCHEDULED_VIA_KANBAN"];
6182
- return v === "1" || v?.toLowerCase() === "true";
7559
+ return !(v === "0" || v?.toLowerCase() === "false");
6183
7560
  }
6184
7561
  var scheduledRunsByCode = /* @__PURE__ */ new Map();
6185
7562
  var claimedScheduledCards = /* @__PURE__ */ new Set();
@@ -6246,8 +7623,8 @@ async function deliverScheduledCardResult(codeName, agentId, cardId) {
6246
7623
  markScheduledCardDeliveryComplete(cardId);
6247
7624
  return "terminal";
6248
7625
  }
6249
- const state5 = claudeSchedulerStates.get(codeName) ?? loadSchedulerState(codeName);
6250
- const task = state5.tasks[card.source_ref];
7626
+ const state6 = claudeSchedulerStates.get(codeName) ?? loadSchedulerState(codeName);
7627
+ const task = state6.tasks[card.source_ref];
6251
7628
  if (!task) {
6252
7629
  log(`[scheduled-kanban] delivery: no scheduler task for source_ref=${card.source_ref} on '${codeName}' \u2014 skipping`);
6253
7630
  markScheduledCardDeliveryComplete(cardId);
@@ -6297,7 +7674,8 @@ async function routeScheduledTaskViaKanban(codeName, agentId, task, prompt) {
6297
7674
  title: task.name,
6298
7675
  description: prompt,
6299
7676
  priority: 2,
6300
- initial_status: "todo"
7677
+ initial_status: "todo",
7678
+ ...deriveScheduledTaskNotify(task)
6301
7679
  }
6302
7680
  });
6303
7681
  if (!run_id || !kanban_item_id) {
@@ -6306,6 +7684,7 @@ async function routeScheduledTaskViaKanban(codeName, agentId, task, prompt) {
6306
7684
  return false;
6307
7685
  }
6308
7686
  const nudge = `You have a new scheduled task on your kanban board: id=${kanban_item_id} title=${JSON.stringify(task.name)}. Call kanban_move("${kanban_item_id}", "in_progress"), do the work described on the card, then write the finished result onto the card and mark it done. Do NOT send, post, or message the result to anyone yourself \u2014 recording it on the card is enough; delivery to the recipient is handled for you.
7687
+ If \u2014 and ONLY if \u2014 the task description contains explicit opt-out wording the user typed (e.g. "DO NOT notify me unless urgent", "only if X", "stay silent unless \u2026") and that condition is NOT met this run, write exactly \`${SUPPRESS_SENTINEL}\` ALONE as the card result (no other text, no "nothing urgent", no notes) and mark the card done \u2014 that tells the delivery pipeline to skip the send. A report with zero items ("nothing urgent", "no follow-ups", "all quiet") is still a valid deliverable when the task asks for a digest, NOT a reason to suppress \u2014 when in doubt, deliver.
6309
7688
  ` + formatRunMarker(run_id);
6310
7689
  let injectStatus;
6311
7690
  try {
@@ -6328,9 +7707,32 @@ async function routeScheduledTaskViaKanban(codeName, agentId, task, prompt) {
6328
7707
  log(`[scheduled-kanban] Routed task '${task.name}' for '${codeName}' via todo card ${kanban_item_id} run_id=${run_id}`);
6329
7708
  return true;
6330
7709
  }
7710
+ async function fireScheduledTaskViaKanban(codeName, agentId, task, prompt) {
7711
+ let routed = false;
7712
+ try {
7713
+ routed = await routeScheduledTaskViaKanban(codeName, agentId, task, prompt);
7714
+ } catch (err) {
7715
+ log(`[scheduled-kanban] route threw for '${task.name}' on '${codeName}': ${err.message}`);
7716
+ routed = false;
7717
+ }
7718
+ try {
7719
+ const updated = markTaskFired(codeName, task.taskId, routed ? "ok" : "error");
7720
+ claudeSchedulerStates.set(codeName, updated);
7721
+ } catch (err) {
7722
+ log(`[scheduled-kanban] markTaskFired failed for '${task.name}' on '${codeName}': ${err.message}`);
7723
+ }
7724
+ if (task.scheduleKind === "at") {
7725
+ try {
7726
+ await api.post("/host/schedules/disable", { agent_id: agentId, task_id: task.taskId });
7727
+ } catch (err) {
7728
+ log(`[scheduled-kanban] Failed to disable one-off task '${task.name}': ${err.message}`);
7729
+ }
7730
+ }
7731
+ return routed;
7732
+ }
6331
7733
  async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
6332
7734
  const projectDir = getProjectDir2(codeName);
6333
- const mcpConfigPath = join6(projectDir, ".mcp.json");
7735
+ const mcpConfigPath = join8(projectDir, ".mcp.json");
6334
7736
  let runId = null;
6335
7737
  let kanbanItemId = null;
6336
7738
  let taskResult;
@@ -6338,11 +7740,11 @@ async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
6338
7740
  const priorRuns = await fetchPriorScheduledRuns(agentId, task.taskId);
6339
7741
  prompt = wrapScheduledTaskPrompt(prompt, { priorRuns });
6340
7742
  try {
6341
- const claudeMdPath = join6(projectDir, "CLAUDE.md");
7743
+ const claudeMdPath = join8(projectDir, "CLAUDE.md");
6342
7744
  const serverNames = [];
6343
- if (existsSync4(mcpConfigPath)) {
7745
+ if (existsSync5(mcpConfigPath)) {
6344
7746
  try {
6345
- const d = JSON.parse(readFileSync5(mcpConfigPath, "utf-8"));
7747
+ const d = JSON.parse(readFileSync9(mcpConfigPath, "utf-8"));
6346
7748
  if (d.mcpServers) serverNames.push(...Object.keys(d.mcpServers));
6347
7749
  } catch {
6348
7750
  }
@@ -6361,18 +7763,14 @@ async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
6361
7763
  "--allowedTools",
6362
7764
  allowedTools
6363
7765
  ];
6364
- if (existsSync4(claudeMdPath)) {
7766
+ if (existsSync5(claudeMdPath)) {
6365
7767
  claudeArgs.push("--system-prompt-file", claudeMdPath);
6366
7768
  }
6367
7769
  const childEnv = { ...process.env };
6368
- const envIntPath = join6(projectDir, ".env.integrations");
6369
- if (existsSync4(envIntPath)) {
7770
+ const envIntPath = join8(projectDir, ".env.integrations");
7771
+ if (existsSync5(envIntPath)) {
6370
7772
  try {
6371
- for (const line of readFileSync5(envIntPath, "utf-8").split("\n")) {
6372
- if (!line || line.startsWith("#") || !line.includes("=")) continue;
6373
- const eqIdx = line.indexOf("=");
6374
- childEnv[line.slice(0, eqIdx)] = line.slice(eqIdx + 1);
6375
- }
7773
+ Object.assign(childEnv, parseEnvIntegrations(readFileSync9(envIntPath, "utf-8")));
6376
7774
  } catch {
6377
7775
  }
6378
7776
  }
@@ -6387,16 +7785,33 @@ async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
6387
7785
  source_type: "scheduled_task",
6388
7786
  source_ref: task.taskId,
6389
7787
  metadata: { template_id: task.templateId, name: task.name },
6390
- materialize_kanban: { title: task.name, priority: 2 }
7788
+ materialize_kanban: { title: task.name, priority: 2, ...deriveScheduledTaskNotify(task) }
6391
7789
  });
6392
7790
  runId = startResult.run_id;
6393
7791
  kanbanItemId = startResult.kanban_item_id;
6394
7792
  if (runId) childEnv["AGT_RUN_ID"] = runId;
7793
+ for (const f of probeMcpEnvSubstitution({
7794
+ mcpConfigPath,
7795
+ envIntegrationsPath: envIntPath,
7796
+ baseEnv: childEnv
7797
+ })) {
7798
+ log(`[claude-scheduler] ${formatMissingVar(f)} agent=${codeName}`);
7799
+ }
7800
+ const claudeKind = task.templateId === "kanban-work" ? "kanban-work" : "scheduled-task";
6395
7801
  const { stdout, stderr } = await execFilePromiseLong(resolveClaudeBinary(), claudeArgs, {
6396
7802
  cwd: projectDir,
6397
7803
  timeout: 3e5,
6398
7804
  stdin: "ignore",
6399
- env: childEnv
7805
+ env: childEnv,
7806
+ onSpawn: (pid) => registerClaudeSpawn({
7807
+ pid,
7808
+ started_at: Date.now(),
7809
+ kind: claudeKind,
7810
+ agent_id: agentId,
7811
+ agent_code_name: codeName,
7812
+ kanban_card_id: kanbanItemId ?? void 0
7813
+ }),
7814
+ onExit: (pid) => unregisterClaudeSpawn(pid)
6400
7815
  });
6401
7816
  if (stderr) {
6402
7817
  log(`[claude-scheduler] Task '${task.name}' stderr for '${codeName}': ${stderr.slice(0, 500)}`);
@@ -6527,8 +7942,8 @@ var claudeAuthTupleBySession = /* @__PURE__ */ new Map();
6527
7942
  async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
6528
7943
  const codeName = agent.code_name;
6529
7944
  const projectDir = getProjectDir(codeName);
6530
- const mcpConfigPath = join6(projectDir, ".mcp.json");
6531
- const claudeMdPath = join6(projectDir, "CLAUDE.md");
7945
+ const mcpConfigPath = join8(projectDir, ".mcp.json");
7946
+ const claudeMdPath = join8(projectDir, "CLAUDE.md");
6532
7947
  if (restartBreaker.isTripped(codeName)) {
6533
7948
  const trip = restartBreaker.getTrip(codeName);
6534
7949
  return {
@@ -6544,23 +7959,11 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
6544
7959
  return typeof tz === "string" && tz.trim() !== "" ? tz.trim() : void 0;
6545
7960
  })();
6546
7961
  const channelConfigs = refreshData.channel_configs;
6547
- const channels = [];
6548
- const devChannels = [];
6549
- if (channelConfigs) {
6550
- const isChannelEnabled = (id) => {
6551
- const entry = channelConfigs[id];
6552
- return !!entry?.config && (entry.status === "active" || entry.status === "pending");
6553
- };
6554
- if (isChannelEnabled("slack")) {
6555
- devChannels.push("server:slack");
6556
- }
6557
- if (isChannelEnabled("telegram")) {
6558
- devChannels.push("server:telegram");
6559
- }
6560
- if (isChannelEnabled("discord")) {
6561
- channels.push("plugin:discord@claude-plugins-official");
6562
- }
6563
- }
7962
+ const { devChannels, pluginChannels } = resolveChannelLaunchFlags(
7963
+ channelConfigs,
7964
+ channelQuarantineStore().getQuarantinedKeys(codeName)
7965
+ );
7966
+ const channels = [...pluginChannels];
6564
7967
  devChannels.push("server:direct-chat");
6565
7968
  let claudeAuthMode;
6566
7969
  let anthropicApiKey;
@@ -6767,9 +8170,9 @@ ${truncateForLog(ctx.tail)}` : `; pane_tail_hash=sha256:${createHash3("sha256").
6767
8170
  } else if (!claudeSchedulerStates.has(codeName)) {
6768
8171
  claudeSchedulerStates.set(codeName, loadSchedulerState(codeName));
6769
8172
  }
6770
- const state5 = claudeSchedulerStates.get(codeName);
6771
- if (state5) {
6772
- const ready = getReadyTasks(state5, inFlightClaudeTasks);
8173
+ const state6 = claudeSchedulerStates.get(codeName);
8174
+ if (state6) {
8175
+ const ready = getReadyTasks(state6, inFlightClaudeTasks);
6773
8176
  if (ready.length > 0) {
6774
8177
  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(", ")}`);
6775
8178
  }
@@ -6794,29 +8197,10 @@ ${truncateForLog(ctx.tail)}` : `; pane_tail_hash=sha256:${createHash3("sha256").
6794
8197
  inFlightClaudeTasks.add(task.taskId);
6795
8198
  claudeTaskConcurrency.set(codeName, (claudeTaskConcurrency.get(codeName) ?? 0) + 1);
6796
8199
  if (isScheduledViaKanbanEnabled() && isPlainScheduledTemplate(task.templateId)) {
6797
- let routed = false;
6798
- try {
6799
- routed = await routeScheduledTaskViaKanban(codeName, agent.agent_id, task, prompt);
6800
- } catch (err) {
6801
- log(`[scheduled-kanban] route threw for '${task.name}' on '${codeName}': ${err.message}`);
6802
- routed = false;
6803
- }
6804
- if (routed) {
6805
- try {
6806
- const updated = markTaskFired(codeName, task.taskId, "ok");
6807
- claudeSchedulerStates.set(codeName, updated);
6808
- } catch (err) {
6809
- log(`[scheduled-kanban] markTaskFired failed for '${task.name}' on '${codeName}': ${err.message}`);
6810
- }
6811
- if (task.scheduleKind === "at") {
6812
- api.post("/host/schedules/disable", { agent_id: agent.agent_id, task_id: task.taskId }).catch(
6813
- (err) => log(`[scheduled-kanban] Failed to disable one-off task '${task.name}': ${err.message}`)
6814
- );
6815
- }
6816
- inFlightClaudeTasks.delete(task.taskId);
6817
- claudeTaskConcurrency.set(codeName, Math.max(0, (claudeTaskConcurrency.get(codeName) ?? 1) - 1));
6818
- break;
6819
- }
8200
+ await fireScheduledTaskViaKanban(codeName, agent.agent_id, task, prompt);
8201
+ inFlightClaudeTasks.delete(task.taskId);
8202
+ claudeTaskConcurrency.set(codeName, Math.max(0, (claudeTaskConcurrency.get(codeName) ?? 1) - 1));
8203
+ break;
6820
8204
  }
6821
8205
  log(`[persistent-session] Firing task '${task.name}' for '${codeName}' via claude -p`);
6822
8206
  executeAndProcessClaudeTask(codeName, agent.agent_id, task, prompt).catch(() => {
@@ -7123,6 +8507,9 @@ function triggerEarlyPoll(reason) {
7123
8507
  }, 0);
7124
8508
  }
7125
8509
  var directChatInFlight = /* @__PURE__ */ new Set();
8510
+ var directChatSpawnGate = new DirectChatSpawnGate({
8511
+ hostMaxConcurrency: directChatHostConcurrency()
8512
+ });
7126
8513
  async function pollDirectChatMessages(agentStates) {
7127
8514
  for (const agent of agentStates) {
7128
8515
  if (agent.status !== "active") continue;
@@ -7143,8 +8530,21 @@ async function pollDirectChatMessages(agentStates) {
7143
8530
  }
7144
8531
  }
7145
8532
  async function processDirectChatMessage(agent, msg) {
8533
+ noteInbound(agent.codeName);
7146
8534
  const fw = agentFrameworkCache.get(agent.codeName) ?? "openclaw";
7147
8535
  log(`[direct-chat] Processing message for '${agent.codeName}' (fw=${fw}): id=${msg.id} len=${msg.content.length}`);
8536
+ if (isDirectChatMessageExpired(msg.created_at, Date.now(), directChatMaxAgeMs())) {
8537
+ log(`[direct-chat] Expired backlog message for '${agent.codeName}' (msg=${msg.id}, created_at=${msg.created_at}) \u2014 draining without delivery`);
8538
+ try {
8539
+ await api.post("/host/direct-chat/reply", {
8540
+ agent_id: agent.agentId,
8541
+ session_id: msg.session_id,
8542
+ content: "[Expired] This message timed out before it could be processed. Please resend it."
8543
+ });
8544
+ } catch {
8545
+ }
8546
+ return;
8547
+ }
7148
8548
  if (isSessionHealthy(agent.codeName)) {
7149
8549
  const escapeXml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
7150
8550
  const channelEnvelope = `<channel source="direct-chat" session_id="${escapeXml(msg.session_id)}" user="webapp">
@@ -7163,16 +8563,31 @@ ${escapeXml(msg.content)}
7163
8563
  }
7164
8564
  log(`[direct-chat] Inject reported unverified for '${agent.codeName}' (msg=${msg.id}) \u2014 falling back to one-shot direct-chat handling`);
7165
8565
  }
8566
+ let releaseSpawnSlot;
7166
8567
  try {
7167
8568
  let reply;
7168
8569
  if (fw === "claude-code") {
8570
+ releaseSpawnSlot = await directChatSpawnGate.acquire(agent.codeName);
8571
+ if (isDirectChatMessageExpired(msg.created_at, Date.now(), directChatMaxAgeMs())) {
8572
+ log(`[direct-chat] Message for '${agent.codeName}' expired while queued behind the spawn gate (msg=${msg.id}) \u2014 draining without a spawn`);
8573
+ try {
8574
+ await api.post("/host/direct-chat/reply", {
8575
+ agent_id: agent.agentId,
8576
+ session_id: msg.session_id,
8577
+ content: "[Expired] This message timed out before it could be processed. Please resend it."
8578
+ });
8579
+ } catch {
8580
+ }
8581
+ return;
8582
+ }
8583
+ log(`[direct-chat] One-shot spawn for '${agent.codeName}' (msg=${msg.id}; host in-flight=${directChatSpawnGate.hostInFlight}, queued=${directChatSpawnGate.queuedCount})`);
7169
8584
  const { getProjectDir: ccProjectDir } = await import("../claude-scheduler-EM24LTGV.js");
7170
8585
  const projDir = ccProjectDir(agent.codeName);
7171
- const mcpConfigPath = join6(projDir, ".mcp.json");
8586
+ const mcpConfigPath = join8(projDir, ".mcp.json");
7172
8587
  const serverNames = [];
7173
- if (existsSync4(mcpConfigPath)) {
8588
+ if (existsSync5(mcpConfigPath)) {
7174
8589
  try {
7175
- const d = JSON.parse(readFileSync5(mcpConfigPath, "utf-8"));
8590
+ const d = JSON.parse(readFileSync9(mcpConfigPath, "utf-8"));
7176
8591
  if (d.mcpServers) serverNames.push(...Object.keys(d.mcpServers));
7177
8592
  } catch {
7178
8593
  }
@@ -7191,19 +8606,15 @@ ${escapeXml(msg.content)}
7191
8606
  "--allowedTools",
7192
8607
  allowedTools
7193
8608
  ];
7194
- const chatClaudeMd = join6(projDir, "CLAUDE.md");
7195
- if (existsSync4(chatClaudeMd)) {
8609
+ const chatClaudeMd = join8(projDir, "CLAUDE.md");
8610
+ if (existsSync5(chatClaudeMd)) {
7196
8611
  chatArgs.push("--system-prompt-file", chatClaudeMd);
7197
8612
  }
7198
- const envIntPath = join6(projDir, ".env.integrations");
8613
+ const envIntPath = join8(projDir, ".env.integrations");
7199
8614
  const childEnv = { ...process.env };
7200
- if (existsSync4(envIntPath)) {
8615
+ if (existsSync5(envIntPath)) {
7201
8616
  try {
7202
- for (const line of readFileSync5(envIntPath, "utf-8").split("\n")) {
7203
- if (!line || line.startsWith("#") || !line.includes("=")) continue;
7204
- const eqIdx = line.indexOf("=");
7205
- childEnv[line.slice(0, eqIdx)] = line.slice(eqIdx + 1);
7206
- }
8617
+ Object.assign(childEnv, parseEnvIntegrations(readFileSync9(envIntPath, "utf-8")));
7207
8618
  } catch {
7208
8619
  }
7209
8620
  }
@@ -7212,7 +8623,26 @@ ${escapeXml(msg.content)}
7212
8623
  } catch (err) {
7213
8624
  throw new Error(`Auth resolve failed for '${agent.codeName}': ${err.message}`);
7214
8625
  }
7215
- const { stdout } = await execFilePromiseLong(resolveClaudeBinary(), chatArgs, { cwd: projDir, stdin: "ignore", env: childEnv });
8626
+ for (const f of probeMcpEnvSubstitution({
8627
+ mcpConfigPath,
8628
+ envIntegrationsPath: envIntPath,
8629
+ baseEnv: childEnv
8630
+ })) {
8631
+ log(`[direct-chat] ${formatMissingVar(f)} agent=${agent.codeName}`);
8632
+ }
8633
+ const { stdout } = await execFilePromiseLong(resolveClaudeBinary(), chatArgs, {
8634
+ cwd: projDir,
8635
+ stdin: "ignore",
8636
+ env: childEnv,
8637
+ onSpawn: (pid) => registerClaudeSpawn({
8638
+ pid,
8639
+ started_at: Date.now(),
8640
+ kind: "direct-chat",
8641
+ agent_id: agent.agentId,
8642
+ agent_code_name: agent.codeName
8643
+ }),
8644
+ onExit: (pid) => unregisterClaudeSpawn(pid)
8645
+ });
7216
8646
  reply = stdout.trim() || "[No response from agent]";
7217
8647
  } else {
7218
8648
  const { stdout } = await execFilePromiseLong("openclaw", [
@@ -7254,6 +8684,8 @@ ${escapeXml(msg.content)}
7254
8684
  });
7255
8685
  } catch {
7256
8686
  }
8687
+ } finally {
8688
+ releaseSpawnSlot?.();
7257
8689
  }
7258
8690
  }
7259
8691
  var STANDUP_TEMPLATES = /* @__PURE__ */ new Set(["daily-standup", "end-of-day-summary"]);
@@ -7560,12 +8992,12 @@ function getBuiltInSkillContent(skillId) {
7560
8992
  if (builtInSkillCache.has(skillId)) return builtInSkillCache.get(skillId);
7561
8993
  try {
7562
8994
  const candidates = [
7563
- join6(process.cwd(), "skills", skillId, "SKILL.md"),
7564
- join6(new URL(".", import.meta.url).pathname, "..", "..", "..", "..", "skills", skillId, "SKILL.md")
8995
+ join8(process.cwd(), "skills", skillId, "SKILL.md"),
8996
+ join8(new URL(".", import.meta.url).pathname, "..", "..", "..", "..", "skills", skillId, "SKILL.md")
7565
8997
  ];
7566
8998
  for (const candidate of candidates) {
7567
- if (existsSync4(candidate)) {
7568
- const content = readFileSync5(candidate, "utf-8");
8999
+ if (existsSync5(candidate)) {
9000
+ const content = readFileSync9(candidate, "utf-8");
7569
9001
  const files = [{ relativePath: "SKILL.md", content }];
7570
9002
  builtInSkillCache.set(skillId, files);
7571
9003
  return files;
@@ -7678,10 +9110,10 @@ function formatBoardForPrompt(items, template) {
7678
9110
  lines.push("=========================");
7679
9111
  lines.push("");
7680
9112
  lines.push("IMPORTANT: Use kanban MCP tools to update the board IN REAL TIME:");
7681
- lines.push("1. FIRST call kanban.move to move your chosen item to in_progress BEFORE starting work");
9113
+ lines.push("1. FIRST call kanban_move to move your chosen item to in_progress BEFORE starting work");
7682
9114
  lines.push("2. Do the work");
7683
- lines.push("3. Call kanban.done with a result summary when finished");
7684
- lines.push("4. If blocked, call kanban.update with notes, then pick the next item");
9115
+ lines.push("3. Call kanban_done with a result summary when finished");
9116
+ lines.push("4. If blocked, call kanban_update with notes, then pick the next item");
7685
9117
  lines.push("");
7686
9118
  if (hasSourceThread) {
7687
9119
  lines.push("THREAD CONTINUITY: If a card shows a `source:` line, the request originated");
@@ -7692,8 +9124,8 @@ function formatBoardForPrompt(items, template) {
7692
9124
  lines.push("");
7693
9125
  }
7694
9126
  lines.push("SELF-MANAGEMENT: When you receive a request from a channel (Slack, Telegram)");
7695
- lines.push("that takes more than a quick response, create a task first with kanban.add,");
7696
- lines.push("move to in_progress, do the work, then kanban.done with a result summary.");
9127
+ lines.push("that takes more than a quick response, create a task first with kanban_add,");
9128
+ lines.push("move to in_progress, do the work, then kanban_done with a result summary.");
7697
9129
  lines.push("");
7698
9130
  lines.push("If MCP tools are unavailable, include a KANBAN UPDATE section in your output:");
7699
9131
  lines.push("KANBAN UPDATE:");
@@ -7736,6 +9168,12 @@ async function execFilePromiseLong(cmd, args, opts) {
7736
9168
  stdio: [opts?.stdin === "ignore" ? "ignore" : "pipe", "pipe", "pipe"],
7737
9169
  ...opts?.env ? { env: opts.env } : {}
7738
9170
  });
9171
+ if (opts?.onSpawn && typeof child.pid === "number") {
9172
+ try {
9173
+ opts.onSpawn(child.pid);
9174
+ } catch {
9175
+ }
9176
+ }
7739
9177
  let stdout = "";
7740
9178
  let stderr = "";
7741
9179
  child.stdout?.on("data", (d) => {
@@ -7750,6 +9188,12 @@ async function execFilePromiseLong(cmd, args, opts) {
7750
9188
  }, opts?.timeout ?? 12e4);
7751
9189
  child.on("close", (code) => {
7752
9190
  clearTimeout(timer);
9191
+ if (opts?.onExit && typeof child.pid === "number") {
9192
+ try {
9193
+ opts.onExit(child.pid);
9194
+ } catch {
9195
+ }
9196
+ }
7753
9197
  if (code !== 0) reject(new ChildProcessError(code, stdout, stderr));
7754
9198
  else resolve({ stdout, stderr });
7755
9199
  });
@@ -7912,7 +9356,7 @@ async function postSlackChannelMessage(agentCodeName, channelId, text, threadTs)
7912
9356
  const data = await response.json();
7913
9357
  if (!data.ok) {
7914
9358
  log(`Slack chat.postMessage failed for '${agentCodeName}' to ${channelId}: ${data.error}`);
7915
- return { ok: false };
9359
+ return { ok: false, error: data.error };
7916
9360
  }
7917
9361
  return { ok: true, ts: data.ts };
7918
9362
  } finally {
@@ -7994,14 +9438,26 @@ async function deliverScheduledTaskOutput(agentCodeName, agentId, rawTarget, bod
7994
9438
  if (parsed.kind === "channel") {
7995
9439
  if (parsed.provider === "slack") {
7996
9440
  const channelId = parsed.channel_id ?? "";
7997
- const sent = await postSlackChannelMessage(agentCodeName, channelId, withLink(body, "slack"));
9441
+ const threadTs = parsed.thread_ts;
9442
+ let sent = await postSlackChannelMessage(agentCodeName, channelId, withLink(body, "slack"), threadTs);
9443
+ let deliveredInThread = Boolean(threadTs);
9444
+ if (!sent.ok && threadTs && (sent.error === "thread_not_found" || sent.error === "message_not_found")) {
9445
+ log(`[delivery] Originating thread ${threadTs} gone in ${channelId} for '${agentCodeName}' \u2014 falling back to top-level post`);
9446
+ sent = await postSlackChannelMessage(agentCodeName, channelId, withLink(body, "slack"));
9447
+ deliveredInThread = false;
9448
+ }
7998
9449
  await reportDeliveryStatus(agentId, taskId, {
7999
9450
  status: sent.ok ? "ok" : "failed",
8000
9451
  medium: "slack",
8001
- error_code: sent.ok ? null : "SLACK_SEND_FAILED"
9452
+ // Preserve Slack's concrete failure code (channel_not_found,
9453
+ // not_in_channel, auth errors, …) in the observability row rather
9454
+ // than collapsing everything to the generic constant.
9455
+ error_code: sent.ok ? null : sent.error ? `SLACK_SEND_FAILED:${sent.error}` : "SLACK_SEND_FAILED"
8002
9456
  });
8003
9457
  if (sent.ok) {
8004
- await maybePostSlackThreadHint(agentCodeName, channelId, sent.ts);
9458
+ if (!deliveredInThread) {
9459
+ await maybePostSlackThreadHint(agentCodeName, channelId, sent.ts);
9460
+ }
8005
9461
  if (sent.ts && taskId) {
8006
9462
  await maybePostScheduledTaskRatingPrompt(agentId, taskId, channelId, sent.ts);
8007
9463
  }
@@ -8196,7 +9652,7 @@ async function processClaudePairSessions(agents) {
8196
9652
  killPairSession,
8197
9653
  pairTmuxSession,
8198
9654
  finalizeClaudePairOnboarding
8199
- } = await import("../claude-pair-runtime-ZBQKBBMT.js");
9655
+ } = await import("../claude-pair-runtime-F6USL3EW.js");
8200
9656
  for (const pairId of pendingResp.cancelled_pair_ids ?? []) {
8201
9657
  log(`[claude-pair] sweeping orphan tmux session for pair ${pairId.slice(0, 8)}`);
8202
9658
  const killed = await killPairSession(pairTmuxSession(pairId));
@@ -8506,8 +9962,8 @@ function parseMemoryFile(raw, fallbackName) {
8506
9962
  };
8507
9963
  }
8508
9964
  async function syncMemories(agent, configDir, log2) {
8509
- const projectDir = join6(configDir, agent.code_name, "project");
8510
- const memoryDir = join6(projectDir, "memory");
9965
+ const projectDir = join8(configDir, agent.code_name, "project");
9966
+ const memoryDir = join8(projectDir, "memory");
8511
9967
  const isFreshSync = pendingFreshMemorySync.has(agent.agent_id);
8512
9968
  if (isFreshSync) {
8513
9969
  log2(`[memory-sync] Fresh-sync requested for '${agent.code_name}' \u2014 pulling DB first`);
@@ -8518,14 +9974,14 @@ async function syncMemories(agent, configDir, log2) {
8518
9974
  }
8519
9975
  pendingFreshMemorySync.delete(agent.agent_id);
8520
9976
  }
8521
- if (existsSync4(memoryDir)) {
9977
+ if (existsSync5(memoryDir)) {
8522
9978
  const prevHashes = memoryFileHashes.get(agent.agent_id) ?? /* @__PURE__ */ new Map();
8523
9979
  const currentHashes = /* @__PURE__ */ new Map();
8524
9980
  const changedMemories = [];
8525
- for (const file of readdirSync3(memoryDir)) {
9981
+ for (const file of readdirSync4(memoryDir)) {
8526
9982
  if (!file.endsWith(".md")) continue;
8527
9983
  try {
8528
- const raw = readFileSync5(join6(memoryDir, file), "utf-8");
9984
+ const raw = readFileSync9(join8(memoryDir, file), "utf-8");
8529
9985
  const fileHash = createHash3("sha256").update(raw).digest("hex").slice(0, 16);
8530
9986
  currentHashes.set(file, fileHash);
8531
9987
  if (prevHashes.get(file) === fileHash) continue;
@@ -8550,7 +10006,7 @@ async function syncMemories(agent, configDir, log2) {
8550
10006
  } catch (err) {
8551
10007
  for (const mem of changedMemories) {
8552
10008
  for (const [file] of currentHashes) {
8553
- const parsed = parseMemoryFile(readFileSync5(join6(memoryDir, file), "utf-8"), file.replace(/\.md$/, ""));
10009
+ const parsed = parseMemoryFile(readFileSync9(join8(memoryDir, file), "utf-8"), file.replace(/\.md$/, ""));
8554
10010
  if (parsed?.name === mem.name) currentHashes.delete(file);
8555
10011
  }
8556
10012
  }
@@ -8563,7 +10019,7 @@ async function syncMemories(agent, configDir, log2) {
8563
10019
  }
8564
10020
  }
8565
10021
  async function downloadMemories(agent, memoryDir, log2, { force }) {
8566
- const localFiles = existsSync4(memoryDir) ? readdirSync3(memoryDir).filter((f) => f.endsWith(".md")).sort() : [];
10022
+ const localFiles = existsSync5(memoryDir) ? readdirSync4(memoryDir).filter((f) => f.endsWith(".md")).sort() : [];
8567
10023
  const localListHash = createHash3("sha256").update(localFiles.join(",")).digest("hex").slice(0, 16);
8568
10024
  const prevLocalHash = lastLocalFileHash.get(agent.agent_id);
8569
10025
  const prevDownload = lastDownloadHash.get(agent.agent_id);
@@ -8578,14 +10034,14 @@ async function downloadMemories(agent, memoryDir, log2, { force }) {
8578
10034
  lastDownloadHash.set(agent.agent_id, responseHash);
8579
10035
  lastLocalFileHash.set(agent.agent_id, localListHash);
8580
10036
  if (dbMemories.memories?.length) {
8581
- mkdirSync2(memoryDir, { recursive: true });
10037
+ mkdirSync4(memoryDir, { recursive: true });
8582
10038
  let written = 0;
8583
10039
  let overwritten = 0;
8584
10040
  for (let i = 0; i < dbMemories.memories.length; i++) {
8585
10041
  const mem = dbMemories.memories[i];
8586
10042
  const rawSlug = mem.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "").slice(0, 60);
8587
10043
  const slug = rawSlug || `memory-${i}`;
8588
- const filePath = join6(memoryDir, `${slug}.md`);
10044
+ const filePath = join8(memoryDir, `${slug}.md`);
8589
10045
  const desired = `---
8590
10046
  name: ${JSON.stringify(mem.name)}
8591
10047
  type: ${mem.type}
@@ -8594,22 +10050,22 @@ description: ${JSON.stringify(mem.content.slice(0, 200))}
8594
10050
 
8595
10051
  ${mem.content}
8596
10052
  `;
8597
- if (existsSync4(filePath)) {
10053
+ if (existsSync5(filePath)) {
8598
10054
  let existing = "";
8599
10055
  try {
8600
- existing = readFileSync5(filePath, "utf-8");
10056
+ existing = readFileSync9(filePath, "utf-8");
8601
10057
  } catch {
8602
10058
  }
8603
10059
  if (existing === desired) continue;
8604
- writeFileSync3(filePath, desired);
10060
+ writeFileSync4(filePath, desired);
8605
10061
  overwritten++;
8606
10062
  } else {
8607
- writeFileSync3(filePath, desired);
10063
+ writeFileSync4(filePath, desired);
8608
10064
  written++;
8609
10065
  }
8610
10066
  }
8611
10067
  if (written > 0 || overwritten > 0) {
8612
- const updatedFiles = readdirSync3(memoryDir).filter((f) => f.endsWith(".md")).sort();
10068
+ const updatedFiles = readdirSync4(memoryDir).filter((f) => f.endsWith(".md")).sort();
8613
10069
  lastLocalFileHash.set(agent.agent_id, createHash3("sha256").update(updatedFiles.join(",")).digest("hex").slice(0, 16));
8614
10070
  log2(`Memory download for '${agent.code_name}': wrote ${written} new, overwrote ${overwritten} stale`);
8615
10071
  }
@@ -8621,7 +10077,7 @@ ${mem.content}
8621
10077
  }
8622
10078
  }
8623
10079
  async function cleanupAgentFiles(codeName, agentDir) {
8624
- if (existsSync4(agentDir)) {
10080
+ if (existsSync5(agentDir)) {
8625
10081
  try {
8626
10082
  rmSync2(agentDir, { recursive: true, force: true });
8627
10083
  log(`Removed provision directory for '${codeName}'`);
@@ -8777,9 +10233,24 @@ async function stopPolling(opts = {}) {
8777
10233
  process.exit(opts.forcedExitCode ?? 1);
8778
10234
  }, 15e3);
8779
10235
  shutdownTimer.unref();
10236
+ const drainStartedAt = Date.now();
10237
+ const livePids = [...inFlightClaudePids.keys()];
10238
+ if (livePids.length > 0) {
10239
+ for (const pid of livePids) {
10240
+ try {
10241
+ process.kill(pid, "SIGTERM");
10242
+ } catch {
10243
+ }
10244
+ }
10245
+ log(formatDrainShutdownLine({ step: "sigterm-claude-pids", elapsedMs: Date.now() - drainStartedAt, killedPids: livePids }));
10246
+ } else {
10247
+ log(formatDrainShutdownLine({ step: "sigterm-claude-pids", elapsedMs: 0, note: "no in-flight claude -p children" }));
10248
+ }
10249
+ const subsysStartedAt = Date.now();
8780
10250
  stopCaffeinate();
8781
10251
  stopRealtimeChat();
8782
10252
  stopGatewayPool();
10253
+ log(formatDrainShutdownLine({ step: "stop-subsystems", elapsedMs: Date.now() - subsysStartedAt }));
8783
10254
  for (const codeName of [...scheduledRunsByCode.keys()]) {
8784
10255
  closeScheduledRunsForCode(codeName, "cancelled", "manager shutdown");
8785
10256
  }
@@ -8795,12 +10266,12 @@ function startManager(opts) {
8795
10266
  config = opts;
8796
10267
  try {
8797
10268
  const stateFile = getStateFile();
8798
- if (existsSync4(stateFile)) {
8799
- const raw = readFileSync5(stateFile, "utf-8");
10269
+ if (existsSync5(stateFile)) {
10270
+ const raw = readFileSync9(stateFile, "utf-8");
8800
10271
  const parsed = JSON.parse(raw);
8801
10272
  if (Array.isArray(parsed.agents)) {
8802
- state4.agents = parsed.agents;
8803
- log(`[startup] rehydrated ${state4.agents.length} agent state(s) from ${stateFile}`);
10273
+ state5.agents = parsed.agents;
10274
+ log(`[startup] rehydrated ${state5.agents.length} agent state(s) from ${stateFile}`);
8804
10275
  }
8805
10276
  if (parsed.circuitBreakerTrips && typeof parsed.circuitBreakerTrips === "object") {
8806
10277
  restartBreaker.hydrate(parsed.circuitBreakerTrips);
@@ -8812,12 +10283,81 @@ function startManager(opts) {
8812
10283
  log(`[startup] state rehydration failed (continuing with empty state): ${err.message}`);
8813
10284
  }
8814
10285
  log(
8815
- `[startup] worker pid=${process.pid} ppid=${process.ppid} node=${process.version} log=${join6(homedir4(), ".augmented", "manager.log")}`
10286
+ `[startup] worker pid=${process.pid} ppid=${process.ppid} node=${process.version} log=${join8(homedir4(), ".augmented", "manager.log")}`
8816
10287
  );
8817
10288
  deployMcpAssets();
8818
10289
  reapOrphanChannelMcps({ log });
8819
- void ensureHostFrameworkBinaries();
8820
- startPolling();
10290
+ void (async () => {
10291
+ try {
10292
+ await reapOrphanedClaudePids();
10293
+ } catch (err) {
10294
+ log(`[drain] boot reaper failed: ${err.message}`);
10295
+ }
10296
+ void ensureHostFrameworkBinaries();
10297
+ startPolling();
10298
+ })();
10299
+ }
10300
+ async function reapOrphanedClaudePids() {
10301
+ const path = claudePidFilePath();
10302
+ const file = readPidFile(path);
10303
+ if (file.spawns.length === 0) return;
10304
+ const looksLikeClaude = (pid) => {
10305
+ if (process.platform !== "linux") return true;
10306
+ try {
10307
+ const comm = readFileSync9(`/proc/${pid}/comm`, "utf-8").trim().toLowerCase();
10308
+ return comm.includes("claude");
10309
+ } catch {
10310
+ return false;
10311
+ }
10312
+ };
10313
+ const decision = decideReaperActions(
10314
+ file,
10315
+ (pid) => {
10316
+ try {
10317
+ process.kill(pid, 0);
10318
+ return true;
10319
+ } catch {
10320
+ return false;
10321
+ }
10322
+ },
10323
+ looksLikeClaude
10324
+ );
10325
+ for (const spawn of decision.toKill) {
10326
+ try {
10327
+ process.kill(spawn.pid, "SIGKILL");
10328
+ } catch (err) {
10329
+ log(`[drain] reaper SIGKILL pid=${spawn.pid} failed: ${err.message}`);
10330
+ }
10331
+ }
10332
+ for (const spawn of decision.pidReusedSkipped) {
10333
+ log(
10334
+ `[drain] reaper skipped pid=${spawn.pid} agent=${spawn.agent_code_name} card=${spawn.kanban_card_id ?? "none"} \u2014 PID reuse suspected (identity probe rejected)`
10335
+ );
10336
+ }
10337
+ let kanbanReset = 0;
10338
+ for (const spawn of decision.toResetKanban) {
10339
+ try {
10340
+ await api.post("/host/kanban", {
10341
+ agent_id: spawn.agent_id,
10342
+ update: [{ id: spawn.kanban_card_id, status: "backlog" }]
10343
+ });
10344
+ kanbanReset += 1;
10345
+ } catch (err) {
10346
+ log(`[drain] reaper kanban-reset failed for card=${spawn.kanban_card_id} agent=${spawn.agent_code_name}: ${err.message}`);
10347
+ }
10348
+ }
10349
+ log(formatReaperBootLine({
10350
+ totalRecorded: file.spawns.length,
10351
+ killed: decision.toKill.length,
10352
+ alreadyDead: decision.alreadyDead.length,
10353
+ pidReusedSkipped: decision.pidReusedSkipped.length,
10354
+ kanbanReset
10355
+ }));
10356
+ try {
10357
+ writePidFile(path, { version: 1, spawns: [] });
10358
+ } catch (err) {
10359
+ log(`[drain] reaper file truncate failed: ${err.message}`);
10360
+ }
8821
10361
  }
8822
10362
  async function ensureHostFrameworkBinaries() {
8823
10363
  const apiKey = getApiKey();
@@ -8874,18 +10414,18 @@ function restartRunningChannelMcps(basenames) {
8874
10414
  }
8875
10415
  }
8876
10416
  function deployMcpAssets() {
8877
- const targetDir = join6(homedir4(), ".augmented", "_mcp");
8878
- mkdirSync2(targetDir, { recursive: true });
8879
- const moduleDir = dirname(fileURLToPath(import.meta.url));
10417
+ const targetDir = join8(homedir4(), ".augmented", "_mcp");
10418
+ mkdirSync4(targetDir, { recursive: true });
10419
+ const moduleDir = dirname3(fileURLToPath(import.meta.url));
8880
10420
  let mcpSourceDir = "";
8881
10421
  let dir = moduleDir;
8882
10422
  for (let i = 0; i < 6; i++) {
8883
- const candidate = join6(dir, "dist", "mcp");
8884
- if (existsSync4(join6(candidate, "index.js"))) {
10423
+ const candidate = join8(dir, "dist", "mcp");
10424
+ if (existsSync5(join8(candidate, "index.js"))) {
8885
10425
  mcpSourceDir = candidate;
8886
10426
  break;
8887
10427
  }
8888
- const parent = dirname(dir);
10428
+ const parent = dirname3(dir);
8889
10429
  if (parent === dir) break;
8890
10430
  dir = parent;
8891
10431
  }
@@ -8896,8 +10436,8 @@ function deployMcpAssets() {
8896
10436
  const changedBasenames = [];
8897
10437
  const fileHash = (p) => {
8898
10438
  try {
8899
- if (!existsSync4(p)) return null;
8900
- return createHash3("sha256").update(readFileSync5(p)).digest("hex");
10439
+ if (!existsSync5(p)) return null;
10440
+ return createHash3("sha256").update(readFileSync9(p)).digest("hex");
8901
10441
  } catch {
8902
10442
  return null;
8903
10443
  }
@@ -8905,12 +10445,22 @@ function deployMcpAssets() {
8905
10445
  const RESTARTABLE_CHANNEL_FILES = /* @__PURE__ */ new Set([
8906
10446
  "slack-channel.js",
8907
10447
  "direct-chat-channel.js",
8908
- "telegram-channel.js"
10448
+ "telegram-channel.js",
10449
+ // ENG-6019: teams-channel was provisioned into .mcp.json but never shipped
10450
+ // in the bundle — the file below has never existed on hosts until now.
10451
+ "teams-channel.js"
8909
10452
  ]);
8910
- for (const file of ["index.js", "slack-channel.js", "direct-chat-channel.js", "telegram-channel.js"]) {
8911
- const src = join6(mcpSourceDir, file);
8912
- const dst = join6(targetDir, file);
8913
- if (!existsSync4(src)) continue;
10453
+ for (const file of [
10454
+ "index.js",
10455
+ "slack-channel.js",
10456
+ "direct-chat-channel.js",
10457
+ "telegram-channel.js",
10458
+ "teams-channel.js"
10459
+ // ENG-6019
10460
+ ]) {
10461
+ const src = join8(mcpSourceDir, file);
10462
+ const dst = join8(targetDir, file);
10463
+ if (!existsSync5(src)) continue;
8914
10464
  const before = fileHash(dst);
8915
10465
  try {
8916
10466
  copyFileSync(src, dst);
@@ -8927,23 +10477,23 @@ function deployMcpAssets() {
8927
10477
  log(`[manager] Bundle(s) updated: ${changedBasenames.join(", ")} \u2014 signalling running instances to restart`);
8928
10478
  restartRunningChannelMcps(changedBasenames);
8929
10479
  }
8930
- const localMcpPath = join6(targetDir, "index.js");
10480
+ const localMcpPath = join8(targetDir, "index.js");
8931
10481
  try {
8932
- const agentsDir = join6(homedir4(), ".augmented", "agents");
8933
- if (existsSync4(agentsDir)) {
8934
- for (const entry of readdirSync3(agentsDir, { withFileTypes: true })) {
10482
+ const agentsDir = join8(homedir4(), ".augmented", "agents");
10483
+ if (existsSync5(agentsDir)) {
10484
+ for (const entry of readdirSync4(agentsDir, { withFileTypes: true })) {
8935
10485
  if (!entry.isDirectory()) continue;
8936
10486
  for (const subdir of ["provision", "project"]) {
8937
- const mcpJsonPath = join6(agentsDir, entry.name, subdir, ".mcp.json");
10487
+ const mcpJsonPath = join8(agentsDir, entry.name, subdir, ".mcp.json");
8938
10488
  try {
8939
- const raw = readFileSync5(mcpJsonPath, "utf-8");
10489
+ const raw = readFileSync9(mcpJsonPath, "utf-8");
8940
10490
  if (!raw.includes("@integrity-labs/augmented-mcp")) continue;
8941
10491
  const mcpConfig = JSON.parse(raw);
8942
10492
  const augServer = mcpConfig.mcpServers?.["augmented"];
8943
10493
  if (!augServer) continue;
8944
10494
  augServer.command = "node";
8945
10495
  augServer.args = [localMcpPath];
8946
- writeFileSync3(mcpJsonPath, JSON.stringify(mcpConfig, null, 2));
10496
+ writeFileSync4(mcpJsonPath, JSON.stringify(mcpConfig, null, 2));
8947
10497
  log(`[manager] Patched ${entry.name}/${subdir}/.mcp.json: npx \u2192 node`);
8948
10498
  } catch {
8949
10499
  }
@@ -8980,15 +10530,19 @@ process.on("disconnect", () => {
8980
10530
  export {
8981
10531
  ChildProcessError,
8982
10532
  __resetScheduledDeliveryDedupeForTest,
10533
+ __setAgentChannelTokensForTest,
8983
10534
  applyRestartAcks,
8984
10535
  buildDoneCardNotification,
8985
10536
  claudeCodeUpgradeMarkerPath,
8986
10537
  claudeCodeUpgradeThrottled,
8987
10538
  claudeManagedSettingsPath,
8988
10539
  deliverScheduledCardResult,
10540
+ deliverScheduledTaskOutput,
10541
+ deriveScheduledTaskNotify,
8989
10542
  ensureClaudeManagedSettings,
8990
10543
  extractCharterSlackPeers,
8991
10544
  extractCharterTelegramPeers,
10545
+ fireScheduledTaskViaKanban,
8992
10546
  formatBoardForPrompt,
8993
10547
  hasActionableItems,
8994
10548
  hasHybridActionableItems,