@raysonmeng/agentbridge 0.1.11 → 0.1.13
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 +1 -1
- package/dist/cli.js +1583 -577
- package/dist/daemon.js +2039 -624
- package/package.json +3 -1
- package/plugins/agentbridge/.claude-plugin/plugin.json +1 -1
- package/plugins/agentbridge/server/bridge-server.js +918 -275
- package/plugins/agentbridge/server/daemon.js +2039 -624
|
@@ -6518,7 +6518,7 @@ var require_dist = __commonJS((exports, module) => {
|
|
|
6518
6518
|
});
|
|
6519
6519
|
|
|
6520
6520
|
// src/bridge.ts
|
|
6521
|
-
import { existsSync as
|
|
6521
|
+
import { existsSync as existsSync7 } from "fs";
|
|
6522
6522
|
|
|
6523
6523
|
// node_modules/zod/v4/core/core.js
|
|
6524
6524
|
var NEVER = Object.freeze({
|
|
@@ -13662,19 +13662,21 @@ class StdioServerTransport {
|
|
|
13662
13662
|
// src/claude-adapter.ts
|
|
13663
13663
|
import { EventEmitter } from "events";
|
|
13664
13664
|
import { randomUUID } from "crypto";
|
|
13665
|
+
import { performance } from "perf_hooks";
|
|
13665
13666
|
|
|
13666
13667
|
// src/rotating-log.ts
|
|
13667
13668
|
import { appendFileSync, existsSync, renameSync, statSync, unlinkSync } from "fs";
|
|
13668
13669
|
import { dirname } from "path";
|
|
13669
13670
|
var DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
|
|
13670
13671
|
var DEFAULT_KEEP = 3;
|
|
13671
|
-
|
|
13672
|
+
var REAL_FS_OPS = { statSync, renameSync, unlinkSync, appendFileSync, existsSync };
|
|
13673
|
+
function appendRotatingLog(path, content, options = {}, fsOps = REAL_FS_OPS) {
|
|
13672
13674
|
const maxBytes = options.maxBytes ?? positiveIntFromEnv("AGENTBRIDGE_LOG_MAX_BYTES", DEFAULT_MAX_BYTES);
|
|
13673
13675
|
const keep = options.keep ?? positiveIntFromEnv("AGENTBRIDGE_LOG_ROTATE_KEEP", DEFAULT_KEEP);
|
|
13674
|
-
if (!existsSync(dirname(path)))
|
|
13676
|
+
if (!fsOps.existsSync(dirname(path)))
|
|
13675
13677
|
return;
|
|
13676
|
-
rotateIfNeeded(path, Buffer.byteLength(content), maxBytes, keep);
|
|
13677
|
-
appendFileSync(path, content, "utf-8");
|
|
13678
|
+
rotateIfNeeded(path, Buffer.byteLength(content), maxBytes, keep, fsOps);
|
|
13679
|
+
fsOps.appendFileSync(path, content, "utf-8");
|
|
13678
13680
|
}
|
|
13679
13681
|
function positiveIntFromEnv(name, fallback) {
|
|
13680
13682
|
const value = process.env[name];
|
|
@@ -13683,26 +13685,48 @@ function positiveIntFromEnv(name, fallback) {
|
|
|
13683
13685
|
const parsed = Number(value);
|
|
13684
13686
|
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
13685
13687
|
}
|
|
13686
|
-
function
|
|
13688
|
+
function isEnoent(error2) {
|
|
13689
|
+
return !!error2 && error2.code === "ENOENT";
|
|
13690
|
+
}
|
|
13691
|
+
function renameIfPresent(from, to, fsOps) {
|
|
13692
|
+
try {
|
|
13693
|
+
fsOps.renameSync(from, to);
|
|
13694
|
+
} catch (error2) {
|
|
13695
|
+
if (!isEnoent(error2))
|
|
13696
|
+
throw error2;
|
|
13697
|
+
}
|
|
13698
|
+
}
|
|
13699
|
+
function unlinkIfPresent(path, fsOps) {
|
|
13700
|
+
try {
|
|
13701
|
+
fsOps.unlinkSync(path);
|
|
13702
|
+
} catch (error2) {
|
|
13703
|
+
if (!isEnoent(error2))
|
|
13704
|
+
throw error2;
|
|
13705
|
+
}
|
|
13706
|
+
}
|
|
13707
|
+
function rotateIfNeeded(path, incomingBytes, maxBytes, keep, fsOps) {
|
|
13687
13708
|
if (!Number.isFinite(maxBytes) || maxBytes <= 0 || keep <= 0)
|
|
13688
13709
|
return;
|
|
13689
|
-
|
|
13690
|
-
|
|
13691
|
-
|
|
13710
|
+
let size;
|
|
13711
|
+
try {
|
|
13712
|
+
size = fsOps.statSync(path).size;
|
|
13713
|
+
} catch (error2) {
|
|
13714
|
+
if (isEnoent(error2))
|
|
13715
|
+
return;
|
|
13716
|
+
throw error2;
|
|
13717
|
+
}
|
|
13692
13718
|
if (size + incomingBytes <= maxBytes)
|
|
13693
13719
|
return;
|
|
13694
13720
|
for (let index = keep;index >= 1; index--) {
|
|
13695
13721
|
const current = `${path}.${index}`;
|
|
13696
13722
|
const next = `${path}.${index + 1}`;
|
|
13697
|
-
if (!existsSync(current))
|
|
13698
|
-
continue;
|
|
13699
13723
|
if (index === keep) {
|
|
13700
|
-
|
|
13724
|
+
unlinkIfPresent(current, fsOps);
|
|
13701
13725
|
} else {
|
|
13702
|
-
|
|
13726
|
+
renameIfPresent(current, next, fsOps);
|
|
13703
13727
|
}
|
|
13704
13728
|
}
|
|
13705
|
-
|
|
13729
|
+
renameIfPresent(path, `${path}.1`, fsOps);
|
|
13706
13730
|
}
|
|
13707
13731
|
|
|
13708
13732
|
// src/process-log.ts
|
|
@@ -13782,6 +13806,10 @@ function formatError2(error2) {
|
|
|
13782
13806
|
import { mkdirSync, existsSync as existsSync2 } from "fs";
|
|
13783
13807
|
import { join } from "path";
|
|
13784
13808
|
import { homedir, platform } from "os";
|
|
13809
|
+
function resolveXdgStateBase(rawXdg = process.env.XDG_STATE_HOME) {
|
|
13810
|
+
const xdgState = rawXdg && rawXdg.length > 0 ? rawXdg : join(homedir(), ".local", "state");
|
|
13811
|
+
return join(xdgState, "agentbridge");
|
|
13812
|
+
}
|
|
13785
13813
|
|
|
13786
13814
|
class StateDirResolver {
|
|
13787
13815
|
stateDir;
|
|
@@ -13789,8 +13817,7 @@ class StateDirResolver {
|
|
|
13789
13817
|
if (platform() === "darwin") {
|
|
13790
13818
|
return join(homedir(), "Library", "Application Support", "AgentBridge");
|
|
13791
13819
|
}
|
|
13792
|
-
|
|
13793
|
-
return join(xdgState, "agentbridge");
|
|
13820
|
+
return resolveXdgStateBase(process.env.XDG_STATE_HOME);
|
|
13794
13821
|
}
|
|
13795
13822
|
constructor(envOverride) {
|
|
13796
13823
|
const override = envOverride ?? process.env.AGENTBRIDGE_STATE_DIR;
|
|
@@ -13816,8 +13843,8 @@ class StateDirResolver {
|
|
|
13816
13843
|
get statusFile() {
|
|
13817
13844
|
return join(this.stateDir, "status.json");
|
|
13818
13845
|
}
|
|
13819
|
-
get
|
|
13820
|
-
return join(this.stateDir, "
|
|
13846
|
+
get daemonRecordFile() {
|
|
13847
|
+
return join(this.stateDir, "daemon.json");
|
|
13821
13848
|
}
|
|
13822
13849
|
get currentThreadFile() {
|
|
13823
13850
|
return join(this.stateDir, "current-thread.json");
|
|
@@ -13859,6 +13886,9 @@ function formatAgent(name, usage, snapshotAt) {
|
|
|
13859
13886
|
if (usage.rateLimitedUntil > 0) {
|
|
13860
13887
|
parts.push(`\u9650\u6D41\u81F3 ${formatEpoch(usage.rateLimitedUntil)}`);
|
|
13861
13888
|
}
|
|
13889
|
+
if (usage.parsedVia === "positional") {
|
|
13890
|
+
parts.push("\u26A0\uFE0F \u7A97\u53E3\u8BC6\u522B\u4F7F\u7528\u4F4D\u7F6E\u515C\u5E95");
|
|
13891
|
+
}
|
|
13862
13892
|
const ageSec = usage.fetchedAt > 0 ? snapshotAt - usage.fetchedAt : 0;
|
|
13863
13893
|
if (ageSec > 300) {
|
|
13864
13894
|
parts.push(`\u26A0\uFE0F \u6570\u636E\u91C7\u96C6\u4E8E ${Math.round(ageSec / 60)} \u5206\u949F\u524D`);
|
|
@@ -13917,6 +13947,10 @@ function renderBudgetSnapshot(snapshot) {
|
|
|
13917
13947
|
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
13948
|
|
|
13919
13949
|
// src/claude-adapter.ts
|
|
13950
|
+
var DEFAULT_MAX_BUFFERED_MESSAGES = 100;
|
|
13951
|
+
var DEFAULT_MAX_BUFFERED_BYTES = 4 * 1024 * 1024;
|
|
13952
|
+
var DEFAULT_DEDUPE_CAPACITY = 2048;
|
|
13953
|
+
var DEFAULT_DEDUPE_TTL_MS = 20 * 60 * 1000;
|
|
13920
13954
|
var CLAUDE_INSTRUCTIONS = [
|
|
13921
13955
|
"Codex is an AI coding agent (OpenAI) running in a separate session on the same machine.",
|
|
13922
13956
|
"",
|
|
@@ -13947,7 +13981,7 @@ var CLAUDE_INSTRUCTIONS = [
|
|
|
13947
13981
|
"## Turn coordination",
|
|
13948
13982
|
"- When you see '\u23F3 Codex is working', do NOT call the reply tool \u2014 wait for '\u2705 Codex finished'.",
|
|
13949
13983
|
"- After Codex finishes a turn, you have an attention window to review and respond before new messages arrive.",
|
|
13950
|
-
'- If the reply tool returns a busy error, Codex is still executing. You decide: wait and retry later,
|
|
13984
|
+
'- If the reply tool returns a busy error, Codex is still executing. You decide: wait and retry later, resend with on_busy="steer" to feed the message INTO the running turn (good for mid-course corrections; it does not interrupt or restart the work), or resend with on_busy="interrupt" to STOP the running turn and start a new one with your message (use only when the current work is obsolete \u2014 prefer steer otherwise).',
|
|
13951
13985
|
"",
|
|
13952
13986
|
"## Budget awareness",
|
|
13953
13987
|
"- Use the get_budget tool to check both agents' subscription quota (5h/weekly windows, drift, pause state).",
|
|
@@ -13965,10 +13999,20 @@ class ClaudeAdapter extends EventEmitter {
|
|
|
13965
13999
|
logFile;
|
|
13966
14000
|
logger;
|
|
13967
14001
|
pendingMessages = [];
|
|
14002
|
+
pendingMessageByteSizes = [];
|
|
14003
|
+
pendingMessageBytes = 0;
|
|
13968
14004
|
maxBufferedMessages;
|
|
14005
|
+
maxBufferedBytes;
|
|
13969
14006
|
droppedMessageCount = 0;
|
|
14007
|
+
oversizedMessageCount = 0;
|
|
14008
|
+
oversizedMessageBytes = 0;
|
|
14009
|
+
oversizedMessageSourceCounts = {};
|
|
14010
|
+
dedupeCapacity;
|
|
14011
|
+
dedupeTtlMs;
|
|
14012
|
+
monotonicNow;
|
|
14013
|
+
deliveredMessageIds = new Map;
|
|
13970
14014
|
budgetSnapshot = null;
|
|
13971
|
-
constructor(logFile = new StateDirResolver().logFile) {
|
|
14015
|
+
constructor(logFile = new StateDirResolver().logFile, options = {}) {
|
|
13972
14016
|
super();
|
|
13973
14017
|
this.logFile = logFile;
|
|
13974
14018
|
this.logger = createProcessLogger({ component: "ClaudeAdapter", logFile: this.logFile });
|
|
@@ -13979,7 +14023,11 @@ class ClaudeAdapter extends EventEmitter {
|
|
|
13979
14023
|
if (process.env.AGENTBRIDGE_MODE) {
|
|
13980
14024
|
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
14025
|
}
|
|
13982
|
-
this.maxBufferedMessages =
|
|
14026
|
+
this.maxBufferedMessages = positiveIntegerOr(options.maxBufferedMessages, parsePositiveIntegerEnv("AGENTBRIDGE_MAX_BUFFERED_MESSAGES", DEFAULT_MAX_BUFFERED_MESSAGES));
|
|
14027
|
+
this.maxBufferedBytes = positiveIntegerOr(options.maxBufferedBytes, parsePositiveIntegerEnv("AGENTBRIDGE_MAX_BUFFERED_BYTES", DEFAULT_MAX_BUFFERED_BYTES));
|
|
14028
|
+
this.dedupeCapacity = positiveIntegerOr(options.dedupeCapacity, DEFAULT_DEDUPE_CAPACITY);
|
|
14029
|
+
this.dedupeTtlMs = positiveIntegerOr(options.dedupeTtlMs, DEFAULT_DEDUPE_TTL_MS);
|
|
14030
|
+
this.monotonicNow = options.now ?? (() => performance.now());
|
|
13983
14031
|
this.server = new Server({ name: "agentbridge", version: "0.1.0" }, {
|
|
13984
14032
|
capabilities: {
|
|
13985
14033
|
experimental: { "claude/channel": {} },
|
|
@@ -14006,10 +14054,12 @@ class ClaudeAdapter extends EventEmitter {
|
|
|
14006
14054
|
}
|
|
14007
14055
|
async pushNotification(message) {
|
|
14008
14056
|
this.log(`pushNotification (instance=${this.instanceId}, msgId=${message.id}, len=${message.content.length})`);
|
|
14057
|
+
if (!this.rememberDelivery(message))
|
|
14058
|
+
return;
|
|
14009
14059
|
await this.pushViaChannel(message);
|
|
14010
14060
|
}
|
|
14011
14061
|
async pushViaChannel(message) {
|
|
14012
|
-
const
|
|
14062
|
+
const deliveryAttemptId = `codex_msg_${this.notificationIdPrefix}_${++this.notificationSeq}`;
|
|
14013
14063
|
const ts = new Date(message.timestamp).toISOString();
|
|
14014
14064
|
try {
|
|
14015
14065
|
await this.server.notification({
|
|
@@ -14018,7 +14068,8 @@ class ClaudeAdapter extends EventEmitter {
|
|
|
14018
14068
|
content: message.content,
|
|
14019
14069
|
meta: {
|
|
14020
14070
|
chat_id: this.sessionId,
|
|
14021
|
-
message_id:
|
|
14071
|
+
message_id: message.id,
|
|
14072
|
+
delivery_attempt_id: deliveryAttemptId,
|
|
14022
14073
|
user: "Codex",
|
|
14023
14074
|
user_id: "codex",
|
|
14024
14075
|
ts,
|
|
@@ -14026,39 +14077,93 @@ class ClaudeAdapter extends EventEmitter {
|
|
|
14026
14077
|
}
|
|
14027
14078
|
}
|
|
14028
14079
|
});
|
|
14029
|
-
this.log(`Pushed notification: ${
|
|
14080
|
+
this.log(`Pushed notification: ${message.id} (attempt=${deliveryAttemptId})`);
|
|
14030
14081
|
} catch (e) {
|
|
14031
14082
|
this.log(`Push notification failed: ${e.message}`);
|
|
14032
14083
|
this.queueFallbackMessage(message);
|
|
14033
14084
|
}
|
|
14034
14085
|
}
|
|
14086
|
+
rememberDelivery(message) {
|
|
14087
|
+
const now = this.monotonicNow();
|
|
14088
|
+
this.pruneDeliveredMessageIds(now);
|
|
14089
|
+
if (this.deliveredMessageIds.has(message.id)) {
|
|
14090
|
+
this.deliveredMessageIds.delete(message.id);
|
|
14091
|
+
this.deliveredMessageIds.set(message.id, now);
|
|
14092
|
+
this.log(`Duplicate Codex message suppressed (msgId=${message.id}, source=${message.source}, ` + `instance=${this.instanceId})`);
|
|
14093
|
+
return false;
|
|
14094
|
+
}
|
|
14095
|
+
this.deliveredMessageIds.set(message.id, now);
|
|
14096
|
+
while (this.deliveredMessageIds.size > this.dedupeCapacity) {
|
|
14097
|
+
const oldest = this.deliveredMessageIds.keys().next().value;
|
|
14098
|
+
if (oldest === undefined)
|
|
14099
|
+
break;
|
|
14100
|
+
this.deliveredMessageIds.delete(oldest);
|
|
14101
|
+
}
|
|
14102
|
+
return true;
|
|
14103
|
+
}
|
|
14104
|
+
pruneDeliveredMessageIds(now) {
|
|
14105
|
+
for (const [id, seenAt] of this.deliveredMessageIds) {
|
|
14106
|
+
if (now - seenAt <= this.dedupeTtlMs)
|
|
14107
|
+
break;
|
|
14108
|
+
this.deliveredMessageIds.delete(id);
|
|
14109
|
+
}
|
|
14110
|
+
}
|
|
14035
14111
|
queueFallbackMessage(message) {
|
|
14036
|
-
|
|
14037
|
-
|
|
14112
|
+
const messageBytes = utf8ByteLength(message.content);
|
|
14113
|
+
if (messageBytes > this.maxBufferedBytes) {
|
|
14114
|
+
this.oversizedMessageCount++;
|
|
14115
|
+
this.oversizedMessageBytes += messageBytes;
|
|
14116
|
+
this.oversizedMessageSourceCounts[message.source] = (this.oversizedMessageSourceCounts[message.source] ?? 0) + 1;
|
|
14117
|
+
this.log(`Fallback queue omitted oversized ${message.source} message ` + `(${formatBytes(messageBytes)} > ${formatBytes(this.maxBufferedBytes)}; ` + `total oversized: ${this.oversizedMessageCount})`);
|
|
14118
|
+
return;
|
|
14119
|
+
}
|
|
14120
|
+
let dropped = 0;
|
|
14121
|
+
while (this.pendingMessages.length >= this.maxBufferedMessages || this.pendingMessageBytes + messageBytes > this.maxBufferedBytes) {
|
|
14122
|
+
const droppedMessage = this.pendingMessages.shift();
|
|
14123
|
+
const droppedBytes = this.pendingMessageByteSizes.shift() ?? 0;
|
|
14124
|
+
if (!droppedMessage)
|
|
14125
|
+
break;
|
|
14126
|
+
this.pendingMessageBytes = Math.max(0, this.pendingMessageBytes - droppedBytes);
|
|
14038
14127
|
this.droppedMessageCount++;
|
|
14039
|
-
|
|
14128
|
+
dropped++;
|
|
14129
|
+
}
|
|
14130
|
+
if (dropped > 0) {
|
|
14131
|
+
this.log(`Fallback queue overflow: dropped ${dropped} oldest message${dropped > 1 ? "s" : ""} ` + `(${this.pendingMessages.length} pending, ${formatBytes(this.pendingMessageBytes)} buffered, ` + `${this.droppedMessageCount} dropped since last drain)`);
|
|
14040
14132
|
}
|
|
14041
14133
|
this.pendingMessages.push(message);
|
|
14042
|
-
this.
|
|
14134
|
+
this.pendingMessageByteSizes.push(messageBytes);
|
|
14135
|
+
this.pendingMessageBytes += messageBytes;
|
|
14136
|
+
this.log(`Queued fallback message (${this.pendingMessages.length} pending, ` + `${formatBytes(this.pendingMessageBytes)} buffered, instance=${this.instanceId})`);
|
|
14043
14137
|
}
|
|
14044
14138
|
drainMessages() {
|
|
14045
|
-
this.log(`get_messages called (instance=${this.instanceId}, pending=${this.pendingMessages.length}, dropped=${this.droppedMessageCount})`);
|
|
14046
|
-
if (this.pendingMessages.length === 0 && this.droppedMessageCount === 0) {
|
|
14139
|
+
this.log(`get_messages called (instance=${this.instanceId}, pending=${this.pendingMessages.length}, ` + `bytes=${this.pendingMessageBytes}, dropped=${this.droppedMessageCount}, oversized=${this.oversizedMessageCount})`);
|
|
14140
|
+
if (this.pendingMessages.length === 0 && this.droppedMessageCount === 0 && this.oversizedMessageCount === 0) {
|
|
14047
14141
|
return {
|
|
14048
14142
|
content: [{ type: "text", text: "No new messages from Codex." }]
|
|
14049
14143
|
};
|
|
14050
14144
|
}
|
|
14051
14145
|
const messages = this.pendingMessages;
|
|
14052
14146
|
this.pendingMessages = [];
|
|
14147
|
+
this.pendingMessageByteSizes = [];
|
|
14148
|
+
this.pendingMessageBytes = 0;
|
|
14053
14149
|
const dropped = this.droppedMessageCount;
|
|
14054
14150
|
this.droppedMessageCount = 0;
|
|
14151
|
+
const oversizedSourceCounts = this.oversizedMessageSourceCounts;
|
|
14152
|
+
const oversized = this.oversizedMessageCount;
|
|
14153
|
+
const oversizedBytes = this.oversizedMessageBytes;
|
|
14154
|
+
this.oversizedMessageSourceCounts = {};
|
|
14155
|
+
this.oversizedMessageCount = 0;
|
|
14156
|
+
this.oversizedMessageBytes = 0;
|
|
14055
14157
|
const count = messages.length;
|
|
14056
|
-
|
|
14158
|
+
const notices = [];
|
|
14057
14159
|
if (dropped > 0) {
|
|
14058
|
-
|
|
14160
|
+
notices.push(`${dropped} older message${dropped > 1 ? "s" : ""} ` + `${dropped > 1 ? "were" : "was"} dropped due to fallback queue overflow`);
|
|
14161
|
+
}
|
|
14162
|
+
if (oversized > 0) {
|
|
14163
|
+
for (const [source, sourceCount] of Object.entries(oversizedSourceCounts)) {
|
|
14164
|
+
notices.push(`${sourceCount} oversized message${sourceCount === 1 ? "" : "s"} ` + `from ${formatSource(source)} omitted ` + `(>${formatBytes(this.maxBufferedBytes)})`);
|
|
14165
|
+
}
|
|
14059
14166
|
}
|
|
14060
|
-
header += `
|
|
14061
|
-
chat_id: ${this.sessionId}`;
|
|
14062
14167
|
const formatted = messages.map((msg, i) => {
|
|
14063
14168
|
const ts = new Date(msg.timestamp).toISOString();
|
|
14064
14169
|
return `---
|
|
@@ -14067,14 +14172,25 @@ Codex: ${msg.content}`;
|
|
|
14067
14172
|
}).join(`
|
|
14068
14173
|
|
|
14069
14174
|
`);
|
|
14070
|
-
|
|
14175
|
+
const noticeText = notices.map((notice) => `WARNING: ${notice}`).join(`
|
|
14176
|
+
`);
|
|
14177
|
+
const parts = [];
|
|
14178
|
+
if (count > 0) {
|
|
14179
|
+
parts.push(`[${count} new message${count > 1 ? "s" : ""} from Codex]
|
|
14180
|
+
chat_id: ${this.sessionId}`);
|
|
14181
|
+
}
|
|
14182
|
+
if (noticeText)
|
|
14183
|
+
parts.push(noticeText);
|
|
14184
|
+
if (formatted)
|
|
14185
|
+
parts.push(formatted);
|
|
14186
|
+
this.log(`get_messages returning ${count} message(s) ` + `(instance=${this.instanceId}, dropped=${dropped}, oversized=${oversized}, oversizedBytes=${oversizedBytes})`);
|
|
14071
14187
|
return {
|
|
14072
14188
|
content: [
|
|
14073
14189
|
{
|
|
14074
14190
|
type: "text",
|
|
14075
|
-
text:
|
|
14191
|
+
text: parts.join(`
|
|
14076
14192
|
|
|
14077
|
-
|
|
14193
|
+
`)
|
|
14078
14194
|
}
|
|
14079
14195
|
]
|
|
14080
14196
|
};
|
|
@@ -14098,12 +14214,16 @@ ${formatted}`
|
|
|
14098
14214
|
},
|
|
14099
14215
|
require_reply: {
|
|
14100
14216
|
type: "boolean",
|
|
14101
|
-
description:
|
|
14217
|
+
description: 'When true, Codex is required to send a reply. All Codex messages from this turn will be forwarded immediately (bypassing STATUS buffering). Use this when you need a direct answer from Codex. Combinable with on_busy="steer": the reply expectation arms once the steer is accepted into the running turn.'
|
|
14102
14218
|
},
|
|
14103
14219
|
on_busy: {
|
|
14104
14220
|
type: "string",
|
|
14105
|
-
enum: ["reject", "steer"],
|
|
14106
|
-
description:
|
|
14221
|
+
enum: ["reject", "steer", "interrupt"],
|
|
14222
|
+
description: 'What to do when Codex is mid-turn. "reject" (default): fail with a busy error \u2014 wait and retry. "steer": feed this message INTO the running turn \u2014 Codex sees it immediately and integrates it without losing work; use it for mid-course corrections, added constraints, or updated acceptance criteria (it does NOT start a new turn). "interrupt": STOP the running turn, wait for it to terminate, then send this message as a NEW turn \u2014 use only when the current work is obsolete; prefer steer otherwise.'
|
|
14223
|
+
},
|
|
14224
|
+
idempotency_key: {
|
|
14225
|
+
type: "string",
|
|
14226
|
+
description: "Optional client-generated key (non-empty, max 128 chars) that makes this reply idempotent: a retry carrying the same key is NOT re-injected \u2014 the bridge answers duplicate_in_flight / duplicate_terminal instead. Use a fresh key per logical message."
|
|
14107
14227
|
}
|
|
14108
14228
|
},
|
|
14109
14229
|
required: ["text"]
|
|
@@ -14163,19 +14283,29 @@ ${formatted}`
|
|
|
14163
14283
|
}
|
|
14164
14284
|
const requireReply = args?.require_reply === true;
|
|
14165
14285
|
const onBusyRaw = args?.on_busy;
|
|
14166
|
-
|
|
14167
|
-
if (onBusyRaw !== undefined && onBusyRaw !== "reject" && onBusyRaw !== "steer") {
|
|
14286
|
+
if (onBusyRaw !== undefined && onBusyRaw !== "reject" && onBusyRaw !== "steer" && onBusyRaw !== "interrupt") {
|
|
14168
14287
|
return {
|
|
14169
|
-
content: [{ type: "text", text: `Error: invalid on_busy value ${JSON.stringify(onBusyRaw)} \u2014 use "reject" or "
|
|
14288
|
+
content: [{ type: "text", text: `Error: invalid on_busy value ${JSON.stringify(onBusyRaw)} \u2014 use "reject", "steer" or "interrupt".` }],
|
|
14170
14289
|
isError: true
|
|
14171
14290
|
};
|
|
14172
14291
|
}
|
|
14173
|
-
|
|
14174
|
-
|
|
14175
|
-
|
|
14176
|
-
|
|
14177
|
-
|
|
14292
|
+
const onBusy = onBusyRaw === "steer" || onBusyRaw === "interrupt" ? onBusyRaw : "reject";
|
|
14293
|
+
const idempotencyKeyRaw = args?.idempotency_key;
|
|
14294
|
+
if (idempotencyKeyRaw !== undefined) {
|
|
14295
|
+
if (typeof idempotencyKeyRaw !== "string" || idempotencyKeyRaw.length === 0) {
|
|
14296
|
+
return {
|
|
14297
|
+
content: [{ type: "text", text: "Error: idempotency_key must be a non-empty string." }],
|
|
14298
|
+
isError: true
|
|
14299
|
+
};
|
|
14300
|
+
}
|
|
14301
|
+
if (idempotencyKeyRaw.length > 128) {
|
|
14302
|
+
return {
|
|
14303
|
+
content: [{ type: "text", text: `Error: idempotency_key is too long (${idempotencyKeyRaw.length} chars, max 128).` }],
|
|
14304
|
+
isError: true
|
|
14305
|
+
};
|
|
14306
|
+
}
|
|
14178
14307
|
}
|
|
14308
|
+
const idempotencyKey = idempotencyKeyRaw;
|
|
14179
14309
|
const bridgeMsg = {
|
|
14180
14310
|
id: args?.chat_id ?? `reply_${Date.now()}`,
|
|
14181
14311
|
source: "claude",
|
|
@@ -14189,16 +14319,22 @@ ${formatted}`
|
|
|
14189
14319
|
isError: true
|
|
14190
14320
|
};
|
|
14191
14321
|
}
|
|
14192
|
-
const result = await this.replySender(bridgeMsg, requireReply, onBusy);
|
|
14322
|
+
const result = await this.replySender(bridgeMsg, requireReply, onBusy, idempotencyKey);
|
|
14193
14323
|
if (!result.success) {
|
|
14194
|
-
this.log(`Reply delivery failed: ${result.error}`);
|
|
14324
|
+
this.log(`Reply delivery failed: ${result.error}${result.code ? ` (code=${result.code})` : ""}`);
|
|
14325
|
+
const codePrefix = result.code ? ` [${result.code}]` : "";
|
|
14195
14326
|
return {
|
|
14196
|
-
content: [{ type: "text", text: `Error: ${result.error}` }],
|
|
14327
|
+
content: [{ type: "text", text: `Error${codePrefix}: ${result.error}` }],
|
|
14197
14328
|
isError: true
|
|
14198
14329
|
};
|
|
14199
14330
|
}
|
|
14200
14331
|
const pending = this.pendingMessages.length;
|
|
14201
|
-
let responseText =
|
|
14332
|
+
let responseText = "Reply sent to Codex.";
|
|
14333
|
+
if (onBusy === "steer") {
|
|
14334
|
+
responseText = "Reply sent to Codex (will be steered into the running turn if one is active; watch for a system_steer_failed notice if the app-server rejects it).";
|
|
14335
|
+
} else if (onBusy === "interrupt") {
|
|
14336
|
+
responseText = "Reply sent to Codex as a new turn (any turn still running was interrupted first; if it had already finished, your message was simply injected).";
|
|
14337
|
+
}
|
|
14202
14338
|
if (pending > 0) {
|
|
14203
14339
|
responseText += ` Note: ${pending} unread Codex message${pending > 1 ? "s" : ""} already waiting \u2014 call get_messages to read them.`;
|
|
14204
14340
|
}
|
|
@@ -14210,11 +14346,37 @@ ${formatted}`
|
|
|
14210
14346
|
this.logger.log(msg);
|
|
14211
14347
|
}
|
|
14212
14348
|
}
|
|
14349
|
+
function parsePositiveIntegerEnv(name, fallback) {
|
|
14350
|
+
return positiveIntegerOr(parseInt(process.env[name] ?? "", 10), fallback);
|
|
14351
|
+
}
|
|
14352
|
+
function positiveIntegerOr(value, fallback) {
|
|
14353
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
14354
|
+
}
|
|
14355
|
+
function utf8ByteLength(value) {
|
|
14356
|
+
return Buffer.byteLength(value, "utf8");
|
|
14357
|
+
}
|
|
14358
|
+
function formatSource(source) {
|
|
14359
|
+
return source === "codex" ? "Codex" : "Claude";
|
|
14360
|
+
}
|
|
14361
|
+
function formatBytes(bytes) {
|
|
14362
|
+
if (bytes < 1024)
|
|
14363
|
+
return `${bytes}B`;
|
|
14364
|
+
if (bytes % (1024 * 1024) === 0)
|
|
14365
|
+
return `${bytes / (1024 * 1024)}MiB`;
|
|
14366
|
+
if (bytes % 1024 === 0)
|
|
14367
|
+
return `${bytes / 1024}KiB`;
|
|
14368
|
+
return `${bytes}B`;
|
|
14369
|
+
}
|
|
14213
14370
|
|
|
14214
14371
|
// src/contract-version.ts
|
|
14215
14372
|
var CONTRACT_VERSION = 1;
|
|
14216
14373
|
|
|
14217
14374
|
// src/build-info.ts
|
|
14375
|
+
var CODE_HASH_SENTINEL = "source";
|
|
14376
|
+
function hasValidCodeHash(build) {
|
|
14377
|
+
const hash = build?.codeHash;
|
|
14378
|
+
return typeof hash === "string" && hash.length > 0 && hash !== CODE_HASH_SENTINEL;
|
|
14379
|
+
}
|
|
14218
14380
|
function defineString(value, fallback) {
|
|
14219
14381
|
return typeof value === "string" && value.length > 0 ? value : fallback;
|
|
14220
14382
|
}
|
|
@@ -14227,15 +14389,23 @@ function defineNumber(value, fallback) {
|
|
|
14227
14389
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
14228
14390
|
}
|
|
14229
14391
|
var BUILD_INFO = Object.freeze({
|
|
14230
|
-
version: defineString("0.1.
|
|
14231
|
-
commit: defineString("
|
|
14392
|
+
version: defineString("0.1.13", "0.0.0-source"),
|
|
14393
|
+
commit: defineString("7a71869", "source"),
|
|
14232
14394
|
bundle: defineBundle("plugin"),
|
|
14233
|
-
contractVersion: defineNumber(1, CONTRACT_VERSION)
|
|
14395
|
+
contractVersion: defineNumber(1, CONTRACT_VERSION),
|
|
14396
|
+
codeHash: defineString("e1fd67d07c62", "source")
|
|
14234
14397
|
});
|
|
14235
14398
|
function sameRuntimeContract(a, b) {
|
|
14236
14399
|
if (!a || !b)
|
|
14237
14400
|
return false;
|
|
14238
|
-
|
|
14401
|
+
if (a.version !== b.version || a.contractVersion !== b.contractVersion)
|
|
14402
|
+
return false;
|
|
14403
|
+
if (hasValidCodeHash(a) && hasValidCodeHash(b))
|
|
14404
|
+
return a.codeHash === b.codeHash;
|
|
14405
|
+
return a.commit === b.commit;
|
|
14406
|
+
}
|
|
14407
|
+
function runtimeContractComparisonBasis(a, b) {
|
|
14408
|
+
return hasValidCodeHash(a) && hasValidCodeHash(b) ? "codeHash" : "commit";
|
|
14239
14409
|
}
|
|
14240
14410
|
function compatibleContractVersion(a, b) {
|
|
14241
14411
|
if (!a || !b)
|
|
@@ -14245,7 +14415,8 @@ function compatibleContractVersion(a, b) {
|
|
|
14245
14415
|
function formatBuildInfo(build) {
|
|
14246
14416
|
if (!build)
|
|
14247
14417
|
return "<unknown>";
|
|
14248
|
-
|
|
14418
|
+
const codeHash = hasValidCodeHash(build) ? `/code-${build.codeHash}` : "";
|
|
14419
|
+
return `${build.version}/${build.commit}/${build.bundle}/contract-v${build.contractVersion}${codeHash}`;
|
|
14249
14420
|
}
|
|
14250
14421
|
|
|
14251
14422
|
// src/daemon-client.ts
|
|
@@ -14256,6 +14427,83 @@ var CLOSE_CODE_REPLACED = 4001;
|
|
|
14256
14427
|
var CLOSE_CODE_EVICTED_STALE = 4002;
|
|
14257
14428
|
var CLOSE_CODE_PROBE_IN_PROGRESS = 4003;
|
|
14258
14429
|
var CLOSE_CODE_PAIR_MISMATCH = 4004;
|
|
14430
|
+
var CLOSE_CODE_TOKEN_MISMATCH = 4005;
|
|
14431
|
+
var CLOSE_CODE_CONTRACT_MISMATCH = 4006;
|
|
14432
|
+
|
|
14433
|
+
// src/interrupt-timing.ts
|
|
14434
|
+
var CLIENT_REPLY_TIMEOUT_MS = 15000;
|
|
14435
|
+
var INTERRUPT_CLIENT_MARGIN_MS = 2000;
|
|
14436
|
+
var MAX_INTERRUPT_TIMEOUT_MS = CLIENT_REPLY_TIMEOUT_MS - INTERRUPT_CLIENT_MARGIN_MS;
|
|
14437
|
+
|
|
14438
|
+
// src/pending-request-registry.ts
|
|
14439
|
+
class PendingRequestRegistry {
|
|
14440
|
+
entries = new Map;
|
|
14441
|
+
setTimer;
|
|
14442
|
+
clearTimer;
|
|
14443
|
+
constructor(deps = {}) {
|
|
14444
|
+
this.setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
|
|
14445
|
+
this.clearTimer = deps.clearTimer ?? ((handle) => clearTimeout(handle));
|
|
14446
|
+
}
|
|
14447
|
+
get size() {
|
|
14448
|
+
return this.entries.size;
|
|
14449
|
+
}
|
|
14450
|
+
has(id) {
|
|
14451
|
+
return this.entries.has(id);
|
|
14452
|
+
}
|
|
14453
|
+
register(id, options) {
|
|
14454
|
+
const existing = this.entries.get(id);
|
|
14455
|
+
if (existing) {
|
|
14456
|
+
this.clearTimer(existing.timer);
|
|
14457
|
+
this.entries.delete(id);
|
|
14458
|
+
}
|
|
14459
|
+
return new Promise((resolve, reject) => {
|
|
14460
|
+
const timer = this.setTimer(() => {
|
|
14461
|
+
if (!this.entries.has(id))
|
|
14462
|
+
return;
|
|
14463
|
+
this.entries.delete(id);
|
|
14464
|
+
options.onTimeout({ resolve, reject });
|
|
14465
|
+
}, options.timeoutMs);
|
|
14466
|
+
if (options.unref) {
|
|
14467
|
+
timer.unref?.();
|
|
14468
|
+
}
|
|
14469
|
+
this.entries.set(id, { resolve, reject, timer });
|
|
14470
|
+
});
|
|
14471
|
+
}
|
|
14472
|
+
settle(id, value) {
|
|
14473
|
+
const entry = this.entries.get(id);
|
|
14474
|
+
if (!entry)
|
|
14475
|
+
return false;
|
|
14476
|
+
this.clearTimer(entry.timer);
|
|
14477
|
+
this.entries.delete(id);
|
|
14478
|
+
entry.resolve(value);
|
|
14479
|
+
return true;
|
|
14480
|
+
}
|
|
14481
|
+
reject(id, error2) {
|
|
14482
|
+
const entry = this.entries.get(id);
|
|
14483
|
+
if (!entry)
|
|
14484
|
+
return false;
|
|
14485
|
+
this.clearTimer(entry.timer);
|
|
14486
|
+
this.entries.delete(id);
|
|
14487
|
+
entry.reject(error2);
|
|
14488
|
+
return true;
|
|
14489
|
+
}
|
|
14490
|
+
settleAll(value) {
|
|
14491
|
+
const make = typeof value === "function" ? value : () => value;
|
|
14492
|
+
for (const [id, entry] of this.entries) {
|
|
14493
|
+
this.clearTimer(entry.timer);
|
|
14494
|
+
this.entries.delete(id);
|
|
14495
|
+
entry.resolve(make(id));
|
|
14496
|
+
}
|
|
14497
|
+
}
|
|
14498
|
+
rejectAll(error2) {
|
|
14499
|
+
const make = typeof error2 === "function" ? error2 : () => error2;
|
|
14500
|
+
for (const [id, entry] of this.entries) {
|
|
14501
|
+
this.clearTimer(entry.timer);
|
|
14502
|
+
this.entries.delete(id);
|
|
14503
|
+
entry.reject(make(id));
|
|
14504
|
+
}
|
|
14505
|
+
}
|
|
14506
|
+
}
|
|
14259
14507
|
|
|
14260
14508
|
// src/daemon-client.ts
|
|
14261
14509
|
var nextSocketId = 0;
|
|
@@ -14266,7 +14514,8 @@ class DaemonClient extends EventEmitter2 {
|
|
|
14266
14514
|
ws = null;
|
|
14267
14515
|
wsId = 0;
|
|
14268
14516
|
nextRequestId = 1;
|
|
14269
|
-
pendingReplies = new
|
|
14517
|
+
pendingReplies = new PendingRequestRegistry;
|
|
14518
|
+
pendingEventWaiters = new PendingRequestRegistry;
|
|
14270
14519
|
constructor(url, options = {}) {
|
|
14271
14520
|
super();
|
|
14272
14521
|
this.url = url;
|
|
@@ -14312,81 +14561,72 @@ class DaemonClient extends EventEmitter2 {
|
|
|
14312
14561
|
});
|
|
14313
14562
|
}
|
|
14314
14563
|
attachClaude() {
|
|
14564
|
+
const identity = this.resolveIdentity();
|
|
14315
14565
|
this.send({
|
|
14316
14566
|
type: "claude_connect",
|
|
14317
|
-
...
|
|
14567
|
+
...identity ? { identity } : {}
|
|
14318
14568
|
});
|
|
14319
14569
|
}
|
|
14570
|
+
resolveIdentity() {
|
|
14571
|
+
const opt = this.options.identity;
|
|
14572
|
+
return typeof opt === "function" ? opt() : opt;
|
|
14573
|
+
}
|
|
14320
14574
|
async attachClaudeAndWaitForStatus(timeoutMs = 1000) {
|
|
14321
14575
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
14322
14576
|
return null;
|
|
14323
14577
|
}
|
|
14324
|
-
return
|
|
14325
|
-
|
|
14326
|
-
|
|
14327
|
-
|
|
14328
|
-
|
|
14329
|
-
|
|
14330
|
-
|
|
14331
|
-
if (timer) {
|
|
14332
|
-
clearTimeout(timer);
|
|
14333
|
-
timer = null;
|
|
14334
|
-
}
|
|
14335
|
-
this.off("status", onStatus);
|
|
14336
|
-
this.off("rejected", onRejected);
|
|
14337
|
-
this.off("disconnect", onDisconnect);
|
|
14338
|
-
};
|
|
14339
|
-
const finish = (value) => {
|
|
14340
|
-
cleanup();
|
|
14341
|
-
resolve(value);
|
|
14342
|
-
};
|
|
14343
|
-
const onStatus = (status) => finish(status);
|
|
14344
|
-
const onRejected = () => finish(null);
|
|
14345
|
-
const onDisconnect = () => finish(null);
|
|
14346
|
-
this.on("status", onStatus);
|
|
14347
|
-
this.on("rejected", onRejected);
|
|
14348
|
-
this.on("disconnect", onDisconnect);
|
|
14349
|
-
timer = setTimeout(() => {
|
|
14350
|
-
finish(null);
|
|
14351
|
-
}, timeoutMs);
|
|
14352
|
-
try {
|
|
14353
|
-
this.attachClaude();
|
|
14354
|
-
} catch {
|
|
14355
|
-
finish(null);
|
|
14356
|
-
}
|
|
14578
|
+
return this.awaitTypedResponse({
|
|
14579
|
+
key: "status",
|
|
14580
|
+
successEvent: "status",
|
|
14581
|
+
successValue: (status) => status,
|
|
14582
|
+
failValue: null,
|
|
14583
|
+
timeoutMs,
|
|
14584
|
+
send: () => this.attachClaude()
|
|
14357
14585
|
});
|
|
14358
14586
|
}
|
|
14359
14587
|
async probeIncumbent(timeoutMs = 3000) {
|
|
14360
14588
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
14361
14589
|
return { connected: false, alive: false };
|
|
14362
14590
|
}
|
|
14363
|
-
return
|
|
14364
|
-
|
|
14365
|
-
|
|
14366
|
-
|
|
14367
|
-
|
|
14368
|
-
|
|
14369
|
-
|
|
14370
|
-
|
|
14371
|
-
|
|
14372
|
-
|
|
14373
|
-
|
|
14374
|
-
|
|
14375
|
-
|
|
14376
|
-
|
|
14377
|
-
|
|
14378
|
-
|
|
14379
|
-
|
|
14380
|
-
|
|
14381
|
-
this.
|
|
14382
|
-
|
|
14383
|
-
|
|
14384
|
-
|
|
14385
|
-
|
|
14386
|
-
} catch {
|
|
14387
|
-
finish({ connected: false, alive: false });
|
|
14388
|
-
}
|
|
14591
|
+
return this.awaitTypedResponse({
|
|
14592
|
+
key: "incumbent_status",
|
|
14593
|
+
successEvent: "incumbentStatus",
|
|
14594
|
+
successValue: (s) => s,
|
|
14595
|
+
failValue: { connected: false, alive: false },
|
|
14596
|
+
timeoutMs,
|
|
14597
|
+
send: () => this.send({ type: "probe_incumbent" })
|
|
14598
|
+
});
|
|
14599
|
+
}
|
|
14600
|
+
awaitTypedResponse(opts) {
|
|
14601
|
+
const { key, successEvent, successValue, failValue, timeoutMs, send } = opts;
|
|
14602
|
+
const onSuccess = (payload) => {
|
|
14603
|
+
this.pendingEventWaiters.settle(key, successValue(payload));
|
|
14604
|
+
};
|
|
14605
|
+
const onRejected = () => {
|
|
14606
|
+
this.pendingEventWaiters.settle(key, failValue);
|
|
14607
|
+
};
|
|
14608
|
+
const onDisconnect = () => {
|
|
14609
|
+
this.pendingEventWaiters.settle(key, failValue);
|
|
14610
|
+
};
|
|
14611
|
+
const pending = this.pendingEventWaiters.register(key, {
|
|
14612
|
+
timeoutMs,
|
|
14613
|
+
onTimeout: ({ resolve }) => resolve(failValue)
|
|
14389
14614
|
});
|
|
14615
|
+
const cleanup = () => {
|
|
14616
|
+
this.off(successEvent, onSuccess);
|
|
14617
|
+
this.off("rejected", onRejected);
|
|
14618
|
+
this.off("disconnect", onDisconnect);
|
|
14619
|
+
};
|
|
14620
|
+
pending.finally(cleanup);
|
|
14621
|
+
this.on(successEvent, onSuccess);
|
|
14622
|
+
this.on("rejected", onRejected);
|
|
14623
|
+
this.on("disconnect", onDisconnect);
|
|
14624
|
+
try {
|
|
14625
|
+
send();
|
|
14626
|
+
} catch {
|
|
14627
|
+
this.pendingEventWaiters.settle(key, failValue);
|
|
14628
|
+
}
|
|
14629
|
+
return pending;
|
|
14390
14630
|
}
|
|
14391
14631
|
async disconnect() {
|
|
14392
14632
|
if (!this.ws)
|
|
@@ -14400,25 +14640,24 @@ class DaemonClient extends EventEmitter2 {
|
|
|
14400
14640
|
this.ws = null;
|
|
14401
14641
|
this.rejectPendingReplies("Daemon connection closed");
|
|
14402
14642
|
}
|
|
14403
|
-
async sendReply(message, requireReply, onBusy) {
|
|
14643
|
+
async sendReply(message, requireReply, onBusy, idempotencyKey) {
|
|
14404
14644
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
14405
14645
|
return { success: false, error: "AgentBridge daemon is not connected." };
|
|
14406
14646
|
}
|
|
14407
14647
|
const requestId = `reply_${Date.now()}_${this.nextRequestId++}`;
|
|
14408
|
-
|
|
14409
|
-
|
|
14410
|
-
|
|
14411
|
-
resolve({ success: false, error: "Timed out waiting for AgentBridge daemon reply." });
|
|
14412
|
-
}, 15000);
|
|
14413
|
-
this.pendingReplies.set(requestId, { resolve, timer });
|
|
14414
|
-
this.send({
|
|
14415
|
-
type: "claude_to_codex",
|
|
14416
|
-
requestId,
|
|
14417
|
-
message,
|
|
14418
|
-
...requireReply ? { requireReply: true } : {},
|
|
14419
|
-
...onBusy && onBusy !== "reject" ? { onBusy } : {}
|
|
14420
|
-
});
|
|
14648
|
+
const pending = this.pendingReplies.register(requestId, {
|
|
14649
|
+
timeoutMs: CLIENT_REPLY_TIMEOUT_MS,
|
|
14650
|
+
onTimeout: ({ resolve }) => resolve({ success: false, error: "Timed out waiting for AgentBridge daemon reply." })
|
|
14421
14651
|
});
|
|
14652
|
+
this.send({
|
|
14653
|
+
type: "claude_to_codex",
|
|
14654
|
+
requestId,
|
|
14655
|
+
message,
|
|
14656
|
+
...requireReply ? { requireReply: true } : {},
|
|
14657
|
+
...onBusy && onBusy !== "reject" ? { onBusy } : {},
|
|
14658
|
+
...idempotencyKey ? { idempotencyKey } : {}
|
|
14659
|
+
});
|
|
14660
|
+
return pending;
|
|
14422
14661
|
}
|
|
14423
14662
|
attachSocketHandlers(ws, socketId) {
|
|
14424
14663
|
ws.onmessage = (event) => {
|
|
@@ -14434,14 +14673,23 @@ class DaemonClient extends EventEmitter2 {
|
|
|
14434
14673
|
this.emit("codexMessage", message.message);
|
|
14435
14674
|
return;
|
|
14436
14675
|
case "claude_to_codex_result": {
|
|
14437
|
-
|
|
14438
|
-
|
|
14439
|
-
|
|
14440
|
-
|
|
14441
|
-
|
|
14442
|
-
|
|
14676
|
+
this.pendingReplies.settle(message.requestId, {
|
|
14677
|
+
success: message.success,
|
|
14678
|
+
error: message.error,
|
|
14679
|
+
...message.code !== undefined ? { code: message.code } : {},
|
|
14680
|
+
...message.phase !== undefined ? { phase: message.phase } : {},
|
|
14681
|
+
...message.retryAfterMs !== undefined ? { retryAfterMs: message.retryAfterMs } : {}
|
|
14682
|
+
});
|
|
14443
14683
|
return;
|
|
14444
14684
|
}
|
|
14685
|
+
case "turn_started":
|
|
14686
|
+
this.emit("turnStarted", {
|
|
14687
|
+
requestId: message.requestId,
|
|
14688
|
+
...message.idempotencyKey !== undefined ? { idempotencyKey: message.idempotencyKey } : {},
|
|
14689
|
+
threadId: message.threadId,
|
|
14690
|
+
turnId: message.turnId
|
|
14691
|
+
});
|
|
14692
|
+
return;
|
|
14445
14693
|
case "status":
|
|
14446
14694
|
this.emit("status", message.status);
|
|
14447
14695
|
return;
|
|
@@ -14456,7 +14704,7 @@ class DaemonClient extends EventEmitter2 {
|
|
|
14456
14704
|
if (isCurrent) {
|
|
14457
14705
|
this.ws = null;
|
|
14458
14706
|
this.rejectPendingReplies("AgentBridge daemon disconnected.");
|
|
14459
|
-
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) {
|
|
14707
|
+
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 || event.code === CLOSE_CODE_TOKEN_MISMATCH || event.code === CLOSE_CODE_CONTRACT_MISMATCH) {
|
|
14460
14708
|
this.emit("rejected", event.code);
|
|
14461
14709
|
} else {
|
|
14462
14710
|
this.emit("disconnect");
|
|
@@ -14466,11 +14714,7 @@ class DaemonClient extends EventEmitter2 {
|
|
|
14466
14714
|
ws.onerror = () => {};
|
|
14467
14715
|
}
|
|
14468
14716
|
rejectPendingReplies(error2) {
|
|
14469
|
-
|
|
14470
|
-
clearTimeout(pending.timer);
|
|
14471
|
-
pending.resolve({ success: false, error: error2 });
|
|
14472
|
-
this.pendingReplies.delete(requestId);
|
|
14473
|
-
}
|
|
14717
|
+
this.pendingReplies.settleAll(() => ({ success: false, error: error2 }));
|
|
14474
14718
|
}
|
|
14475
14719
|
send(message) {
|
|
14476
14720
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
@@ -14486,9 +14730,44 @@ class DaemonClient extends EventEmitter2 {
|
|
|
14486
14730
|
|
|
14487
14731
|
// src/daemon-lifecycle.ts
|
|
14488
14732
|
import { spawn } from "child_process";
|
|
14489
|
-
import { existsSync as existsSync3, readFileSync, statSync as statSync2, unlinkSync as
|
|
14733
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, statSync as statSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync2, openSync as openSync2, closeSync as closeSync2, constants } from "fs";
|
|
14490
14734
|
import { fileURLToPath } from "url";
|
|
14491
14735
|
|
|
14736
|
+
// src/atomic-json.ts
|
|
14737
|
+
import * as fs from "fs";
|
|
14738
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
14739
|
+
import { dirname as dirname2 } from "path";
|
|
14740
|
+
function tmpPathFor(targetPath) {
|
|
14741
|
+
return `${targetPath}.tmp.${process.pid}.${randomUUID2()}`;
|
|
14742
|
+
}
|
|
14743
|
+
function atomicWriteText(path, content, options = {}) {
|
|
14744
|
+
fs.mkdirSync(dirname2(path), { recursive: true });
|
|
14745
|
+
const tmp = tmpPathFor(path);
|
|
14746
|
+
let renamed = false;
|
|
14747
|
+
const fd = fs.openSync(tmp, "w", options.mode ?? 438);
|
|
14748
|
+
try {
|
|
14749
|
+
try {
|
|
14750
|
+
fs.writeFileSync(fd, content, "utf-8");
|
|
14751
|
+
if (options.fsync)
|
|
14752
|
+
fs.fsyncSync(fd);
|
|
14753
|
+
} finally {
|
|
14754
|
+
fs.closeSync(fd);
|
|
14755
|
+
}
|
|
14756
|
+
fs.renameSync(tmp, path);
|
|
14757
|
+
renamed = true;
|
|
14758
|
+
} finally {
|
|
14759
|
+
if (!renamed) {
|
|
14760
|
+
try {
|
|
14761
|
+
fs.unlinkSync(tmp);
|
|
14762
|
+
} catch {}
|
|
14763
|
+
}
|
|
14764
|
+
}
|
|
14765
|
+
}
|
|
14766
|
+
function atomicWriteJson(path, value, options = {}) {
|
|
14767
|
+
atomicWriteText(path, JSON.stringify(value, null, 2) + `
|
|
14768
|
+
`, options);
|
|
14769
|
+
}
|
|
14770
|
+
|
|
14492
14771
|
// src/env-utils.ts
|
|
14493
14772
|
function parsePositiveIntEnv(name, fallback, log = () => {}, env = process.env) {
|
|
14494
14773
|
const raw = env[name];
|
|
@@ -14537,23 +14816,208 @@ function isAgentBridgeProcess(pid, lookup = commandForPid) {
|
|
|
14537
14816
|
return cmd.includes("agentbridge") || cmd.includes("agent_bridge");
|
|
14538
14817
|
}
|
|
14539
14818
|
|
|
14819
|
+
// src/daemon-record.ts
|
|
14820
|
+
import { readFileSync } from "fs";
|
|
14821
|
+
var defaultRead = (path) => readFileSync(path, "utf-8");
|
|
14822
|
+
function writeDaemonRecord(path, record3) {
|
|
14823
|
+
atomicWriteJson(path, record3);
|
|
14824
|
+
}
|
|
14825
|
+
function sanitizePorts(value) {
|
|
14826
|
+
if (typeof value !== "object" || value === null)
|
|
14827
|
+
return;
|
|
14828
|
+
const raw = value;
|
|
14829
|
+
const ports = {};
|
|
14830
|
+
if (typeof raw.appPort === "number")
|
|
14831
|
+
ports.appPort = raw.appPort;
|
|
14832
|
+
if (typeof raw.proxyPort === "number")
|
|
14833
|
+
ports.proxyPort = raw.proxyPort;
|
|
14834
|
+
if (typeof raw.controlPort === "number")
|
|
14835
|
+
ports.controlPort = raw.controlPort;
|
|
14836
|
+
return Object.keys(ports).length > 0 ? ports : undefined;
|
|
14837
|
+
}
|
|
14838
|
+
function readDaemonRecord(path, read = defaultRead) {
|
|
14839
|
+
let parsed;
|
|
14840
|
+
try {
|
|
14841
|
+
parsed = JSON.parse(read(path));
|
|
14842
|
+
} catch {
|
|
14843
|
+
return null;
|
|
14844
|
+
}
|
|
14845
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
14846
|
+
return null;
|
|
14847
|
+
const obj = parsed;
|
|
14848
|
+
if (typeof obj.pid !== "number" || !Number.isFinite(obj.pid))
|
|
14849
|
+
return null;
|
|
14850
|
+
const phase = obj.phase === "ready" ? "ready" : "booting";
|
|
14851
|
+
const record3 = { pid: obj.pid, phase };
|
|
14852
|
+
if (typeof obj.startedAt === "number")
|
|
14853
|
+
record3.startedAt = obj.startedAt;
|
|
14854
|
+
if (typeof obj.nonce === "string")
|
|
14855
|
+
record3.nonce = obj.nonce;
|
|
14856
|
+
if (obj.pairId === null || typeof obj.pairId === "string")
|
|
14857
|
+
record3.pairId = obj.pairId;
|
|
14858
|
+
if (obj.cwd === null || typeof obj.cwd === "string")
|
|
14859
|
+
record3.cwd = obj.cwd;
|
|
14860
|
+
if (obj.stateDir === null || typeof obj.stateDir === "string")
|
|
14861
|
+
record3.stateDir = obj.stateDir;
|
|
14862
|
+
if (typeof obj.proxyUrl === "string")
|
|
14863
|
+
record3.proxyUrl = obj.proxyUrl;
|
|
14864
|
+
if (typeof obj.appServerUrl === "string")
|
|
14865
|
+
record3.appServerUrl = obj.appServerUrl;
|
|
14866
|
+
const ports = sanitizePorts(obj.ports);
|
|
14867
|
+
if (ports !== undefined)
|
|
14868
|
+
record3.ports = ports;
|
|
14869
|
+
if (typeof obj.build === "object" && obj.build !== null) {
|
|
14870
|
+
record3.build = obj.build;
|
|
14871
|
+
}
|
|
14872
|
+
if (typeof obj.turnPhase === "string")
|
|
14873
|
+
record3.turnPhase = obj.turnPhase;
|
|
14874
|
+
if (typeof obj.turnInProgress === "boolean")
|
|
14875
|
+
record3.turnInProgress = obj.turnInProgress;
|
|
14876
|
+
if (typeof obj.attentionWindowActive === "boolean") {
|
|
14877
|
+
record3.attentionWindowActive = obj.attentionWindowActive;
|
|
14878
|
+
}
|
|
14879
|
+
return record3;
|
|
14880
|
+
}
|
|
14881
|
+
function synthesizeLegacyRecord(pidFilePath, statusFilePath, read = defaultRead) {
|
|
14882
|
+
let pidFromPidFile = null;
|
|
14883
|
+
try {
|
|
14884
|
+
const raw = read(pidFilePath).trim();
|
|
14885
|
+
const n = Number.parseInt(raw, 10);
|
|
14886
|
+
if (Number.isFinite(n))
|
|
14887
|
+
pidFromPidFile = n;
|
|
14888
|
+
} catch {}
|
|
14889
|
+
let status = null;
|
|
14890
|
+
try {
|
|
14891
|
+
const parsed = JSON.parse(read(statusFilePath));
|
|
14892
|
+
if (typeof parsed === "object" && parsed !== null)
|
|
14893
|
+
status = parsed;
|
|
14894
|
+
} catch {}
|
|
14895
|
+
const pidFromStatus = status && typeof status.pid === "number" && Number.isFinite(status.pid) ? status.pid : null;
|
|
14896
|
+
const pid = pidFromPidFile ?? pidFromStatus;
|
|
14897
|
+
if (pid === null)
|
|
14898
|
+
return null;
|
|
14899
|
+
const record3 = {
|
|
14900
|
+
pid,
|
|
14901
|
+
phase: status ? "ready" : "booting"
|
|
14902
|
+
};
|
|
14903
|
+
if (status) {
|
|
14904
|
+
if (typeof status.proxyUrl === "string")
|
|
14905
|
+
record3.proxyUrl = status.proxyUrl;
|
|
14906
|
+
if (typeof status.appServerUrl === "string")
|
|
14907
|
+
record3.appServerUrl = status.appServerUrl;
|
|
14908
|
+
const controlPort = typeof status.controlPort === "number" ? status.controlPort : undefined;
|
|
14909
|
+
const proxyPort = portFromUrl(status.proxyUrl);
|
|
14910
|
+
const appPort = portFromUrl(status.appServerUrl);
|
|
14911
|
+
if (controlPort !== undefined || proxyPort !== undefined || appPort !== undefined) {
|
|
14912
|
+
record3.ports = {};
|
|
14913
|
+
if (appPort !== undefined)
|
|
14914
|
+
record3.ports.appPort = appPort;
|
|
14915
|
+
if (proxyPort !== undefined)
|
|
14916
|
+
record3.ports.proxyPort = proxyPort;
|
|
14917
|
+
if (controlPort !== undefined)
|
|
14918
|
+
record3.ports.controlPort = controlPort;
|
|
14919
|
+
}
|
|
14920
|
+
if (status.pairId === null || typeof status.pairId === "string")
|
|
14921
|
+
record3.pairId = status.pairId;
|
|
14922
|
+
if (status.cwd === null || typeof status.cwd === "string")
|
|
14923
|
+
record3.cwd = status.cwd;
|
|
14924
|
+
if (status.stateDir === null || typeof status.stateDir === "string")
|
|
14925
|
+
record3.stateDir = status.stateDir;
|
|
14926
|
+
if (typeof status.build === "object" && status.build !== null) {
|
|
14927
|
+
record3.build = status.build;
|
|
14928
|
+
}
|
|
14929
|
+
if (typeof status.turnPhase === "string")
|
|
14930
|
+
record3.turnPhase = status.turnPhase;
|
|
14931
|
+
if (typeof status.turnInProgress === "boolean")
|
|
14932
|
+
record3.turnInProgress = status.turnInProgress;
|
|
14933
|
+
if (typeof status.attentionWindowActive === "boolean") {
|
|
14934
|
+
record3.attentionWindowActive = status.attentionWindowActive;
|
|
14935
|
+
}
|
|
14936
|
+
}
|
|
14937
|
+
return record3;
|
|
14938
|
+
}
|
|
14939
|
+
function readUnifiedDaemonRecord(paths, read = defaultRead) {
|
|
14940
|
+
return readDaemonRecord(paths.daemonRecordFile, read) ?? synthesizeLegacyRecord(paths.pidFile, paths.statusFile, read);
|
|
14941
|
+
}
|
|
14942
|
+
function portFromUrl(url) {
|
|
14943
|
+
if (typeof url !== "string")
|
|
14944
|
+
return;
|
|
14945
|
+
const match = url.match(/:(\d+)(?:[/?]|$)/);
|
|
14946
|
+
return match ? Number.parseInt(match[1], 10) : undefined;
|
|
14947
|
+
}
|
|
14948
|
+
|
|
14540
14949
|
// src/daemon-lifecycle.ts
|
|
14541
14950
|
var DEFAULT_DAEMON_ENTRY = import.meta.url.endsWith(".ts") ? "./daemon.ts" : "./daemon.js";
|
|
14542
14951
|
var DAEMON_ENTRY = process.env.AGENTBRIDGE_DAEMON_ENTRY || DEFAULT_DAEMON_ENTRY;
|
|
14543
14952
|
var DAEMON_PATH = fileURLToPath(new URL(DAEMON_ENTRY, import.meta.url));
|
|
14544
14953
|
var REUSE_READY_RETRIES = parsePositiveIntEnv("AGENTBRIDGE_REUSE_READY_RETRIES", 12);
|
|
14545
14954
|
var REUSE_READY_DELAY_MS = 250;
|
|
14955
|
+
var WAIT_READY_RETRIES = 40;
|
|
14956
|
+
var WAIT_READY_DELAY_MS = 250;
|
|
14546
14957
|
var HEALTH_FETCH_TIMEOUT_MS = 500;
|
|
14547
14958
|
var LOCK_IDENTITY_GRACE_MS = parsePositiveIntEnv("AGENTBRIDGE_LOCK_IDENTITY_GRACE_MS", 120000);
|
|
14959
|
+
function isReuseVerdict(verdict) {
|
|
14960
|
+
return verdict === "reuse" || verdict === "reuse-despite-drift";
|
|
14961
|
+
}
|
|
14962
|
+
function classifyDaemon(expectedPairId, status, buildInfo) {
|
|
14963
|
+
if (!status) {
|
|
14964
|
+
return { verdict: "unreachable", reason: "daemon status is unavailable or unparseable" };
|
|
14965
|
+
}
|
|
14966
|
+
const reportedPairId = status.pairId;
|
|
14967
|
+
if (!expectedPairId && reportedPairId != null) {
|
|
14968
|
+
return {
|
|
14969
|
+
verdict: "manual-conflict",
|
|
14970
|
+
reason: `manual mode must not adopt registered pair ${reportedPairId}`
|
|
14971
|
+
};
|
|
14972
|
+
}
|
|
14973
|
+
if (expectedPairId) {
|
|
14974
|
+
if (reportedPairId == null) {
|
|
14975
|
+
return {
|
|
14976
|
+
verdict: "replace-foreign",
|
|
14977
|
+
reason: `pair ${expectedPairId} found daemon without pair identity`
|
|
14978
|
+
};
|
|
14979
|
+
}
|
|
14980
|
+
if (reportedPairId !== expectedPairId) {
|
|
14981
|
+
return {
|
|
14982
|
+
verdict: "replace-foreign",
|
|
14983
|
+
reason: `pair ${expectedPairId} found daemon for pair ${reportedPairId}`
|
|
14984
|
+
};
|
|
14985
|
+
}
|
|
14986
|
+
}
|
|
14987
|
+
if (!sameRuntimeContract(status.build, buildInfo)) {
|
|
14988
|
+
if (compatibleContractVersion(status.build, buildInfo) && status.tuiConnected === true) {
|
|
14989
|
+
return {
|
|
14990
|
+
verdict: "reuse-despite-drift",
|
|
14991
|
+
reason: "runtime build drift has a compatible contract and a live Codex TUI is attached"
|
|
14992
|
+
};
|
|
14993
|
+
}
|
|
14994
|
+
const basis = runtimeContractComparisonBasis(status.build, buildInfo) === "codeHash" ? "compared by codeHash" : "compared by commit stamp; legacy build without codeHash";
|
|
14995
|
+
return {
|
|
14996
|
+
verdict: "replace-drifted",
|
|
14997
|
+
reason: `runtime build ${formatBuildInfo(status.build)} does not match launcher ` + `${formatBuildInfo(buildInfo)} (${basis})`
|
|
14998
|
+
};
|
|
14999
|
+
}
|
|
15000
|
+
return { verdict: "reuse", reason: "daemon pair and runtime contract match" };
|
|
15001
|
+
}
|
|
15002
|
+
function resolveTiming(timing) {
|
|
15003
|
+
return {
|
|
15004
|
+
reuseReadyRetries: timing?.reuseReadyRetries ?? REUSE_READY_RETRIES,
|
|
15005
|
+
reuseReadyDelayMs: timing?.reuseReadyDelayMs ?? REUSE_READY_DELAY_MS,
|
|
15006
|
+
waitReadyRetries: timing?.waitReadyRetries ?? WAIT_READY_RETRIES,
|
|
15007
|
+
waitReadyDelayMs: timing?.waitReadyDelayMs ?? WAIT_READY_DELAY_MS
|
|
15008
|
+
};
|
|
15009
|
+
}
|
|
14548
15010
|
|
|
14549
15011
|
class DaemonLifecycle {
|
|
14550
15012
|
stateDir;
|
|
14551
15013
|
controlPort;
|
|
14552
15014
|
log;
|
|
15015
|
+
timing;
|
|
14553
15016
|
constructor(opts) {
|
|
14554
15017
|
this.stateDir = opts.stateDir;
|
|
14555
15018
|
this.controlPort = opts.controlPort;
|
|
14556
15019
|
this.log = opts.log;
|
|
15020
|
+
this.timing = resolveTiming(opts.timing);
|
|
14557
15021
|
}
|
|
14558
15022
|
get healthUrl() {
|
|
14559
15023
|
return `http://127.0.0.1:${this.controlPort}/healthz`;
|
|
@@ -14577,55 +15041,40 @@ class DaemonLifecycle {
|
|
|
14577
15041
|
return null;
|
|
14578
15042
|
}
|
|
14579
15043
|
}
|
|
14580
|
-
|
|
14581
|
-
const
|
|
14582
|
-
if (
|
|
14583
|
-
return
|
|
14584
|
-
|
|
14585
|
-
|
|
14586
|
-
const reported = status.pairId;
|
|
14587
|
-
if (reported == null)
|
|
14588
|
-
return true;
|
|
14589
|
-
return reported !== expected;
|
|
14590
|
-
}
|
|
14591
|
-
isRegisteredPairDaemonInManualMode(status) {
|
|
14592
|
-
return !this.expectedPairId && status?.pairId != null;
|
|
14593
|
-
}
|
|
14594
|
-
isBuildDrifted(status) {
|
|
14595
|
-
if (process.env.AGENTBRIDGE_ALLOW_BUILD_DRIFT === "1")
|
|
14596
|
-
return false;
|
|
14597
|
-
const runtime = status?.build;
|
|
14598
|
-
if (!runtime)
|
|
14599
|
-
return true;
|
|
14600
|
-
return !sameRuntimeContract(runtime, BUILD_INFO);
|
|
15044
|
+
classifyDaemon(status) {
|
|
15045
|
+
const classification = classifyDaemon(this.expectedPairId, status, BUILD_INFO);
|
|
15046
|
+
if (process.env.AGENTBRIDGE_ALLOW_BUILD_DRIFT === "1" && (classification.verdict === "replace-drifted" || classification.verdict === "unreachable")) {
|
|
15047
|
+
return { verdict: "reuse", reason: "build drift replacement disabled by AGENTBRIDGE_ALLOW_BUILD_DRIFT" };
|
|
15048
|
+
}
|
|
15049
|
+
return classification;
|
|
14601
15050
|
}
|
|
14602
|
-
|
|
14603
|
-
|
|
14604
|
-
return false;
|
|
14605
|
-
return status?.tuiConnected === true;
|
|
15051
|
+
manualConflictError(status) {
|
|
15052
|
+
return 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.`);
|
|
14606
15053
|
}
|
|
14607
15054
|
async ensureRunning() {
|
|
14608
15055
|
if (await this.isHealthy()) {
|
|
14609
15056
|
const status = await this.fetchStatus();
|
|
14610
|
-
|
|
14611
|
-
|
|
14612
|
-
|
|
14613
|
-
|
|
14614
|
-
|
|
14615
|
-
|
|
14616
|
-
|
|
14617
|
-
|
|
14618
|
-
|
|
14619
|
-
|
|
14620
|
-
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)`);
|
|
14621
|
-
} else {
|
|
15057
|
+
const classification = this.classifyDaemon(status);
|
|
15058
|
+
switch (classification.verdict) {
|
|
15059
|
+
case "manual-conflict":
|
|
15060
|
+
throw this.manualConflictError(status);
|
|
15061
|
+
case "replace-foreign":
|
|
15062
|
+
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`);
|
|
15063
|
+
await this.replaceUnhealthyDaemon(status?.pid);
|
|
15064
|
+
return;
|
|
15065
|
+
case "replace-drifted":
|
|
15066
|
+
case "unreachable":
|
|
14622
15067
|
this.log(`Daemon on control port ${this.controlPort} is running build ${formatBuildInfo(status?.build)} ` + `but launcher is ${formatBuildInfo(BUILD_INFO)} \u2014 replacing drifted daemon`);
|
|
14623
15068
|
await this.replaceUnhealthyDaemon(status?.pid);
|
|
14624
15069
|
return;
|
|
14625
|
-
|
|
15070
|
+
case "reuse-despite-drift":
|
|
15071
|
+
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)`);
|
|
15072
|
+
break;
|
|
15073
|
+
case "reuse":
|
|
15074
|
+
break;
|
|
14626
15075
|
}
|
|
14627
15076
|
try {
|
|
14628
|
-
await this.waitForReady(
|
|
15077
|
+
await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
|
|
14629
15078
|
return;
|
|
14630
15079
|
} catch {
|
|
14631
15080
|
this.log(`Daemon on control port ${this.controlPort} is healthy but not ready within reuse window \u2014 replacing`);
|
|
@@ -14638,7 +15087,7 @@ class DaemonLifecycle {
|
|
|
14638
15087
|
if (isProcessAlive(existingPid)) {
|
|
14639
15088
|
if (isAgentBridgeDaemon(existingPid)) {
|
|
14640
15089
|
try {
|
|
14641
|
-
await this.waitForReady(
|
|
15090
|
+
await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
|
|
14642
15091
|
return;
|
|
14643
15092
|
} catch {
|
|
14644
15093
|
this.log(`Existing daemon process ${existingPid} never became ready \u2014 replacing`);
|
|
@@ -14652,18 +15101,21 @@ class DaemonLifecycle {
|
|
|
14652
15101
|
}
|
|
14653
15102
|
await this.withStartupLockStrict(async (locked) => {
|
|
14654
15103
|
if (!locked) {
|
|
14655
|
-
this.
|
|
14656
|
-
await this.waitForReadyAndOurs();
|
|
15104
|
+
await this.waitForContendedStartupLock();
|
|
14657
15105
|
return;
|
|
14658
15106
|
}
|
|
14659
15107
|
if (await this.isHealthy()) {
|
|
14660
15108
|
const status = await this.fetchStatus();
|
|
14661
|
-
|
|
14662
|
-
|
|
15109
|
+
const classification = this.classifyDaemon(status);
|
|
15110
|
+
if (classification.verdict === "manual-conflict") {
|
|
15111
|
+
throw this.manualConflictError(status);
|
|
15112
|
+
}
|
|
15113
|
+
if (!isReuseVerdict(classification.verdict)) {
|
|
15114
|
+
this.log(`Daemon on control port ${this.controlPort} is not reusable under startup lock ` + `(pair=${status?.pairId ?? "<none>"}, build=${formatBuildInfo(status?.build)}, ` + `reason=${classification.reason}) \u2014 replacing`);
|
|
14663
15115
|
await this.kill(3000, status?.pid);
|
|
14664
15116
|
} else {
|
|
14665
15117
|
try {
|
|
14666
|
-
await this.waitForReady(
|
|
15118
|
+
await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
|
|
14667
15119
|
return;
|
|
14668
15120
|
} catch {
|
|
14669
15121
|
this.log(`Daemon on control port ${this.controlPort} is healthy but not ready under startup lock \u2014 replacing`);
|
|
@@ -14672,7 +15124,7 @@ class DaemonLifecycle {
|
|
|
14672
15124
|
}
|
|
14673
15125
|
}
|
|
14674
15126
|
this.launch();
|
|
14675
|
-
await this.waitForReady();
|
|
15127
|
+
await this.waitForReady(this.timing.waitReadyRetries, this.timing.waitReadyDelayMs);
|
|
14676
15128
|
});
|
|
14677
15129
|
}
|
|
14678
15130
|
async isHealthy() {
|
|
@@ -14699,7 +15151,7 @@ class DaemonLifecycle {
|
|
|
14699
15151
|
return false;
|
|
14700
15152
|
}
|
|
14701
15153
|
}
|
|
14702
|
-
async waitForReady(maxRetries =
|
|
15154
|
+
async waitForReady(maxRetries = WAIT_READY_RETRIES, delayMs = WAIT_READY_DELAY_MS) {
|
|
14703
15155
|
for (let attempt = 0;attempt < maxRetries; attempt++) {
|
|
14704
15156
|
if (await this.isReady())
|
|
14705
15157
|
return;
|
|
@@ -14707,11 +15159,15 @@ class DaemonLifecycle {
|
|
|
14707
15159
|
}
|
|
14708
15160
|
throw new Error(`Timed out waiting for AgentBridge daemon readiness on ${this.readyUrl}`);
|
|
14709
15161
|
}
|
|
14710
|
-
async waitForReadyAndOurs(maxRetries =
|
|
15162
|
+
async waitForReadyAndOurs(maxRetries = WAIT_READY_RETRIES, delayMs = WAIT_READY_DELAY_MS) {
|
|
14711
15163
|
for (let attempt = 0;attempt < maxRetries; attempt++) {
|
|
14712
15164
|
if (await this.isReady()) {
|
|
14713
15165
|
const status = await this.fetchStatus();
|
|
14714
|
-
|
|
15166
|
+
const classification = this.classifyDaemon(status);
|
|
15167
|
+
if (classification.verdict === "manual-conflict") {
|
|
15168
|
+
throw this.manualConflictError(status);
|
|
15169
|
+
}
|
|
15170
|
+
if (isReuseVerdict(classification.verdict)) {
|
|
14715
15171
|
return;
|
|
14716
15172
|
}
|
|
14717
15173
|
}
|
|
@@ -14719,22 +15175,35 @@ class DaemonLifecycle {
|
|
|
14719
15175
|
}
|
|
14720
15176
|
throw new Error(`Timed out waiting for AgentBridge daemon readiness+identity on ${this.readyUrl} (control port ${this.controlPort})`);
|
|
14721
15177
|
}
|
|
15178
|
+
readDaemonRecord() {
|
|
15179
|
+
return readUnifiedDaemonRecord({
|
|
15180
|
+
daemonRecordFile: this.stateDir.daemonRecordFile,
|
|
15181
|
+
pidFile: this.stateDir.pidFile,
|
|
15182
|
+
statusFile: this.stateDir.statusFile
|
|
15183
|
+
});
|
|
15184
|
+
}
|
|
15185
|
+
writeDaemonRecord(record3) {
|
|
15186
|
+
writeDaemonRecord(this.stateDir.daemonRecordFile, record3);
|
|
15187
|
+
}
|
|
15188
|
+
removeDaemonRecord() {
|
|
15189
|
+
try {
|
|
15190
|
+
unlinkSync3(this.stateDir.daemonRecordFile);
|
|
15191
|
+
} catch {}
|
|
15192
|
+
}
|
|
14722
15193
|
readStatus() {
|
|
14723
15194
|
try {
|
|
14724
|
-
const raw =
|
|
15195
|
+
const raw = readFileSync2(this.stateDir.statusFile, "utf-8");
|
|
14725
15196
|
return JSON.parse(raw);
|
|
14726
15197
|
} catch {
|
|
14727
15198
|
return null;
|
|
14728
15199
|
}
|
|
14729
15200
|
}
|
|
14730
15201
|
writeStatus(status) {
|
|
14731
|
-
this.stateDir.
|
|
14732
|
-
writeFileSync(this.stateDir.statusFile, JSON.stringify(status, null, 2) + `
|
|
14733
|
-
`, "utf-8");
|
|
15202
|
+
atomicWriteJson(this.stateDir.statusFile, status);
|
|
14734
15203
|
}
|
|
14735
15204
|
readPid() {
|
|
14736
15205
|
try {
|
|
14737
|
-
const raw =
|
|
15206
|
+
const raw = readFileSync2(this.stateDir.pidFile, "utf-8").trim();
|
|
14738
15207
|
if (!raw)
|
|
14739
15208
|
return null;
|
|
14740
15209
|
const pid = Number.parseInt(raw, 10);
|
|
@@ -14744,28 +15213,27 @@ class DaemonLifecycle {
|
|
|
14744
15213
|
}
|
|
14745
15214
|
}
|
|
14746
15215
|
writePid(pid) {
|
|
14747
|
-
this.stateDir.
|
|
14748
|
-
|
|
14749
|
-
`, "utf-8");
|
|
15216
|
+
atomicWriteText(this.stateDir.pidFile, `${pid ?? process.pid}
|
|
15217
|
+
`);
|
|
14750
15218
|
}
|
|
14751
15219
|
removePidFile() {
|
|
14752
15220
|
try {
|
|
14753
|
-
|
|
15221
|
+
unlinkSync3(this.stateDir.pidFile);
|
|
14754
15222
|
} catch {}
|
|
14755
15223
|
}
|
|
14756
15224
|
removeStatusFile() {
|
|
14757
15225
|
try {
|
|
14758
|
-
|
|
15226
|
+
unlinkSync3(this.stateDir.statusFile);
|
|
14759
15227
|
} catch {}
|
|
14760
15228
|
}
|
|
14761
15229
|
markKilled() {
|
|
14762
15230
|
this.stateDir.ensure();
|
|
14763
|
-
|
|
15231
|
+
writeFileSync2(this.stateDir.killedFile, `${Date.now()}
|
|
14764
15232
|
`, "utf-8");
|
|
14765
15233
|
}
|
|
14766
15234
|
clearKilled() {
|
|
14767
15235
|
try {
|
|
14768
|
-
|
|
15236
|
+
unlinkSync3(this.stateDir.killedFile);
|
|
14769
15237
|
} catch {}
|
|
14770
15238
|
}
|
|
14771
15239
|
wasKilled() {
|
|
@@ -14787,21 +15255,26 @@ class DaemonLifecycle {
|
|
|
14787
15255
|
daemonProc.unref();
|
|
14788
15256
|
}
|
|
14789
15257
|
removeStalePidFile() {
|
|
14790
|
-
this.log("Removing stale
|
|
15258
|
+
this.log("Removing stale daemon identity files");
|
|
14791
15259
|
this.removePidFile();
|
|
15260
|
+
this.removeStatusFile();
|
|
15261
|
+
this.removeDaemonRecord();
|
|
14792
15262
|
}
|
|
14793
15263
|
async replaceUnhealthyDaemon(statusPid) {
|
|
14794
15264
|
await this.withStartupLockStrict(async (locked) => {
|
|
14795
15265
|
if (!locked) {
|
|
14796
|
-
this.
|
|
14797
|
-
await this.waitForReadyAndOurs();
|
|
15266
|
+
await this.waitForContendedStartupLock();
|
|
14798
15267
|
return;
|
|
14799
15268
|
}
|
|
14800
15269
|
if (await this.isHealthy()) {
|
|
14801
15270
|
const status = await this.fetchStatus();
|
|
14802
|
-
|
|
15271
|
+
const classification = this.classifyDaemon(status);
|
|
15272
|
+
if (classification.verdict === "manual-conflict") {
|
|
15273
|
+
throw this.manualConflictError(status);
|
|
15274
|
+
}
|
|
15275
|
+
if (isReuseVerdict(classification.verdict)) {
|
|
14803
15276
|
try {
|
|
14804
|
-
await this.waitForReady(
|
|
15277
|
+
await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
|
|
14805
15278
|
return;
|
|
14806
15279
|
} catch {}
|
|
14807
15280
|
}
|
|
@@ -14809,9 +15282,13 @@ class DaemonLifecycle {
|
|
|
14809
15282
|
this.log(`Killing unhealthy daemon on control port ${this.controlPort} and relaunching`);
|
|
14810
15283
|
await this.kill(3000, statusPid);
|
|
14811
15284
|
this.launch();
|
|
14812
|
-
await this.waitForReady();
|
|
15285
|
+
await this.waitForReady(this.timing.waitReadyRetries, this.timing.waitReadyDelayMs);
|
|
14813
15286
|
});
|
|
14814
15287
|
}
|
|
15288
|
+
async waitForContendedStartupLock() {
|
|
15289
|
+
this.log("Another process holds the startup lock, waiting for readiness+identity...");
|
|
15290
|
+
await this.waitForReadyAndOurs(this.timing.waitReadyRetries, this.timing.waitReadyDelayMs);
|
|
15291
|
+
}
|
|
14815
15292
|
async withStartupLockStrict(fn) {
|
|
14816
15293
|
const locked = this.acquireLockStrict();
|
|
14817
15294
|
try {
|
|
@@ -14825,15 +15302,15 @@ class DaemonLifecycle {
|
|
|
14825
15302
|
this.stateDir.ensure();
|
|
14826
15303
|
let fd = null;
|
|
14827
15304
|
try {
|
|
14828
|
-
fd =
|
|
14829
|
-
|
|
15305
|
+
fd = openSync2(this.stateDir.lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
|
|
15306
|
+
writeFileSync2(fd, `${process.pid}
|
|
14830
15307
|
`);
|
|
14831
|
-
|
|
15308
|
+
closeSync2(fd);
|
|
14832
15309
|
return true;
|
|
14833
15310
|
} catch (err) {
|
|
14834
15311
|
if (fd !== null && err.code !== "EEXIST") {
|
|
14835
15312
|
try {
|
|
14836
|
-
|
|
15313
|
+
closeSync2(fd);
|
|
14837
15314
|
} catch {}
|
|
14838
15315
|
this.releaseLock();
|
|
14839
15316
|
}
|
|
@@ -14841,7 +15318,7 @@ class DaemonLifecycle {
|
|
|
14841
15318
|
if (reclaimed)
|
|
14842
15319
|
return false;
|
|
14843
15320
|
try {
|
|
14844
|
-
const holderPid = Number.parseInt(
|
|
15321
|
+
const holderPid = Number.parseInt(readFileSync2(this.stateDir.lockFile, "utf-8").trim(), 10);
|
|
14845
15322
|
if (Number.isFinite(holderPid) && !isProcessAlive(holderPid)) {
|
|
14846
15323
|
this.log(`Stale startup lock from dead process ${holderPid}, reclaiming`);
|
|
14847
15324
|
this.releaseLock();
|
|
@@ -14870,7 +15347,7 @@ class DaemonLifecycle {
|
|
|
14870
15347
|
}
|
|
14871
15348
|
releaseLock() {
|
|
14872
15349
|
try {
|
|
14873
|
-
|
|
15350
|
+
unlinkSync3(this.stateDir.lockFile);
|
|
14874
15351
|
} catch {}
|
|
14875
15352
|
}
|
|
14876
15353
|
async kill(gracefulTimeoutMs = 3000, pidOverride) {
|
|
@@ -14916,6 +15393,7 @@ class DaemonLifecycle {
|
|
|
14916
15393
|
cleanup() {
|
|
14917
15394
|
this.removePidFile();
|
|
14918
15395
|
this.removeStatusFile();
|
|
15396
|
+
this.removeDaemonRecord();
|
|
14919
15397
|
}
|
|
14920
15398
|
}
|
|
14921
15399
|
async function fetchWithTimeout(url, timeoutMs = HEALTH_FETCH_TIMEOUT_MS) {
|
|
@@ -14929,11 +15407,11 @@ async function fetchWithTimeout(url, timeoutMs = HEALTH_FETCH_TIMEOUT_MS) {
|
|
|
14929
15407
|
}
|
|
14930
15408
|
|
|
14931
15409
|
// src/config-service.ts
|
|
14932
|
-
import { readFileSync as
|
|
15410
|
+
import { readFileSync as readFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
|
|
14933
15411
|
import { join as join2 } from "path";
|
|
14934
15412
|
var DEFAULT_BUDGET_CONFIG = {
|
|
14935
15413
|
enabled: true,
|
|
14936
|
-
pollSeconds:
|
|
15414
|
+
pollSeconds: 300,
|
|
14937
15415
|
pauseAt: 90,
|
|
14938
15416
|
resumeBelow: 30,
|
|
14939
15417
|
syncDriftPct: 10,
|
|
@@ -14962,9 +15440,52 @@ var DEFAULT_CONFIG = {
|
|
|
14962
15440
|
};
|
|
14963
15441
|
var CONFIG_DIR = ".agentbridge";
|
|
14964
15442
|
var CONFIG_FILE = "config.json";
|
|
15443
|
+
var NOOP_LOGGER = () => {};
|
|
14965
15444
|
function isRecord(value) {
|
|
14966
15445
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14967
15446
|
}
|
|
15447
|
+
function isCoercibleNumber(value) {
|
|
15448
|
+
if (typeof value === "number")
|
|
15449
|
+
return Number.isFinite(value);
|
|
15450
|
+
if (typeof value === "string")
|
|
15451
|
+
return Number.isFinite(Number(value));
|
|
15452
|
+
return false;
|
|
15453
|
+
}
|
|
15454
|
+
function findShapeViolation(raw) {
|
|
15455
|
+
if ("idleShutdownSeconds" in raw && !isCoercibleNumber(raw.idleShutdownSeconds)) {
|
|
15456
|
+
return "idleShutdownSeconds is present but not a number";
|
|
15457
|
+
}
|
|
15458
|
+
if ("budget" in raw) {
|
|
15459
|
+
const budget = raw.budget;
|
|
15460
|
+
if (!isRecord(budget)) {
|
|
15461
|
+
return "budget is present but not an object";
|
|
15462
|
+
}
|
|
15463
|
+
const numericKeys = ["pauseAt", "resumeBelow", "pollSeconds", "syncDriftPct"];
|
|
15464
|
+
for (const key of numericKeys) {
|
|
15465
|
+
if (key in budget && !isCoercibleNumber(budget[key])) {
|
|
15466
|
+
return `budget.${key} is present but not a number`;
|
|
15467
|
+
}
|
|
15468
|
+
}
|
|
15469
|
+
if ("parallel" in budget) {
|
|
15470
|
+
const parallel = budget.parallel;
|
|
15471
|
+
if (!isRecord(parallel)) {
|
|
15472
|
+
return "budget.parallel is present but not an object";
|
|
15473
|
+
}
|
|
15474
|
+
for (const key of ["minRemainingPct", "timeWindowSec"]) {
|
|
15475
|
+
if (key in parallel && !isCoercibleNumber(parallel[key])) {
|
|
15476
|
+
return `budget.parallel.${key} is present but not a number`;
|
|
15477
|
+
}
|
|
15478
|
+
}
|
|
15479
|
+
}
|
|
15480
|
+
}
|
|
15481
|
+
return null;
|
|
15482
|
+
}
|
|
15483
|
+
function hasCustomDecisionValues(config2) {
|
|
15484
|
+
const d = DEFAULT_CONFIG;
|
|
15485
|
+
const b = config2.budget;
|
|
15486
|
+
const db = d.budget;
|
|
15487
|
+
return config2.idleShutdownSeconds !== d.idleShutdownSeconds || config2.turnCoordination.attentionWindowSeconds !== d.turnCoordination.attentionWindowSeconds || config2.codex.appPort !== d.codex.appPort || config2.codex.proxyPort !== d.codex.proxyPort || b.enabled !== db.enabled || b.pollSeconds !== db.pollSeconds || b.pauseAt !== db.pauseAt || b.resumeBelow !== db.resumeBelow || b.syncDriftPct !== db.syncDriftPct || b.parallel.minRemainingPct !== db.parallel.minRemainingPct || b.parallel.timeWindowSec !== db.parallel.timeWindowSec || b.codexTierControl !== db.codexTierControl;
|
|
15488
|
+
}
|
|
14968
15489
|
function normalizeInteger(value, fallback) {
|
|
14969
15490
|
if (typeof value === "number" && Number.isFinite(value))
|
|
14970
15491
|
return value;
|
|
@@ -15000,35 +15521,35 @@ function normalizeCodexOverride(raw) {
|
|
|
15000
15521
|
override.effort = raw.effort.trim();
|
|
15001
15522
|
return Object.keys(override).length > 0 ? override : null;
|
|
15002
15523
|
}
|
|
15003
|
-
function normalizeCodexTiers(raw) {
|
|
15524
|
+
function normalizeCodexTiers(raw, fallback = DEFAULT_BUDGET_CONFIG.codexTiers) {
|
|
15004
15525
|
const tiers = isRecord(raw) ? raw : {};
|
|
15005
15526
|
return {
|
|
15006
15527
|
full: normalizeCodexOverride(tiers.full),
|
|
15007
|
-
balanced: normalizeCodexOverride(tiers.balanced) ??
|
|
15008
|
-
eco: normalizeCodexOverride(tiers.eco) ??
|
|
15528
|
+
balanced: normalizeCodexOverride(tiers.balanced) ?? fallback.balanced,
|
|
15529
|
+
eco: normalizeCodexOverride(tiers.eco) ?? fallback.eco
|
|
15009
15530
|
};
|
|
15010
15531
|
}
|
|
15011
|
-
function normalizeBudgetConfig(raw) {
|
|
15532
|
+
function normalizeBudgetConfig(raw, fallback = DEFAULT_BUDGET_CONFIG) {
|
|
15012
15533
|
const budget = isRecord(raw) ? raw : {};
|
|
15013
15534
|
const parallel = isRecord(budget.parallel) ? budget.parallel : {};
|
|
15014
|
-
const codexTiers = normalizeCodexTiers(budget.codexTiers);
|
|
15015
|
-
let pauseAt = normalizeBoundedInteger(budget.pauseAt,
|
|
15016
|
-
let resumeBelow = normalizeBoundedInteger(budget.resumeBelow,
|
|
15535
|
+
const codexTiers = normalizeCodexTiers(budget.codexTiers, fallback.codexTiers);
|
|
15536
|
+
let pauseAt = normalizeBoundedInteger(budget.pauseAt, fallback.pauseAt, 1, 100);
|
|
15537
|
+
let resumeBelow = normalizeBoundedInteger(budget.resumeBelow, fallback.resumeBelow, 0, 99);
|
|
15017
15538
|
if (pauseAt <= resumeBelow) {
|
|
15018
15539
|
pauseAt = DEFAULT_BUDGET_CONFIG.pauseAt;
|
|
15019
15540
|
resumeBelow = DEFAULT_BUDGET_CONFIG.resumeBelow;
|
|
15020
15541
|
}
|
|
15021
15542
|
return {
|
|
15022
|
-
enabled: normalizeBoolean(budget.enabled,
|
|
15023
|
-
pollSeconds: normalizeBoundedInteger(budget.pollSeconds,
|
|
15543
|
+
enabled: normalizeBoolean(budget.enabled, fallback.enabled),
|
|
15544
|
+
pollSeconds: normalizeBoundedInteger(budget.pollSeconds, fallback.pollSeconds, 5, 3600),
|
|
15024
15545
|
pauseAt,
|
|
15025
15546
|
resumeBelow,
|
|
15026
|
-
syncDriftPct: normalizeBoundedInteger(budget.syncDriftPct,
|
|
15547
|
+
syncDriftPct: normalizeBoundedInteger(budget.syncDriftPct, fallback.syncDriftPct, 1, 100),
|
|
15027
15548
|
parallel: {
|
|
15028
|
-
minRemainingPct: normalizeBoundedInteger(parallel.minRemainingPct,
|
|
15029
|
-
timeWindowSec: normalizeBoundedInteger(parallel.timeWindowSec,
|
|
15549
|
+
minRemainingPct: normalizeBoundedInteger(parallel.minRemainingPct, fallback.parallel.minRemainingPct, 1, 100),
|
|
15550
|
+
timeWindowSec: normalizeBoundedInteger(parallel.timeWindowSec, fallback.parallel.timeWindowSec, 60, 604800)
|
|
15030
15551
|
},
|
|
15031
|
-
codexTierControl: normalizeBoolean(budget.codexTierControl,
|
|
15552
|
+
codexTierControl: normalizeBoolean(budget.codexTierControl, fallback.codexTierControl) && codexTiers.full !== null,
|
|
15032
15553
|
codexTiers
|
|
15033
15554
|
};
|
|
15034
15555
|
}
|
|
@@ -15042,13 +15563,13 @@ function normalizeConfig(raw) {
|
|
|
15042
15563
|
return {
|
|
15043
15564
|
version: typeof config2.version === "string" ? config2.version : DEFAULT_CONFIG.version,
|
|
15044
15565
|
codex: {
|
|
15045
|
-
appPort:
|
|
15046
|
-
proxyPort:
|
|
15566
|
+
appPort: normalizeBoundedInteger(codex.appPort ?? daemon.port, DEFAULT_CONFIG.codex.appPort, 1, 65535),
|
|
15567
|
+
proxyPort: normalizeBoundedInteger(codex.proxyPort ?? daemon.proxyPort, DEFAULT_CONFIG.codex.proxyPort, 1, 65535)
|
|
15047
15568
|
},
|
|
15048
15569
|
turnCoordination: {
|
|
15049
|
-
attentionWindowSeconds:
|
|
15570
|
+
attentionWindowSeconds: normalizeBoundedInteger(turnCoordination.attentionWindowSeconds, DEFAULT_CONFIG.turnCoordination.attentionWindowSeconds, 0, Number.MAX_SAFE_INTEGER)
|
|
15050
15571
|
},
|
|
15051
|
-
idleShutdownSeconds:
|
|
15572
|
+
idleShutdownSeconds: normalizeBoundedInteger(config2.idleShutdownSeconds, DEFAULT_CONFIG.idleShutdownSeconds, 1, Number.MAX_SAFE_INTEGER),
|
|
15052
15573
|
budget: normalizeBudgetConfig(config2.budget)
|
|
15053
15574
|
};
|
|
15054
15575
|
}
|
|
@@ -15065,20 +15586,62 @@ class ConfigService {
|
|
|
15065
15586
|
return existsSync4(this.configPath);
|
|
15066
15587
|
}
|
|
15067
15588
|
load() {
|
|
15589
|
+
let raw;
|
|
15068
15590
|
try {
|
|
15069
|
-
|
|
15070
|
-
|
|
15071
|
-
|
|
15072
|
-
|
|
15591
|
+
raw = readFileSync3(this.configPath, "utf-8");
|
|
15592
|
+
} catch (err) {
|
|
15593
|
+
if (err?.code === "ENOENT") {
|
|
15594
|
+
return { state: "absent" };
|
|
15595
|
+
}
|
|
15596
|
+
return { state: "corrupt", reason: `config.json is unreadable: ${err.message}` };
|
|
15073
15597
|
}
|
|
15598
|
+
let parsed;
|
|
15599
|
+
try {
|
|
15600
|
+
parsed = JSON.parse(raw);
|
|
15601
|
+
} catch (err) {
|
|
15602
|
+
return {
|
|
15603
|
+
state: "corrupt",
|
|
15604
|
+
reason: `config.json is not valid JSON: ${err.message}`
|
|
15605
|
+
};
|
|
15606
|
+
}
|
|
15607
|
+
if (!isRecord(parsed)) {
|
|
15608
|
+
return { state: "corrupt", reason: "config.json is not a JSON object" };
|
|
15609
|
+
}
|
|
15610
|
+
const violation = findShapeViolation(parsed);
|
|
15611
|
+
if (violation) {
|
|
15612
|
+
return { state: "corrupt", reason: `config.json is shape-invalid: ${violation}` };
|
|
15613
|
+
}
|
|
15614
|
+
const config2 = normalizeConfig(parsed);
|
|
15615
|
+
if (!config2) {
|
|
15616
|
+
return { state: "corrupt", reason: "config.json could not be normalized" };
|
|
15617
|
+
}
|
|
15618
|
+
return { state: "parsed", config: config2 };
|
|
15619
|
+
}
|
|
15620
|
+
loadOrDefault(log = NOOP_LOGGER) {
|
|
15621
|
+
const result = this.load();
|
|
15622
|
+
if (result.state === "parsed")
|
|
15623
|
+
return result.config;
|
|
15624
|
+
if (result.state === "corrupt") {
|
|
15625
|
+
log(`config.json at ${this.configPath} is unusable (${result.reason}); ` + "falling back to defaults \u2014 your custom budget thresholds / idle-shutdown settings are NOT in effect. " + "Fix the file and restart to re-apply them.");
|
|
15626
|
+
}
|
|
15627
|
+
return structuredClone(DEFAULT_CONFIG);
|
|
15074
15628
|
}
|
|
15075
|
-
|
|
15076
|
-
|
|
15629
|
+
describeConfig() {
|
|
15630
|
+
const result = this.load();
|
|
15631
|
+
if (result.state === "absent") {
|
|
15632
|
+
return { state: "absent", path: this.configPath, customValues: false };
|
|
15633
|
+
}
|
|
15634
|
+
if (result.state === "corrupt") {
|
|
15635
|
+
return { state: "corrupt", path: this.configPath, reason: result.reason, customValues: false };
|
|
15636
|
+
}
|
|
15637
|
+
return {
|
|
15638
|
+
state: "parsed",
|
|
15639
|
+
path: this.configPath,
|
|
15640
|
+
customValues: hasCustomDecisionValues(result.config)
|
|
15641
|
+
};
|
|
15077
15642
|
}
|
|
15078
15643
|
save(config2) {
|
|
15079
|
-
this.
|
|
15080
|
-
writeFileSync2(this.configPath, JSON.stringify(config2, null, 2) + `
|
|
15081
|
-
`, "utf-8");
|
|
15644
|
+
atomicWriteJson(this.configPath, config2);
|
|
15082
15645
|
}
|
|
15083
15646
|
initDefaults() {
|
|
15084
15647
|
this.ensureConfigDir();
|
|
@@ -15094,34 +15657,46 @@ class ConfigService {
|
|
|
15094
15657
|
}
|
|
15095
15658
|
ensureConfigDir() {
|
|
15096
15659
|
if (!existsSync4(this.configDir)) {
|
|
15097
|
-
|
|
15660
|
+
mkdirSync3(this.configDir, { recursive: true });
|
|
15098
15661
|
}
|
|
15099
15662
|
}
|
|
15100
15663
|
}
|
|
15101
15664
|
|
|
15665
|
+
// src/cli-invocation.ts
|
|
15666
|
+
import { basename } from "path";
|
|
15667
|
+
var CLI_NAMES = ["abg", "agentbridge"];
|
|
15668
|
+
var DEFAULT_CLI_NAME = "abg";
|
|
15669
|
+
function cliInvocationName(argv = process.argv) {
|
|
15670
|
+
const raw = argv[1];
|
|
15671
|
+
if (typeof raw !== "string" || raw.length === 0)
|
|
15672
|
+
return DEFAULT_CLI_NAME;
|
|
15673
|
+
const name = basename(raw).replace(/\.(ts|js|mjs|cjs)$/, "");
|
|
15674
|
+
return isCliName(name) ? name : DEFAULT_CLI_NAME;
|
|
15675
|
+
}
|
|
15676
|
+
function isCliName(value) {
|
|
15677
|
+
return CLI_NAMES.includes(value);
|
|
15678
|
+
}
|
|
15679
|
+
|
|
15102
15680
|
// src/pair-registry.ts
|
|
15103
15681
|
import {
|
|
15104
|
-
closeSync as closeSync2,
|
|
15105
15682
|
existsSync as existsSync5,
|
|
15106
|
-
fsyncSync,
|
|
15107
15683
|
linkSync,
|
|
15108
15684
|
lstatSync,
|
|
15109
|
-
mkdirSync as
|
|
15110
|
-
openSync as openSync2,
|
|
15685
|
+
mkdirSync as mkdirSync4,
|
|
15111
15686
|
readdirSync,
|
|
15112
|
-
readFileSync as
|
|
15687
|
+
readFileSync as readFileSync4,
|
|
15113
15688
|
realpathSync,
|
|
15114
|
-
renameSync as renameSync2,
|
|
15115
15689
|
rmSync,
|
|
15116
15690
|
statSync as statSync3,
|
|
15117
|
-
unlinkSync as
|
|
15691
|
+
unlinkSync as unlinkSync4,
|
|
15118
15692
|
writeFileSync as writeFileSync3
|
|
15119
15693
|
} from "fs";
|
|
15120
|
-
import { createHash, randomUUID as
|
|
15121
|
-
import { basename, join as join3, resolve, sep } from "path";
|
|
15694
|
+
import { createHash, randomUUID as randomUUID3 } from "crypto";
|
|
15695
|
+
import { basename as basename2, join as join3, resolve, sep } from "path";
|
|
15122
15696
|
var PAIR_BASE_PORT = 4500;
|
|
15123
15697
|
var PAIR_SLOT_STRIDE = 10;
|
|
15124
15698
|
var PAIR_ID_REGEX = /^[A-Za-z0-9._-]{1,64}$/;
|
|
15699
|
+
var RECLAIMABLE_MIN_AGE_MS = 24 * 60 * 60 * 1000;
|
|
15125
15700
|
var REGISTRY_FILE_NAME = "registry.json";
|
|
15126
15701
|
class PairError extends Error {
|
|
15127
15702
|
code;
|
|
@@ -15157,7 +15732,7 @@ function readRegistry(base) {
|
|
|
15157
15732
|
return { version: 1, pairs: [] };
|
|
15158
15733
|
let parsed;
|
|
15159
15734
|
try {
|
|
15160
|
-
parsed = JSON.parse(
|
|
15735
|
+
parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
15161
15736
|
} catch (err) {
|
|
15162
15737
|
throw new PairError("PAIR_REGISTRY_CORRUPT", `Registry JSON is not parseable at ${path}: ${err.message}`, {
|
|
15163
15738
|
path
|
|
@@ -15198,10 +15773,10 @@ function findPair(base, pairId) {
|
|
|
15198
15773
|
}
|
|
15199
15774
|
|
|
15200
15775
|
// src/pair-command.ts
|
|
15201
|
-
function pairScopedCommand(cmd) {
|
|
15776
|
+
function pairScopedCommand(cmd, name = cliInvocationName()) {
|
|
15202
15777
|
const pairId = process.env.AGENTBRIDGE_PAIR_ID;
|
|
15203
15778
|
if (!pairId)
|
|
15204
|
-
return
|
|
15779
|
+
return `${name} ${cmd}`;
|
|
15205
15780
|
let selector = process.env.AGENTBRIDGE_PAIR_NAME;
|
|
15206
15781
|
if (!selector) {
|
|
15207
15782
|
try {
|
|
@@ -15210,10 +15785,13 @@ function pairScopedCommand(cmd) {
|
|
|
15210
15785
|
selector = pairId;
|
|
15211
15786
|
}
|
|
15212
15787
|
}
|
|
15213
|
-
return
|
|
15788
|
+
return `${name} --pair ${selector} ${cmd}`;
|
|
15214
15789
|
}
|
|
15215
15790
|
|
|
15216
15791
|
// src/bridge-disabled-state.ts
|
|
15792
|
+
function shouldEmitReconnectSuccess(state) {
|
|
15793
|
+
return !state.daemonDisabled;
|
|
15794
|
+
}
|
|
15217
15795
|
function disabledReplyError(reason) {
|
|
15218
15796
|
const claudeCmd = pairScopedCommand("claude");
|
|
15219
15797
|
switch (reason) {
|
|
@@ -15226,7 +15804,7 @@ function disabledReplyError(reason) {
|
|
|
15226
15804
|
case "auto_recovery_exhausted":
|
|
15227
15805
|
return `AgentBridge auto-recovery gave up after exhausting its retry budget for the in-flight liveness probe contention. Retry manually with \`${claudeCmd}\`.`;
|
|
15228
15806
|
case "killed":
|
|
15229
|
-
return `AgentBridge is disabled by
|
|
15807
|
+
return `AgentBridge is disabled by \`${pairScopedCommand("kill")}\`. Restart Claude Code (\`${claudeCmd}\`), switch to a new conversation, or run \`/resume\` to reconnect.`;
|
|
15230
15808
|
}
|
|
15231
15809
|
}
|
|
15232
15810
|
|
|
@@ -15305,9 +15883,27 @@ function nonEmpty(value) {
|
|
|
15305
15883
|
return value && value.length > 0 ? value : null;
|
|
15306
15884
|
}
|
|
15307
15885
|
|
|
15308
|
-
// src/
|
|
15309
|
-
import {
|
|
15886
|
+
// src/control-token.ts
|
|
15887
|
+
import { chmodSync, readFileSync as readFileSync5 } from "fs";
|
|
15310
15888
|
import { join as join4 } from "path";
|
|
15889
|
+
var CONTROL_TOKEN_FILENAME = "control-token";
|
|
15890
|
+
function resolveControlTokenPath(stateDir) {
|
|
15891
|
+
return join4(stateDir, CONTROL_TOKEN_FILENAME);
|
|
15892
|
+
}
|
|
15893
|
+
function readControlToken(path) {
|
|
15894
|
+
try {
|
|
15895
|
+
const raw = readFileSync5(path, "utf-8").trim();
|
|
15896
|
+
return raw.length > 0 ? raw : null;
|
|
15897
|
+
} catch {
|
|
15898
|
+
return null;
|
|
15899
|
+
}
|
|
15900
|
+
}
|
|
15901
|
+
|
|
15902
|
+
// src/trace-log.ts
|
|
15903
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync6, mkdirSync as mkdirSync5, readdirSync as readdirSync2, statSync as statSync4, unlinkSync as unlinkSync5 } from "fs";
|
|
15904
|
+
import { join as join5 } from "path";
|
|
15905
|
+
var TRACE_RETENTION_DAYS = 7;
|
|
15906
|
+
var TRACE_FILE_RE = /^trace-\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
15311
15907
|
var SECRET_KEY_RE = /(token|secret|password|passwd|api[_-]?key|auth|cookie|session)/i;
|
|
15312
15908
|
var SECRET_ARG_RE = /^--?(?:token|secret|password|passwd|apikey|api-key|api_key|auth|cookie|session)(?:=.*)?$/i;
|
|
15313
15909
|
var RELEVANT_ENV_RE = /^(AGENTBRIDGE_|CODEX_)/;
|
|
@@ -15345,7 +15941,7 @@ function redactArgv(argv) {
|
|
|
15345
15941
|
}
|
|
15346
15942
|
function traceLogPath(cwd, timestamp) {
|
|
15347
15943
|
const day = timestamp.slice(0, 10);
|
|
15348
|
-
return
|
|
15944
|
+
return join5(cwd, ".agentbridge", "logs", `trace-${day}.jsonl`);
|
|
15349
15945
|
}
|
|
15350
15946
|
function appendTraceEvent(input) {
|
|
15351
15947
|
const timestamp = input.timestamp ?? new Date().toISOString();
|
|
@@ -15359,11 +15955,39 @@ function appendTraceEvent(input) {
|
|
|
15359
15955
|
...input.env ? { env: pickRelevantEnv(input.env) } : {},
|
|
15360
15956
|
...input.data ? { data: redactData(input.data) } : {}
|
|
15361
15957
|
};
|
|
15362
|
-
|
|
15958
|
+
const logsDir = join5(input.cwd, ".agentbridge", "logs");
|
|
15959
|
+
const isNewDayFile = !existsSync6(path);
|
|
15960
|
+
mkdirSync5(logsDir, { recursive: true });
|
|
15961
|
+
if (isNewDayFile) {
|
|
15962
|
+
pruneOldTraceLogs(logsDir, path, Date.parse(timestamp));
|
|
15963
|
+
}
|
|
15363
15964
|
appendFileSync2(path, JSON.stringify(event) + `
|
|
15364
15965
|
`, "utf-8");
|
|
15365
15966
|
return path;
|
|
15366
15967
|
}
|
|
15968
|
+
function pruneOldTraceLogs(logsDir, keepPath, nowMs) {
|
|
15969
|
+
if (!Number.isFinite(nowMs))
|
|
15970
|
+
return;
|
|
15971
|
+
const cutoff = nowMs - TRACE_RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
|
15972
|
+
let entries;
|
|
15973
|
+
try {
|
|
15974
|
+
entries = readdirSync2(logsDir);
|
|
15975
|
+
} catch {
|
|
15976
|
+
return;
|
|
15977
|
+
}
|
|
15978
|
+
for (const name of entries) {
|
|
15979
|
+
if (!TRACE_FILE_RE.test(name))
|
|
15980
|
+
continue;
|
|
15981
|
+
const filePath = join5(logsDir, name);
|
|
15982
|
+
if (filePath === keepPath)
|
|
15983
|
+
continue;
|
|
15984
|
+
try {
|
|
15985
|
+
if (statSync4(filePath).mtimeMs < cutoff) {
|
|
15986
|
+
unlinkSync5(filePath);
|
|
15987
|
+
}
|
|
15988
|
+
} catch {}
|
|
15989
|
+
}
|
|
15990
|
+
}
|
|
15367
15991
|
function isEnvSnapshot(key, value) {
|
|
15368
15992
|
return /env$/i.test(key) && !!value && typeof value === "object" && !Array.isArray(value);
|
|
15369
15993
|
}
|
|
@@ -15402,14 +16026,14 @@ var envGuardResult = guardAgentBridgeEnv({
|
|
|
15402
16026
|
});
|
|
15403
16027
|
var stateDir = new StateDirResolver;
|
|
15404
16028
|
stateDir.ensure();
|
|
16029
|
+
var processLogger = createProcessLogger({ component: "AgentBridgeFrontend", logFile: stateDir.logFile });
|
|
15405
16030
|
var configService = new ConfigService;
|
|
15406
|
-
var config2 = configService.loadOrDefault();
|
|
16031
|
+
var config2 = configService.loadOrDefault(processLogger.log);
|
|
15407
16032
|
var CONTROL_PORT = parseInt(process.env.AGENTBRIDGE_CONTROL_PORT ?? "4502", 10);
|
|
15408
|
-
var processLogger = createProcessLogger({ component: "AgentBridgeFrontend", logFile: stateDir.logFile });
|
|
15409
16033
|
var daemonLifecycle = new DaemonLifecycle({ stateDir, controlPort: CONTROL_PORT, log });
|
|
15410
16034
|
var CONTROL_WS_URL = daemonLifecycle.controlWsUrl;
|
|
15411
16035
|
var claude = new ClaudeAdapter(stateDir.logFile);
|
|
15412
|
-
var daemonClient = new DaemonClient(CONTROL_WS_URL, { identity: currentClientIdentity
|
|
16036
|
+
var daemonClient = new DaemonClient(CONTROL_WS_URL, { identity: currentClientIdentity });
|
|
15413
16037
|
var shuttingDown = false;
|
|
15414
16038
|
var daemonDisabled = false;
|
|
15415
16039
|
var daemonDisabledReason = null;
|
|
@@ -15422,6 +16046,7 @@ var lastReconnectNotifyTs = 0;
|
|
|
15422
16046
|
var disabledRecoveryTimer = null;
|
|
15423
16047
|
var disabledRecoveryInFlight = false;
|
|
15424
16048
|
var disabledRecoveryAttempts = 0;
|
|
16049
|
+
var nextSystemMessageId = 0;
|
|
15425
16050
|
var DISABLED_RECOVERY_MAX_ATTEMPTS = 6;
|
|
15426
16051
|
var DISABLED_RECOVERY_CONFIRM_TIMEOUT_MS = 1000;
|
|
15427
16052
|
if (process.env.AGENTBRIDGE_TRACE === "1") {
|
|
@@ -15445,7 +16070,7 @@ if (process.env.AGENTBRIDGE_TRACE === "1") {
|
|
|
15445
16070
|
});
|
|
15446
16071
|
} catch {}
|
|
15447
16072
|
}
|
|
15448
|
-
claude.setReplySender(async (msg, requireReply, onBusy) => {
|
|
16073
|
+
claude.setReplySender(async (msg, requireReply, onBusy, idempotencyKey) => {
|
|
15449
16074
|
if (msg.source !== "claude") {
|
|
15450
16075
|
return { success: false, error: "Invalid message source" };
|
|
15451
16076
|
}
|
|
@@ -15455,7 +16080,10 @@ claude.setReplySender(async (msg, requireReply, onBusy) => {
|
|
|
15455
16080
|
error: disabledReplyError(daemonDisabledReason ?? "killed")
|
|
15456
16081
|
};
|
|
15457
16082
|
}
|
|
15458
|
-
return daemonClient.sendReply(msg, requireReply, onBusy);
|
|
16083
|
+
return daemonClient.sendReply(msg, requireReply, onBusy, idempotencyKey);
|
|
16084
|
+
});
|
|
16085
|
+
daemonClient.on("turnStarted", ({ requestId, idempotencyKey, threadId, turnId }) => {
|
|
16086
|
+
log(`Codex turn started for reply ${requestId} (turn=${turnId}, thread=${threadId}` + `${idempotencyKey ? `, idempotencyKey=${idempotencyKey}` : ""})`);
|
|
15459
16087
|
});
|
|
15460
16088
|
daemonClient.on("codexMessage", (message) => {
|
|
15461
16089
|
log(`Forwarding daemon \u2192 Claude (${message.content.length} chars)`);
|
|
@@ -15513,6 +16141,16 @@ daemonClient.on("rejected", async (code) => {
|
|
|
15513
16141
|
notificationId = "system_bridge_pair_mismatch";
|
|
15514
16142
|
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`;
|
|
15515
16143
|
break;
|
|
16144
|
+
case CLOSE_CODE_TOKEN_MISMATCH:
|
|
16145
|
+
reason = "rejected";
|
|
16146
|
+
notificationId = "system_bridge_token_mismatch";
|
|
16147
|
+
notificationContent = `\u26A0\uFE0F AgentBridge daemon rejected this session \u2014 control token mismatch (the daemon likely restarted and rotated its token). Start a fresh session with \`${pairScopedCommand("claude")}\` to pick up the current token. AgentBridge \u62D2\u7EDD\u4E86\u6B64\u4F1A\u8BDD\u2014\u2014\u63A7\u5236\u4EE4\u724C\u4E0D\u5339\u914D\uFF08daemon \u53EF\u80FD\u5DF2\u91CD\u542F\u5E76\u8F6E\u6362\u4EE4\u724C\uFF09\u3002\u8BF7\u7528 \`${pairScopedCommand("claude")}\` \u91CD\u65B0\u542F\u52A8\u4EE5\u83B7\u53D6\u6700\u65B0\u4EE4\u724C\u3002`;
|
|
16148
|
+
break;
|
|
16149
|
+
case CLOSE_CODE_CONTRACT_MISMATCH:
|
|
16150
|
+
reason = "rejected";
|
|
16151
|
+
notificationId = "system_bridge_contract_mismatch";
|
|
16152
|
+
notificationContent = `\u26A0\uFE0F AgentBridge daemon rejected this session \u2014 protocol contract mismatch. The installed plugin and the running daemon are built from out-of-sync protocol versions. Run \`bun run install:global\` to rebuild + reinstall, then close and reopen Claude Code. Do NOT kill other pairs \u2014 this is local build skew, not a session conflict. AgentBridge \u62D2\u7EDD\u4E86\u6B64\u4F1A\u8BDD\u2014\u2014\u534F\u8BAE\u5951\u7EA6\u7248\u672C\u4E0D\u5339\u914D\u3002\u5DF2\u5B89\u88C5\u7684\u63D2\u4EF6\u4E0E\u8FD0\u884C\u4E2D\u7684 daemon \u534F\u8BAE\u7248\u672C\u4E0D\u4E00\u81F4\u3002\u8BF7\u8FD0\u884C \`bun run install:global\` \u91CD\u65B0\u7F16\u8BD1\u5E76\u5B89\u88C5\uFF0C\u7136\u540E\u5173\u95ED\u5E76\u91CD\u65B0\u6253\u5F00 Claude Code\u3002\u8BF7\u52FF kill \u5176\u5B83 pair\u2014\u2014\u8FD9\u662F\u672C\u5730\u6784\u5EFA\u7248\u672C\u6F02\u79FB\uFF0C\u4E0D\u662F\u4F1A\u8BDD\u51B2\u7A81\u3002`;
|
|
16153
|
+
break;
|
|
15516
16154
|
default:
|
|
15517
16155
|
reason = "rejected";
|
|
15518
16156
|
notificationId = "system_bridge_replaced";
|
|
@@ -15532,7 +16170,7 @@ daemonClient.on("rejected", async (code) => {
|
|
|
15532
16170
|
claude.on("ready", async () => {
|
|
15533
16171
|
log("MCP server ready (push delivery) \u2014 ensuring AgentBridge daemon...");
|
|
15534
16172
|
if (daemonLifecycle.wasKilled()) {
|
|
15535
|
-
await enterDisabledState("Killed sentinel found \u2014 bridge staying idle", `\u26D4 AgentBridge was stopped by
|
|
16173
|
+
await enterDisabledState("Killed sentinel found \u2014 bridge staying idle", `\u26D4 AgentBridge was stopped by \`${pairScopedCommand("kill")}\`. Bridge is staying idle. Restart Claude Code (\`${pairScopedCommand("claude")}\`), switch to a new conversation, or run \`/resume\` to reconnect.`);
|
|
15536
16174
|
return;
|
|
15537
16175
|
}
|
|
15538
16176
|
try {
|
|
@@ -15594,11 +16232,11 @@ var reconnectTask = null;
|
|
|
15594
16232
|
async function notifyIfDaemonKilled(logMessage) {
|
|
15595
16233
|
if (!daemonLifecycle.wasKilled())
|
|
15596
16234
|
return false;
|
|
15597
|
-
await enterDisabledState(logMessage, `\u26D4 AgentBridge was stopped by
|
|
16235
|
+
await enterDisabledState(logMessage, `\u26D4 AgentBridge was stopped by \`${pairScopedCommand("kill")}\`. Bridge is staying idle. Restart Claude Code (\`${pairScopedCommand("claude")}\`), switch to a new conversation, or run \`/resume\` to reconnect.`);
|
|
15598
16236
|
return true;
|
|
15599
16237
|
}
|
|
15600
16238
|
async function notifyIfPairRemoved(logMessage) {
|
|
15601
|
-
if (
|
|
16239
|
+
if (existsSync7(stateDir.dir))
|
|
15602
16240
|
return false;
|
|
15603
16241
|
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`);
|
|
15604
16242
|
return true;
|
|
@@ -15631,6 +16269,9 @@ function reconnectToDaemon() {
|
|
|
15631
16269
|
}
|
|
15632
16270
|
try {
|
|
15633
16271
|
await connectToDaemon(true);
|
|
16272
|
+
if (!shouldEmitReconnectSuccess({ daemonDisabled })) {
|
|
16273
|
+
return;
|
|
16274
|
+
}
|
|
15634
16275
|
log("Reconnected to AgentBridge daemon successfully");
|
|
15635
16276
|
const now = Date.now();
|
|
15636
16277
|
if (now - lastReconnectNotifyTs >= RECONNECT_NOTIFY_COOLDOWN_MS) {
|
|
@@ -15749,13 +16390,14 @@ async function pollDisabledRecovery() {
|
|
|
15749
16390
|
}
|
|
15750
16391
|
function systemMessage(idPrefix, content) {
|
|
15751
16392
|
return {
|
|
15752
|
-
id: `${idPrefix}_${
|
|
16393
|
+
id: `${idPrefix}_${++nextSystemMessageId}`,
|
|
15753
16394
|
source: "codex",
|
|
15754
16395
|
content,
|
|
15755
16396
|
timestamp: Date.now()
|
|
15756
16397
|
};
|
|
15757
16398
|
}
|
|
15758
16399
|
function currentClientIdentity() {
|
|
16400
|
+
const controlToken = readControlToken(resolveControlTokenPath(stateDir.dir));
|
|
15759
16401
|
return {
|
|
15760
16402
|
pairId: process.env.AGENTBRIDGE_PAIR_ID ?? null,
|
|
15761
16403
|
pairName: process.env.AGENTBRIDGE_PAIR_NAME ?? null,
|
|
@@ -15763,7 +16405,8 @@ function currentClientIdentity() {
|
|
|
15763
16405
|
baseDir: process.env.AGENTBRIDGE_BASE_DIR ?? null,
|
|
15764
16406
|
stateDir: stateDir.dir,
|
|
15765
16407
|
clientPid: process.pid,
|
|
15766
|
-
contractVersion: BUILD_INFO.contractVersion
|
|
16408
|
+
contractVersion: BUILD_INFO.contractVersion,
|
|
16409
|
+
...controlToken ? { controlToken } : {}
|
|
15767
16410
|
};
|
|
15768
16411
|
}
|
|
15769
16412
|
function shutdown(reason) {
|