@raysonmeng/agentbridge 0.1.5 → 0.1.7
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/.claude-plugin/marketplace.json +1 -1
- package/README.md +55 -9
- package/README.zh-CN.md +39 -4
- package/dist/cli.js +4242 -464
- package/dist/daemon.js +4634 -0
- package/package.json +18 -5
- package/plugins/agentbridge/.claude-plugin/plugin.json +1 -1
- package/plugins/agentbridge/README.md +2 -2
- package/plugins/agentbridge/scripts/health-check.sh +22 -3
- package/plugins/agentbridge/scripts/plugin-update-notice.mjs +73 -0
- package/plugins/agentbridge/server/bridge-server.js +1247 -235
- package/plugins/agentbridge/server/daemon.js +2897 -432
- package/scripts/install-safety.cjs +209 -0
- package/scripts/postinstall.cjs +129 -9
|
@@ -6518,7 +6518,7 @@ var require_dist = __commonJS((exports, module) => {
|
|
|
6518
6518
|
});
|
|
6519
6519
|
|
|
6520
6520
|
// src/bridge.ts
|
|
6521
|
-
import {
|
|
6521
|
+
import { existsSync as existsSync6 } from "fs";
|
|
6522
6522
|
|
|
6523
6523
|
// node_modules/zod/v4/core/core.js
|
|
6524
6524
|
var NEVER = Object.freeze({
|
|
@@ -13662,14 +13662,267 @@ class StdioServerTransport {
|
|
|
13662
13662
|
// src/claude-adapter.ts
|
|
13663
13663
|
import { EventEmitter } from "events";
|
|
13664
13664
|
import { randomUUID } from "crypto";
|
|
13665
|
-
|
|
13665
|
+
|
|
13666
|
+
// src/rotating-log.ts
|
|
13667
|
+
import { appendFileSync, existsSync, renameSync, statSync, unlinkSync } from "fs";
|
|
13668
|
+
import { dirname } from "path";
|
|
13669
|
+
var DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
|
|
13670
|
+
var DEFAULT_KEEP = 3;
|
|
13671
|
+
function appendRotatingLog(path, content, options = {}) {
|
|
13672
|
+
const maxBytes = options.maxBytes ?? positiveIntFromEnv("AGENTBRIDGE_LOG_MAX_BYTES", DEFAULT_MAX_BYTES);
|
|
13673
|
+
const keep = options.keep ?? positiveIntFromEnv("AGENTBRIDGE_LOG_ROTATE_KEEP", DEFAULT_KEEP);
|
|
13674
|
+
if (!existsSync(dirname(path)))
|
|
13675
|
+
return;
|
|
13676
|
+
rotateIfNeeded(path, Buffer.byteLength(content), maxBytes, keep);
|
|
13677
|
+
appendFileSync(path, content, "utf-8");
|
|
13678
|
+
}
|
|
13679
|
+
function positiveIntFromEnv(name, fallback) {
|
|
13680
|
+
const value = process.env[name];
|
|
13681
|
+
if (!value)
|
|
13682
|
+
return fallback;
|
|
13683
|
+
const parsed = Number(value);
|
|
13684
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
13685
|
+
}
|
|
13686
|
+
function rotateIfNeeded(path, incomingBytes, maxBytes, keep) {
|
|
13687
|
+
if (!Number.isFinite(maxBytes) || maxBytes <= 0 || keep <= 0)
|
|
13688
|
+
return;
|
|
13689
|
+
if (!existsSync(path))
|
|
13690
|
+
return;
|
|
13691
|
+
const size = statSync(path).size;
|
|
13692
|
+
if (size + incomingBytes <= maxBytes)
|
|
13693
|
+
return;
|
|
13694
|
+
for (let index = keep;index >= 1; index--) {
|
|
13695
|
+
const current = `${path}.${index}`;
|
|
13696
|
+
const next = `${path}.${index + 1}`;
|
|
13697
|
+
if (!existsSync(current))
|
|
13698
|
+
continue;
|
|
13699
|
+
if (index === keep) {
|
|
13700
|
+
unlinkSync(current);
|
|
13701
|
+
} else {
|
|
13702
|
+
renameSync(current, next);
|
|
13703
|
+
}
|
|
13704
|
+
}
|
|
13705
|
+
renameSync(path, `${path}.1`);
|
|
13706
|
+
}
|
|
13707
|
+
|
|
13708
|
+
// src/process-log.ts
|
|
13709
|
+
var stderrStates = new WeakMap;
|
|
13710
|
+
function createProcessLogger(options) {
|
|
13711
|
+
let fatalInProgress = false;
|
|
13712
|
+
const stderr = options.stderr ?? process.stderr;
|
|
13713
|
+
const stderrState = stateForStderr(stderr);
|
|
13714
|
+
const write = (message) => {
|
|
13715
|
+
const line = `[${new Date().toISOString()}] [${options.component}] ${message}
|
|
13716
|
+
`;
|
|
13717
|
+
if (options.logFile) {
|
|
13718
|
+
try {
|
|
13719
|
+
appendRotatingLog(options.logFile, line);
|
|
13720
|
+
} catch {}
|
|
13721
|
+
}
|
|
13722
|
+
if (!stderrState.enabled)
|
|
13723
|
+
return;
|
|
13724
|
+
try {
|
|
13725
|
+
stderr.write(line);
|
|
13726
|
+
} catch (error2) {
|
|
13727
|
+
if (error2?.code === "EPIPE")
|
|
13728
|
+
stderrState.enabled = false;
|
|
13729
|
+
}
|
|
13730
|
+
};
|
|
13731
|
+
return {
|
|
13732
|
+
log: write,
|
|
13733
|
+
fatal(label, error2) {
|
|
13734
|
+
if (fatalInProgress)
|
|
13735
|
+
return;
|
|
13736
|
+
fatalInProgress = true;
|
|
13737
|
+
try {
|
|
13738
|
+
write(`${label}: ${safeFormatError(error2)}`);
|
|
13739
|
+
} finally {
|
|
13740
|
+
fatalInProgress = false;
|
|
13741
|
+
}
|
|
13742
|
+
}
|
|
13743
|
+
};
|
|
13744
|
+
}
|
|
13745
|
+
function stateForStderr(stderr) {
|
|
13746
|
+
const key = stderr;
|
|
13747
|
+
let state = stderrStates.get(key);
|
|
13748
|
+
if (state)
|
|
13749
|
+
return state;
|
|
13750
|
+
state = { enabled: true };
|
|
13751
|
+
stderrStates.set(key, state);
|
|
13752
|
+
if (typeof stderr.on === "function") {
|
|
13753
|
+
stderr.on("error", (error2) => {
|
|
13754
|
+
if (error2?.code === "EPIPE") {
|
|
13755
|
+
state.enabled = false;
|
|
13756
|
+
return;
|
|
13757
|
+
}
|
|
13758
|
+
setTimeout(() => {
|
|
13759
|
+
throw error2;
|
|
13760
|
+
}, 0);
|
|
13761
|
+
});
|
|
13762
|
+
}
|
|
13763
|
+
return state;
|
|
13764
|
+
}
|
|
13765
|
+
function safeFormatError(error2) {
|
|
13766
|
+
try {
|
|
13767
|
+
return formatError2(error2);
|
|
13768
|
+
} catch {
|
|
13769
|
+
return "<failed to format error>";
|
|
13770
|
+
}
|
|
13771
|
+
}
|
|
13772
|
+
function formatError2(error2) {
|
|
13773
|
+
if (error2 instanceof Error)
|
|
13774
|
+
return error2.stack ?? error2.message;
|
|
13775
|
+
if (typeof error2 === "object" && error2 !== null && "stack" in error2) {
|
|
13776
|
+
return String(error2.stack);
|
|
13777
|
+
}
|
|
13778
|
+
return String(error2);
|
|
13779
|
+
}
|
|
13780
|
+
|
|
13781
|
+
// src/state-dir.ts
|
|
13782
|
+
import { mkdirSync, existsSync as existsSync2 } from "fs";
|
|
13783
|
+
import { join } from "path";
|
|
13784
|
+
import { homedir, platform } from "os";
|
|
13785
|
+
|
|
13786
|
+
class StateDirResolver {
|
|
13787
|
+
stateDir;
|
|
13788
|
+
static platformBaseDir() {
|
|
13789
|
+
if (platform() === "darwin") {
|
|
13790
|
+
return join(homedir(), "Library", "Application Support", "AgentBridge");
|
|
13791
|
+
}
|
|
13792
|
+
const xdgState = process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state");
|
|
13793
|
+
return join(xdgState, "agentbridge");
|
|
13794
|
+
}
|
|
13795
|
+
constructor(envOverride) {
|
|
13796
|
+
const override = envOverride ?? process.env.AGENTBRIDGE_STATE_DIR;
|
|
13797
|
+
this.stateDir = override && override.length > 0 ? override : StateDirResolver.platformBaseDir();
|
|
13798
|
+
}
|
|
13799
|
+
ensure() {
|
|
13800
|
+
if (!existsSync2(this.stateDir)) {
|
|
13801
|
+
mkdirSync(this.stateDir, { recursive: true });
|
|
13802
|
+
}
|
|
13803
|
+
}
|
|
13804
|
+
get dir() {
|
|
13805
|
+
return this.stateDir;
|
|
13806
|
+
}
|
|
13807
|
+
get pidFile() {
|
|
13808
|
+
return join(this.stateDir, "daemon.pid");
|
|
13809
|
+
}
|
|
13810
|
+
get tuiPidFile() {
|
|
13811
|
+
return join(this.stateDir, "codex-tui.pid");
|
|
13812
|
+
}
|
|
13813
|
+
get lockFile() {
|
|
13814
|
+
return join(this.stateDir, "daemon.lock");
|
|
13815
|
+
}
|
|
13816
|
+
get statusFile() {
|
|
13817
|
+
return join(this.stateDir, "status.json");
|
|
13818
|
+
}
|
|
13819
|
+
get portsFile() {
|
|
13820
|
+
return join(this.stateDir, "ports.json");
|
|
13821
|
+
}
|
|
13822
|
+
get currentThreadFile() {
|
|
13823
|
+
return join(this.stateDir, "current-thread.json");
|
|
13824
|
+
}
|
|
13825
|
+
get logFile() {
|
|
13826
|
+
return join(this.stateDir, "agentbridge.log");
|
|
13827
|
+
}
|
|
13828
|
+
get codexWrapperLogFile() {
|
|
13829
|
+
return join(this.stateDir, "codex-wrapper.log");
|
|
13830
|
+
}
|
|
13831
|
+
get killedFile() {
|
|
13832
|
+
return join(this.stateDir, "killed");
|
|
13833
|
+
}
|
|
13834
|
+
get updateCheckFile() {
|
|
13835
|
+
return join(this.stateDir, "update-check.json");
|
|
13836
|
+
}
|
|
13837
|
+
}
|
|
13838
|
+
|
|
13839
|
+
// src/budget/render.ts
|
|
13840
|
+
function formatEpoch(epochSeconds) {
|
|
13841
|
+
if (!epochSeconds || epochSeconds <= 0)
|
|
13842
|
+
return "\u672A\u77E5";
|
|
13843
|
+
return new Date(epochSeconds * 1000).toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z");
|
|
13844
|
+
}
|
|
13845
|
+
function formatWindow(window, label) {
|
|
13846
|
+
if (!window)
|
|
13847
|
+
return `${label} \u672A\u77E5`;
|
|
13848
|
+
return `${label} ${window.util}%\uFF08\u91CD\u7F6E ${formatEpoch(window.resetEpoch)}\uFF09`;
|
|
13849
|
+
}
|
|
13850
|
+
function formatAgent(name, usage, snapshotAt) {
|
|
13851
|
+
if (!usage)
|
|
13852
|
+
return `${name}\uFF1A\u672A\u77E5\uFF08\u63A2\u6D4B\u4E0D\u53EF\u7528\uFF09`;
|
|
13853
|
+
const parts = [
|
|
13854
|
+
formatWindow(usage.fiveHour, "5h"),
|
|
13855
|
+
formatWindow(usage.weekly, "\u5468"),
|
|
13856
|
+
`\u95E8\u63A7 ${usage.gateUtil}%`,
|
|
13857
|
+
`\u9884\u8B66 ${usage.warnUtil}%`
|
|
13858
|
+
];
|
|
13859
|
+
if (usage.rateLimitedUntil > 0) {
|
|
13860
|
+
parts.push(`\u9650\u6D41\u81F3 ${formatEpoch(usage.rateLimitedUntil)}`);
|
|
13861
|
+
}
|
|
13862
|
+
const ageSec = usage.fetchedAt > 0 ? snapshotAt - usage.fetchedAt : 0;
|
|
13863
|
+
if (ageSec > 300) {
|
|
13864
|
+
parts.push(`\u26A0\uFE0F \u6570\u636E\u91C7\u96C6\u4E8E ${Math.round(ageSec / 60)} \u5206\u949F\u524D`);
|
|
13865
|
+
} else if (usage.stale) {
|
|
13866
|
+
parts.push("\uFF08\u7F13\u5B58\u6570\u636E\uFF09");
|
|
13867
|
+
}
|
|
13868
|
+
return `${name}\uFF1A${parts.join(" \xB7 ")}`;
|
|
13869
|
+
}
|
|
13870
|
+
var PHASE_LABELS = {
|
|
13871
|
+
normal: "normal\uFF08\u6B63\u5E38\uFF09",
|
|
13872
|
+
balance: "balance\uFF08\u9700\u5747\u8861\uFF09",
|
|
13873
|
+
parallel: "parallel\uFF08\u5EFA\u8BAE\u5E76\u884C\u63D0\u901F\uFF09",
|
|
13874
|
+
paused: "paused\uFF08\u9884\u7B97\u5E72\u9884\u4E2D\uFF09"
|
|
13875
|
+
};
|
|
13876
|
+
function renderBudgetSnapshot(snapshot) {
|
|
13877
|
+
const lines = [];
|
|
13878
|
+
lines.push(`\u3010\u9884\u7B97\u5FEB\u7167 \xB7 \u8D26\u53F7\u7EA7\u3011\u9636\u6BB5\uFF1A${PHASE_LABELS[snapshot.phase]} \xB7 \u66F4\u65B0\u4E8E ${formatEpoch(snapshot.updatedAt)}`);
|
|
13879
|
+
lines.push(formatAgent("Claude", snapshot.claude, snapshot.updatedAt));
|
|
13880
|
+
lines.push(formatAgent("Codex", snapshot.codex, snapshot.updatedAt));
|
|
13881
|
+
if (snapshot.claude && snapshot.codex) {
|
|
13882
|
+
const abs = Math.abs(snapshot.driftPct);
|
|
13883
|
+
if (abs > 0) {
|
|
13884
|
+
const heavier = snapshot.driftPct > 0 ? "Claude" : "Codex";
|
|
13885
|
+
const lighter = snapshot.driftPct > 0 ? "Codex" : "Claude";
|
|
13886
|
+
lines.push(`\u6F02\u79FB\uFF1A${heavier} \u6BD4 ${lighter} \u9AD8 ${abs} \u4E2A\u767E\u5206\u70B9`);
|
|
13887
|
+
} else {
|
|
13888
|
+
lines.push("\u6F02\u79FB\uFF1A\u53CC\u65B9\u6301\u5E73");
|
|
13889
|
+
}
|
|
13890
|
+
}
|
|
13891
|
+
if (snapshot.paused) {
|
|
13892
|
+
const resume = snapshot.resumeAfterEpoch ? `\uFF1B\u9884\u8BA1\u6062\u590D ${formatEpoch(snapshot.resumeAfterEpoch)}\uFF08\u4EE5\u5B9E\u6D4B\u4E3A\u51C6\uFF1B\u63D0\u524D\u5237\u65B0\u4F1A\u66F4\u65E9\u89E3\u9664\uFF09` : "";
|
|
13893
|
+
const reason = snapshot.pauseReason ?? "\u989D\u5EA6\u63A5\u8FD1\u8017\u5C3D";
|
|
13894
|
+
if (snapshot.pauseSide === "claude" && !snapshot.gateClosed) {
|
|
13895
|
+
lines.push(`\u63A5\u529B\u4E2D\uFF1AClaude \u4FA7\u989D\u5EA6\u8017\u5C3D\uFF0C\u5DF2\u4EA4\u63A5 Codex \u7EE7\u7EED\u63A8\u8FDB\uFF08\u95F8\u95E8\u5F00\u653E\uFF09 \u2014 ${reason}${resume}`);
|
|
13896
|
+
} else if (snapshot.pauseSide === "codex") {
|
|
13897
|
+
lines.push(`\u6682\u505C\uFF1ACodex \u4FA7\u989D\u5EA6\u8017\u5C3D\uFF08\u95F8\u95E8\u5173\u95ED\uFF0CClaude \u53EF solo \u63A8\u8FDB\u72EC\u7ACB\u90E8\u5206\uFF09 \u2014 ${reason}${resume}`);
|
|
13898
|
+
} else {
|
|
13899
|
+
lines.push(`\u6682\u505C\uFF1A\u53CC\u4FA7\u8054\u5408\u6682\u505C\uFF08\u95F8\u95E8\u5173\u95ED\uFF09 \u2014 ${reason}${resume}`);
|
|
13900
|
+
}
|
|
13901
|
+
} else {
|
|
13902
|
+
lines.push("\u6682\u505C\uFF1A\u5426");
|
|
13903
|
+
}
|
|
13904
|
+
if (snapshot.parallelRecommended) {
|
|
13905
|
+
lines.push("\u5E76\u884C\u5EFA\u8BAE\uFF1A\u989D\u5EA6\u5BCC\u4F59\u4E14\u4E34\u8FD1\u7ED3\u7B97\uFF0C\u5EFA\u8BAE\u62C6\u5206\u66F4\u591A\u5E76\u884C\u5B50\u4EFB\u52A1");
|
|
13906
|
+
}
|
|
13907
|
+
if (snapshot.codexTier !== "full") {
|
|
13908
|
+
lines.push(`Codex \u6863\u4F4D\uFF1A${snapshot.codexTier}`);
|
|
13909
|
+
}
|
|
13910
|
+
if (snapshot.claudeAdvice) {
|
|
13911
|
+
lines.push(`Claude \u5EFA\u8BAE\uFF1A${snapshot.claudeAdvice}`);
|
|
13912
|
+
}
|
|
13913
|
+
lines.push("\u6CE8\uFF1A\u767E\u5206\u6BD4\u4E3A\u8BA2\u9605\u8D26\u53F7\u7EA7\u7528\u91CF\uFF08\u540C\u673A\u5176\u4ED6\u4F1A\u8BDD\u5171\u4EAB\u540C\u4E00\u989D\u5EA6\u6C60\uFF09\u3002");
|
|
13914
|
+
return lines.join(`
|
|
13915
|
+
`);
|
|
13916
|
+
}
|
|
13917
|
+
var BUDGET_UNAVAILABLE_TEXT = "\u9884\u7B97\u611F\u77E5\u4E0D\u53EF\u7528\uFF1A\u672A\u68C0\u6D4B\u5230 agent-quota-guard \u63A2\u9488\uFF08~/.budget-guard/bin/budget-probe\uFF09\u6216 budget \u529F\u80FD\u5DF2\u7981\u7528\u3002\u534F\u4F5C\u4E0D\u53D7\u5F71\u54CD\u3002";
|
|
13918
|
+
|
|
13919
|
+
// src/claude-adapter.ts
|
|
13666
13920
|
var CLAUDE_INSTRUCTIONS = [
|
|
13667
13921
|
"Codex is an AI coding agent (OpenAI) running in a separate session on the same machine.",
|
|
13668
13922
|
"",
|
|
13669
13923
|
"## Message delivery",
|
|
13670
|
-
|
|
13671
|
-
|
|
13672
|
-
"- Via the get_messages tool (pull mode)",
|
|
13924
|
+
'Messages from Codex arrive as <channel source="agentbridge" chat_id="..." user="Codex" ...> tags (push).',
|
|
13925
|
+
"If a push fails, the message is queued \u2014 call get_messages to drain the fallback queue.",
|
|
13673
13926
|
"",
|
|
13674
13927
|
"## Collaboration roles",
|
|
13675
13928
|
"Default roles in this setup:",
|
|
@@ -13694,28 +13947,38 @@ var CLAUDE_INSTRUCTIONS = [
|
|
|
13694
13947
|
"## Turn coordination",
|
|
13695
13948
|
"- When you see '\u23F3 Codex is working', do NOT call the reply tool \u2014 wait for '\u2705 Codex finished'.",
|
|
13696
13949
|
"- After Codex finishes a turn, you have an attention window to review and respond before new messages arrive.",
|
|
13697
|
-
"- If the reply tool returns a busy error, Codex is still executing \u2014 wait and try again later."
|
|
13950
|
+
"- If the reply tool returns a busy error, Codex is still executing \u2014 wait and try again later.",
|
|
13951
|
+
"",
|
|
13952
|
+
"## Budget awareness",
|
|
13953
|
+
"- Use the get_budget tool to check both agents' subscription quota (5h/weekly windows, drift, pause state).",
|
|
13954
|
+
"- If the reply tool returns a budget-pause error, do NOT retry; checkpoint your work and wait for the resume notice."
|
|
13698
13955
|
].join(`
|
|
13699
13956
|
`);
|
|
13700
|
-
var LOG_FILE = "/tmp/agentbridge.log";
|
|
13701
13957
|
|
|
13702
13958
|
class ClaudeAdapter extends EventEmitter {
|
|
13703
13959
|
server;
|
|
13704
13960
|
notificationSeq = 0;
|
|
13705
13961
|
sessionId;
|
|
13706
13962
|
notificationIdPrefix;
|
|
13963
|
+
instanceId;
|
|
13707
13964
|
replySender = null;
|
|
13708
|
-
|
|
13709
|
-
|
|
13965
|
+
logFile;
|
|
13966
|
+
logger;
|
|
13710
13967
|
pendingMessages = [];
|
|
13711
13968
|
maxBufferedMessages;
|
|
13712
13969
|
droppedMessageCount = 0;
|
|
13713
|
-
|
|
13970
|
+
budgetSnapshot = null;
|
|
13971
|
+
constructor(logFile = new StateDirResolver().logFile) {
|
|
13714
13972
|
super();
|
|
13973
|
+
this.logFile = logFile;
|
|
13974
|
+
this.logger = createProcessLogger({ component: "ClaudeAdapter", logFile: this.logFile });
|
|
13975
|
+
this.instanceId = randomUUID().slice(0, 8);
|
|
13715
13976
|
this.sessionId = `codex_${Date.now()}`;
|
|
13716
13977
|
this.notificationIdPrefix = randomUUID().replace(/-/g, "").slice(0, 12);
|
|
13717
|
-
|
|
13718
|
-
|
|
13978
|
+
this.log(`ClaudeAdapter created (instance=${this.instanceId})`);
|
|
13979
|
+
if (process.env.AGENTBRIDGE_MODE) {
|
|
13980
|
+
this.log(`AGENTBRIDGE_MODE="${process.env.AGENTBRIDGE_MODE}" is no longer supported \u2014 ` + "pull mode was removed; push delivery (with per-message fallback queue) is always used.");
|
|
13981
|
+
}
|
|
13719
13982
|
this.maxBufferedMessages = parseInt(process.env.AGENTBRIDGE_MAX_BUFFERED_MESSAGES ?? "100", 10);
|
|
13720
13983
|
this.server = new Server({ name: "agentbridge", version: "0.1.0" }, {
|
|
13721
13984
|
capabilities: {
|
|
@@ -13728,37 +13991,22 @@ class ClaudeAdapter extends EventEmitter {
|
|
|
13728
13991
|
}
|
|
13729
13992
|
async start() {
|
|
13730
13993
|
const transport = new StdioServerTransport;
|
|
13731
|
-
this.resolveMode();
|
|
13732
13994
|
await this.server.connect(transport);
|
|
13733
|
-
this.log(
|
|
13995
|
+
this.log("MCP server connected (push delivery)");
|
|
13734
13996
|
this.emit("ready");
|
|
13735
13997
|
}
|
|
13736
13998
|
setReplySender(sender) {
|
|
13737
13999
|
this.replySender = sender;
|
|
13738
14000
|
}
|
|
13739
|
-
getDeliveryMode() {
|
|
13740
|
-
return this.resolvedMode ?? "pull";
|
|
13741
|
-
}
|
|
13742
14001
|
getPendingMessageCount() {
|
|
13743
14002
|
return this.pendingMessages.length;
|
|
13744
14003
|
}
|
|
13745
|
-
|
|
13746
|
-
|
|
13747
|
-
return;
|
|
13748
|
-
if (this.configuredMode === "push" || this.configuredMode === "pull") {
|
|
13749
|
-
this.resolvedMode = this.configuredMode;
|
|
13750
|
-
this.log(`Delivery mode set by AGENTBRIDGE_MODE: ${this.resolvedMode}`);
|
|
13751
|
-
} else {
|
|
13752
|
-
this.resolvedMode = "pull";
|
|
13753
|
-
this.log("Delivery mode defaulting to pull (set AGENTBRIDGE_MODE=push to opt into channel delivery)");
|
|
13754
|
-
}
|
|
14004
|
+
setBudgetSnapshot(snapshot) {
|
|
14005
|
+
this.budgetSnapshot = snapshot;
|
|
13755
14006
|
}
|
|
13756
14007
|
async pushNotification(message) {
|
|
13757
|
-
|
|
13758
|
-
|
|
13759
|
-
} else {
|
|
13760
|
-
this.queueForPull(message);
|
|
13761
|
-
}
|
|
14008
|
+
this.log(`pushNotification (instance=${this.instanceId}, msgId=${message.id}, len=${message.content.length})`);
|
|
14009
|
+
await this.pushViaChannel(message);
|
|
13762
14010
|
}
|
|
13763
14011
|
async pushViaChannel(message) {
|
|
13764
14012
|
const msgId = `codex_msg_${this.notificationIdPrefix}_${++this.notificationSeq}`;
|
|
@@ -13781,19 +14029,20 @@ class ClaudeAdapter extends EventEmitter {
|
|
|
13781
14029
|
this.log(`Pushed notification: ${msgId}`);
|
|
13782
14030
|
} catch (e) {
|
|
13783
14031
|
this.log(`Push notification failed: ${e.message}`);
|
|
13784
|
-
this.
|
|
14032
|
+
this.queueFallbackMessage(message);
|
|
13785
14033
|
}
|
|
13786
14034
|
}
|
|
13787
|
-
|
|
14035
|
+
queueFallbackMessage(message) {
|
|
13788
14036
|
if (this.pendingMessages.length >= this.maxBufferedMessages) {
|
|
13789
14037
|
this.pendingMessages.shift();
|
|
13790
14038
|
this.droppedMessageCount++;
|
|
13791
|
-
this.log(`
|
|
14039
|
+
this.log(`Fallback queue full, dropped oldest message (total dropped: ${this.droppedMessageCount})`);
|
|
13792
14040
|
}
|
|
13793
14041
|
this.pendingMessages.push(message);
|
|
13794
|
-
this.log(`Queued message
|
|
14042
|
+
this.log(`Queued fallback message (${this.pendingMessages.length} pending, instance=${this.instanceId})`);
|
|
13795
14043
|
}
|
|
13796
14044
|
drainMessages() {
|
|
14045
|
+
this.log(`get_messages called (instance=${this.instanceId}, pending=${this.pendingMessages.length}, dropped=${this.droppedMessageCount})`);
|
|
13797
14046
|
if (this.pendingMessages.length === 0 && this.droppedMessageCount === 0) {
|
|
13798
14047
|
return {
|
|
13799
14048
|
content: [{ type: "text", text: "No new messages from Codex." }]
|
|
@@ -13818,6 +14067,7 @@ Codex: ${msg.content}`;
|
|
|
13818
14067
|
}).join(`
|
|
13819
14068
|
|
|
13820
14069
|
`);
|
|
14070
|
+
this.log(`get_messages returning ${count} message(s) (instance=${this.instanceId}, dropped=${dropped})`);
|
|
13821
14071
|
return {
|
|
13822
14072
|
content: [
|
|
13823
14073
|
{
|
|
@@ -13862,6 +14112,15 @@ ${formatted}`
|
|
|
13862
14112
|
properties: {},
|
|
13863
14113
|
required: []
|
|
13864
14114
|
}
|
|
14115
|
+
},
|
|
14116
|
+
{
|
|
14117
|
+
name: "get_budget",
|
|
14118
|
+
description: "Check both agents' subscription quota usage (Claude + Codex): 5h/weekly window percentages, drift between the two sides, joint-pause state and model/effort tier recommendation.",
|
|
14119
|
+
inputSchema: {
|
|
14120
|
+
type: "object",
|
|
14121
|
+
properties: {},
|
|
14122
|
+
required: []
|
|
14123
|
+
}
|
|
13865
14124
|
}
|
|
13866
14125
|
]
|
|
13867
14126
|
}));
|
|
@@ -13873,12 +14132,22 @@ ${formatted}`
|
|
|
13873
14132
|
if (name === "get_messages") {
|
|
13874
14133
|
return this.drainMessages();
|
|
13875
14134
|
}
|
|
14135
|
+
if (name === "get_budget") {
|
|
14136
|
+
return this.handleGetBudget();
|
|
14137
|
+
}
|
|
13876
14138
|
return {
|
|
13877
14139
|
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
13878
14140
|
isError: true
|
|
13879
14141
|
};
|
|
13880
14142
|
});
|
|
13881
14143
|
}
|
|
14144
|
+
handleGetBudget() {
|
|
14145
|
+
this.log(`get_budget called (instance=${this.instanceId}, hasSnapshot=${this.budgetSnapshot !== null})`);
|
|
14146
|
+
const text = this.budgetSnapshot ? renderBudgetSnapshot(this.budgetSnapshot) : BUDGET_UNAVAILABLE_TEXT;
|
|
14147
|
+
return {
|
|
14148
|
+
content: [{ type: "text", text }]
|
|
14149
|
+
};
|
|
14150
|
+
}
|
|
13882
14151
|
async handleReply(args) {
|
|
13883
14152
|
const text = args?.text;
|
|
13884
14153
|
if (!text) {
|
|
@@ -13919,33 +14188,70 @@ ${formatted}`
|
|
|
13919
14188
|
};
|
|
13920
14189
|
}
|
|
13921
14190
|
log(msg) {
|
|
13922
|
-
|
|
13923
|
-
`;
|
|
13924
|
-
process.stderr.write(line);
|
|
13925
|
-
try {
|
|
13926
|
-
appendFileSync(LOG_FILE, line);
|
|
13927
|
-
} catch {}
|
|
14191
|
+
this.logger.log(msg);
|
|
13928
14192
|
}
|
|
13929
14193
|
}
|
|
13930
14194
|
|
|
14195
|
+
// src/contract-version.ts
|
|
14196
|
+
var CONTRACT_VERSION = 1;
|
|
14197
|
+
|
|
14198
|
+
// src/build-info.ts
|
|
14199
|
+
function defineString(value, fallback) {
|
|
14200
|
+
return typeof value === "string" && value.length > 0 ? value : fallback;
|
|
14201
|
+
}
|
|
14202
|
+
function defineBundle(value) {
|
|
14203
|
+
if (value === "source" || value === "dist" || value === "plugin")
|
|
14204
|
+
return value;
|
|
14205
|
+
return import.meta.url.endsWith(".ts") ? "source" : "dist";
|
|
14206
|
+
}
|
|
14207
|
+
function defineNumber(value, fallback) {
|
|
14208
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
14209
|
+
}
|
|
14210
|
+
var BUILD_INFO = Object.freeze({
|
|
14211
|
+
version: defineString("0.1.7", "0.0.0-source"),
|
|
14212
|
+
commit: defineString("1df8b91", "source"),
|
|
14213
|
+
bundle: defineBundle("plugin"),
|
|
14214
|
+
contractVersion: defineNumber(1, CONTRACT_VERSION)
|
|
14215
|
+
});
|
|
14216
|
+
function sameRuntimeContract(a, b) {
|
|
14217
|
+
if (!a || !b)
|
|
14218
|
+
return false;
|
|
14219
|
+
return a.version === b.version && a.commit === b.commit && a.contractVersion === b.contractVersion;
|
|
14220
|
+
}
|
|
14221
|
+
function compatibleContractVersion(a, b) {
|
|
14222
|
+
if (!a || !b)
|
|
14223
|
+
return false;
|
|
14224
|
+
return a.contractVersion === b.contractVersion;
|
|
14225
|
+
}
|
|
14226
|
+
function formatBuildInfo(build) {
|
|
14227
|
+
if (!build)
|
|
14228
|
+
return "<unknown>";
|
|
14229
|
+
return `${build.version}/${build.commit}/${build.bundle}/contract-v${build.contractVersion}`;
|
|
14230
|
+
}
|
|
14231
|
+
|
|
13931
14232
|
// src/daemon-client.ts
|
|
13932
14233
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
13933
14234
|
|
|
13934
14235
|
// src/control-protocol.ts
|
|
13935
14236
|
var CLOSE_CODE_REPLACED = 4001;
|
|
14237
|
+
var CLOSE_CODE_EVICTED_STALE = 4002;
|
|
14238
|
+
var CLOSE_CODE_PROBE_IN_PROGRESS = 4003;
|
|
14239
|
+
var CLOSE_CODE_PAIR_MISMATCH = 4004;
|
|
13936
14240
|
|
|
13937
14241
|
// src/daemon-client.ts
|
|
13938
14242
|
var nextSocketId = 0;
|
|
13939
14243
|
|
|
13940
14244
|
class DaemonClient extends EventEmitter2 {
|
|
13941
14245
|
url;
|
|
14246
|
+
options;
|
|
13942
14247
|
ws = null;
|
|
13943
14248
|
wsId = 0;
|
|
13944
14249
|
nextRequestId = 1;
|
|
13945
14250
|
pendingReplies = new Map;
|
|
13946
|
-
constructor(url) {
|
|
14251
|
+
constructor(url, options = {}) {
|
|
13947
14252
|
super();
|
|
13948
14253
|
this.url = url;
|
|
14254
|
+
this.options = options;
|
|
13949
14255
|
}
|
|
13950
14256
|
async connect() {
|
|
13951
14257
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
@@ -13987,7 +14293,81 @@ class DaemonClient extends EventEmitter2 {
|
|
|
13987
14293
|
});
|
|
13988
14294
|
}
|
|
13989
14295
|
attachClaude() {
|
|
13990
|
-
this.send({
|
|
14296
|
+
this.send({
|
|
14297
|
+
type: "claude_connect",
|
|
14298
|
+
...this.options.identity ? { identity: this.options.identity } : {}
|
|
14299
|
+
});
|
|
14300
|
+
}
|
|
14301
|
+
async attachClaudeAndWaitForStatus(timeoutMs = 1000) {
|
|
14302
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
14303
|
+
return null;
|
|
14304
|
+
}
|
|
14305
|
+
return await new Promise((resolve) => {
|
|
14306
|
+
let settled = false;
|
|
14307
|
+
let timer = null;
|
|
14308
|
+
const cleanup = () => {
|
|
14309
|
+
if (settled)
|
|
14310
|
+
return;
|
|
14311
|
+
settled = true;
|
|
14312
|
+
if (timer) {
|
|
14313
|
+
clearTimeout(timer);
|
|
14314
|
+
timer = null;
|
|
14315
|
+
}
|
|
14316
|
+
this.off("status", onStatus);
|
|
14317
|
+
this.off("rejected", onRejected);
|
|
14318
|
+
this.off("disconnect", onDisconnect);
|
|
14319
|
+
};
|
|
14320
|
+
const finish = (value) => {
|
|
14321
|
+
cleanup();
|
|
14322
|
+
resolve(value);
|
|
14323
|
+
};
|
|
14324
|
+
const onStatus = (status) => finish(status);
|
|
14325
|
+
const onRejected = () => finish(null);
|
|
14326
|
+
const onDisconnect = () => finish(null);
|
|
14327
|
+
this.on("status", onStatus);
|
|
14328
|
+
this.on("rejected", onRejected);
|
|
14329
|
+
this.on("disconnect", onDisconnect);
|
|
14330
|
+
timer = setTimeout(() => {
|
|
14331
|
+
finish(null);
|
|
14332
|
+
}, timeoutMs);
|
|
14333
|
+
try {
|
|
14334
|
+
this.attachClaude();
|
|
14335
|
+
} catch {
|
|
14336
|
+
finish(null);
|
|
14337
|
+
}
|
|
14338
|
+
});
|
|
14339
|
+
}
|
|
14340
|
+
async probeIncumbent(timeoutMs = 3000) {
|
|
14341
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
14342
|
+
return { connected: false, alive: false };
|
|
14343
|
+
}
|
|
14344
|
+
return await new Promise((resolve) => {
|
|
14345
|
+
let settled = false;
|
|
14346
|
+
let timer = null;
|
|
14347
|
+
const finish = (value) => {
|
|
14348
|
+
if (settled)
|
|
14349
|
+
return;
|
|
14350
|
+
settled = true;
|
|
14351
|
+
if (timer)
|
|
14352
|
+
clearTimeout(timer);
|
|
14353
|
+
this.off("incumbentStatus", onStatus);
|
|
14354
|
+
this.off("disconnect", onDisconnect);
|
|
14355
|
+
this.off("rejected", onRejected);
|
|
14356
|
+
resolve(value);
|
|
14357
|
+
};
|
|
14358
|
+
const onStatus = (s) => finish(s);
|
|
14359
|
+
const onDisconnect = () => finish({ connected: false, alive: false });
|
|
14360
|
+
const onRejected = () => finish({ connected: false, alive: false });
|
|
14361
|
+
this.on("incumbentStatus", onStatus);
|
|
14362
|
+
this.on("disconnect", onDisconnect);
|
|
14363
|
+
this.on("rejected", onRejected);
|
|
14364
|
+
timer = setTimeout(() => finish({ connected: false, alive: false }), timeoutMs);
|
|
14365
|
+
try {
|
|
14366
|
+
this.send({ type: "probe_incumbent" });
|
|
14367
|
+
} catch {
|
|
14368
|
+
finish({ connected: false, alive: false });
|
|
14369
|
+
}
|
|
14370
|
+
});
|
|
13991
14371
|
}
|
|
13992
14372
|
async disconnect() {
|
|
13993
14373
|
if (!this.ws)
|
|
@@ -14045,6 +14425,9 @@ class DaemonClient extends EventEmitter2 {
|
|
|
14045
14425
|
case "status":
|
|
14046
14426
|
this.emit("status", message.status);
|
|
14047
14427
|
return;
|
|
14428
|
+
case "incumbent_status":
|
|
14429
|
+
this.emit("incumbentStatus", { connected: message.connected, alive: message.alive });
|
|
14430
|
+
return;
|
|
14048
14431
|
}
|
|
14049
14432
|
};
|
|
14050
14433
|
ws.onclose = (event) => {
|
|
@@ -14053,8 +14436,8 @@ class DaemonClient extends EventEmitter2 {
|
|
|
14053
14436
|
if (isCurrent) {
|
|
14054
14437
|
this.ws = null;
|
|
14055
14438
|
this.rejectPendingReplies("AgentBridge daemon disconnected.");
|
|
14056
|
-
if (event.code === CLOSE_CODE_REPLACED) {
|
|
14057
|
-
this.emit("rejected");
|
|
14439
|
+
if (event.code === CLOSE_CODE_REPLACED || event.code === CLOSE_CODE_EVICTED_STALE || event.code === CLOSE_CODE_PROBE_IN_PROGRESS || event.code === CLOSE_CODE_PAIR_MISMATCH) {
|
|
14440
|
+
this.emit("rejected", event.code);
|
|
14058
14441
|
} else {
|
|
14059
14442
|
this.emit("disconnect");
|
|
14060
14443
|
}
|
|
@@ -14083,10 +14466,30 @@ class DaemonClient extends EventEmitter2 {
|
|
|
14083
14466
|
|
|
14084
14467
|
// src/daemon-lifecycle.ts
|
|
14085
14468
|
import { spawn, execFileSync } from "child_process";
|
|
14086
|
-
import { existsSync, readFileSync, unlinkSync, writeFileSync, openSync, closeSync, constants } from "fs";
|
|
14469
|
+
import { existsSync as existsSync3, readFileSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync, openSync, closeSync, constants } from "fs";
|
|
14087
14470
|
import { fileURLToPath } from "url";
|
|
14088
|
-
|
|
14471
|
+
|
|
14472
|
+
// src/env-utils.ts
|
|
14473
|
+
function parsePositiveIntEnv(name, fallback, log = () => {}, env = process.env) {
|
|
14474
|
+
const raw = env[name];
|
|
14475
|
+
if (raw == null || raw === "")
|
|
14476
|
+
return fallback;
|
|
14477
|
+
const parsed = Number(raw);
|
|
14478
|
+
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > Number.MAX_SAFE_INTEGER) {
|
|
14479
|
+
log(`Invalid ${name}=${JSON.stringify(raw)} (must be a positive integer within ` + `Number.MAX_SAFE_INTEGER); falling back to ${fallback}`);
|
|
14480
|
+
return fallback;
|
|
14481
|
+
}
|
|
14482
|
+
return parsed;
|
|
14483
|
+
}
|
|
14484
|
+
|
|
14485
|
+
// src/daemon-lifecycle.ts
|
|
14486
|
+
var DEFAULT_DAEMON_ENTRY = import.meta.url.endsWith(".ts") ? "./daemon.ts" : "./daemon.js";
|
|
14487
|
+
var DAEMON_ENTRY = process.env.AGENTBRIDGE_DAEMON_ENTRY || DEFAULT_DAEMON_ENTRY;
|
|
14089
14488
|
var DAEMON_PATH = fileURLToPath(new URL(DAEMON_ENTRY, import.meta.url));
|
|
14489
|
+
var REUSE_READY_RETRIES = parsePositiveIntEnv("AGENTBRIDGE_REUSE_READY_RETRIES", 12);
|
|
14490
|
+
var REUSE_READY_DELAY_MS = 250;
|
|
14491
|
+
var HEALTH_FETCH_TIMEOUT_MS = 500;
|
|
14492
|
+
var LOCK_IDENTITY_GRACE_MS = parsePositiveIntEnv("AGENTBRIDGE_LOCK_IDENTITY_GRACE_MS", 120000);
|
|
14090
14493
|
|
|
14091
14494
|
class DaemonLifecycle {
|
|
14092
14495
|
stateDir;
|
|
@@ -14106,42 +14509,120 @@ class DaemonLifecycle {
|
|
|
14106
14509
|
get controlWsUrl() {
|
|
14107
14510
|
return `ws://127.0.0.1:${this.controlPort}/ws`;
|
|
14108
14511
|
}
|
|
14512
|
+
get expectedPairId() {
|
|
14513
|
+
return process.env.AGENTBRIDGE_PAIR_ID || null;
|
|
14514
|
+
}
|
|
14515
|
+
async fetchStatus() {
|
|
14516
|
+
try {
|
|
14517
|
+
const response = await fetchWithTimeout(this.healthUrl);
|
|
14518
|
+
if (!response.ok)
|
|
14519
|
+
return null;
|
|
14520
|
+
return await response.json();
|
|
14521
|
+
} catch {
|
|
14522
|
+
return null;
|
|
14523
|
+
}
|
|
14524
|
+
}
|
|
14525
|
+
isForeignDaemon(status) {
|
|
14526
|
+
const expected = this.expectedPairId;
|
|
14527
|
+
if (!expected)
|
|
14528
|
+
return false;
|
|
14529
|
+
if (!status)
|
|
14530
|
+
return false;
|
|
14531
|
+
const reported = status.pairId;
|
|
14532
|
+
if (reported == null)
|
|
14533
|
+
return true;
|
|
14534
|
+
return reported !== expected;
|
|
14535
|
+
}
|
|
14536
|
+
isRegisteredPairDaemonInManualMode(status) {
|
|
14537
|
+
return !this.expectedPairId && status?.pairId != null;
|
|
14538
|
+
}
|
|
14539
|
+
isBuildDrifted(status) {
|
|
14540
|
+
if (process.env.AGENTBRIDGE_ALLOW_BUILD_DRIFT === "1")
|
|
14541
|
+
return false;
|
|
14542
|
+
const runtime = status?.build;
|
|
14543
|
+
if (!runtime)
|
|
14544
|
+
return true;
|
|
14545
|
+
return !sameRuntimeContract(runtime, BUILD_INFO);
|
|
14546
|
+
}
|
|
14547
|
+
canReuseDespiteDrift(status) {
|
|
14548
|
+
if (!compatibleContractVersion(status?.build, BUILD_INFO))
|
|
14549
|
+
return false;
|
|
14550
|
+
return status?.tuiConnected === true;
|
|
14551
|
+
}
|
|
14109
14552
|
async ensureRunning() {
|
|
14110
14553
|
if (await this.isHealthy()) {
|
|
14111
|
-
await this.
|
|
14112
|
-
|
|
14554
|
+
const status = await this.fetchStatus();
|
|
14555
|
+
if (this.isRegisteredPairDaemonInManualMode(status)) {
|
|
14556
|
+
throw new Error(`Control port ${this.controlPort} is owned by registered pair ${status?.pairId}. ` + `This session has no pair identity (manual mode) and will not reuse or replace it \u2014 ` + `start with \`agentbridge claude\` from that pair's directory, or set AGENTBRIDGE_CONTROL_PORT to a free port.`);
|
|
14557
|
+
}
|
|
14558
|
+
if (this.isForeignDaemon(status)) {
|
|
14559
|
+
this.log(`Control port ${this.controlPort} held by a daemon for pair ${status?.pairId ?? "<none>"}, ` + `but this pair is ${this.expectedPairId} \u2014 replacing foreign daemon`);
|
|
14560
|
+
await this.replaceUnhealthyDaemon(status?.pid);
|
|
14561
|
+
return;
|
|
14562
|
+
}
|
|
14563
|
+
if (this.isBuildDrifted(status)) {
|
|
14564
|
+
if (this.canReuseDespiteDrift(status)) {
|
|
14565
|
+
this.log(`Daemon on control port ${this.controlPort} is running build ${formatBuildInfo(status?.build)} ` + `(launcher ${formatBuildInfo(BUILD_INFO)}) but a live Codex TUI is attached \u2014 reusing instead of ` + `replacing; the new build is picked up at the next restart (abg kill, then relaunch)`);
|
|
14566
|
+
} else {
|
|
14567
|
+
this.log(`Daemon on control port ${this.controlPort} is running build ${formatBuildInfo(status?.build)} ` + `but launcher is ${formatBuildInfo(BUILD_INFO)} \u2014 replacing drifted daemon`);
|
|
14568
|
+
await this.replaceUnhealthyDaemon(status?.pid);
|
|
14569
|
+
return;
|
|
14570
|
+
}
|
|
14571
|
+
}
|
|
14572
|
+
try {
|
|
14573
|
+
await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
|
|
14574
|
+
return;
|
|
14575
|
+
} catch {
|
|
14576
|
+
this.log(`Daemon on control port ${this.controlPort} is healthy but not ready within reuse window \u2014 replacing`);
|
|
14577
|
+
await this.replaceUnhealthyDaemon(status?.pid);
|
|
14578
|
+
return;
|
|
14579
|
+
}
|
|
14113
14580
|
}
|
|
14114
14581
|
const existingPid = this.readPid();
|
|
14115
14582
|
if (existingPid) {
|
|
14116
14583
|
if (isProcessAlive(existingPid)) {
|
|
14117
14584
|
if (this.isDaemonProcess(existingPid)) {
|
|
14118
14585
|
try {
|
|
14119
|
-
await this.waitForReady(
|
|
14586
|
+
await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
|
|
14120
14587
|
return;
|
|
14121
14588
|
} catch {
|
|
14122
|
-
|
|
14589
|
+
this.log(`Existing daemon process ${existingPid} never became ready \u2014 replacing`);
|
|
14590
|
+
await this.replaceUnhealthyDaemon(existingPid);
|
|
14591
|
+
return;
|
|
14123
14592
|
}
|
|
14124
14593
|
}
|
|
14125
14594
|
this.log(`Pid ${existingPid} is alive but not an AgentBridge daemon, removing stale pid file`);
|
|
14126
14595
|
}
|
|
14127
14596
|
this.removeStalePidFile();
|
|
14128
14597
|
}
|
|
14129
|
-
|
|
14130
|
-
|
|
14131
|
-
|
|
14132
|
-
|
|
14133
|
-
|
|
14134
|
-
|
|
14135
|
-
|
|
14598
|
+
await this.withStartupLockStrict(async (locked) => {
|
|
14599
|
+
if (!locked) {
|
|
14600
|
+
this.log("Another process holds the startup lock, waiting for readiness+identity...");
|
|
14601
|
+
await this.waitForReadyAndOurs();
|
|
14602
|
+
return;
|
|
14603
|
+
}
|
|
14604
|
+
if (await this.isHealthy()) {
|
|
14605
|
+
const status = await this.fetchStatus();
|
|
14606
|
+
if (this.isForeignDaemon(status) || this.isBuildDrifted(status) && !this.canReuseDespiteDrift(status)) {
|
|
14607
|
+
this.log(`Daemon on control port ${this.controlPort} is not reusable under startup lock ` + `(pair=${status?.pairId ?? "<none>"}, build=${formatBuildInfo(status?.build)}) \u2014 replacing`);
|
|
14608
|
+
await this.kill(3000, status?.pid);
|
|
14609
|
+
} else {
|
|
14610
|
+
try {
|
|
14611
|
+
await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
|
|
14612
|
+
return;
|
|
14613
|
+
} catch {
|
|
14614
|
+
this.log(`Daemon on control port ${this.controlPort} is healthy but not ready under startup lock \u2014 replacing`);
|
|
14615
|
+
await this.kill(3000, status?.pid);
|
|
14616
|
+
}
|
|
14617
|
+
}
|
|
14618
|
+
}
|
|
14136
14619
|
this.launch();
|
|
14137
14620
|
await this.waitForReady();
|
|
14138
|
-
}
|
|
14139
|
-
this.releaseLock();
|
|
14140
|
-
}
|
|
14621
|
+
});
|
|
14141
14622
|
}
|
|
14142
14623
|
async isHealthy() {
|
|
14143
14624
|
try {
|
|
14144
|
-
const response = await
|
|
14625
|
+
const response = await fetchWithTimeout(this.healthUrl);
|
|
14145
14626
|
return response.ok;
|
|
14146
14627
|
} catch {
|
|
14147
14628
|
return false;
|
|
@@ -14157,7 +14638,7 @@ class DaemonLifecycle {
|
|
|
14157
14638
|
}
|
|
14158
14639
|
async isReady() {
|
|
14159
14640
|
try {
|
|
14160
|
-
const response = await
|
|
14641
|
+
const response = await fetchWithTimeout(this.readyUrl);
|
|
14161
14642
|
return response.ok;
|
|
14162
14643
|
} catch {
|
|
14163
14644
|
return false;
|
|
@@ -14171,6 +14652,18 @@ class DaemonLifecycle {
|
|
|
14171
14652
|
}
|
|
14172
14653
|
throw new Error(`Timed out waiting for AgentBridge daemon readiness on ${this.readyUrl}`);
|
|
14173
14654
|
}
|
|
14655
|
+
async waitForReadyAndOurs(maxRetries = 40, delayMs = 250) {
|
|
14656
|
+
for (let attempt = 0;attempt < maxRetries; attempt++) {
|
|
14657
|
+
if (await this.isReady()) {
|
|
14658
|
+
const status = await this.fetchStatus();
|
|
14659
|
+
if (!this.isForeignDaemon(status) && (!this.isBuildDrifted(status) || this.canReuseDespiteDrift(status))) {
|
|
14660
|
+
return;
|
|
14661
|
+
}
|
|
14662
|
+
}
|
|
14663
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
14664
|
+
}
|
|
14665
|
+
throw new Error(`Timed out waiting for AgentBridge daemon readiness+identity on ${this.readyUrl} (control port ${this.controlPort})`);
|
|
14666
|
+
}
|
|
14174
14667
|
readStatus() {
|
|
14175
14668
|
try {
|
|
14176
14669
|
const raw = readFileSync(this.stateDir.statusFile, "utf-8");
|
|
@@ -14202,12 +14695,12 @@ class DaemonLifecycle {
|
|
|
14202
14695
|
}
|
|
14203
14696
|
removePidFile() {
|
|
14204
14697
|
try {
|
|
14205
|
-
|
|
14698
|
+
unlinkSync2(this.stateDir.pidFile);
|
|
14206
14699
|
} catch {}
|
|
14207
14700
|
}
|
|
14208
14701
|
removeStatusFile() {
|
|
14209
14702
|
try {
|
|
14210
|
-
|
|
14703
|
+
unlinkSync2(this.stateDir.statusFile);
|
|
14211
14704
|
} catch {}
|
|
14212
14705
|
}
|
|
14213
14706
|
markKilled() {
|
|
@@ -14217,11 +14710,11 @@ class DaemonLifecycle {
|
|
|
14217
14710
|
}
|
|
14218
14711
|
clearKilled() {
|
|
14219
14712
|
try {
|
|
14220
|
-
|
|
14713
|
+
unlinkSync2(this.stateDir.killedFile);
|
|
14221
14714
|
} catch {}
|
|
14222
14715
|
}
|
|
14223
14716
|
wasKilled() {
|
|
14224
|
-
return
|
|
14717
|
+
return existsSync3(this.stateDir.killedFile);
|
|
14225
14718
|
}
|
|
14226
14719
|
launch() {
|
|
14227
14720
|
this.stateDir.ensure();
|
|
@@ -14242,45 +14735,99 @@ class DaemonLifecycle {
|
|
|
14242
14735
|
this.log("Removing stale pid file");
|
|
14243
14736
|
this.removePidFile();
|
|
14244
14737
|
}
|
|
14245
|
-
|
|
14246
|
-
|
|
14247
|
-
|
|
14248
|
-
|
|
14738
|
+
async replaceUnhealthyDaemon(statusPid) {
|
|
14739
|
+
await this.withStartupLockStrict(async (locked) => {
|
|
14740
|
+
if (!locked) {
|
|
14741
|
+
this.log("Another process holds the startup lock, waiting for readiness+identity...");
|
|
14742
|
+
await this.waitForReadyAndOurs();
|
|
14743
|
+
return;
|
|
14744
|
+
}
|
|
14745
|
+
if (await this.isHealthy()) {
|
|
14746
|
+
const status = await this.fetchStatus();
|
|
14747
|
+
if (!this.isForeignDaemon(status) && (!this.isBuildDrifted(status) || this.canReuseDespiteDrift(status))) {
|
|
14748
|
+
try {
|
|
14749
|
+
await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
|
|
14750
|
+
return;
|
|
14751
|
+
} catch {}
|
|
14752
|
+
}
|
|
14753
|
+
}
|
|
14754
|
+
this.log(`Killing unhealthy daemon on control port ${this.controlPort} and relaunching`);
|
|
14755
|
+
await this.kill(3000, statusPid);
|
|
14756
|
+
this.launch();
|
|
14757
|
+
await this.waitForReady();
|
|
14758
|
+
});
|
|
14759
|
+
}
|
|
14760
|
+
async withStartupLockStrict(fn) {
|
|
14761
|
+
const locked = this.acquireLockStrict();
|
|
14762
|
+
try {
|
|
14763
|
+
return await fn(locked);
|
|
14764
|
+
} finally {
|
|
14765
|
+
if (locked)
|
|
14766
|
+
this.releaseLock();
|
|
14249
14767
|
}
|
|
14768
|
+
}
|
|
14769
|
+
acquireLockStrict(reclaimed = false) {
|
|
14250
14770
|
this.stateDir.ensure();
|
|
14771
|
+
let fd = null;
|
|
14251
14772
|
try {
|
|
14252
|
-
|
|
14773
|
+
fd = openSync(this.stateDir.lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
|
|
14253
14774
|
writeFileSync(fd, `${process.pid}
|
|
14254
14775
|
`);
|
|
14255
14776
|
closeSync(fd);
|
|
14256
14777
|
return true;
|
|
14257
14778
|
} catch (err) {
|
|
14779
|
+
if (fd !== null && err.code !== "EEXIST") {
|
|
14780
|
+
try {
|
|
14781
|
+
closeSync(fd);
|
|
14782
|
+
} catch {}
|
|
14783
|
+
this.releaseLock();
|
|
14784
|
+
}
|
|
14258
14785
|
if (err.code === "EEXIST") {
|
|
14786
|
+
if (reclaimed)
|
|
14787
|
+
return false;
|
|
14259
14788
|
try {
|
|
14260
14789
|
const holderPid = Number.parseInt(readFileSync(this.stateDir.lockFile, "utf-8").trim(), 10);
|
|
14261
14790
|
if (Number.isFinite(holderPid) && !isProcessAlive(holderPid)) {
|
|
14262
|
-
this.log(`Stale lock
|
|
14791
|
+
this.log(`Stale startup lock from dead process ${holderPid}, reclaiming`);
|
|
14263
14792
|
this.releaseLock();
|
|
14264
|
-
return this.
|
|
14793
|
+
return this.acquireLockStrict(true);
|
|
14794
|
+
}
|
|
14795
|
+
if (Number.isFinite(holderPid) && this.lockAgeMs() > LOCK_IDENTITY_GRACE_MS && !this.isAgentBridgeProcess(holderPid)) {
|
|
14796
|
+
this.log(`Startup lock is ${Math.round(this.lockAgeMs() / 1000)}s old and holder pid ${holderPid} ` + `is an unrelated process (pid recycled), reclaiming`);
|
|
14797
|
+
this.releaseLock();
|
|
14798
|
+
return this.acquireLockStrict(true);
|
|
14265
14799
|
}
|
|
14266
14800
|
} catch {
|
|
14267
|
-
|
|
14268
|
-
this.releaseLock();
|
|
14269
|
-
return this.acquireLock(depth + 1);
|
|
14801
|
+
return false;
|
|
14270
14802
|
}
|
|
14271
14803
|
return false;
|
|
14272
14804
|
}
|
|
14273
|
-
this.log(`
|
|
14274
|
-
return
|
|
14805
|
+
this.log(`Could not acquire strict startup lock: ${err.message}`);
|
|
14806
|
+
return false;
|
|
14807
|
+
}
|
|
14808
|
+
}
|
|
14809
|
+
lockAgeMs() {
|
|
14810
|
+
try {
|
|
14811
|
+
return Date.now() - statSync2(this.stateDir.lockFile).mtimeMs;
|
|
14812
|
+
} catch {
|
|
14813
|
+
return 0;
|
|
14814
|
+
}
|
|
14815
|
+
}
|
|
14816
|
+
isAgentBridgeProcess(pid) {
|
|
14817
|
+
try {
|
|
14818
|
+
const cmd = execFileSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf-8" }).trim();
|
|
14819
|
+
return cmd.includes("agentbridge") || cmd.includes("agent_bridge");
|
|
14820
|
+
} catch {
|
|
14821
|
+
return false;
|
|
14275
14822
|
}
|
|
14276
14823
|
}
|
|
14277
14824
|
releaseLock() {
|
|
14278
14825
|
try {
|
|
14279
|
-
|
|
14826
|
+
unlinkSync2(this.stateDir.lockFile);
|
|
14280
14827
|
} catch {}
|
|
14281
14828
|
}
|
|
14282
|
-
async kill(gracefulTimeoutMs = 3000) {
|
|
14283
|
-
const pid = this.readPid();
|
|
14829
|
+
async kill(gracefulTimeoutMs = 3000, pidOverride) {
|
|
14830
|
+
const pid = pidOverride ?? this.readPid();
|
|
14284
14831
|
if (!pid) {
|
|
14285
14832
|
this.log("No daemon pid file found");
|
|
14286
14833
|
this.cleanup();
|
|
@@ -14322,7 +14869,9 @@ class DaemonLifecycle {
|
|
|
14322
14869
|
isDaemonProcess(pid) {
|
|
14323
14870
|
try {
|
|
14324
14871
|
const cmd = execFileSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf-8" }).trim();
|
|
14325
|
-
|
|
14872
|
+
const hasDaemonEntry = /(?:^|[\s/\\])[\w.-]*-?daemon\.(?:ts|js)(?:\s|$)/.test(cmd);
|
|
14873
|
+
const hasAgentbridge = cmd.includes("agentbridge") || cmd.includes("agent_bridge");
|
|
14874
|
+
return hasDaemonEntry && hasAgentbridge;
|
|
14326
14875
|
} catch {
|
|
14327
14876
|
return false;
|
|
14328
14877
|
}
|
|
@@ -14330,7 +14879,15 @@ class DaemonLifecycle {
|
|
|
14330
14879
|
cleanup() {
|
|
14331
14880
|
this.removePidFile();
|
|
14332
14881
|
this.removeStatusFile();
|
|
14333
|
-
|
|
14882
|
+
}
|
|
14883
|
+
}
|
|
14884
|
+
async function fetchWithTimeout(url, timeoutMs = HEALTH_FETCH_TIMEOUT_MS) {
|
|
14885
|
+
const controller = new AbortController;
|
|
14886
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
14887
|
+
try {
|
|
14888
|
+
return await fetch(url, { signal: controller.signal });
|
|
14889
|
+
} finally {
|
|
14890
|
+
clearTimeout(timer);
|
|
14334
14891
|
}
|
|
14335
14892
|
}
|
|
14336
14893
|
function isProcessAlive(pid) {
|
|
@@ -14342,124 +14899,146 @@ function isProcessAlive(pid) {
|
|
|
14342
14899
|
}
|
|
14343
14900
|
}
|
|
14344
14901
|
|
|
14345
|
-
// src/state-dir.ts
|
|
14346
|
-
import { mkdirSync, existsSync as existsSync2 } from "fs";
|
|
14347
|
-
import { join } from "path";
|
|
14348
|
-
import { homedir, platform } from "os";
|
|
14349
|
-
|
|
14350
|
-
class StateDirResolver {
|
|
14351
|
-
stateDir;
|
|
14352
|
-
constructor(envOverride) {
|
|
14353
|
-
const override = envOverride ?? process.env.AGENTBRIDGE_STATE_DIR;
|
|
14354
|
-
if (override) {
|
|
14355
|
-
this.stateDir = override;
|
|
14356
|
-
} else if (platform() === "darwin") {
|
|
14357
|
-
this.stateDir = join(homedir(), "Library", "Application Support", "AgentBridge");
|
|
14358
|
-
} else {
|
|
14359
|
-
const xdgState = process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state");
|
|
14360
|
-
this.stateDir = join(xdgState, "agentbridge");
|
|
14361
|
-
}
|
|
14362
|
-
}
|
|
14363
|
-
ensure() {
|
|
14364
|
-
if (!existsSync2(this.stateDir)) {
|
|
14365
|
-
mkdirSync(this.stateDir, { recursive: true });
|
|
14366
|
-
}
|
|
14367
|
-
}
|
|
14368
|
-
get dir() {
|
|
14369
|
-
return this.stateDir;
|
|
14370
|
-
}
|
|
14371
|
-
get pidFile() {
|
|
14372
|
-
return join(this.stateDir, "daemon.pid");
|
|
14373
|
-
}
|
|
14374
|
-
get tuiPidFile() {
|
|
14375
|
-
return join(this.stateDir, "codex-tui.pid");
|
|
14376
|
-
}
|
|
14377
|
-
get lockFile() {
|
|
14378
|
-
return join(this.stateDir, "daemon.lock");
|
|
14379
|
-
}
|
|
14380
|
-
get statusFile() {
|
|
14381
|
-
return join(this.stateDir, "status.json");
|
|
14382
|
-
}
|
|
14383
|
-
get portsFile() {
|
|
14384
|
-
return join(this.stateDir, "ports.json");
|
|
14385
|
-
}
|
|
14386
|
-
get logFile() {
|
|
14387
|
-
return join(this.stateDir, "agentbridge.log");
|
|
14388
|
-
}
|
|
14389
|
-
get killedFile() {
|
|
14390
|
-
return join(this.stateDir, "killed");
|
|
14391
|
-
}
|
|
14392
|
-
}
|
|
14393
|
-
|
|
14394
14902
|
// src/config-service.ts
|
|
14395
|
-
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as
|
|
14903
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
|
|
14396
14904
|
import { join as join2 } from "path";
|
|
14905
|
+
var DEFAULT_BUDGET_CONFIG = {
|
|
14906
|
+
enabled: true,
|
|
14907
|
+
pollSeconds: 60,
|
|
14908
|
+
pauseAt: 90,
|
|
14909
|
+
resumeBelow: 30,
|
|
14910
|
+
syncDriftPct: 10,
|
|
14911
|
+
parallel: {
|
|
14912
|
+
minRemainingPct: 60,
|
|
14913
|
+
timeWindowSec: 3600
|
|
14914
|
+
},
|
|
14915
|
+
codexTierControl: false,
|
|
14916
|
+
codexTiers: {
|
|
14917
|
+
full: null,
|
|
14918
|
+
balanced: { effort: "medium" },
|
|
14919
|
+
eco: { effort: "low" }
|
|
14920
|
+
}
|
|
14921
|
+
};
|
|
14397
14922
|
var DEFAULT_CONFIG = {
|
|
14398
14923
|
version: "1.0",
|
|
14399
|
-
|
|
14400
|
-
|
|
14924
|
+
codex: {
|
|
14925
|
+
appPort: 4500,
|
|
14401
14926
|
proxyPort: 4501
|
|
14402
14927
|
},
|
|
14403
|
-
agents: {
|
|
14404
|
-
claude: {
|
|
14405
|
-
role: "Reviewer, Planner",
|
|
14406
|
-
mode: "push"
|
|
14407
|
-
},
|
|
14408
|
-
codex: {
|
|
14409
|
-
role: "Implementer, Executor"
|
|
14410
|
-
}
|
|
14411
|
-
},
|
|
14412
|
-
markers: ["IMPORTANT", "STATUS", "FYI"],
|
|
14413
14928
|
turnCoordination: {
|
|
14414
|
-
attentionWindowSeconds: 15
|
|
14415
|
-
busyGuard: true
|
|
14929
|
+
attentionWindowSeconds: 15
|
|
14416
14930
|
},
|
|
14417
|
-
idleShutdownSeconds: 30
|
|
14931
|
+
idleShutdownSeconds: 30,
|
|
14932
|
+
budget: DEFAULT_BUDGET_CONFIG
|
|
14418
14933
|
};
|
|
14419
|
-
var DEFAULT_COLLABORATION_MD = `# Collaboration Rules
|
|
14420
|
-
|
|
14421
|
-
## Roles
|
|
14422
|
-
- Claude: Reviewer, Planner, Hypothesis Challenger
|
|
14423
|
-
- Codex: Implementer, Executor, Reproducer/Verifier
|
|
14424
|
-
|
|
14425
|
-
## Thinking Patterns
|
|
14426
|
-
- Analytical/review tasks: Independent Analysis & Convergence
|
|
14427
|
-
- Implementation tasks: Architect -> Builder -> Critic
|
|
14428
|
-
- Debugging tasks: Hypothesis -> Experiment -> Interpretation
|
|
14429
|
-
|
|
14430
|
-
## Communication
|
|
14431
|
-
- Use explicit phrases: "My independent view is:", "I agree on:", "I disagree on:", "Current consensus:"
|
|
14432
|
-
- Tag messages with [IMPORTANT], [STATUS], or [FYI]
|
|
14433
|
-
|
|
14434
|
-
## Review Process
|
|
14435
|
-
- Cross-review: author never reviews their own code
|
|
14436
|
-
- All changes go through feature/fix branches + PR
|
|
14437
|
-
- Merge via squash merge
|
|
14438
|
-
|
|
14439
|
-
## Custom Rules
|
|
14440
|
-
<!-- Add your project-specific collaboration rules here -->
|
|
14441
|
-
`;
|
|
14442
14934
|
var CONFIG_DIR = ".agentbridge";
|
|
14443
14935
|
var CONFIG_FILE = "config.json";
|
|
14444
|
-
|
|
14936
|
+
function isRecord(value) {
|
|
14937
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14938
|
+
}
|
|
14939
|
+
function normalizeInteger(value, fallback) {
|
|
14940
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
14941
|
+
return value;
|
|
14942
|
+
if (typeof value === "string") {
|
|
14943
|
+
const parsed = Number(value);
|
|
14944
|
+
if (Number.isFinite(parsed))
|
|
14945
|
+
return parsed;
|
|
14946
|
+
}
|
|
14947
|
+
return fallback;
|
|
14948
|
+
}
|
|
14949
|
+
function normalizeBoundedInteger(value, fallback, min, max) {
|
|
14950
|
+
const parsed = normalizeInteger(value, fallback);
|
|
14951
|
+
if (parsed < min || parsed > max)
|
|
14952
|
+
return fallback;
|
|
14953
|
+
return parsed;
|
|
14954
|
+
}
|
|
14955
|
+
function normalizeBoolean(value, fallback) {
|
|
14956
|
+
if (typeof value === "boolean")
|
|
14957
|
+
return value;
|
|
14958
|
+
if (value === "true" || value === "1")
|
|
14959
|
+
return true;
|
|
14960
|
+
if (value === "false" || value === "0")
|
|
14961
|
+
return false;
|
|
14962
|
+
return fallback;
|
|
14963
|
+
}
|
|
14964
|
+
function normalizeCodexOverride(raw) {
|
|
14965
|
+
if (!isRecord(raw))
|
|
14966
|
+
return null;
|
|
14967
|
+
const override = {};
|
|
14968
|
+
if (typeof raw.model === "string" && raw.model.trim() !== "")
|
|
14969
|
+
override.model = raw.model.trim();
|
|
14970
|
+
if (typeof raw.effort === "string" && raw.effort.trim() !== "")
|
|
14971
|
+
override.effort = raw.effort.trim();
|
|
14972
|
+
return Object.keys(override).length > 0 ? override : null;
|
|
14973
|
+
}
|
|
14974
|
+
function normalizeCodexTiers(raw) {
|
|
14975
|
+
const tiers = isRecord(raw) ? raw : {};
|
|
14976
|
+
return {
|
|
14977
|
+
full: normalizeCodexOverride(tiers.full),
|
|
14978
|
+
balanced: normalizeCodexOverride(tiers.balanced) ?? DEFAULT_BUDGET_CONFIG.codexTiers.balanced,
|
|
14979
|
+
eco: normalizeCodexOverride(tiers.eco) ?? DEFAULT_BUDGET_CONFIG.codexTiers.eco
|
|
14980
|
+
};
|
|
14981
|
+
}
|
|
14982
|
+
function normalizeBudgetConfig(raw) {
|
|
14983
|
+
const budget = isRecord(raw) ? raw : {};
|
|
14984
|
+
const parallel = isRecord(budget.parallel) ? budget.parallel : {};
|
|
14985
|
+
const codexTiers = normalizeCodexTiers(budget.codexTiers);
|
|
14986
|
+
let pauseAt = normalizeBoundedInteger(budget.pauseAt, DEFAULT_BUDGET_CONFIG.pauseAt, 1, 100);
|
|
14987
|
+
let resumeBelow = normalizeBoundedInteger(budget.resumeBelow, DEFAULT_BUDGET_CONFIG.resumeBelow, 0, 99);
|
|
14988
|
+
if (pauseAt <= resumeBelow) {
|
|
14989
|
+
pauseAt = DEFAULT_BUDGET_CONFIG.pauseAt;
|
|
14990
|
+
resumeBelow = DEFAULT_BUDGET_CONFIG.resumeBelow;
|
|
14991
|
+
}
|
|
14992
|
+
return {
|
|
14993
|
+
enabled: normalizeBoolean(budget.enabled, DEFAULT_BUDGET_CONFIG.enabled),
|
|
14994
|
+
pollSeconds: normalizeBoundedInteger(budget.pollSeconds, DEFAULT_BUDGET_CONFIG.pollSeconds, 5, 3600),
|
|
14995
|
+
pauseAt,
|
|
14996
|
+
resumeBelow,
|
|
14997
|
+
syncDriftPct: normalizeBoundedInteger(budget.syncDriftPct, DEFAULT_BUDGET_CONFIG.syncDriftPct, 1, 100),
|
|
14998
|
+
parallel: {
|
|
14999
|
+
minRemainingPct: normalizeBoundedInteger(parallel.minRemainingPct, DEFAULT_BUDGET_CONFIG.parallel.minRemainingPct, 1, 100),
|
|
15000
|
+
timeWindowSec: normalizeBoundedInteger(parallel.timeWindowSec, DEFAULT_BUDGET_CONFIG.parallel.timeWindowSec, 60, 604800)
|
|
15001
|
+
},
|
|
15002
|
+
codexTierControl: normalizeBoolean(budget.codexTierControl, DEFAULT_BUDGET_CONFIG.codexTierControl) && codexTiers.full !== null,
|
|
15003
|
+
codexTiers
|
|
15004
|
+
};
|
|
15005
|
+
}
|
|
15006
|
+
function normalizeConfig(raw) {
|
|
15007
|
+
if (!isRecord(raw))
|
|
15008
|
+
return null;
|
|
15009
|
+
const config2 = raw;
|
|
15010
|
+
const codex = isRecord(config2.codex) ? config2.codex : {};
|
|
15011
|
+
const daemon = isRecord(config2.daemon) ? config2.daemon : {};
|
|
15012
|
+
const turnCoordination = isRecord(config2.turnCoordination) ? config2.turnCoordination : {};
|
|
15013
|
+
return {
|
|
15014
|
+
version: typeof config2.version === "string" ? config2.version : DEFAULT_CONFIG.version,
|
|
15015
|
+
codex: {
|
|
15016
|
+
appPort: normalizeInteger(codex.appPort ?? daemon.port, DEFAULT_CONFIG.codex.appPort),
|
|
15017
|
+
proxyPort: normalizeInteger(codex.proxyPort ?? daemon.proxyPort, DEFAULT_CONFIG.codex.proxyPort)
|
|
15018
|
+
},
|
|
15019
|
+
turnCoordination: {
|
|
15020
|
+
attentionWindowSeconds: normalizeInteger(turnCoordination.attentionWindowSeconds, DEFAULT_CONFIG.turnCoordination.attentionWindowSeconds)
|
|
15021
|
+
},
|
|
15022
|
+
idleShutdownSeconds: normalizeInteger(config2.idleShutdownSeconds, DEFAULT_CONFIG.idleShutdownSeconds),
|
|
15023
|
+
budget: normalizeBudgetConfig(config2.budget)
|
|
15024
|
+
};
|
|
15025
|
+
}
|
|
14445
15026
|
|
|
14446
15027
|
class ConfigService {
|
|
14447
15028
|
configDir;
|
|
14448
15029
|
configPath;
|
|
14449
|
-
collaborationPath;
|
|
14450
15030
|
constructor(projectRoot) {
|
|
14451
15031
|
const root = projectRoot ?? process.cwd();
|
|
14452
15032
|
this.configDir = join2(root, CONFIG_DIR);
|
|
14453
15033
|
this.configPath = join2(this.configDir, CONFIG_FILE);
|
|
14454
|
-
this.collaborationPath = join2(this.configDir, COLLABORATION_FILE);
|
|
14455
15034
|
}
|
|
14456
15035
|
hasConfig() {
|
|
14457
|
-
return
|
|
15036
|
+
return existsSync4(this.configPath);
|
|
14458
15037
|
}
|
|
14459
15038
|
load() {
|
|
14460
15039
|
try {
|
|
14461
15040
|
const raw = readFileSync2(this.configPath, "utf-8");
|
|
14462
|
-
return JSON.parse(raw);
|
|
15041
|
+
return normalizeConfig(JSON.parse(raw));
|
|
14463
15042
|
} catch {
|
|
14464
15043
|
return null;
|
|
14465
15044
|
}
|
|
@@ -14472,71 +15051,371 @@ class ConfigService {
|
|
|
14472
15051
|
writeFileSync2(this.configPath, JSON.stringify(config2, null, 2) + `
|
|
14473
15052
|
`, "utf-8");
|
|
14474
15053
|
}
|
|
14475
|
-
loadCollaboration() {
|
|
14476
|
-
try {
|
|
14477
|
-
return readFileSync2(this.collaborationPath, "utf-8");
|
|
14478
|
-
} catch {
|
|
14479
|
-
return null;
|
|
14480
|
-
}
|
|
14481
|
-
}
|
|
14482
|
-
saveCollaboration(content) {
|
|
14483
|
-
this.ensureConfigDir();
|
|
14484
|
-
writeFileSync2(this.collaborationPath, content, "utf-8");
|
|
14485
|
-
}
|
|
14486
15054
|
initDefaults() {
|
|
14487
15055
|
this.ensureConfigDir();
|
|
14488
15056
|
const created = [];
|
|
14489
|
-
if (!
|
|
15057
|
+
if (!existsSync4(this.configPath)) {
|
|
14490
15058
|
this.save(DEFAULT_CONFIG);
|
|
14491
15059
|
created.push(this.configPath);
|
|
14492
15060
|
}
|
|
14493
|
-
if (!existsSync3(this.collaborationPath)) {
|
|
14494
|
-
this.saveCollaboration(DEFAULT_COLLABORATION_MD);
|
|
14495
|
-
created.push(this.collaborationPath);
|
|
14496
|
-
}
|
|
14497
15061
|
return created;
|
|
14498
15062
|
}
|
|
14499
15063
|
get configFilePath() {
|
|
14500
15064
|
return this.configPath;
|
|
14501
15065
|
}
|
|
14502
|
-
get collaborationFilePath() {
|
|
14503
|
-
return this.collaborationPath;
|
|
14504
|
-
}
|
|
14505
15066
|
ensureConfigDir() {
|
|
14506
|
-
if (!
|
|
15067
|
+
if (!existsSync4(this.configDir)) {
|
|
14507
15068
|
mkdirSync2(this.configDir, { recursive: true });
|
|
14508
15069
|
}
|
|
14509
15070
|
}
|
|
14510
15071
|
}
|
|
14511
15072
|
|
|
15073
|
+
// src/pair-registry.ts
|
|
15074
|
+
import {
|
|
15075
|
+
closeSync as closeSync2,
|
|
15076
|
+
existsSync as existsSync5,
|
|
15077
|
+
fsyncSync,
|
|
15078
|
+
linkSync,
|
|
15079
|
+
lstatSync,
|
|
15080
|
+
mkdirSync as mkdirSync3,
|
|
15081
|
+
openSync as openSync2,
|
|
15082
|
+
readdirSync,
|
|
15083
|
+
readFileSync as readFileSync3,
|
|
15084
|
+
realpathSync,
|
|
15085
|
+
renameSync as renameSync2,
|
|
15086
|
+
rmSync,
|
|
15087
|
+
statSync as statSync3,
|
|
15088
|
+
unlinkSync as unlinkSync3,
|
|
15089
|
+
writeFileSync as writeFileSync3
|
|
15090
|
+
} from "fs";
|
|
15091
|
+
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
15092
|
+
import { basename, join as join3, resolve, sep } from "path";
|
|
15093
|
+
var PAIR_BASE_PORT = 4500;
|
|
15094
|
+
var PAIR_SLOT_STRIDE = 10;
|
|
15095
|
+
var PAIR_ID_REGEX = /^[A-Za-z0-9._-]{1,64}$/;
|
|
15096
|
+
var REGISTRY_FILE_NAME = "registry.json";
|
|
15097
|
+
class PairError extends Error {
|
|
15098
|
+
code;
|
|
15099
|
+
details;
|
|
15100
|
+
constructor(code, message, details) {
|
|
15101
|
+
super(message);
|
|
15102
|
+
this.name = "PairError";
|
|
15103
|
+
this.code = code;
|
|
15104
|
+
this.details = details;
|
|
15105
|
+
}
|
|
15106
|
+
}
|
|
15107
|
+
var MAX_PAIR_SLOT = Math.floor((65535 - 2 - PAIR_BASE_PORT) / PAIR_SLOT_STRIDE);
|
|
15108
|
+
function derivePairId(cwd, name) {
|
|
15109
|
+
let real;
|
|
15110
|
+
try {
|
|
15111
|
+
real = realpathSync(cwd);
|
|
15112
|
+
} catch {
|
|
15113
|
+
real = cwd;
|
|
15114
|
+
}
|
|
15115
|
+
const hash = createHash("sha256").update(real).update("\x00").update(name.toLowerCase()).digest("hex").slice(0, 8);
|
|
15116
|
+
const slug = name.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32) || "pair";
|
|
15117
|
+
return `${slug}-${hash}`;
|
|
15118
|
+
}
|
|
15119
|
+
function pairsDir(base) {
|
|
15120
|
+
return join3(base, "pairs");
|
|
15121
|
+
}
|
|
15122
|
+
function registryPath(base) {
|
|
15123
|
+
return join3(pairsDir(base), REGISTRY_FILE_NAME);
|
|
15124
|
+
}
|
|
15125
|
+
function readRegistry(base) {
|
|
15126
|
+
const path = registryPath(base);
|
|
15127
|
+
if (!existsSync5(path))
|
|
15128
|
+
return { version: 1, pairs: [] };
|
|
15129
|
+
let parsed;
|
|
15130
|
+
try {
|
|
15131
|
+
parsed = JSON.parse(readFileSync3(path, "utf-8"));
|
|
15132
|
+
} catch (err) {
|
|
15133
|
+
throw new PairError("PAIR_REGISTRY_CORRUPT", `Registry JSON is not parseable at ${path}: ${err.message}`, {
|
|
15134
|
+
path
|
|
15135
|
+
});
|
|
15136
|
+
}
|
|
15137
|
+
if (!parsed || typeof parsed !== "object" || parsed.version !== 1 || !Array.isArray(parsed.pairs)) {
|
|
15138
|
+
throw new PairError("PAIR_REGISTRY_CORRUPT", `Registry shape is invalid at ${path}`, { path });
|
|
15139
|
+
}
|
|
15140
|
+
const entries = parsed.pairs;
|
|
15141
|
+
const seenSlots = new Set;
|
|
15142
|
+
const seenIds = new Set;
|
|
15143
|
+
for (const e of entries) {
|
|
15144
|
+
const idValid = e && typeof e.pairId === "string" && e.pairId !== "." && e.pairId !== ".." && PAIR_ID_REGEX.test(e.pairId);
|
|
15145
|
+
if (!idValid || !Number.isInteger(e.slot) || e.slot < 0) {
|
|
15146
|
+
throw new PairError("PAIR_REGISTRY_CORRUPT", `Registry has a malformed entry at ${path}`, { path, entry: e });
|
|
15147
|
+
}
|
|
15148
|
+
const lower = e.pairId.toLowerCase();
|
|
15149
|
+
if (seenSlots.has(e.slot) || seenIds.has(lower)) {
|
|
15150
|
+
throw new PairError("PAIR_REGISTRY_CORRUPT", `Registry has duplicate slot/pairId at ${path}`, {
|
|
15151
|
+
path,
|
|
15152
|
+
pairId: e.pairId,
|
|
15153
|
+
slot: e.slot
|
|
15154
|
+
});
|
|
15155
|
+
}
|
|
15156
|
+
seenSlots.add(e.slot);
|
|
15157
|
+
seenIds.add(lower);
|
|
15158
|
+
}
|
|
15159
|
+
return parsed;
|
|
15160
|
+
}
|
|
15161
|
+
|
|
15162
|
+
// src/pair-resolver.ts
|
|
15163
|
+
function computeBaseDir() {
|
|
15164
|
+
return process.env.AGENTBRIDGE_BASE_DIR || process.env.AGENTBRIDGE_STATE_DIR || StateDirResolver.platformBaseDir();
|
|
15165
|
+
}
|
|
15166
|
+
function findPair(base, pairId) {
|
|
15167
|
+
const lower = pairId.toLowerCase();
|
|
15168
|
+
return readRegistry(base).pairs.find((p) => p.pairId.toLowerCase() === lower) ?? null;
|
|
15169
|
+
}
|
|
15170
|
+
|
|
15171
|
+
// src/pair-command.ts
|
|
15172
|
+
function pairScopedCommand(cmd) {
|
|
15173
|
+
const pairId = process.env.AGENTBRIDGE_PAIR_ID;
|
|
15174
|
+
if (!pairId)
|
|
15175
|
+
return `agentbridge ${cmd}`;
|
|
15176
|
+
let selector = process.env.AGENTBRIDGE_PAIR_NAME;
|
|
15177
|
+
if (!selector) {
|
|
15178
|
+
try {
|
|
15179
|
+
selector = findPair(computeBaseDir(), pairId)?.name || pairId;
|
|
15180
|
+
} catch {
|
|
15181
|
+
selector = pairId;
|
|
15182
|
+
}
|
|
15183
|
+
}
|
|
15184
|
+
return `agentbridge --pair ${selector} ${cmd}`;
|
|
15185
|
+
}
|
|
15186
|
+
|
|
14512
15187
|
// src/bridge-disabled-state.ts
|
|
14513
15188
|
function disabledReplyError(reason) {
|
|
15189
|
+
const claudeCmd = pairScopedCommand("claude");
|
|
14514
15190
|
switch (reason) {
|
|
14515
15191
|
case "rejected":
|
|
14516
|
-
return
|
|
15192
|
+
return `AgentBridge rejected this session \u2014 another Claude Code session is already connected. Close the other session first, or run \`${pairScopedCommand("kill")}\` to reset.`;
|
|
15193
|
+
case "evicted":
|
|
15194
|
+
return `AgentBridge evicted this session because it stopped responding to liveness probes \u2014 a newer Claude Code session has taken over. Close this session and start a new one with \`${claudeCmd}\`.`;
|
|
15195
|
+
case "probe_in_progress":
|
|
15196
|
+
return `AgentBridge rejected this session \u2014 a liveness probe is currently checking the incumbent Claude session. Retry in a few seconds with \`${claudeCmd}\`.`;
|
|
15197
|
+
case "auto_recovery_exhausted":
|
|
15198
|
+
return `AgentBridge auto-recovery gave up after exhausting its retry budget for the in-flight liveness probe contention. Retry manually with \`${claudeCmd}\`.`;
|
|
14517
15199
|
case "killed":
|
|
14518
|
-
return
|
|
15200
|
+
return `AgentBridge is disabled by \`agentbridge kill\`. Restart Claude Code (\`${claudeCmd}\`), switch to a new conversation, or run \`/resume\` to reconnect.`;
|
|
15201
|
+
}
|
|
15202
|
+
}
|
|
15203
|
+
|
|
15204
|
+
// src/env-guard.ts
|
|
15205
|
+
var GENERATED_ENV_KEYS = [
|
|
15206
|
+
"AGENTBRIDGE_BASE_DIR",
|
|
15207
|
+
"AGENTBRIDGE_PAIR_ID",
|
|
15208
|
+
"AGENTBRIDGE_PAIR_NAME",
|
|
15209
|
+
"AGENTBRIDGE_STATE_DIR",
|
|
15210
|
+
"AGENTBRIDGE_CONTROL_PORT",
|
|
15211
|
+
"AGENTBRIDGE_MODE",
|
|
15212
|
+
"AGENTBRIDGE_FILTER_MODE",
|
|
15213
|
+
"AGENTBRIDGE_MAX_BUFFERED_MESSAGES",
|
|
15214
|
+
"AGENTBRIDGE_CODEX_TRANSPORT",
|
|
15215
|
+
"CODEX_WS_PORT",
|
|
15216
|
+
"CODEX_PROXY_PORT"
|
|
15217
|
+
];
|
|
15218
|
+
function normalizeEnvGuardMode(raw, fallback = "fix") {
|
|
15219
|
+
if (raw === "off" || raw === "warn" || raw === "fix" || raw === "strict")
|
|
15220
|
+
return raw;
|
|
15221
|
+
return fallback;
|
|
15222
|
+
}
|
|
15223
|
+
function inspectAgentBridgeEnv(opts) {
|
|
15224
|
+
const env = opts.env ?? process.env;
|
|
15225
|
+
const actualPairId = nonEmpty(env.AGENTBRIDGE_PAIR_ID);
|
|
15226
|
+
const pairName = nonEmpty(env.AGENTBRIDGE_PAIR_NAME) ?? "main";
|
|
15227
|
+
const stateDir = nonEmpty(env.AGENTBRIDGE_STATE_DIR);
|
|
15228
|
+
const baseDir = nonEmpty(env.AGENTBRIDGE_BASE_DIR);
|
|
15229
|
+
const manualOptIn = env.AGENTBRIDGE_MANUAL === "1";
|
|
15230
|
+
const manualRuntimeEnv = !!stateDir || !!nonEmpty(env.AGENTBRIDGE_CONTROL_PORT) || !!nonEmpty(env.CODEX_WS_PORT) || !!nonEmpty(env.CODEX_PROXY_PORT);
|
|
15231
|
+
const expectedPairId = actualPairId ? derivePairId(opts.cwd, pairName) : null;
|
|
15232
|
+
const reasons = [];
|
|
15233
|
+
if (!actualPairId && manualRuntimeEnv && !manualOptIn) {
|
|
15234
|
+
reasons.push("AgentBridge runtime env is set without AGENTBRIDGE_PAIR_ID or AGENTBRIDGE_MANUAL=1");
|
|
15235
|
+
}
|
|
15236
|
+
if (actualPairId && expectedPairId && actualPairId !== expectedPairId) {
|
|
15237
|
+
reasons.push(`AGENTBRIDGE_PAIR_ID=${actualPairId} does not match cwd-derived ${expectedPairId}`);
|
|
15238
|
+
}
|
|
15239
|
+
if (actualPairId && stateDir && !stateDir.endsWith(`/pairs/${actualPairId}`)) {
|
|
15240
|
+
reasons.push(`AGENTBRIDGE_STATE_DIR does not end with /pairs/${actualPairId}`);
|
|
15241
|
+
}
|
|
15242
|
+
if (actualPairId && baseDir && stateDir && !stateDir.startsWith(`${baseDir}/`)) {
|
|
15243
|
+
reasons.push("AGENTBRIDGE_BASE_DIR and AGENTBRIDGE_STATE_DIR disagree");
|
|
15244
|
+
}
|
|
15245
|
+
return {
|
|
15246
|
+
ok: reasons.length === 0,
|
|
15247
|
+
expectedPairId,
|
|
15248
|
+
actualPairId,
|
|
15249
|
+
pairName,
|
|
15250
|
+
reasons
|
|
15251
|
+
};
|
|
15252
|
+
}
|
|
15253
|
+
function guardAgentBridgeEnv(opts) {
|
|
15254
|
+
const env = opts.env ?? process.env;
|
|
15255
|
+
const mode = normalizeEnvGuardMode(opts.mode, "fix");
|
|
15256
|
+
const effectiveMode = mode === "strict" && opts.allowStrict === false ? "fix" : mode;
|
|
15257
|
+
const inspection = inspectAgentBridgeEnv({ cwd: opts.cwd, env });
|
|
15258
|
+
if (effectiveMode === "off" || inspection.ok) {
|
|
15259
|
+
return { ...inspection, action: "none" };
|
|
15260
|
+
}
|
|
15261
|
+
const message = `stale AgentBridge environment detected for ${opts.cwd}: ${inspection.reasons.join("; ")}`;
|
|
15262
|
+
if (effectiveMode === "strict") {
|
|
15263
|
+
throw new Error(message);
|
|
15264
|
+
}
|
|
15265
|
+
opts.log?.(`[agentbridge] ${message}`);
|
|
15266
|
+
if (effectiveMode === "warn") {
|
|
15267
|
+
return { ...inspection, action: "warned" };
|
|
15268
|
+
}
|
|
15269
|
+
for (const key of GENERATED_ENV_KEYS) {
|
|
15270
|
+
delete env[key];
|
|
15271
|
+
}
|
|
15272
|
+
opts.log?.("[agentbridge] cleared stale AgentBridge environment variables");
|
|
15273
|
+
return { ...inspection, action: "fixed" };
|
|
15274
|
+
}
|
|
15275
|
+
function nonEmpty(value) {
|
|
15276
|
+
return value && value.length > 0 ? value : null;
|
|
15277
|
+
}
|
|
15278
|
+
|
|
15279
|
+
// src/trace-log.ts
|
|
15280
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync4 } from "fs";
|
|
15281
|
+
import { join as join4 } from "path";
|
|
15282
|
+
var SECRET_KEY_RE = /(token|secret|password|passwd|api[_-]?key|auth|cookie|session)/i;
|
|
15283
|
+
var SECRET_ARG_RE = /^--?(?:token|secret|password|passwd|apikey|api-key|api_key|auth|cookie|session)(?:=.*)?$/i;
|
|
15284
|
+
var RELEVANT_ENV_RE = /^(AGENTBRIDGE_|CODEX_)/;
|
|
15285
|
+
function pickRelevantEnv(env) {
|
|
15286
|
+
const picked = {};
|
|
15287
|
+
for (const [key, value] of Object.entries(env)) {
|
|
15288
|
+
if (!RELEVANT_ENV_RE.test(key))
|
|
15289
|
+
continue;
|
|
15290
|
+
picked[key] = SECRET_KEY_RE.test(key) && value !== undefined ? "<redacted>" : value;
|
|
15291
|
+
}
|
|
15292
|
+
return picked;
|
|
15293
|
+
}
|
|
15294
|
+
function redactArgv(argv) {
|
|
15295
|
+
const redacted = [];
|
|
15296
|
+
let redactNext = false;
|
|
15297
|
+
for (const arg of argv) {
|
|
15298
|
+
if (redactNext) {
|
|
15299
|
+
redacted.push("<redacted>");
|
|
15300
|
+
redactNext = false;
|
|
15301
|
+
continue;
|
|
15302
|
+
}
|
|
15303
|
+
if (SECRET_ARG_RE.test(arg)) {
|
|
15304
|
+
if (arg.includes("=")) {
|
|
15305
|
+
const [key] = arg.split("=", 1);
|
|
15306
|
+
redacted.push(`${key}=<redacted>`);
|
|
15307
|
+
} else {
|
|
15308
|
+
redacted.push(arg);
|
|
15309
|
+
redactNext = true;
|
|
15310
|
+
}
|
|
15311
|
+
continue;
|
|
15312
|
+
}
|
|
15313
|
+
redacted.push(arg);
|
|
15314
|
+
}
|
|
15315
|
+
return redacted;
|
|
15316
|
+
}
|
|
15317
|
+
function traceLogPath(cwd, timestamp) {
|
|
15318
|
+
const day = timestamp.slice(0, 10);
|
|
15319
|
+
return join4(cwd, ".agentbridge", "logs", `trace-${day}.jsonl`);
|
|
15320
|
+
}
|
|
15321
|
+
function appendTraceEvent(input) {
|
|
15322
|
+
const timestamp = input.timestamp ?? new Date().toISOString();
|
|
15323
|
+
const path = traceLogPath(input.cwd, timestamp);
|
|
15324
|
+
const event = {
|
|
15325
|
+
timestamp,
|
|
15326
|
+
event: input.event,
|
|
15327
|
+
cwd: input.cwd,
|
|
15328
|
+
pid: input.pid ?? process.pid,
|
|
15329
|
+
...input.argv ? { argv: redactArgv(input.argv) } : {},
|
|
15330
|
+
...input.env ? { env: pickRelevantEnv(input.env) } : {},
|
|
15331
|
+
...input.data ? { data: redactData(input.data) } : {}
|
|
15332
|
+
};
|
|
15333
|
+
mkdirSync4(join4(input.cwd, ".agentbridge", "logs"), { recursive: true });
|
|
15334
|
+
appendFileSync2(path, JSON.stringify(event) + `
|
|
15335
|
+
`, "utf-8");
|
|
15336
|
+
return path;
|
|
15337
|
+
}
|
|
15338
|
+
function isEnvSnapshot(key, value) {
|
|
15339
|
+
return /env$/i.test(key) && !!value && typeof value === "object" && !Array.isArray(value);
|
|
15340
|
+
}
|
|
15341
|
+
function redactData(value, key = "") {
|
|
15342
|
+
if (typeof value === "string") {
|
|
15343
|
+
return SECRET_KEY_RE.test(key) ? "<redacted>" : value;
|
|
15344
|
+
}
|
|
15345
|
+
if (Array.isArray(value)) {
|
|
15346
|
+
return value.map((item) => redactData(item, key));
|
|
14519
15347
|
}
|
|
15348
|
+
if (value && typeof value === "object") {
|
|
15349
|
+
const redacted = {};
|
|
15350
|
+
for (const [childKey, childValue] of Object.entries(value)) {
|
|
15351
|
+
if (SECRET_KEY_RE.test(childKey)) {
|
|
15352
|
+
redacted[childKey] = "<redacted>";
|
|
15353
|
+
} else if (isEnvSnapshot(childKey, childValue)) {
|
|
15354
|
+
redacted[childKey] = pickRelevantEnv(childValue);
|
|
15355
|
+
} else {
|
|
15356
|
+
redacted[childKey] = redactData(childValue, childKey);
|
|
15357
|
+
}
|
|
15358
|
+
}
|
|
15359
|
+
return redacted;
|
|
15360
|
+
}
|
|
15361
|
+
return value;
|
|
14520
15362
|
}
|
|
14521
15363
|
|
|
14522
15364
|
// src/bridge.ts
|
|
15365
|
+
var originalEnv = { ...process.env };
|
|
15366
|
+
var bootstrapLogger = createProcessLogger({ component: "AgentBridgeFrontend" });
|
|
15367
|
+
var envGuardResult = guardAgentBridgeEnv({
|
|
15368
|
+
cwd: process.cwd(),
|
|
15369
|
+
env: process.env,
|
|
15370
|
+
mode: normalizeEnvGuardMode(process.env.AGENTBRIDGE_ENV_GUARD),
|
|
15371
|
+
allowStrict: false,
|
|
15372
|
+
log: bootstrapLogger.log
|
|
15373
|
+
});
|
|
14523
15374
|
var stateDir = new StateDirResolver;
|
|
15375
|
+
stateDir.ensure();
|
|
14524
15376
|
var configService = new ConfigService;
|
|
14525
15377
|
var config2 = configService.loadOrDefault();
|
|
14526
15378
|
var CONTROL_PORT = parseInt(process.env.AGENTBRIDGE_CONTROL_PORT ?? "4502", 10);
|
|
15379
|
+
var processLogger = createProcessLogger({ component: "AgentBridgeFrontend", logFile: stateDir.logFile });
|
|
14527
15380
|
var daemonLifecycle = new DaemonLifecycle({ stateDir, controlPort: CONTROL_PORT, log });
|
|
14528
15381
|
var CONTROL_WS_URL = daemonLifecycle.controlWsUrl;
|
|
14529
|
-
var claude = new ClaudeAdapter;
|
|
14530
|
-
var daemonClient = new DaemonClient(CONTROL_WS_URL);
|
|
15382
|
+
var claude = new ClaudeAdapter(stateDir.logFile);
|
|
15383
|
+
var daemonClient = new DaemonClient(CONTROL_WS_URL, { identity: currentClientIdentity() });
|
|
14531
15384
|
var shuttingDown = false;
|
|
14532
15385
|
var daemonDisabled = false;
|
|
14533
15386
|
var daemonDisabledReason = null;
|
|
15387
|
+
var hasSeenTuiConnect = false;
|
|
15388
|
+
var previousTuiConnected = false;
|
|
14534
15389
|
var RECONNECT_NOTIFY_COOLDOWN_MS = 30000;
|
|
14535
15390
|
var DISABLED_RECOVERY_INTERVAL_MS = 5000;
|
|
14536
15391
|
var lastDisconnectNotifyTs = 0;
|
|
14537
15392
|
var lastReconnectNotifyTs = 0;
|
|
14538
15393
|
var disabledRecoveryTimer = null;
|
|
14539
15394
|
var disabledRecoveryInFlight = false;
|
|
15395
|
+
var disabledRecoveryAttempts = 0;
|
|
15396
|
+
var DISABLED_RECOVERY_MAX_ATTEMPTS = 6;
|
|
15397
|
+
var DISABLED_RECOVERY_CONFIRM_TIMEOUT_MS = 1000;
|
|
15398
|
+
if (process.env.AGENTBRIDGE_TRACE === "1") {
|
|
15399
|
+
try {
|
|
15400
|
+
appendTraceEvent({
|
|
15401
|
+
cwd: process.cwd(),
|
|
15402
|
+
event: "bridge.start",
|
|
15403
|
+
pid: process.pid,
|
|
15404
|
+
argv: process.argv,
|
|
15405
|
+
env: process.env,
|
|
15406
|
+
data: {
|
|
15407
|
+
originalEnv: pickRelevantEnv(originalEnv),
|
|
15408
|
+
effectiveEnv: pickRelevantEnv(process.env),
|
|
15409
|
+
envGuardAction: envGuardResult.action,
|
|
15410
|
+
pairId: process.env.AGENTBRIDGE_PAIR_ID ?? null,
|
|
15411
|
+
pairName: process.env.AGENTBRIDGE_PAIR_NAME ?? null,
|
|
15412
|
+
stateDir: stateDir.dir,
|
|
15413
|
+
controlPort: CONTROL_PORT,
|
|
15414
|
+
build: BUILD_INFO
|
|
15415
|
+
}
|
|
15416
|
+
});
|
|
15417
|
+
} catch {}
|
|
15418
|
+
}
|
|
14540
15419
|
claude.setReplySender(async (msg, requireReply) => {
|
|
14541
15420
|
if (msg.source !== "claude") {
|
|
14542
15421
|
return { success: false, error: "Invalid message source" };
|
|
@@ -14555,10 +15434,24 @@ daemonClient.on("codexMessage", (message) => {
|
|
|
14555
15434
|
});
|
|
14556
15435
|
daemonClient.on("status", (status) => {
|
|
14557
15436
|
log(`Daemon status: ready=${status.bridgeReady} tui=${status.tuiConnected} thread=${status.threadId ?? "none"} queued=${status.queuedMessageCount}`);
|
|
15437
|
+
claude.setBudgetSnapshot(status.budget ?? null);
|
|
15438
|
+
if (!hasSeenTuiConnect && status.tuiConnected && !previousTuiConnected) {
|
|
15439
|
+
hasSeenTuiConnect = true;
|
|
15440
|
+
log("First TUI connect detected \u2014 sending kickoff message to Claude");
|
|
15441
|
+
claude.pushNotification(systemMessage("system_tui_kickoff", [
|
|
15442
|
+
"\uD83E\uDD1D Codex has connected via AgentBridge.",
|
|
15443
|
+
"You are now in a multi-agent collaboration session.",
|
|
15444
|
+
"When you receive a complex task, propose a division of labor to Codex.",
|
|
15445
|
+
"Use `reply` to send messages and `get_messages` to check for responses."
|
|
15446
|
+
].join(`
|
|
15447
|
+
`)));
|
|
15448
|
+
}
|
|
15449
|
+
previousTuiConnected = status.tuiConnected;
|
|
14558
15450
|
});
|
|
14559
15451
|
daemonClient.on("disconnect", () => {
|
|
14560
15452
|
if (shuttingDown || daemonDisabled)
|
|
14561
15453
|
return;
|
|
15454
|
+
claude.setBudgetSnapshot(null);
|
|
14562
15455
|
log("Daemon control connection closed \u2014 will attempt to reconnect");
|
|
14563
15456
|
const now = Date.now();
|
|
14564
15457
|
if (now - lastDisconnectNotifyTs >= RECONNECT_NOTIFY_COOLDOWN_MS) {
|
|
@@ -14569,22 +15462,55 @@ daemonClient.on("disconnect", () => {
|
|
|
14569
15462
|
}
|
|
14570
15463
|
reconnectToDaemon();
|
|
14571
15464
|
});
|
|
14572
|
-
daemonClient.on("rejected", async () => {
|
|
15465
|
+
daemonClient.on("rejected", async (code) => {
|
|
14573
15466
|
if (shuttingDown || daemonDisabled)
|
|
14574
15467
|
return;
|
|
14575
|
-
|
|
15468
|
+
let reason;
|
|
15469
|
+
let notificationId;
|
|
15470
|
+
let notificationContent;
|
|
15471
|
+
switch (code) {
|
|
15472
|
+
case CLOSE_CODE_EVICTED_STALE:
|
|
15473
|
+
reason = "evicted";
|
|
15474
|
+
notificationId = "system_bridge_evicted";
|
|
15475
|
+
notificationContent = `\u26A0\uFE0F AgentBridge evicted this session because it stopped responding to liveness probes \u2014 a newer Claude Code session has taken over. Close this session and start a new one with \`${pairScopedCommand("claude")}\` if you want to reconnect. AgentBridge \u56E0\u6B64\u4F1A\u8BDD\u672A\u54CD\u5E94\u5B58\u6D3B\u63A2\u6D4B\u800C\u5C06\u5176\u9A71\u9010\u2014\u2014\u66F4\u65B0\u7684 Claude Code \u4F1A\u8BDD\u5DF2\u63A5\u7BA1\u3002\u5982\u9700\u91CD\u8FDE\uFF0C\u8BF7\u5173\u95ED\u6B64\u4F1A\u8BDD\u5E76\u8FD0\u884C \`${pairScopedCommand("claude")}\` \u542F\u52A8\u65B0\u4F1A\u8BDD\u3002`;
|
|
15476
|
+
break;
|
|
15477
|
+
case CLOSE_CODE_PROBE_IN_PROGRESS:
|
|
15478
|
+
reason = "probe_in_progress";
|
|
15479
|
+
notificationId = "system_bridge_probe_in_progress";
|
|
15480
|
+
notificationContent = `\u26A0\uFE0F AgentBridge rejected this session \u2014 a liveness probe is currently checking whether the incumbent Claude session is still alive. Retry in a few seconds with \`${pairScopedCommand("claude")}\`. AgentBridge \u62D2\u7EDD\u4E86\u6B64\u4F1A\u8BDD\u2014\u2014\u6B63\u5728\u901A\u8FC7\u5B58\u6D3B\u63A2\u6D4B\u68C0\u67E5\u73B0\u6709 Claude \u4F1A\u8BDD\u662F\u5426\u4ECD\u7136\u5728\u7EBF\u3002\u8BF7\u7A0D\u540E\u7528 \`${pairScopedCommand("claude")}\` \u91CD\u8BD5\u3002`;
|
|
15481
|
+
break;
|
|
15482
|
+
case CLOSE_CODE_PAIR_MISMATCH:
|
|
15483
|
+
reason = "rejected";
|
|
15484
|
+
notificationId = "system_bridge_pair_mismatch";
|
|
15485
|
+
notificationContent = `\u26A0\uFE0F AgentBridge daemon rejected this session \u2014 pair/cwd identity mismatch (this daemon belongs to a different pair or directory). Do NOT kill it; start Claude Code from the pair's own directory, or pick another pair name with \`agentbridge --pair <name> claude\`. AgentBridge \u62D2\u7EDD\u4E86\u6B64\u4F1A\u8BDD\u2014\u2014pair/\u76EE\u5F55\u8EAB\u4EFD\u4E0D\u5339\u914D\uFF08\u8BE5 daemon \u5C5E\u4E8E\u5176\u4ED6 pair \u6216\u76EE\u5F55\uFF09\u3002\u65E0\u9700 kill\uFF1B\u8BF7\u5230\u5BF9\u5E94\u76EE\u5F55\u542F\u52A8\uFF0C\u6216\u6362\u4E00\u4E2A pair \u540D\uFF1A\`agentbridge --pair <\u540D\u5B57> claude\`\u3002`;
|
|
15486
|
+
break;
|
|
15487
|
+
default:
|
|
15488
|
+
reason = "rejected";
|
|
15489
|
+
notificationId = "system_bridge_replaced";
|
|
15490
|
+
notificationContent = `\u26A0\uFE0F AgentBridge daemon rejected this session \u2014 another Claude Code session is already connected. Close the other session first, or run \`${pairScopedCommand("kill")}\` to reset. AgentBridge \u5B88\u62A4\u8FDB\u7A0B\u62D2\u7EDD\u4E86\u6B64\u4F1A\u8BDD\u2014\u2014\u53E6\u4E00\u4E2A Claude Code \u4F1A\u8BDD\u5DF2\u5728\u8FDE\u63A5\u4E2D\u3002\u8BF7\u5148\u5173\u95ED\u53E6\u4E00\u4E2A\u4F1A\u8BDD\uFF0C\u6216\u8FD0\u884C \`${pairScopedCommand("kill")}\` \u91CD\u7F6E\u3002`;
|
|
15491
|
+
break;
|
|
15492
|
+
}
|
|
15493
|
+
log(`Daemon rejected this session (close code ${code}, reason=${reason})`);
|
|
14576
15494
|
daemonDisabled = true;
|
|
14577
|
-
daemonDisabledReason =
|
|
14578
|
-
await claude.pushNotification(systemMessage(
|
|
15495
|
+
daemonDisabledReason = reason;
|
|
15496
|
+
await claude.pushNotification(systemMessage(notificationId, notificationContent));
|
|
14579
15497
|
await daemonClient.disconnect();
|
|
15498
|
+
if (reason === "probe_in_progress") {
|
|
15499
|
+
disabledRecoveryAttempts = 0;
|
|
15500
|
+
startDisabledRecoveryPoller();
|
|
15501
|
+
}
|
|
14580
15502
|
});
|
|
14581
15503
|
claude.on("ready", async () => {
|
|
14582
|
-
log(
|
|
15504
|
+
log("MCP server ready (push delivery) \u2014 ensuring AgentBridge daemon...");
|
|
14583
15505
|
if (daemonLifecycle.wasKilled()) {
|
|
14584
|
-
await enterDisabledState("Killed sentinel found \u2014 bridge staying idle",
|
|
15506
|
+
await enterDisabledState("Killed sentinel found \u2014 bridge staying idle", `\u26D4 AgentBridge was stopped by \`agentbridge kill\`. Bridge is staying idle. Restart Claude Code (\`${pairScopedCommand("claude")}\`), switch to a new conversation, or run \`/resume\` to reconnect.`);
|
|
14585
15507
|
return;
|
|
14586
15508
|
}
|
|
14587
|
-
|
|
15509
|
+
try {
|
|
15510
|
+
await connectToDaemon();
|
|
15511
|
+
} catch {
|
|
15512
|
+
reconnectToDaemon();
|
|
15513
|
+
}
|
|
14588
15514
|
});
|
|
14589
15515
|
async function connectToDaemon(isReconnect = false) {
|
|
14590
15516
|
if (daemonDisabled) {
|
|
@@ -14594,10 +15520,14 @@ async function connectToDaemon(isReconnect = false) {
|
|
|
14594
15520
|
try {
|
|
14595
15521
|
await daemonLifecycle.ensureRunning();
|
|
14596
15522
|
await daemonClient.connect();
|
|
14597
|
-
daemonClient.
|
|
15523
|
+
const status = await daemonClient.attachClaudeAndWaitForStatus(5000);
|
|
15524
|
+
if (!status) {
|
|
15525
|
+
throw new Error("Daemon did not confirm Claude attach.");
|
|
15526
|
+
}
|
|
15527
|
+
assertAttachedToExpectedDaemon(status);
|
|
14598
15528
|
daemonDisabledReason = null;
|
|
14599
15529
|
if (!isReconnect) {
|
|
14600
|
-
claude.pushNotification(systemMessage("system_bridge_ready"
|
|
15530
|
+
claude.pushNotification(systemMessage(status.bridgeReady ? "system_bridge_ready" : "system_bridge_waiting", initialAttachMessage(status)));
|
|
14601
15531
|
}
|
|
14602
15532
|
} catch (err) {
|
|
14603
15533
|
log(`Failed to connect to daemon: ${err.message}`);
|
|
@@ -14605,6 +15535,21 @@ async function connectToDaemon(isReconnect = false) {
|
|
|
14605
15535
|
throw err;
|
|
14606
15536
|
}
|
|
14607
15537
|
}
|
|
15538
|
+
function assertAttachedToExpectedDaemon(status) {
|
|
15539
|
+
const expectedPairId = process.env.AGENTBRIDGE_PAIR_ID || null;
|
|
15540
|
+
if (expectedPairId && status.pairId !== expectedPairId) {
|
|
15541
|
+
throw new Error(`Daemon identity mismatch after attach: expected pair ${expectedPairId}, got ${status.pairId ?? "<none>"}.`);
|
|
15542
|
+
}
|
|
15543
|
+
}
|
|
15544
|
+
function initialAttachMessage(status) {
|
|
15545
|
+
if (status.bridgeReady) {
|
|
15546
|
+
return "\u2705 AgentBridge bridge is ready. Codex TUI is connected.";
|
|
15547
|
+
}
|
|
15548
|
+
if (status.tuiConnected) {
|
|
15549
|
+
return "\u23F3 AgentBridge attached to daemon. Waiting for Codex to finish creating a thread.";
|
|
15550
|
+
}
|
|
15551
|
+
return `\u23F3 AgentBridge attached to daemon. Waiting for Codex TUI. Start Codex in another terminal with: ${pairScopedCommand("codex")}`;
|
|
15552
|
+
}
|
|
14608
15553
|
async function enterDisabledState(logMessage, notificationContent) {
|
|
14609
15554
|
if (daemonDisabled)
|
|
14610
15555
|
return;
|
|
@@ -14620,7 +15565,13 @@ var reconnectTask = null;
|
|
|
14620
15565
|
async function notifyIfDaemonKilled(logMessage) {
|
|
14621
15566
|
if (!daemonLifecycle.wasKilled())
|
|
14622
15567
|
return false;
|
|
14623
|
-
await enterDisabledState(logMessage,
|
|
15568
|
+
await enterDisabledState(logMessage, `\u26D4 AgentBridge was stopped by \`agentbridge kill\`. Bridge is staying idle. Restart Claude Code (\`${pairScopedCommand("claude")}\`), switch to a new conversation, or run \`/resume\` to reconnect.`);
|
|
15569
|
+
return true;
|
|
15570
|
+
}
|
|
15571
|
+
async function notifyIfPairRemoved(logMessage) {
|
|
15572
|
+
if (existsSync6(stateDir.dir))
|
|
15573
|
+
return false;
|
|
15574
|
+
await enterDisabledState(logMessage, `\u26D4 This pair's state directory was removed (\`abg pairs rm\` / \`prune\`). Bridge is staying idle. Start fresh with \`${pairScopedCommand("claude")}\` if you still need this pair. \u8BE5 pair \u7684\u72B6\u6001\u76EE\u5F55\u5DF2\u88AB\u5220\u9664\uFF08pairs rm / prune\uFF09\uFF0C\u6865\u63A5\u4FDD\u6301\u5F85\u673A\uFF1B\u5982\u4ECD\u9700\u8981\u8BF7\u7528 \`${pairScopedCommand("claude")}\` \u91CD\u65B0\u542F\u52A8\u3002`);
|
|
14624
15575
|
return true;
|
|
14625
15576
|
}
|
|
14626
15577
|
function reconnectToDaemon() {
|
|
@@ -14636,11 +15587,14 @@ function reconnectToDaemon() {
|
|
|
14636
15587
|
if (await notifyIfDaemonKilled("Daemon was intentionally killed by user (killed sentinel found) \u2014 not reconnecting")) {
|
|
14637
15588
|
return;
|
|
14638
15589
|
}
|
|
15590
|
+
if (await notifyIfPairRemoved("Pair state directory removed \u2014 not reconnecting")) {
|
|
15591
|
+
return;
|
|
15592
|
+
}
|
|
14639
15593
|
const delayMs = Math.min(1000 * 2 ** attempt, MAX_RECONNECT_DELAY_MS);
|
|
14640
15594
|
if (attempt > 0) {
|
|
14641
15595
|
log(`Reconnect attempt ${attempt + 1}, waiting ${delayMs}ms...`);
|
|
14642
15596
|
}
|
|
14643
|
-
await new Promise((
|
|
15597
|
+
await new Promise((resolve2) => setTimeout(resolve2, delayMs));
|
|
14644
15598
|
if (shuttingDown)
|
|
14645
15599
|
return;
|
|
14646
15600
|
if (await notifyIfDaemonKilled("Daemon was intentionally killed during reconnect backoff \u2014 not reconnecting")) {
|
|
@@ -14693,20 +15647,72 @@ async function pollDisabledRecovery() {
|
|
|
14693
15647
|
if (!healthy) {
|
|
14694
15648
|
return;
|
|
14695
15649
|
}
|
|
14696
|
-
|
|
14697
|
-
|
|
14698
|
-
|
|
14699
|
-
|
|
14700
|
-
|
|
14701
|
-
|
|
14702
|
-
|
|
14703
|
-
|
|
14704
|
-
|
|
14705
|
-
|
|
14706
|
-
|
|
14707
|
-
|
|
14708
|
-
|
|
14709
|
-
|
|
15650
|
+
const recoveredFrom = daemonDisabledReason;
|
|
15651
|
+
switch (recoveredFrom) {
|
|
15652
|
+
case "probe_in_progress": {
|
|
15653
|
+
if (disabledRecoveryAttempts >= DISABLED_RECOVERY_MAX_ATTEMPTS) {
|
|
15654
|
+
log(`Disabled-state auto-recovery gave up after ${DISABLED_RECOVERY_MAX_ATTEMPTS} attempts ` + "\u2014 switching to auto_recovery_exhausted terminal state");
|
|
15655
|
+
daemonDisabledReason = "auto_recovery_exhausted";
|
|
15656
|
+
disabledRecoveryAttempts = 0;
|
|
15657
|
+
stopDisabledRecoveryPoller();
|
|
15658
|
+
claude.pushNotification(systemMessage("system_bridge_auto_recovery_gave_up", `\u26A0\uFE0F AgentBridge auto-recovery gave up after exhausting its retry budget for the in-flight liveness probe contention. Retry manually with \`${pairScopedCommand("claude")}\`. AgentBridge \u81EA\u52A8\u6062\u590D\u5DF2\u653E\u5F03\u2014\u2014\u5B58\u6D3B\u63A2\u6D4B\u4E89\u7528\u7684\u91CD\u8BD5\u9884\u7B97\u5DF2\u7528\u5C3D\u3002\u8BF7\u4F7F\u7528 \`${pairScopedCommand("claude")}\` \u624B\u52A8\u91CD\u8BD5\u3002`));
|
|
15659
|
+
return;
|
|
15660
|
+
}
|
|
15661
|
+
disabledRecoveryAttempts += 1;
|
|
15662
|
+
log(`Disabled-state recovery attempt ${disabledRecoveryAttempts}/${DISABLED_RECOVERY_MAX_ATTEMPTS} ` + "for probe_in_progress \u2014 attempting direct daemon reconnect");
|
|
15663
|
+
try {
|
|
15664
|
+
await daemonClient.connect();
|
|
15665
|
+
const attached = await daemonClient.attachClaudeAndWaitForStatus(DISABLED_RECOVERY_CONFIRM_TIMEOUT_MS);
|
|
15666
|
+
if (!attached) {
|
|
15667
|
+
log(`Disabled-state probe_in_progress recovery attempt ${disabledRecoveryAttempts} did not confirm readiness`);
|
|
15668
|
+
await daemonClient.disconnect();
|
|
15669
|
+
return;
|
|
15670
|
+
}
|
|
15671
|
+
daemonDisabled = false;
|
|
15672
|
+
daemonDisabledReason = null;
|
|
15673
|
+
disabledRecoveryAttempts = 0;
|
|
15674
|
+
stopDisabledRecoveryPoller();
|
|
15675
|
+
claude.pushNotification(systemMessage("system_bridge_recovered", "\u2705 AgentBridge recovered after the liveness probe completed. Daemon reconnected."));
|
|
15676
|
+
} catch (err) {
|
|
15677
|
+
log(`Disabled-state probe_in_progress recovery attempt failed: ${err.message}`);
|
|
15678
|
+
await daemonClient.disconnect();
|
|
15679
|
+
}
|
|
15680
|
+
return;
|
|
15681
|
+
}
|
|
15682
|
+
case "killed": {
|
|
15683
|
+
log("Disabled-state recovery conditions met \u2014 attempting direct daemon reconnect");
|
|
15684
|
+
try {
|
|
15685
|
+
await daemonClient.connect();
|
|
15686
|
+
const attached = await daemonClient.attachClaudeAndWaitForStatus(DISABLED_RECOVERY_CONFIRM_TIMEOUT_MS);
|
|
15687
|
+
if (!attached) {
|
|
15688
|
+
throw new Error("daemon did not confirm reconnect");
|
|
15689
|
+
}
|
|
15690
|
+
daemonDisabled = false;
|
|
15691
|
+
daemonDisabledReason = null;
|
|
15692
|
+
disabledRecoveryAttempts = 0;
|
|
15693
|
+
stopDisabledRecoveryPoller();
|
|
15694
|
+
claude.pushNotification(systemMessage("system_bridge_recovered", "\u2705 AgentBridge recovered after the killed sentinel was cleared. Daemon reconnected."));
|
|
15695
|
+
} catch (err) {
|
|
15696
|
+
log(`Disabled-state direct reconnect failed: ${err.message}`);
|
|
15697
|
+
daemonDisabled = false;
|
|
15698
|
+
daemonDisabledReason = null;
|
|
15699
|
+
disabledRecoveryAttempts = 0;
|
|
15700
|
+
stopDisabledRecoveryPoller();
|
|
15701
|
+
reconnectToDaemon();
|
|
15702
|
+
}
|
|
15703
|
+
return;
|
|
15704
|
+
}
|
|
15705
|
+
case "evicted":
|
|
15706
|
+
case "rejected":
|
|
15707
|
+
case "auto_recovery_exhausted":
|
|
15708
|
+
case null:
|
|
15709
|
+
log(`Disabled-state recovery poller encountered terminal/unexpected reason ${recoveredFrom ?? "null"} \u2014 stopping`);
|
|
15710
|
+
stopDisabledRecoveryPoller();
|
|
15711
|
+
return;
|
|
15712
|
+
default: {
|
|
15713
|
+
const exhaustive = recoveredFrom;
|
|
15714
|
+
return exhaustive;
|
|
15715
|
+
}
|
|
14710
15716
|
}
|
|
14711
15717
|
} finally {
|
|
14712
15718
|
disabledRecoveryInFlight = false;
|
|
@@ -14720,6 +15726,17 @@ function systemMessage(idPrefix, content) {
|
|
|
14720
15726
|
timestamp: Date.now()
|
|
14721
15727
|
};
|
|
14722
15728
|
}
|
|
15729
|
+
function currentClientIdentity() {
|
|
15730
|
+
return {
|
|
15731
|
+
pairId: process.env.AGENTBRIDGE_PAIR_ID ?? null,
|
|
15732
|
+
pairName: process.env.AGENTBRIDGE_PAIR_NAME ?? null,
|
|
15733
|
+
cwd: process.cwd(),
|
|
15734
|
+
baseDir: process.env.AGENTBRIDGE_BASE_DIR ?? null,
|
|
15735
|
+
stateDir: stateDir.dir,
|
|
15736
|
+
clientPid: process.pid,
|
|
15737
|
+
contractVersion: BUILD_INFO.contractVersion
|
|
15738
|
+
};
|
|
15739
|
+
}
|
|
14723
15740
|
function shutdown(reason) {
|
|
14724
15741
|
if (shuttingDown)
|
|
14725
15742
|
return;
|
|
@@ -14745,18 +15762,13 @@ process.on("exit", () => {
|
|
|
14745
15762
|
daemonClient.disconnect();
|
|
14746
15763
|
});
|
|
14747
15764
|
process.on("uncaughtException", (err) => {
|
|
14748
|
-
|
|
15765
|
+
processLogger.fatal("UNCAUGHT EXCEPTION", err);
|
|
14749
15766
|
});
|
|
14750
15767
|
process.on("unhandledRejection", (reason) => {
|
|
14751
|
-
|
|
15768
|
+
processLogger.fatal("UNHANDLED REJECTION", reason);
|
|
14752
15769
|
});
|
|
14753
15770
|
function log(msg) {
|
|
14754
|
-
|
|
14755
|
-
`;
|
|
14756
|
-
process.stderr.write(line);
|
|
14757
|
-
try {
|
|
14758
|
-
appendFileSync2(stateDir.logFile, line);
|
|
14759
|
-
} catch {}
|
|
15771
|
+
processLogger.log(msg);
|
|
14760
15772
|
}
|
|
14761
15773
|
log(`Starting AgentBridge frontend (daemon ws ${CONTROL_WS_URL})`);
|
|
14762
15774
|
(async () => {
|