@raysonmeng/agentbridge 0.1.12 → 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 +951 -412
- package/dist/daemon.js +1117 -422
- package/package.json +3 -1
- package/plugins/agentbridge/.claude-plugin/plugin.json +1 -1
- package/plugins/agentbridge/server/bridge-server.js +596 -180
- package/plugins/agentbridge/server/daemon.js +1117 -422
|
@@ -13662,6 +13662,7 @@ 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";
|
|
@@ -13805,6 +13806,10 @@ function formatError2(error2) {
|
|
|
13805
13806
|
import { mkdirSync, existsSync as existsSync2 } from "fs";
|
|
13806
13807
|
import { join } from "path";
|
|
13807
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
|
+
}
|
|
13808
13813
|
|
|
13809
13814
|
class StateDirResolver {
|
|
13810
13815
|
stateDir;
|
|
@@ -13812,8 +13817,7 @@ class StateDirResolver {
|
|
|
13812
13817
|
if (platform() === "darwin") {
|
|
13813
13818
|
return join(homedir(), "Library", "Application Support", "AgentBridge");
|
|
13814
13819
|
}
|
|
13815
|
-
|
|
13816
|
-
return join(xdgState, "agentbridge");
|
|
13820
|
+
return resolveXdgStateBase(process.env.XDG_STATE_HOME);
|
|
13817
13821
|
}
|
|
13818
13822
|
constructor(envOverride) {
|
|
13819
13823
|
const override = envOverride ?? process.env.AGENTBRIDGE_STATE_DIR;
|
|
@@ -13839,8 +13843,8 @@ class StateDirResolver {
|
|
|
13839
13843
|
get statusFile() {
|
|
13840
13844
|
return join(this.stateDir, "status.json");
|
|
13841
13845
|
}
|
|
13842
|
-
get
|
|
13843
|
-
return join(this.stateDir, "
|
|
13846
|
+
get daemonRecordFile() {
|
|
13847
|
+
return join(this.stateDir, "daemon.json");
|
|
13844
13848
|
}
|
|
13845
13849
|
get currentThreadFile() {
|
|
13846
13850
|
return join(this.stateDir, "current-thread.json");
|
|
@@ -13943,6 +13947,10 @@ function renderBudgetSnapshot(snapshot) {
|
|
|
13943
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";
|
|
13944
13948
|
|
|
13945
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;
|
|
13946
13954
|
var CLAUDE_INSTRUCTIONS = [
|
|
13947
13955
|
"Codex is an AI coding agent (OpenAI) running in a separate session on the same machine.",
|
|
13948
13956
|
"",
|
|
@@ -13991,10 +13999,20 @@ class ClaudeAdapter extends EventEmitter {
|
|
|
13991
13999
|
logFile;
|
|
13992
14000
|
logger;
|
|
13993
14001
|
pendingMessages = [];
|
|
14002
|
+
pendingMessageByteSizes = [];
|
|
14003
|
+
pendingMessageBytes = 0;
|
|
13994
14004
|
maxBufferedMessages;
|
|
14005
|
+
maxBufferedBytes;
|
|
13995
14006
|
droppedMessageCount = 0;
|
|
14007
|
+
oversizedMessageCount = 0;
|
|
14008
|
+
oversizedMessageBytes = 0;
|
|
14009
|
+
oversizedMessageSourceCounts = {};
|
|
14010
|
+
dedupeCapacity;
|
|
14011
|
+
dedupeTtlMs;
|
|
14012
|
+
monotonicNow;
|
|
14013
|
+
deliveredMessageIds = new Map;
|
|
13996
14014
|
budgetSnapshot = null;
|
|
13997
|
-
constructor(logFile = new StateDirResolver().logFile) {
|
|
14015
|
+
constructor(logFile = new StateDirResolver().logFile, options = {}) {
|
|
13998
14016
|
super();
|
|
13999
14017
|
this.logFile = logFile;
|
|
14000
14018
|
this.logger = createProcessLogger({ component: "ClaudeAdapter", logFile: this.logFile });
|
|
@@ -14005,7 +14023,11 @@ class ClaudeAdapter extends EventEmitter {
|
|
|
14005
14023
|
if (process.env.AGENTBRIDGE_MODE) {
|
|
14006
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.");
|
|
14007
14025
|
}
|
|
14008
|
-
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());
|
|
14009
14031
|
this.server = new Server({ name: "agentbridge", version: "0.1.0" }, {
|
|
14010
14032
|
capabilities: {
|
|
14011
14033
|
experimental: { "claude/channel": {} },
|
|
@@ -14032,10 +14054,12 @@ class ClaudeAdapter extends EventEmitter {
|
|
|
14032
14054
|
}
|
|
14033
14055
|
async pushNotification(message) {
|
|
14034
14056
|
this.log(`pushNotification (instance=${this.instanceId}, msgId=${message.id}, len=${message.content.length})`);
|
|
14057
|
+
if (!this.rememberDelivery(message))
|
|
14058
|
+
return;
|
|
14035
14059
|
await this.pushViaChannel(message);
|
|
14036
14060
|
}
|
|
14037
14061
|
async pushViaChannel(message) {
|
|
14038
|
-
const
|
|
14062
|
+
const deliveryAttemptId = `codex_msg_${this.notificationIdPrefix}_${++this.notificationSeq}`;
|
|
14039
14063
|
const ts = new Date(message.timestamp).toISOString();
|
|
14040
14064
|
try {
|
|
14041
14065
|
await this.server.notification({
|
|
@@ -14044,7 +14068,8 @@ class ClaudeAdapter extends EventEmitter {
|
|
|
14044
14068
|
content: message.content,
|
|
14045
14069
|
meta: {
|
|
14046
14070
|
chat_id: this.sessionId,
|
|
14047
|
-
message_id:
|
|
14071
|
+
message_id: message.id,
|
|
14072
|
+
delivery_attempt_id: deliveryAttemptId,
|
|
14048
14073
|
user: "Codex",
|
|
14049
14074
|
user_id: "codex",
|
|
14050
14075
|
ts,
|
|
@@ -14052,39 +14077,93 @@ class ClaudeAdapter extends EventEmitter {
|
|
|
14052
14077
|
}
|
|
14053
14078
|
}
|
|
14054
14079
|
});
|
|
14055
|
-
this.log(`Pushed notification: ${
|
|
14080
|
+
this.log(`Pushed notification: ${message.id} (attempt=${deliveryAttemptId})`);
|
|
14056
14081
|
} catch (e) {
|
|
14057
14082
|
this.log(`Push notification failed: ${e.message}`);
|
|
14058
14083
|
this.queueFallbackMessage(message);
|
|
14059
14084
|
}
|
|
14060
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
|
+
}
|
|
14061
14111
|
queueFallbackMessage(message) {
|
|
14062
|
-
|
|
14063
|
-
|
|
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);
|
|
14064
14127
|
this.droppedMessageCount++;
|
|
14065
|
-
|
|
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)`);
|
|
14066
14132
|
}
|
|
14067
14133
|
this.pendingMessages.push(message);
|
|
14068
|
-
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})`);
|
|
14069
14137
|
}
|
|
14070
14138
|
drainMessages() {
|
|
14071
|
-
this.log(`get_messages called (instance=${this.instanceId}, pending=${this.pendingMessages.length}, dropped=${this.droppedMessageCount})`);
|
|
14072
|
-
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) {
|
|
14073
14141
|
return {
|
|
14074
14142
|
content: [{ type: "text", text: "No new messages from Codex." }]
|
|
14075
14143
|
};
|
|
14076
14144
|
}
|
|
14077
14145
|
const messages = this.pendingMessages;
|
|
14078
14146
|
this.pendingMessages = [];
|
|
14147
|
+
this.pendingMessageByteSizes = [];
|
|
14148
|
+
this.pendingMessageBytes = 0;
|
|
14079
14149
|
const dropped = this.droppedMessageCount;
|
|
14080
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;
|
|
14081
14157
|
const count = messages.length;
|
|
14082
|
-
|
|
14158
|
+
const notices = [];
|
|
14083
14159
|
if (dropped > 0) {
|
|
14084
|
-
|
|
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
|
+
}
|
|
14085
14166
|
}
|
|
14086
|
-
header += `
|
|
14087
|
-
chat_id: ${this.sessionId}`;
|
|
14088
14167
|
const formatted = messages.map((msg, i) => {
|
|
14089
14168
|
const ts = new Date(msg.timestamp).toISOString();
|
|
14090
14169
|
return `---
|
|
@@ -14093,14 +14172,25 @@ Codex: ${msg.content}`;
|
|
|
14093
14172
|
}).join(`
|
|
14094
14173
|
|
|
14095
14174
|
`);
|
|
14096
|
-
|
|
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})`);
|
|
14097
14187
|
return {
|
|
14098
14188
|
content: [
|
|
14099
14189
|
{
|
|
14100
14190
|
type: "text",
|
|
14101
|
-
text:
|
|
14191
|
+
text: parts.join(`
|
|
14102
14192
|
|
|
14103
|
-
|
|
14193
|
+
`)
|
|
14104
14194
|
}
|
|
14105
14195
|
]
|
|
14106
14196
|
};
|
|
@@ -14256,11 +14346,37 @@ ${formatted}`
|
|
|
14256
14346
|
this.logger.log(msg);
|
|
14257
14347
|
}
|
|
14258
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
|
+
}
|
|
14259
14370
|
|
|
14260
14371
|
// src/contract-version.ts
|
|
14261
14372
|
var CONTRACT_VERSION = 1;
|
|
14262
14373
|
|
|
14263
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
|
+
}
|
|
14264
14380
|
function defineString(value, fallback) {
|
|
14265
14381
|
return typeof value === "string" && value.length > 0 ? value : fallback;
|
|
14266
14382
|
}
|
|
@@ -14273,15 +14389,23 @@ function defineNumber(value, fallback) {
|
|
|
14273
14389
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
14274
14390
|
}
|
|
14275
14391
|
var BUILD_INFO = Object.freeze({
|
|
14276
|
-
version: defineString("0.1.
|
|
14277
|
-
commit: defineString("
|
|
14392
|
+
version: defineString("0.1.13", "0.0.0-source"),
|
|
14393
|
+
commit: defineString("7a71869", "source"),
|
|
14278
14394
|
bundle: defineBundle("plugin"),
|
|
14279
|
-
contractVersion: defineNumber(1, CONTRACT_VERSION)
|
|
14395
|
+
contractVersion: defineNumber(1, CONTRACT_VERSION),
|
|
14396
|
+
codeHash: defineString("e1fd67d07c62", "source")
|
|
14280
14397
|
});
|
|
14281
14398
|
function sameRuntimeContract(a, b) {
|
|
14282
14399
|
if (!a || !b)
|
|
14283
14400
|
return false;
|
|
14284
|
-
|
|
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";
|
|
14285
14409
|
}
|
|
14286
14410
|
function compatibleContractVersion(a, b) {
|
|
14287
14411
|
if (!a || !b)
|
|
@@ -14291,7 +14415,8 @@ function compatibleContractVersion(a, b) {
|
|
|
14291
14415
|
function formatBuildInfo(build) {
|
|
14292
14416
|
if (!build)
|
|
14293
14417
|
return "<unknown>";
|
|
14294
|
-
|
|
14418
|
+
const codeHash = hasValidCodeHash(build) ? `/code-${build.codeHash}` : "";
|
|
14419
|
+
return `${build.version}/${build.commit}/${build.bundle}/contract-v${build.contractVersion}${codeHash}`;
|
|
14295
14420
|
}
|
|
14296
14421
|
|
|
14297
14422
|
// src/daemon-client.ts
|
|
@@ -14302,12 +14427,84 @@ var CLOSE_CODE_REPLACED = 4001;
|
|
|
14302
14427
|
var CLOSE_CODE_EVICTED_STALE = 4002;
|
|
14303
14428
|
var CLOSE_CODE_PROBE_IN_PROGRESS = 4003;
|
|
14304
14429
|
var CLOSE_CODE_PAIR_MISMATCH = 4004;
|
|
14430
|
+
var CLOSE_CODE_TOKEN_MISMATCH = 4005;
|
|
14431
|
+
var CLOSE_CODE_CONTRACT_MISMATCH = 4006;
|
|
14305
14432
|
|
|
14306
14433
|
// src/interrupt-timing.ts
|
|
14307
14434
|
var CLIENT_REPLY_TIMEOUT_MS = 15000;
|
|
14308
14435
|
var INTERRUPT_CLIENT_MARGIN_MS = 2000;
|
|
14309
14436
|
var MAX_INTERRUPT_TIMEOUT_MS = CLIENT_REPLY_TIMEOUT_MS - INTERRUPT_CLIENT_MARGIN_MS;
|
|
14310
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
|
+
}
|
|
14507
|
+
|
|
14311
14508
|
// src/daemon-client.ts
|
|
14312
14509
|
var nextSocketId = 0;
|
|
14313
14510
|
|
|
@@ -14317,7 +14514,8 @@ class DaemonClient extends EventEmitter2 {
|
|
|
14317
14514
|
ws = null;
|
|
14318
14515
|
wsId = 0;
|
|
14319
14516
|
nextRequestId = 1;
|
|
14320
|
-
pendingReplies = new
|
|
14517
|
+
pendingReplies = new PendingRequestRegistry;
|
|
14518
|
+
pendingEventWaiters = new PendingRequestRegistry;
|
|
14321
14519
|
constructor(url, options = {}) {
|
|
14322
14520
|
super();
|
|
14323
14521
|
this.url = url;
|
|
@@ -14363,82 +14561,73 @@ class DaemonClient extends EventEmitter2 {
|
|
|
14363
14561
|
});
|
|
14364
14562
|
}
|
|
14365
14563
|
attachClaude() {
|
|
14564
|
+
const identity = this.resolveIdentity();
|
|
14366
14565
|
this.send({
|
|
14367
14566
|
type: "claude_connect",
|
|
14368
|
-
...
|
|
14567
|
+
...identity ? { identity } : {}
|
|
14369
14568
|
});
|
|
14370
14569
|
}
|
|
14570
|
+
resolveIdentity() {
|
|
14571
|
+
const opt = this.options.identity;
|
|
14572
|
+
return typeof opt === "function" ? opt() : opt;
|
|
14573
|
+
}
|
|
14371
14574
|
async attachClaudeAndWaitForStatus(timeoutMs = 1000) {
|
|
14372
14575
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
14373
14576
|
return null;
|
|
14374
14577
|
}
|
|
14375
|
-
return
|
|
14376
|
-
|
|
14377
|
-
|
|
14378
|
-
|
|
14379
|
-
|
|
14380
|
-
|
|
14381
|
-
|
|
14382
|
-
if (timer) {
|
|
14383
|
-
clearTimeout(timer);
|
|
14384
|
-
timer = null;
|
|
14385
|
-
}
|
|
14386
|
-
this.off("status", onStatus);
|
|
14387
|
-
this.off("rejected", onRejected);
|
|
14388
|
-
this.off("disconnect", onDisconnect);
|
|
14389
|
-
};
|
|
14390
|
-
const finish = (value) => {
|
|
14391
|
-
cleanup();
|
|
14392
|
-
resolve(value);
|
|
14393
|
-
};
|
|
14394
|
-
const onStatus = (status) => finish(status);
|
|
14395
|
-
const onRejected = () => finish(null);
|
|
14396
|
-
const onDisconnect = () => finish(null);
|
|
14397
|
-
this.on("status", onStatus);
|
|
14398
|
-
this.on("rejected", onRejected);
|
|
14399
|
-
this.on("disconnect", onDisconnect);
|
|
14400
|
-
timer = setTimeout(() => {
|
|
14401
|
-
finish(null);
|
|
14402
|
-
}, timeoutMs);
|
|
14403
|
-
try {
|
|
14404
|
-
this.attachClaude();
|
|
14405
|
-
} catch {
|
|
14406
|
-
finish(null);
|
|
14407
|
-
}
|
|
14578
|
+
return this.awaitTypedResponse({
|
|
14579
|
+
key: "status",
|
|
14580
|
+
successEvent: "status",
|
|
14581
|
+
successValue: (status) => status,
|
|
14582
|
+
failValue: null,
|
|
14583
|
+
timeoutMs,
|
|
14584
|
+
send: () => this.attachClaude()
|
|
14408
14585
|
});
|
|
14409
14586
|
}
|
|
14410
14587
|
async probeIncumbent(timeoutMs = 3000) {
|
|
14411
14588
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
14412
14589
|
return { connected: false, alive: false };
|
|
14413
14590
|
}
|
|
14414
|
-
return
|
|
14415
|
-
|
|
14416
|
-
|
|
14417
|
-
|
|
14418
|
-
|
|
14419
|
-
|
|
14420
|
-
|
|
14421
|
-
if (timer)
|
|
14422
|
-
clearTimeout(timer);
|
|
14423
|
-
this.off("incumbentStatus", onStatus);
|
|
14424
|
-
this.off("disconnect", onDisconnect);
|
|
14425
|
-
this.off("rejected", onRejected);
|
|
14426
|
-
resolve(value);
|
|
14427
|
-
};
|
|
14428
|
-
const onStatus = (s) => finish(s);
|
|
14429
|
-
const onDisconnect = () => finish({ connected: false, alive: false });
|
|
14430
|
-
const onRejected = () => finish({ connected: false, alive: false });
|
|
14431
|
-
this.on("incumbentStatus", onStatus);
|
|
14432
|
-
this.on("disconnect", onDisconnect);
|
|
14433
|
-
this.on("rejected", onRejected);
|
|
14434
|
-
timer = setTimeout(() => finish({ connected: false, alive: false }), timeoutMs);
|
|
14435
|
-
try {
|
|
14436
|
-
this.send({ type: "probe_incumbent" });
|
|
14437
|
-
} catch {
|
|
14438
|
-
finish({ connected: false, alive: false });
|
|
14439
|
-
}
|
|
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" })
|
|
14440
14598
|
});
|
|
14441
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)
|
|
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;
|
|
14630
|
+
}
|
|
14442
14631
|
async disconnect() {
|
|
14443
14632
|
if (!this.ws)
|
|
14444
14633
|
return;
|
|
@@ -14456,21 +14645,19 @@ class DaemonClient extends EventEmitter2 {
|
|
|
14456
14645
|
return { success: false, error: "AgentBridge daemon is not connected." };
|
|
14457
14646
|
}
|
|
14458
14647
|
const requestId = `reply_${Date.now()}_${this.nextRequestId++}`;
|
|
14459
|
-
|
|
14460
|
-
|
|
14461
|
-
|
|
14462
|
-
|
|
14463
|
-
|
|
14464
|
-
|
|
14465
|
-
|
|
14466
|
-
|
|
14467
|
-
|
|
14468
|
-
|
|
14469
|
-
|
|
14470
|
-
...onBusy && onBusy !== "reject" ? { onBusy } : {},
|
|
14471
|
-
...idempotencyKey ? { idempotencyKey } : {}
|
|
14472
|
-
});
|
|
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." })
|
|
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 } : {}
|
|
14473
14659
|
});
|
|
14660
|
+
return pending;
|
|
14474
14661
|
}
|
|
14475
14662
|
attachSocketHandlers(ws, socketId) {
|
|
14476
14663
|
ws.onmessage = (event) => {
|
|
@@ -14486,12 +14673,7 @@ class DaemonClient extends EventEmitter2 {
|
|
|
14486
14673
|
this.emit("codexMessage", message.message);
|
|
14487
14674
|
return;
|
|
14488
14675
|
case "claude_to_codex_result": {
|
|
14489
|
-
|
|
14490
|
-
if (!pending)
|
|
14491
|
-
return;
|
|
14492
|
-
clearTimeout(pending.timer);
|
|
14493
|
-
this.pendingReplies.delete(message.requestId);
|
|
14494
|
-
pending.resolve({
|
|
14676
|
+
this.pendingReplies.settle(message.requestId, {
|
|
14495
14677
|
success: message.success,
|
|
14496
14678
|
error: message.error,
|
|
14497
14679
|
...message.code !== undefined ? { code: message.code } : {},
|
|
@@ -14522,7 +14704,7 @@ class DaemonClient extends EventEmitter2 {
|
|
|
14522
14704
|
if (isCurrent) {
|
|
14523
14705
|
this.ws = null;
|
|
14524
14706
|
this.rejectPendingReplies("AgentBridge daemon disconnected.");
|
|
14525
|
-
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) {
|
|
14526
14708
|
this.emit("rejected", event.code);
|
|
14527
14709
|
} else {
|
|
14528
14710
|
this.emit("disconnect");
|
|
@@ -14532,11 +14714,7 @@ class DaemonClient extends EventEmitter2 {
|
|
|
14532
14714
|
ws.onerror = () => {};
|
|
14533
14715
|
}
|
|
14534
14716
|
rejectPendingReplies(error2) {
|
|
14535
|
-
|
|
14536
|
-
clearTimeout(pending.timer);
|
|
14537
|
-
pending.resolve({ success: false, error: error2 });
|
|
14538
|
-
this.pendingReplies.delete(requestId);
|
|
14539
|
-
}
|
|
14717
|
+
this.pendingReplies.settleAll(() => ({ success: false, error: error2 }));
|
|
14540
14718
|
}
|
|
14541
14719
|
send(message) {
|
|
14542
14720
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
@@ -14552,9 +14730,44 @@ class DaemonClient extends EventEmitter2 {
|
|
|
14552
14730
|
|
|
14553
14731
|
// src/daemon-lifecycle.ts
|
|
14554
14732
|
import { spawn } from "child_process";
|
|
14555
|
-
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";
|
|
14556
14734
|
import { fileURLToPath } from "url";
|
|
14557
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
|
+
|
|
14558
14771
|
// src/env-utils.ts
|
|
14559
14772
|
function parsePositiveIntEnv(name, fallback, log = () => {}, env = process.env) {
|
|
14560
14773
|
const raw = env[name];
|
|
@@ -14603,12 +14816,144 @@ function isAgentBridgeProcess(pid, lookup = commandForPid) {
|
|
|
14603
14816
|
return cmd.includes("agentbridge") || cmd.includes("agent_bridge");
|
|
14604
14817
|
}
|
|
14605
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
|
+
|
|
14606
14949
|
// src/daemon-lifecycle.ts
|
|
14607
14950
|
var DEFAULT_DAEMON_ENTRY = import.meta.url.endsWith(".ts") ? "./daemon.ts" : "./daemon.js";
|
|
14608
14951
|
var DAEMON_ENTRY = process.env.AGENTBRIDGE_DAEMON_ENTRY || DEFAULT_DAEMON_ENTRY;
|
|
14609
14952
|
var DAEMON_PATH = fileURLToPath(new URL(DAEMON_ENTRY, import.meta.url));
|
|
14610
14953
|
var REUSE_READY_RETRIES = parsePositiveIntEnv("AGENTBRIDGE_REUSE_READY_RETRIES", 12);
|
|
14611
14954
|
var REUSE_READY_DELAY_MS = 250;
|
|
14955
|
+
var WAIT_READY_RETRIES = 40;
|
|
14956
|
+
var WAIT_READY_DELAY_MS = 250;
|
|
14612
14957
|
var HEALTH_FETCH_TIMEOUT_MS = 500;
|
|
14613
14958
|
var LOCK_IDENTITY_GRACE_MS = parsePositiveIntEnv("AGENTBRIDGE_LOCK_IDENTITY_GRACE_MS", 120000);
|
|
14614
14959
|
function isReuseVerdict(verdict) {
|
|
@@ -14646,22 +14991,33 @@ function classifyDaemon(expectedPairId, status, buildInfo) {
|
|
|
14646
14991
|
reason: "runtime build drift has a compatible contract and a live Codex TUI is attached"
|
|
14647
14992
|
};
|
|
14648
14993
|
}
|
|
14994
|
+
const basis = runtimeContractComparisonBasis(status.build, buildInfo) === "codeHash" ? "compared by codeHash" : "compared by commit stamp; legacy build without codeHash";
|
|
14649
14995
|
return {
|
|
14650
14996
|
verdict: "replace-drifted",
|
|
14651
|
-
reason: `runtime build ${formatBuildInfo(status.build)} does not match launcher
|
|
14997
|
+
reason: `runtime build ${formatBuildInfo(status.build)} does not match launcher ` + `${formatBuildInfo(buildInfo)} (${basis})`
|
|
14652
14998
|
};
|
|
14653
14999
|
}
|
|
14654
15000
|
return { verdict: "reuse", reason: "daemon pair and runtime contract match" };
|
|
14655
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
|
+
}
|
|
14656
15010
|
|
|
14657
15011
|
class DaemonLifecycle {
|
|
14658
15012
|
stateDir;
|
|
14659
15013
|
controlPort;
|
|
14660
15014
|
log;
|
|
15015
|
+
timing;
|
|
14661
15016
|
constructor(opts) {
|
|
14662
15017
|
this.stateDir = opts.stateDir;
|
|
14663
15018
|
this.controlPort = opts.controlPort;
|
|
14664
15019
|
this.log = opts.log;
|
|
15020
|
+
this.timing = resolveTiming(opts.timing);
|
|
14665
15021
|
}
|
|
14666
15022
|
get healthUrl() {
|
|
14667
15023
|
return `http://127.0.0.1:${this.controlPort}/healthz`;
|
|
@@ -14718,7 +15074,7 @@ class DaemonLifecycle {
|
|
|
14718
15074
|
break;
|
|
14719
15075
|
}
|
|
14720
15076
|
try {
|
|
14721
|
-
await this.waitForReady(
|
|
15077
|
+
await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
|
|
14722
15078
|
return;
|
|
14723
15079
|
} catch {
|
|
14724
15080
|
this.log(`Daemon on control port ${this.controlPort} is healthy but not ready within reuse window \u2014 replacing`);
|
|
@@ -14731,7 +15087,7 @@ class DaemonLifecycle {
|
|
|
14731
15087
|
if (isProcessAlive(existingPid)) {
|
|
14732
15088
|
if (isAgentBridgeDaemon(existingPid)) {
|
|
14733
15089
|
try {
|
|
14734
|
-
await this.waitForReady(
|
|
15090
|
+
await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
|
|
14735
15091
|
return;
|
|
14736
15092
|
} catch {
|
|
14737
15093
|
this.log(`Existing daemon process ${existingPid} never became ready \u2014 replacing`);
|
|
@@ -14759,7 +15115,7 @@ class DaemonLifecycle {
|
|
|
14759
15115
|
await this.kill(3000, status?.pid);
|
|
14760
15116
|
} else {
|
|
14761
15117
|
try {
|
|
14762
|
-
await this.waitForReady(
|
|
15118
|
+
await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
|
|
14763
15119
|
return;
|
|
14764
15120
|
} catch {
|
|
14765
15121
|
this.log(`Daemon on control port ${this.controlPort} is healthy but not ready under startup lock \u2014 replacing`);
|
|
@@ -14768,7 +15124,7 @@ class DaemonLifecycle {
|
|
|
14768
15124
|
}
|
|
14769
15125
|
}
|
|
14770
15126
|
this.launch();
|
|
14771
|
-
await this.waitForReady();
|
|
15127
|
+
await this.waitForReady(this.timing.waitReadyRetries, this.timing.waitReadyDelayMs);
|
|
14772
15128
|
});
|
|
14773
15129
|
}
|
|
14774
15130
|
async isHealthy() {
|
|
@@ -14795,7 +15151,7 @@ class DaemonLifecycle {
|
|
|
14795
15151
|
return false;
|
|
14796
15152
|
}
|
|
14797
15153
|
}
|
|
14798
|
-
async waitForReady(maxRetries =
|
|
15154
|
+
async waitForReady(maxRetries = WAIT_READY_RETRIES, delayMs = WAIT_READY_DELAY_MS) {
|
|
14799
15155
|
for (let attempt = 0;attempt < maxRetries; attempt++) {
|
|
14800
15156
|
if (await this.isReady())
|
|
14801
15157
|
return;
|
|
@@ -14803,7 +15159,7 @@ class DaemonLifecycle {
|
|
|
14803
15159
|
}
|
|
14804
15160
|
throw new Error(`Timed out waiting for AgentBridge daemon readiness on ${this.readyUrl}`);
|
|
14805
15161
|
}
|
|
14806
|
-
async waitForReadyAndOurs(maxRetries =
|
|
15162
|
+
async waitForReadyAndOurs(maxRetries = WAIT_READY_RETRIES, delayMs = WAIT_READY_DELAY_MS) {
|
|
14807
15163
|
for (let attempt = 0;attempt < maxRetries; attempt++) {
|
|
14808
15164
|
if (await this.isReady()) {
|
|
14809
15165
|
const status = await this.fetchStatus();
|
|
@@ -14819,22 +15175,35 @@ class DaemonLifecycle {
|
|
|
14819
15175
|
}
|
|
14820
15176
|
throw new Error(`Timed out waiting for AgentBridge daemon readiness+identity on ${this.readyUrl} (control port ${this.controlPort})`);
|
|
14821
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
|
+
}
|
|
14822
15193
|
readStatus() {
|
|
14823
15194
|
try {
|
|
14824
|
-
const raw =
|
|
15195
|
+
const raw = readFileSync2(this.stateDir.statusFile, "utf-8");
|
|
14825
15196
|
return JSON.parse(raw);
|
|
14826
15197
|
} catch {
|
|
14827
15198
|
return null;
|
|
14828
15199
|
}
|
|
14829
15200
|
}
|
|
14830
15201
|
writeStatus(status) {
|
|
14831
|
-
this.stateDir.
|
|
14832
|
-
writeFileSync(this.stateDir.statusFile, JSON.stringify(status, null, 2) + `
|
|
14833
|
-
`, "utf-8");
|
|
15202
|
+
atomicWriteJson(this.stateDir.statusFile, status);
|
|
14834
15203
|
}
|
|
14835
15204
|
readPid() {
|
|
14836
15205
|
try {
|
|
14837
|
-
const raw =
|
|
15206
|
+
const raw = readFileSync2(this.stateDir.pidFile, "utf-8").trim();
|
|
14838
15207
|
if (!raw)
|
|
14839
15208
|
return null;
|
|
14840
15209
|
const pid = Number.parseInt(raw, 10);
|
|
@@ -14844,28 +15213,27 @@ class DaemonLifecycle {
|
|
|
14844
15213
|
}
|
|
14845
15214
|
}
|
|
14846
15215
|
writePid(pid) {
|
|
14847
|
-
this.stateDir.
|
|
14848
|
-
|
|
14849
|
-
`, "utf-8");
|
|
15216
|
+
atomicWriteText(this.stateDir.pidFile, `${pid ?? process.pid}
|
|
15217
|
+
`);
|
|
14850
15218
|
}
|
|
14851
15219
|
removePidFile() {
|
|
14852
15220
|
try {
|
|
14853
|
-
|
|
15221
|
+
unlinkSync3(this.stateDir.pidFile);
|
|
14854
15222
|
} catch {}
|
|
14855
15223
|
}
|
|
14856
15224
|
removeStatusFile() {
|
|
14857
15225
|
try {
|
|
14858
|
-
|
|
15226
|
+
unlinkSync3(this.stateDir.statusFile);
|
|
14859
15227
|
} catch {}
|
|
14860
15228
|
}
|
|
14861
15229
|
markKilled() {
|
|
14862
15230
|
this.stateDir.ensure();
|
|
14863
|
-
|
|
15231
|
+
writeFileSync2(this.stateDir.killedFile, `${Date.now()}
|
|
14864
15232
|
`, "utf-8");
|
|
14865
15233
|
}
|
|
14866
15234
|
clearKilled() {
|
|
14867
15235
|
try {
|
|
14868
|
-
|
|
15236
|
+
unlinkSync3(this.stateDir.killedFile);
|
|
14869
15237
|
} catch {}
|
|
14870
15238
|
}
|
|
14871
15239
|
wasKilled() {
|
|
@@ -14887,8 +15255,10 @@ class DaemonLifecycle {
|
|
|
14887
15255
|
daemonProc.unref();
|
|
14888
15256
|
}
|
|
14889
15257
|
removeStalePidFile() {
|
|
14890
|
-
this.log("Removing stale
|
|
15258
|
+
this.log("Removing stale daemon identity files");
|
|
14891
15259
|
this.removePidFile();
|
|
15260
|
+
this.removeStatusFile();
|
|
15261
|
+
this.removeDaemonRecord();
|
|
14892
15262
|
}
|
|
14893
15263
|
async replaceUnhealthyDaemon(statusPid) {
|
|
14894
15264
|
await this.withStartupLockStrict(async (locked) => {
|
|
@@ -14904,7 +15274,7 @@ class DaemonLifecycle {
|
|
|
14904
15274
|
}
|
|
14905
15275
|
if (isReuseVerdict(classification.verdict)) {
|
|
14906
15276
|
try {
|
|
14907
|
-
await this.waitForReady(
|
|
15277
|
+
await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
|
|
14908
15278
|
return;
|
|
14909
15279
|
} catch {}
|
|
14910
15280
|
}
|
|
@@ -14912,12 +15282,12 @@ class DaemonLifecycle {
|
|
|
14912
15282
|
this.log(`Killing unhealthy daemon on control port ${this.controlPort} and relaunching`);
|
|
14913
15283
|
await this.kill(3000, statusPid);
|
|
14914
15284
|
this.launch();
|
|
14915
|
-
await this.waitForReady();
|
|
15285
|
+
await this.waitForReady(this.timing.waitReadyRetries, this.timing.waitReadyDelayMs);
|
|
14916
15286
|
});
|
|
14917
15287
|
}
|
|
14918
15288
|
async waitForContendedStartupLock() {
|
|
14919
15289
|
this.log("Another process holds the startup lock, waiting for readiness+identity...");
|
|
14920
|
-
await this.waitForReadyAndOurs();
|
|
15290
|
+
await this.waitForReadyAndOurs(this.timing.waitReadyRetries, this.timing.waitReadyDelayMs);
|
|
14921
15291
|
}
|
|
14922
15292
|
async withStartupLockStrict(fn) {
|
|
14923
15293
|
const locked = this.acquireLockStrict();
|
|
@@ -14932,15 +15302,15 @@ class DaemonLifecycle {
|
|
|
14932
15302
|
this.stateDir.ensure();
|
|
14933
15303
|
let fd = null;
|
|
14934
15304
|
try {
|
|
14935
|
-
fd =
|
|
14936
|
-
|
|
15305
|
+
fd = openSync2(this.stateDir.lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
|
|
15306
|
+
writeFileSync2(fd, `${process.pid}
|
|
14937
15307
|
`);
|
|
14938
|
-
|
|
15308
|
+
closeSync2(fd);
|
|
14939
15309
|
return true;
|
|
14940
15310
|
} catch (err) {
|
|
14941
15311
|
if (fd !== null && err.code !== "EEXIST") {
|
|
14942
15312
|
try {
|
|
14943
|
-
|
|
15313
|
+
closeSync2(fd);
|
|
14944
15314
|
} catch {}
|
|
14945
15315
|
this.releaseLock();
|
|
14946
15316
|
}
|
|
@@ -14948,7 +15318,7 @@ class DaemonLifecycle {
|
|
|
14948
15318
|
if (reclaimed)
|
|
14949
15319
|
return false;
|
|
14950
15320
|
try {
|
|
14951
|
-
const holderPid = Number.parseInt(
|
|
15321
|
+
const holderPid = Number.parseInt(readFileSync2(this.stateDir.lockFile, "utf-8").trim(), 10);
|
|
14952
15322
|
if (Number.isFinite(holderPid) && !isProcessAlive(holderPid)) {
|
|
14953
15323
|
this.log(`Stale startup lock from dead process ${holderPid}, reclaiming`);
|
|
14954
15324
|
this.releaseLock();
|
|
@@ -14977,7 +15347,7 @@ class DaemonLifecycle {
|
|
|
14977
15347
|
}
|
|
14978
15348
|
releaseLock() {
|
|
14979
15349
|
try {
|
|
14980
|
-
|
|
15350
|
+
unlinkSync3(this.stateDir.lockFile);
|
|
14981
15351
|
} catch {}
|
|
14982
15352
|
}
|
|
14983
15353
|
async kill(gracefulTimeoutMs = 3000, pidOverride) {
|
|
@@ -15023,6 +15393,7 @@ class DaemonLifecycle {
|
|
|
15023
15393
|
cleanup() {
|
|
15024
15394
|
this.removePidFile();
|
|
15025
15395
|
this.removeStatusFile();
|
|
15396
|
+
this.removeDaemonRecord();
|
|
15026
15397
|
}
|
|
15027
15398
|
}
|
|
15028
15399
|
async function fetchWithTimeout(url, timeoutMs = HEALTH_FETCH_TIMEOUT_MS) {
|
|
@@ -15036,7 +15407,7 @@ async function fetchWithTimeout(url, timeoutMs = HEALTH_FETCH_TIMEOUT_MS) {
|
|
|
15036
15407
|
}
|
|
15037
15408
|
|
|
15038
15409
|
// src/config-service.ts
|
|
15039
|
-
import { readFileSync as
|
|
15410
|
+
import { readFileSync as readFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
|
|
15040
15411
|
import { join as join2 } from "path";
|
|
15041
15412
|
var DEFAULT_BUDGET_CONFIG = {
|
|
15042
15413
|
enabled: true,
|
|
@@ -15192,13 +15563,13 @@ function normalizeConfig(raw) {
|
|
|
15192
15563
|
return {
|
|
15193
15564
|
version: typeof config2.version === "string" ? config2.version : DEFAULT_CONFIG.version,
|
|
15194
15565
|
codex: {
|
|
15195
|
-
appPort:
|
|
15196
|
-
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)
|
|
15197
15568
|
},
|
|
15198
15569
|
turnCoordination: {
|
|
15199
|
-
attentionWindowSeconds:
|
|
15570
|
+
attentionWindowSeconds: normalizeBoundedInteger(turnCoordination.attentionWindowSeconds, DEFAULT_CONFIG.turnCoordination.attentionWindowSeconds, 0, Number.MAX_SAFE_INTEGER)
|
|
15200
15571
|
},
|
|
15201
|
-
idleShutdownSeconds:
|
|
15572
|
+
idleShutdownSeconds: normalizeBoundedInteger(config2.idleShutdownSeconds, DEFAULT_CONFIG.idleShutdownSeconds, 1, Number.MAX_SAFE_INTEGER),
|
|
15202
15573
|
budget: normalizeBudgetConfig(config2.budget)
|
|
15203
15574
|
};
|
|
15204
15575
|
}
|
|
@@ -15217,7 +15588,7 @@ class ConfigService {
|
|
|
15217
15588
|
load() {
|
|
15218
15589
|
let raw;
|
|
15219
15590
|
try {
|
|
15220
|
-
raw =
|
|
15591
|
+
raw = readFileSync3(this.configPath, "utf-8");
|
|
15221
15592
|
} catch (err) {
|
|
15222
15593
|
if (err?.code === "ENOENT") {
|
|
15223
15594
|
return { state: "absent" };
|
|
@@ -15270,9 +15641,7 @@ class ConfigService {
|
|
|
15270
15641
|
};
|
|
15271
15642
|
}
|
|
15272
15643
|
save(config2) {
|
|
15273
|
-
this.
|
|
15274
|
-
writeFileSync2(this.configPath, JSON.stringify(config2, null, 2) + `
|
|
15275
|
-
`, "utf-8");
|
|
15644
|
+
atomicWriteJson(this.configPath, config2);
|
|
15276
15645
|
}
|
|
15277
15646
|
initDefaults() {
|
|
15278
15647
|
this.ensureConfigDir();
|
|
@@ -15288,34 +15657,46 @@ class ConfigService {
|
|
|
15288
15657
|
}
|
|
15289
15658
|
ensureConfigDir() {
|
|
15290
15659
|
if (!existsSync4(this.configDir)) {
|
|
15291
|
-
|
|
15660
|
+
mkdirSync3(this.configDir, { recursive: true });
|
|
15292
15661
|
}
|
|
15293
15662
|
}
|
|
15294
15663
|
}
|
|
15295
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
|
+
|
|
15296
15680
|
// src/pair-registry.ts
|
|
15297
15681
|
import {
|
|
15298
|
-
closeSync as closeSync2,
|
|
15299
15682
|
existsSync as existsSync5,
|
|
15300
|
-
fsyncSync,
|
|
15301
15683
|
linkSync,
|
|
15302
15684
|
lstatSync,
|
|
15303
|
-
mkdirSync as
|
|
15304
|
-
openSync as openSync2,
|
|
15685
|
+
mkdirSync as mkdirSync4,
|
|
15305
15686
|
readdirSync,
|
|
15306
|
-
readFileSync as
|
|
15687
|
+
readFileSync as readFileSync4,
|
|
15307
15688
|
realpathSync,
|
|
15308
|
-
renameSync as renameSync2,
|
|
15309
15689
|
rmSync,
|
|
15310
15690
|
statSync as statSync3,
|
|
15311
|
-
unlinkSync as
|
|
15691
|
+
unlinkSync as unlinkSync4,
|
|
15312
15692
|
writeFileSync as writeFileSync3
|
|
15313
15693
|
} from "fs";
|
|
15314
|
-
import { createHash, randomUUID as
|
|
15315
|
-
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";
|
|
15316
15696
|
var PAIR_BASE_PORT = 4500;
|
|
15317
15697
|
var PAIR_SLOT_STRIDE = 10;
|
|
15318
15698
|
var PAIR_ID_REGEX = /^[A-Za-z0-9._-]{1,64}$/;
|
|
15699
|
+
var RECLAIMABLE_MIN_AGE_MS = 24 * 60 * 60 * 1000;
|
|
15319
15700
|
var REGISTRY_FILE_NAME = "registry.json";
|
|
15320
15701
|
class PairError extends Error {
|
|
15321
15702
|
code;
|
|
@@ -15351,7 +15732,7 @@ function readRegistry(base) {
|
|
|
15351
15732
|
return { version: 1, pairs: [] };
|
|
15352
15733
|
let parsed;
|
|
15353
15734
|
try {
|
|
15354
|
-
parsed = JSON.parse(
|
|
15735
|
+
parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
15355
15736
|
} catch (err) {
|
|
15356
15737
|
throw new PairError("PAIR_REGISTRY_CORRUPT", `Registry JSON is not parseable at ${path}: ${err.message}`, {
|
|
15357
15738
|
path
|
|
@@ -15392,10 +15773,10 @@ function findPair(base, pairId) {
|
|
|
15392
15773
|
}
|
|
15393
15774
|
|
|
15394
15775
|
// src/pair-command.ts
|
|
15395
|
-
function pairScopedCommand(cmd) {
|
|
15776
|
+
function pairScopedCommand(cmd, name = cliInvocationName()) {
|
|
15396
15777
|
const pairId = process.env.AGENTBRIDGE_PAIR_ID;
|
|
15397
15778
|
if (!pairId)
|
|
15398
|
-
return
|
|
15779
|
+
return `${name} ${cmd}`;
|
|
15399
15780
|
let selector = process.env.AGENTBRIDGE_PAIR_NAME;
|
|
15400
15781
|
if (!selector) {
|
|
15401
15782
|
try {
|
|
@@ -15404,10 +15785,13 @@ function pairScopedCommand(cmd) {
|
|
|
15404
15785
|
selector = pairId;
|
|
15405
15786
|
}
|
|
15406
15787
|
}
|
|
15407
|
-
return
|
|
15788
|
+
return `${name} --pair ${selector} ${cmd}`;
|
|
15408
15789
|
}
|
|
15409
15790
|
|
|
15410
15791
|
// src/bridge-disabled-state.ts
|
|
15792
|
+
function shouldEmitReconnectSuccess(state) {
|
|
15793
|
+
return !state.daemonDisabled;
|
|
15794
|
+
}
|
|
15411
15795
|
function disabledReplyError(reason) {
|
|
15412
15796
|
const claudeCmd = pairScopedCommand("claude");
|
|
15413
15797
|
switch (reason) {
|
|
@@ -15420,7 +15804,7 @@ function disabledReplyError(reason) {
|
|
|
15420
15804
|
case "auto_recovery_exhausted":
|
|
15421
15805
|
return `AgentBridge auto-recovery gave up after exhausting its retry budget for the in-flight liveness probe contention. Retry manually with \`${claudeCmd}\`.`;
|
|
15422
15806
|
case "killed":
|
|
15423
|
-
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.`;
|
|
15424
15808
|
}
|
|
15425
15809
|
}
|
|
15426
15810
|
|
|
@@ -15499,9 +15883,25 @@ function nonEmpty(value) {
|
|
|
15499
15883
|
return value && value.length > 0 ? value : null;
|
|
15500
15884
|
}
|
|
15501
15885
|
|
|
15502
|
-
// src/
|
|
15503
|
-
import {
|
|
15886
|
+
// src/control-token.ts
|
|
15887
|
+
import { chmodSync, readFileSync as readFileSync5 } from "fs";
|
|
15504
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";
|
|
15505
15905
|
var TRACE_RETENTION_DAYS = 7;
|
|
15506
15906
|
var TRACE_FILE_RE = /^trace-\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
15507
15907
|
var SECRET_KEY_RE = /(token|secret|password|passwd|api[_-]?key|auth|cookie|session)/i;
|
|
@@ -15541,7 +15941,7 @@ function redactArgv(argv) {
|
|
|
15541
15941
|
}
|
|
15542
15942
|
function traceLogPath(cwd, timestamp) {
|
|
15543
15943
|
const day = timestamp.slice(0, 10);
|
|
15544
|
-
return
|
|
15944
|
+
return join5(cwd, ".agentbridge", "logs", `trace-${day}.jsonl`);
|
|
15545
15945
|
}
|
|
15546
15946
|
function appendTraceEvent(input) {
|
|
15547
15947
|
const timestamp = input.timestamp ?? new Date().toISOString();
|
|
@@ -15555,9 +15955,9 @@ function appendTraceEvent(input) {
|
|
|
15555
15955
|
...input.env ? { env: pickRelevantEnv(input.env) } : {},
|
|
15556
15956
|
...input.data ? { data: redactData(input.data) } : {}
|
|
15557
15957
|
};
|
|
15558
|
-
const logsDir =
|
|
15958
|
+
const logsDir = join5(input.cwd, ".agentbridge", "logs");
|
|
15559
15959
|
const isNewDayFile = !existsSync6(path);
|
|
15560
|
-
|
|
15960
|
+
mkdirSync5(logsDir, { recursive: true });
|
|
15561
15961
|
if (isNewDayFile) {
|
|
15562
15962
|
pruneOldTraceLogs(logsDir, path, Date.parse(timestamp));
|
|
15563
15963
|
}
|
|
@@ -15578,12 +15978,12 @@ function pruneOldTraceLogs(logsDir, keepPath, nowMs) {
|
|
|
15578
15978
|
for (const name of entries) {
|
|
15579
15979
|
if (!TRACE_FILE_RE.test(name))
|
|
15580
15980
|
continue;
|
|
15581
|
-
const filePath =
|
|
15981
|
+
const filePath = join5(logsDir, name);
|
|
15582
15982
|
if (filePath === keepPath)
|
|
15583
15983
|
continue;
|
|
15584
15984
|
try {
|
|
15585
15985
|
if (statSync4(filePath).mtimeMs < cutoff) {
|
|
15586
|
-
|
|
15986
|
+
unlinkSync5(filePath);
|
|
15587
15987
|
}
|
|
15588
15988
|
} catch {}
|
|
15589
15989
|
}
|
|
@@ -15633,7 +16033,7 @@ var CONTROL_PORT = parseInt(process.env.AGENTBRIDGE_CONTROL_PORT ?? "4502", 10);
|
|
|
15633
16033
|
var daemonLifecycle = new DaemonLifecycle({ stateDir, controlPort: CONTROL_PORT, log });
|
|
15634
16034
|
var CONTROL_WS_URL = daemonLifecycle.controlWsUrl;
|
|
15635
16035
|
var claude = new ClaudeAdapter(stateDir.logFile);
|
|
15636
|
-
var daemonClient = new DaemonClient(CONTROL_WS_URL, { identity: currentClientIdentity
|
|
16036
|
+
var daemonClient = new DaemonClient(CONTROL_WS_URL, { identity: currentClientIdentity });
|
|
15637
16037
|
var shuttingDown = false;
|
|
15638
16038
|
var daemonDisabled = false;
|
|
15639
16039
|
var daemonDisabledReason = null;
|
|
@@ -15646,6 +16046,7 @@ var lastReconnectNotifyTs = 0;
|
|
|
15646
16046
|
var disabledRecoveryTimer = null;
|
|
15647
16047
|
var disabledRecoveryInFlight = false;
|
|
15648
16048
|
var disabledRecoveryAttempts = 0;
|
|
16049
|
+
var nextSystemMessageId = 0;
|
|
15649
16050
|
var DISABLED_RECOVERY_MAX_ATTEMPTS = 6;
|
|
15650
16051
|
var DISABLED_RECOVERY_CONFIRM_TIMEOUT_MS = 1000;
|
|
15651
16052
|
if (process.env.AGENTBRIDGE_TRACE === "1") {
|
|
@@ -15740,6 +16141,16 @@ daemonClient.on("rejected", async (code) => {
|
|
|
15740
16141
|
notificationId = "system_bridge_pair_mismatch";
|
|
15741
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`;
|
|
15742
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;
|
|
15743
16154
|
default:
|
|
15744
16155
|
reason = "rejected";
|
|
15745
16156
|
notificationId = "system_bridge_replaced";
|
|
@@ -15759,7 +16170,7 @@ daemonClient.on("rejected", async (code) => {
|
|
|
15759
16170
|
claude.on("ready", async () => {
|
|
15760
16171
|
log("MCP server ready (push delivery) \u2014 ensuring AgentBridge daemon...");
|
|
15761
16172
|
if (daemonLifecycle.wasKilled()) {
|
|
15762
|
-
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.`);
|
|
15763
16174
|
return;
|
|
15764
16175
|
}
|
|
15765
16176
|
try {
|
|
@@ -15821,7 +16232,7 @@ var reconnectTask = null;
|
|
|
15821
16232
|
async function notifyIfDaemonKilled(logMessage) {
|
|
15822
16233
|
if (!daemonLifecycle.wasKilled())
|
|
15823
16234
|
return false;
|
|
15824
|
-
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.`);
|
|
15825
16236
|
return true;
|
|
15826
16237
|
}
|
|
15827
16238
|
async function notifyIfPairRemoved(logMessage) {
|
|
@@ -15858,6 +16269,9 @@ function reconnectToDaemon() {
|
|
|
15858
16269
|
}
|
|
15859
16270
|
try {
|
|
15860
16271
|
await connectToDaemon(true);
|
|
16272
|
+
if (!shouldEmitReconnectSuccess({ daemonDisabled })) {
|
|
16273
|
+
return;
|
|
16274
|
+
}
|
|
15861
16275
|
log("Reconnected to AgentBridge daemon successfully");
|
|
15862
16276
|
const now = Date.now();
|
|
15863
16277
|
if (now - lastReconnectNotifyTs >= RECONNECT_NOTIFY_COOLDOWN_MS) {
|
|
@@ -15976,13 +16390,14 @@ async function pollDisabledRecovery() {
|
|
|
15976
16390
|
}
|
|
15977
16391
|
function systemMessage(idPrefix, content) {
|
|
15978
16392
|
return {
|
|
15979
|
-
id: `${idPrefix}_${
|
|
16393
|
+
id: `${idPrefix}_${++nextSystemMessageId}`,
|
|
15980
16394
|
source: "codex",
|
|
15981
16395
|
content,
|
|
15982
16396
|
timestamp: Date.now()
|
|
15983
16397
|
};
|
|
15984
16398
|
}
|
|
15985
16399
|
function currentClientIdentity() {
|
|
16400
|
+
const controlToken = readControlToken(resolveControlTokenPath(stateDir.dir));
|
|
15986
16401
|
return {
|
|
15987
16402
|
pairId: process.env.AGENTBRIDGE_PAIR_ID ?? null,
|
|
15988
16403
|
pairName: process.env.AGENTBRIDGE_PAIR_NAME ?? null,
|
|
@@ -15990,7 +16405,8 @@ function currentClientIdentity() {
|
|
|
15990
16405
|
baseDir: process.env.AGENTBRIDGE_BASE_DIR ?? null,
|
|
15991
16406
|
stateDir: stateDir.dir,
|
|
15992
16407
|
clientPid: process.pid,
|
|
15993
|
-
contractVersion: BUILD_INFO.contractVersion
|
|
16408
|
+
contractVersion: BUILD_INFO.contractVersion,
|
|
16409
|
+
...controlToken ? { controlToken } : {}
|
|
15994
16410
|
};
|
|
15995
16411
|
}
|
|
15996
16412
|
function shutdown(reason) {
|