@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.
- package/dist/assets/impersonate-statusline.sh +7 -2
- package/dist/bin/agt.js +946 -129
- package/dist/bin/agt.js.map +1 -1
- package/dist/{chunk-YSBGIXJG.js → chunk-I2OTQJH3.js} +361 -26
- package/dist/chunk-I2OTQJH3.js.map +1 -0
- package/dist/{chunk-GN4XPQWJ.js → chunk-MQJ5DMPT.js} +535 -94
- package/dist/chunk-MQJ5DMPT.js.map +1 -0
- package/dist/{chunk-IB655E5U.js → chunk-SXBZDUKN.js} +1086 -285
- package/dist/chunk-SXBZDUKN.js.map +1 -0
- package/dist/{claude-pair-runtime-ZBQKBBMT.js → claude-pair-runtime-F6USL3EW.js} +2 -2
- package/dist/lib/manager-worker.js +2071 -517
- package/dist/lib/manager-worker.js.map +1 -1
- package/dist/mcp/direct-chat-channel.js +49 -455
- package/dist/mcp/index.js +232 -137
- package/dist/mcp/slack-channel.js +1379 -856
- package/dist/mcp/teams-channel.js +16018 -0
- package/dist/mcp/telegram-channel.js +1264 -618
- package/dist/{persistent-session-ICYFLUAM.js → persistent-session-OL5EDYB4.js} +5 -3
- package/dist/responsiveness-probe-B6LJJRUD.js +74 -0
- package/dist/responsiveness-probe-B6LJJRUD.js.map +1 -0
- package/package.json +2 -2
- package/dist/chunk-GN4XPQWJ.js.map +0 -1
- package/dist/chunk-IB655E5U.js.map +0 -1
- package/dist/chunk-YSBGIXJG.js.map +0 -1
- package/dist/responsiveness-probe-WZNQ2762.js +0 -33
- package/dist/responsiveness-probe-WZNQ2762.js.map +0 -1
- /package/dist/{claude-pair-runtime-ZBQKBBMT.js.map → claude-pair-runtime-F6USL3EW.js.map} +0 -0
- /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
|
-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
464
|
-
if (
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
|
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 (
|
|
490
|
-
|
|
491
|
-
|
|
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,
|
|
494
|
-
if (
|
|
523
|
+
presenceReaperState.set(sk, state6);
|
|
524
|
+
if (state6.attempts > MAX_PRESENCE_RESTART_ATTEMPTS) {
|
|
495
525
|
givenUp.push(key);
|
|
496
|
-
if (!
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
1930
|
+
import { existsSync as existsSync2, readFileSync as readFileSync6 } from "fs";
|
|
1097
1931
|
import { homedir } from "os";
|
|
1098
|
-
import { join as
|
|
1099
|
-
var
|
|
1100
|
-
var STATS_CACHE_PATH =
|
|
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
|
|
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 -
|
|
1142
|
-
|
|
1143
|
-
if (!
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1614
|
-
|
|
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(
|
|
1623
|
-
candidates.push(
|
|
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
|
|
1716
|
-
import { join as
|
|
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
|
|
2553
|
+
return join6(configDir, CACHE_FILENAME);
|
|
1720
2554
|
}
|
|
1721
2555
|
function loadChannelHashCache(target, configDir) {
|
|
1722
2556
|
const path = getChannelHashCacheFile(configDir);
|
|
1723
|
-
if (!
|
|
2557
|
+
if (!existsSync3(path)) return;
|
|
1724
2558
|
let parsed;
|
|
1725
2559
|
try {
|
|
1726
|
-
parsed = JSON.parse(
|
|
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
|
-
|
|
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
|
|
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
|
|
3025
|
+
import { join as join7 } from "path";
|
|
2263
3026
|
import { randomUUID } from "crypto";
|
|
2264
3027
|
function restartFlagsDir() {
|
|
2265
|
-
return
|
|
3028
|
+
return join7(homedir3(), ".augmented", "restart-flags");
|
|
2266
3029
|
}
|
|
2267
3030
|
function flagPath(codeName) {
|
|
2268
|
-
return
|
|
3031
|
+
return join7(restartFlagsDir(), `${codeName}.flag`);
|
|
2269
3032
|
}
|
|
2270
3033
|
function readRestartFlags() {
|
|
2271
3034
|
const dir = restartFlagsDir();
|
|
2272
|
-
if (!
|
|
3035
|
+
if (!existsSync4(dir)) return [];
|
|
2273
3036
|
const out = [];
|
|
2274
|
-
for (const entry of
|
|
3037
|
+
for (const entry of readdirSync3(dir)) {
|
|
2275
3038
|
if (!entry.endsWith(".flag")) continue;
|
|
2276
3039
|
try {
|
|
2277
|
-
const raw =
|
|
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 (
|
|
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("
|
|
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 =
|
|
2807
|
-
var GATEWAY_PORTS_FILE =
|
|
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(
|
|
2915
|
-
return
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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.
|
|
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 (
|
|
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) =>
|
|
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 =
|
|
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 (
|
|
3346
|
-
const raw =
|
|
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
|
-
|
|
3363
|
-
|
|
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 =
|
|
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 (
|
|
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
|
|
4290
|
+
return join8(homedir4(), ".augmented", ".last-claude-code-upgrade-check");
|
|
3419
4291
|
}
|
|
3420
4292
|
function stampClaudeCodeUpgradeMarker() {
|
|
3421
4293
|
try {
|
|
3422
|
-
|
|
4294
|
+
writeFileSync4(claudeCodeUpgradeMarkerPath(), String(Date.now()));
|
|
3423
4295
|
} catch {
|
|
3424
4296
|
}
|
|
3425
4297
|
}
|
|
3426
4298
|
function claudeCodeUpgradeThrottled() {
|
|
3427
4299
|
try {
|
|
3428
|
-
const lastCheck = parseInt(
|
|
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 =
|
|
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
|
|
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@${
|
|
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(
|
|
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 =
|
|
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 =
|
|
4633
|
+
const claudeDir = join8(homedir4(), ".claude");
|
|
3686
4634
|
for (const filename of [".credentials.json", "credentials.json"]) {
|
|
3687
|
-
const p =
|
|
3688
|
-
if (
|
|
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(
|
|
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
|
-
|
|
3709
|
-
|
|
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
|
|
4757
|
+
return join8(config?.configDir ?? join8(process.env["HOME"] ?? "/tmp", ".augmented"), "manager-state.json");
|
|
3733
4758
|
}
|
|
3734
4759
|
function channelHashCacheDir() {
|
|
3735
|
-
return config?.configDir ??
|
|
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
|
-
|
|
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 =
|
|
3778
|
-
|
|
3779
|
-
if (
|
|
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 =
|
|
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:
|
|
3832
|
-
const skillsDir =
|
|
3833
|
-
const claudeMdPath =
|
|
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
|
|
3837
|
-
const skillFile =
|
|
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 **\`
|
|
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 \`
|
|
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
|
-
|
|
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 =
|
|
4934
|
+
const sharedConfigPath = join8(homeDir, ".openclaw", "openclaw.json");
|
|
3886
4935
|
let sharedConfig;
|
|
3887
4936
|
try {
|
|
3888
|
-
sharedConfig = JSON.parse(
|
|
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 =
|
|
3902
|
-
if (
|
|
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 =
|
|
3908
|
-
const profileAuthDir =
|
|
3909
|
-
const authFile =
|
|
3910
|
-
if (
|
|
3911
|
-
|
|
3912
|
-
const authContent =
|
|
3913
|
-
|
|
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(
|
|
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 =
|
|
3961
|
-
if (!
|
|
5009
|
+
const jobsPath = join8(homeDir, `.openclaw-${codeName}`, "cron", "jobs.json");
|
|
5010
|
+
if (!existsSync5(jobsPath)) return false;
|
|
3962
5011
|
try {
|
|
3963
|
-
const data = JSON.parse(
|
|
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
|
|
3969
|
-
if (!
|
|
3970
|
-
const runStartedAt =
|
|
3971
|
-
const isRunning =
|
|
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 =
|
|
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 =
|
|
4003
|
-
if (
|
|
4004
|
-
const cfg = JSON.parse(
|
|
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
|
-
|
|
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 =
|
|
4029
|
-
if (
|
|
4030
|
-
const cfg = JSON.parse(
|
|
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
|
-
|
|
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-
|
|
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 =
|
|
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-
|
|
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
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
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-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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:
|
|
5436
|
+
priorAgents: state5.agents,
|
|
4361
5437
|
restartAcks
|
|
4362
5438
|
});
|
|
4363
5439
|
if (restartAckStateChanged) {
|
|
4364
5440
|
try {
|
|
4365
|
-
const ackedState = { ...
|
|
4366
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
4485
|
-
...
|
|
5600
|
+
state5 = {
|
|
5601
|
+
...state5,
|
|
4486
5602
|
lastPollAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4487
|
-
pollCount:
|
|
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:
|
|
5614
|
+
send({ type: "state-update", state: state5 });
|
|
4499
5615
|
} catch (err) {
|
|
4500
|
-
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
4725
|
-
const
|
|
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
|
-
|
|
5880
|
+
mkdirSync4(agentDir, { recursive: true });
|
|
4746
5881
|
for (const artifact of artifacts) {
|
|
4747
|
-
const filePath =
|
|
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 =
|
|
4767
|
-
const existing =
|
|
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 =
|
|
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 = !
|
|
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 =
|
|
4813
|
-
|
|
4814
|
-
|
|
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 =
|
|
4818
|
-
if (
|
|
4819
|
-
for (const folder of
|
|
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(
|
|
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(
|
|
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 &&
|
|
6039
|
+
if (written && existsSync5(agentDir)) {
|
|
4901
6040
|
const driftedFiles = [];
|
|
4902
6041
|
for (const [file, expectedHash] of written) {
|
|
4903
|
-
const localHash = hashFile(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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 =
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
const provisionMcpPath =
|
|
5076
|
-
const projectMcpPath =
|
|
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(
|
|
6372
|
+
mcpConfig = JSON.parse(readFileSync9(provisionMcpPath, "utf-8"));
|
|
5080
6373
|
if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
|
|
5081
6374
|
} catch {
|
|
5082
6375
|
}
|
|
5083
|
-
const localDirectChatChannel =
|
|
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 (
|
|
5090
|
-
|
|
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
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
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 =
|
|
5106
|
-
if (
|
|
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 =
|
|
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 =
|
|
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 =
|
|
5181
|
-
const envIntPath =
|
|
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 =
|
|
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 =
|
|
5197
|
-
const postWriteEnv =
|
|
5198
|
-
const mcpContent =
|
|
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:
|
|
5267
|
-
const mcpConfig = JSON.parse(
|
|
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 =
|
|
5364
|
-
if (
|
|
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:
|
|
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
|
-
|
|
6735
|
+
join8(homedir5(), ".augmented", agent.code_name, "skills"),
|
|
5435
6736
|
// Claude Code — project tree
|
|
5436
|
-
|
|
6737
|
+
join8(homedir5(), ".augmented", agent.code_name, "project", ".claude", "skills"),
|
|
5437
6738
|
// OpenClaw — framework runtime tree
|
|
5438
|
-
|
|
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
|
-
|
|
6742
|
+
join8(agentDir, ".claude", "skills")
|
|
5442
6743
|
];
|
|
5443
|
-
const existingDirs = candidateSkillDirs.filter((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
|
|
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 =
|
|
5458
|
-
if (
|
|
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 =
|
|
5620
|
-
mcpJsonParsed = JSON.parse(
|
|
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 =
|
|
5775
|
-
if (
|
|
7113
|
+
const jobsPath = join8(homeDir, `.openclaw-${agent.code_name}`, "cron", "jobs.json");
|
|
7114
|
+
if (existsSync5(jobsPath)) {
|
|
5776
7115
|
try {
|
|
5777
|
-
const jobsData = JSON.parse(
|
|
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 &&
|
|
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(
|
|
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 =
|
|
7287
|
+
const sessionsDir = join8(homeDir, `.openclaw-${codeName}`, "agents", agentDir, "sessions");
|
|
5949
7288
|
cleanupCronSessions(sessionsDir, CRON_SESSION_KEEP_COUNT);
|
|
5950
7289
|
}
|
|
5951
|
-
const cronRunsDir =
|
|
7290
|
+
const cronRunsDir = join8(homeDir, `.openclaw-${codeName}`, "cron", "runs");
|
|
5952
7291
|
cleanupOldFiles(cronRunsDir, CRON_RUN_RETENTION_DAYS, ".jsonl");
|
|
5953
|
-
const cronJobsPath =
|
|
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 =
|
|
5958
|
-
if (!
|
|
7296
|
+
const indexPath = join8(sessionsDir, "sessions.json");
|
|
7297
|
+
if (!existsSync5(indexPath)) return;
|
|
5959
7298
|
try {
|
|
5960
|
-
const raw =
|
|
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 =
|
|
7312
|
+
const sessionFile = join8(sessionsDir, `${entry.sessionId}.jsonl`);
|
|
5974
7313
|
try {
|
|
5975
|
-
if (
|
|
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 =
|
|
5996
|
-
if (
|
|
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
|
-
|
|
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 (!
|
|
7353
|
+
if (!existsSync5(jobsPath)) return;
|
|
6015
7354
|
try {
|
|
6016
|
-
const raw =
|
|
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
|
|
6024
|
-
if (!
|
|
6025
|
-
const runStartedAt =
|
|
6026
|
-
const isRunning =
|
|
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
|
-
|
|
6029
|
-
delete
|
|
6030
|
-
delete
|
|
6031
|
-
delete
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
|
7390
|
+
for (const f of readdirSync4(dir)) {
|
|
6052
7391
|
if (!f.endsWith(ext)) continue;
|
|
6053
|
-
const fullPath =
|
|
7392
|
+
const fullPath = join8(dir, f);
|
|
6054
7393
|
try {
|
|
6055
|
-
const st =
|
|
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
|
|
6100
|
-
claudeSchedulerStates.set(codeName,
|
|
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
|
|
6108
|
-
const ready = getReadyTasks(
|
|
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 === "
|
|
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
|
|
6250
|
-
const task =
|
|
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 =
|
|
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 =
|
|
7743
|
+
const claudeMdPath = join8(projectDir, "CLAUDE.md");
|
|
6342
7744
|
const serverNames = [];
|
|
6343
|
-
if (
|
|
7745
|
+
if (existsSync5(mcpConfigPath)) {
|
|
6344
7746
|
try {
|
|
6345
|
-
const d = JSON.parse(
|
|
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 (
|
|
7766
|
+
if (existsSync5(claudeMdPath)) {
|
|
6365
7767
|
claudeArgs.push("--system-prompt-file", claudeMdPath);
|
|
6366
7768
|
}
|
|
6367
7769
|
const childEnv = { ...process.env };
|
|
6368
|
-
const envIntPath =
|
|
6369
|
-
if (
|
|
7770
|
+
const envIntPath = join8(projectDir, ".env.integrations");
|
|
7771
|
+
if (existsSync5(envIntPath)) {
|
|
6370
7772
|
try {
|
|
6371
|
-
|
|
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 =
|
|
6531
|
-
const claudeMdPath =
|
|
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
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
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
|
|
6771
|
-
if (
|
|
6772
|
-
const ready = getReadyTasks(
|
|
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
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
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 =
|
|
8586
|
+
const mcpConfigPath = join8(projDir, ".mcp.json");
|
|
7172
8587
|
const serverNames = [];
|
|
7173
|
-
if (
|
|
8588
|
+
if (existsSync5(mcpConfigPath)) {
|
|
7174
8589
|
try {
|
|
7175
|
-
const d = JSON.parse(
|
|
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 =
|
|
7195
|
-
if (
|
|
8609
|
+
const chatClaudeMd = join8(projDir, "CLAUDE.md");
|
|
8610
|
+
if (existsSync5(chatClaudeMd)) {
|
|
7196
8611
|
chatArgs.push("--system-prompt-file", chatClaudeMd);
|
|
7197
8612
|
}
|
|
7198
|
-
const envIntPath =
|
|
8613
|
+
const envIntPath = join8(projDir, ".env.integrations");
|
|
7199
8614
|
const childEnv = { ...process.env };
|
|
7200
|
-
if (
|
|
8615
|
+
if (existsSync5(envIntPath)) {
|
|
7201
8616
|
try {
|
|
7202
|
-
|
|
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
|
|
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
|
-
|
|
7564
|
-
|
|
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 (
|
|
7568
|
-
const content =
|
|
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
|
|
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
|
|
7684
|
-
lines.push("4. If blocked, call
|
|
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
|
|
7696
|
-
lines.push("move to in_progress, do the work, then
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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 =
|
|
8510
|
-
const memoryDir =
|
|
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 (
|
|
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
|
|
9981
|
+
for (const file of readdirSync4(memoryDir)) {
|
|
8526
9982
|
if (!file.endsWith(".md")) continue;
|
|
8527
9983
|
try {
|
|
8528
|
-
const raw =
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
10053
|
+
if (existsSync5(filePath)) {
|
|
8598
10054
|
let existing = "";
|
|
8599
10055
|
try {
|
|
8600
|
-
existing =
|
|
10056
|
+
existing = readFileSync9(filePath, "utf-8");
|
|
8601
10057
|
} catch {
|
|
8602
10058
|
}
|
|
8603
10059
|
if (existing === desired) continue;
|
|
8604
|
-
|
|
10060
|
+
writeFileSync4(filePath, desired);
|
|
8605
10061
|
overwritten++;
|
|
8606
10062
|
} else {
|
|
8607
|
-
|
|
10063
|
+
writeFileSync4(filePath, desired);
|
|
8608
10064
|
written++;
|
|
8609
10065
|
}
|
|
8610
10066
|
}
|
|
8611
10067
|
if (written > 0 || overwritten > 0) {
|
|
8612
|
-
const updatedFiles =
|
|
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 (
|
|
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 (
|
|
8799
|
-
const raw =
|
|
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
|
-
|
|
8803
|
-
log(`[startup] rehydrated ${
|
|
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=${
|
|
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
|
|
8820
|
-
|
|
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 =
|
|
8878
|
-
|
|
8879
|
-
const moduleDir =
|
|
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 =
|
|
8884
|
-
if (
|
|
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 =
|
|
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 (!
|
|
8900
|
-
return createHash3("sha256").update(
|
|
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 [
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
|
|
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 =
|
|
10480
|
+
const localMcpPath = join8(targetDir, "index.js");
|
|
8931
10481
|
try {
|
|
8932
|
-
const agentsDir =
|
|
8933
|
-
if (
|
|
8934
|
-
for (const entry of
|
|
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 =
|
|
10487
|
+
const mcpJsonPath = join8(agentsDir, entry.name, subdir, ".mcp.json");
|
|
8938
10488
|
try {
|
|
8939
|
-
const raw =
|
|
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
|
-
|
|
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,
|