@raysonmeng/agentbridge 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/dist/cli.js +859 -421
- package/dist/daemon.js +968 -239
- package/package.json +1 -1
- package/plugins/agentbridge/.claude-plugin/plugin.json +1 -1
- package/plugins/agentbridge/server/bridge-server.js +375 -139
- package/plugins/agentbridge/server/daemon.js +968 -239
|
@@ -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({
|
|
@@ -13668,13 +13668,14 @@ import { appendFileSync, existsSync, renameSync, statSync, unlinkSync } from "fs
|
|
|
13668
13668
|
import { dirname } from "path";
|
|
13669
13669
|
var DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
|
|
13670
13670
|
var DEFAULT_KEEP = 3;
|
|
13671
|
-
|
|
13671
|
+
var REAL_FS_OPS = { statSync, renameSync, unlinkSync, appendFileSync, existsSync };
|
|
13672
|
+
function appendRotatingLog(path, content, options = {}, fsOps = REAL_FS_OPS) {
|
|
13672
13673
|
const maxBytes = options.maxBytes ?? positiveIntFromEnv("AGENTBRIDGE_LOG_MAX_BYTES", DEFAULT_MAX_BYTES);
|
|
13673
13674
|
const keep = options.keep ?? positiveIntFromEnv("AGENTBRIDGE_LOG_ROTATE_KEEP", DEFAULT_KEEP);
|
|
13674
|
-
if (!existsSync(dirname(path)))
|
|
13675
|
+
if (!fsOps.existsSync(dirname(path)))
|
|
13675
13676
|
return;
|
|
13676
|
-
rotateIfNeeded(path, Buffer.byteLength(content), maxBytes, keep);
|
|
13677
|
-
appendFileSync(path, content, "utf-8");
|
|
13677
|
+
rotateIfNeeded(path, Buffer.byteLength(content), maxBytes, keep, fsOps);
|
|
13678
|
+
fsOps.appendFileSync(path, content, "utf-8");
|
|
13678
13679
|
}
|
|
13679
13680
|
function positiveIntFromEnv(name, fallback) {
|
|
13680
13681
|
const value = process.env[name];
|
|
@@ -13683,26 +13684,48 @@ function positiveIntFromEnv(name, fallback) {
|
|
|
13683
13684
|
const parsed = Number(value);
|
|
13684
13685
|
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
13685
13686
|
}
|
|
13686
|
-
function
|
|
13687
|
+
function isEnoent(error2) {
|
|
13688
|
+
return !!error2 && error2.code === "ENOENT";
|
|
13689
|
+
}
|
|
13690
|
+
function renameIfPresent(from, to, fsOps) {
|
|
13691
|
+
try {
|
|
13692
|
+
fsOps.renameSync(from, to);
|
|
13693
|
+
} catch (error2) {
|
|
13694
|
+
if (!isEnoent(error2))
|
|
13695
|
+
throw error2;
|
|
13696
|
+
}
|
|
13697
|
+
}
|
|
13698
|
+
function unlinkIfPresent(path, fsOps) {
|
|
13699
|
+
try {
|
|
13700
|
+
fsOps.unlinkSync(path);
|
|
13701
|
+
} catch (error2) {
|
|
13702
|
+
if (!isEnoent(error2))
|
|
13703
|
+
throw error2;
|
|
13704
|
+
}
|
|
13705
|
+
}
|
|
13706
|
+
function rotateIfNeeded(path, incomingBytes, maxBytes, keep, fsOps) {
|
|
13687
13707
|
if (!Number.isFinite(maxBytes) || maxBytes <= 0 || keep <= 0)
|
|
13688
13708
|
return;
|
|
13689
|
-
|
|
13690
|
-
|
|
13691
|
-
|
|
13709
|
+
let size;
|
|
13710
|
+
try {
|
|
13711
|
+
size = fsOps.statSync(path).size;
|
|
13712
|
+
} catch (error2) {
|
|
13713
|
+
if (isEnoent(error2))
|
|
13714
|
+
return;
|
|
13715
|
+
throw error2;
|
|
13716
|
+
}
|
|
13692
13717
|
if (size + incomingBytes <= maxBytes)
|
|
13693
13718
|
return;
|
|
13694
13719
|
for (let index = keep;index >= 1; index--) {
|
|
13695
13720
|
const current = `${path}.${index}`;
|
|
13696
13721
|
const next = `${path}.${index + 1}`;
|
|
13697
|
-
if (!existsSync(current))
|
|
13698
|
-
continue;
|
|
13699
13722
|
if (index === keep) {
|
|
13700
|
-
|
|
13723
|
+
unlinkIfPresent(current, fsOps);
|
|
13701
13724
|
} else {
|
|
13702
|
-
|
|
13725
|
+
renameIfPresent(current, next, fsOps);
|
|
13703
13726
|
}
|
|
13704
13727
|
}
|
|
13705
|
-
|
|
13728
|
+
renameIfPresent(path, `${path}.1`, fsOps);
|
|
13706
13729
|
}
|
|
13707
13730
|
|
|
13708
13731
|
// src/process-log.ts
|
|
@@ -13859,6 +13882,9 @@ function formatAgent(name, usage, snapshotAt) {
|
|
|
13859
13882
|
if (usage.rateLimitedUntil > 0) {
|
|
13860
13883
|
parts.push(`\u9650\u6D41\u81F3 ${formatEpoch(usage.rateLimitedUntil)}`);
|
|
13861
13884
|
}
|
|
13885
|
+
if (usage.parsedVia === "positional") {
|
|
13886
|
+
parts.push("\u26A0\uFE0F \u7A97\u53E3\u8BC6\u522B\u4F7F\u7528\u4F4D\u7F6E\u515C\u5E95");
|
|
13887
|
+
}
|
|
13862
13888
|
const ageSec = usage.fetchedAt > 0 ? snapshotAt - usage.fetchedAt : 0;
|
|
13863
13889
|
if (ageSec > 300) {
|
|
13864
13890
|
parts.push(`\u26A0\uFE0F \u6570\u636E\u91C7\u96C6\u4E8E ${Math.round(ageSec / 60)} \u5206\u949F\u524D`);
|
|
@@ -13947,7 +13973,7 @@ var CLAUDE_INSTRUCTIONS = [
|
|
|
13947
13973
|
"## Turn coordination",
|
|
13948
13974
|
"- When you see '\u23F3 Codex is working', do NOT call the reply tool \u2014 wait for '\u2705 Codex finished'.",
|
|
13949
13975
|
"- 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,
|
|
13976
|
+
'- 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
13977
|
"",
|
|
13952
13978
|
"## Budget awareness",
|
|
13953
13979
|
"- Use the get_budget tool to check both agents' subscription quota (5h/weekly windows, drift, pause state).",
|
|
@@ -14098,12 +14124,16 @@ ${formatted}`
|
|
|
14098
14124
|
},
|
|
14099
14125
|
require_reply: {
|
|
14100
14126
|
type: "boolean",
|
|
14101
|
-
description:
|
|
14127
|
+
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
14128
|
},
|
|
14103
14129
|
on_busy: {
|
|
14104
14130
|
type: "string",
|
|
14105
|
-
enum: ["reject", "steer"],
|
|
14106
|
-
description:
|
|
14131
|
+
enum: ["reject", "steer", "interrupt"],
|
|
14132
|
+
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.'
|
|
14133
|
+
},
|
|
14134
|
+
idempotency_key: {
|
|
14135
|
+
type: "string",
|
|
14136
|
+
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
14137
|
}
|
|
14108
14138
|
},
|
|
14109
14139
|
required: ["text"]
|
|
@@ -14163,19 +14193,29 @@ ${formatted}`
|
|
|
14163
14193
|
}
|
|
14164
14194
|
const requireReply = args?.require_reply === true;
|
|
14165
14195
|
const onBusyRaw = args?.on_busy;
|
|
14166
|
-
|
|
14167
|
-
if (onBusyRaw !== undefined && onBusyRaw !== "reject" && onBusyRaw !== "steer") {
|
|
14196
|
+
if (onBusyRaw !== undefined && onBusyRaw !== "reject" && onBusyRaw !== "steer" && onBusyRaw !== "interrupt") {
|
|
14168
14197
|
return {
|
|
14169
|
-
content: [{ type: "text", text: `Error: invalid on_busy value ${JSON.stringify(onBusyRaw)} \u2014 use "reject" or "
|
|
14198
|
+
content: [{ type: "text", text: `Error: invalid on_busy value ${JSON.stringify(onBusyRaw)} \u2014 use "reject", "steer" or "interrupt".` }],
|
|
14170
14199
|
isError: true
|
|
14171
14200
|
};
|
|
14172
14201
|
}
|
|
14173
|
-
|
|
14174
|
-
|
|
14175
|
-
|
|
14176
|
-
|
|
14177
|
-
|
|
14202
|
+
const onBusy = onBusyRaw === "steer" || onBusyRaw === "interrupt" ? onBusyRaw : "reject";
|
|
14203
|
+
const idempotencyKeyRaw = args?.idempotency_key;
|
|
14204
|
+
if (idempotencyKeyRaw !== undefined) {
|
|
14205
|
+
if (typeof idempotencyKeyRaw !== "string" || idempotencyKeyRaw.length === 0) {
|
|
14206
|
+
return {
|
|
14207
|
+
content: [{ type: "text", text: "Error: idempotency_key must be a non-empty string." }],
|
|
14208
|
+
isError: true
|
|
14209
|
+
};
|
|
14210
|
+
}
|
|
14211
|
+
if (idempotencyKeyRaw.length > 128) {
|
|
14212
|
+
return {
|
|
14213
|
+
content: [{ type: "text", text: `Error: idempotency_key is too long (${idempotencyKeyRaw.length} chars, max 128).` }],
|
|
14214
|
+
isError: true
|
|
14215
|
+
};
|
|
14216
|
+
}
|
|
14178
14217
|
}
|
|
14218
|
+
const idempotencyKey = idempotencyKeyRaw;
|
|
14179
14219
|
const bridgeMsg = {
|
|
14180
14220
|
id: args?.chat_id ?? `reply_${Date.now()}`,
|
|
14181
14221
|
source: "claude",
|
|
@@ -14189,16 +14229,22 @@ ${formatted}`
|
|
|
14189
14229
|
isError: true
|
|
14190
14230
|
};
|
|
14191
14231
|
}
|
|
14192
|
-
const result = await this.replySender(bridgeMsg, requireReply, onBusy);
|
|
14232
|
+
const result = await this.replySender(bridgeMsg, requireReply, onBusy, idempotencyKey);
|
|
14193
14233
|
if (!result.success) {
|
|
14194
|
-
this.log(`Reply delivery failed: ${result.error}`);
|
|
14234
|
+
this.log(`Reply delivery failed: ${result.error}${result.code ? ` (code=${result.code})` : ""}`);
|
|
14235
|
+
const codePrefix = result.code ? ` [${result.code}]` : "";
|
|
14195
14236
|
return {
|
|
14196
|
-
content: [{ type: "text", text: `Error: ${result.error}` }],
|
|
14237
|
+
content: [{ type: "text", text: `Error${codePrefix}: ${result.error}` }],
|
|
14197
14238
|
isError: true
|
|
14198
14239
|
};
|
|
14199
14240
|
}
|
|
14200
14241
|
const pending = this.pendingMessages.length;
|
|
14201
|
-
let responseText =
|
|
14242
|
+
let responseText = "Reply sent to Codex.";
|
|
14243
|
+
if (onBusy === "steer") {
|
|
14244
|
+
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).";
|
|
14245
|
+
} else if (onBusy === "interrupt") {
|
|
14246
|
+
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).";
|
|
14247
|
+
}
|
|
14202
14248
|
if (pending > 0) {
|
|
14203
14249
|
responseText += ` Note: ${pending} unread Codex message${pending > 1 ? "s" : ""} already waiting \u2014 call get_messages to read them.`;
|
|
14204
14250
|
}
|
|
@@ -14227,8 +14273,8 @@ function defineNumber(value, fallback) {
|
|
|
14227
14273
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
14228
14274
|
}
|
|
14229
14275
|
var BUILD_INFO = Object.freeze({
|
|
14230
|
-
version: defineString("0.1.
|
|
14231
|
-
commit: defineString("
|
|
14276
|
+
version: defineString("0.1.12", "0.0.0-source"),
|
|
14277
|
+
commit: defineString("eec6018", "source"),
|
|
14232
14278
|
bundle: defineBundle("plugin"),
|
|
14233
14279
|
contractVersion: defineNumber(1, CONTRACT_VERSION)
|
|
14234
14280
|
});
|
|
@@ -14257,6 +14303,11 @@ var CLOSE_CODE_EVICTED_STALE = 4002;
|
|
|
14257
14303
|
var CLOSE_CODE_PROBE_IN_PROGRESS = 4003;
|
|
14258
14304
|
var CLOSE_CODE_PAIR_MISMATCH = 4004;
|
|
14259
14305
|
|
|
14306
|
+
// src/interrupt-timing.ts
|
|
14307
|
+
var CLIENT_REPLY_TIMEOUT_MS = 15000;
|
|
14308
|
+
var INTERRUPT_CLIENT_MARGIN_MS = 2000;
|
|
14309
|
+
var MAX_INTERRUPT_TIMEOUT_MS = CLIENT_REPLY_TIMEOUT_MS - INTERRUPT_CLIENT_MARGIN_MS;
|
|
14310
|
+
|
|
14260
14311
|
// src/daemon-client.ts
|
|
14261
14312
|
var nextSocketId = 0;
|
|
14262
14313
|
|
|
@@ -14400,7 +14451,7 @@ class DaemonClient extends EventEmitter2 {
|
|
|
14400
14451
|
this.ws = null;
|
|
14401
14452
|
this.rejectPendingReplies("Daemon connection closed");
|
|
14402
14453
|
}
|
|
14403
|
-
async sendReply(message, requireReply, onBusy) {
|
|
14454
|
+
async sendReply(message, requireReply, onBusy, idempotencyKey) {
|
|
14404
14455
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
14405
14456
|
return { success: false, error: "AgentBridge daemon is not connected." };
|
|
14406
14457
|
}
|
|
@@ -14409,14 +14460,15 @@ class DaemonClient extends EventEmitter2 {
|
|
|
14409
14460
|
const timer = setTimeout(() => {
|
|
14410
14461
|
this.pendingReplies.delete(requestId);
|
|
14411
14462
|
resolve({ success: false, error: "Timed out waiting for AgentBridge daemon reply." });
|
|
14412
|
-
},
|
|
14463
|
+
}, CLIENT_REPLY_TIMEOUT_MS);
|
|
14413
14464
|
this.pendingReplies.set(requestId, { resolve, timer });
|
|
14414
14465
|
this.send({
|
|
14415
14466
|
type: "claude_to_codex",
|
|
14416
14467
|
requestId,
|
|
14417
14468
|
message,
|
|
14418
14469
|
...requireReply ? { requireReply: true } : {},
|
|
14419
|
-
...onBusy && onBusy !== "reject" ? { onBusy } : {}
|
|
14470
|
+
...onBusy && onBusy !== "reject" ? { onBusy } : {},
|
|
14471
|
+
...idempotencyKey ? { idempotencyKey } : {}
|
|
14420
14472
|
});
|
|
14421
14473
|
});
|
|
14422
14474
|
}
|
|
@@ -14439,9 +14491,23 @@ class DaemonClient extends EventEmitter2 {
|
|
|
14439
14491
|
return;
|
|
14440
14492
|
clearTimeout(pending.timer);
|
|
14441
14493
|
this.pendingReplies.delete(message.requestId);
|
|
14442
|
-
pending.resolve({
|
|
14494
|
+
pending.resolve({
|
|
14495
|
+
success: message.success,
|
|
14496
|
+
error: message.error,
|
|
14497
|
+
...message.code !== undefined ? { code: message.code } : {},
|
|
14498
|
+
...message.phase !== undefined ? { phase: message.phase } : {},
|
|
14499
|
+
...message.retryAfterMs !== undefined ? { retryAfterMs: message.retryAfterMs } : {}
|
|
14500
|
+
});
|
|
14443
14501
|
return;
|
|
14444
14502
|
}
|
|
14503
|
+
case "turn_started":
|
|
14504
|
+
this.emit("turnStarted", {
|
|
14505
|
+
requestId: message.requestId,
|
|
14506
|
+
...message.idempotencyKey !== undefined ? { idempotencyKey: message.idempotencyKey } : {},
|
|
14507
|
+
threadId: message.threadId,
|
|
14508
|
+
turnId: message.turnId
|
|
14509
|
+
});
|
|
14510
|
+
return;
|
|
14445
14511
|
case "status":
|
|
14446
14512
|
this.emit("status", message.status);
|
|
14447
14513
|
return;
|
|
@@ -14485,7 +14551,7 @@ class DaemonClient extends EventEmitter2 {
|
|
|
14485
14551
|
}
|
|
14486
14552
|
|
|
14487
14553
|
// src/daemon-lifecycle.ts
|
|
14488
|
-
import { spawn
|
|
14554
|
+
import { spawn } from "child_process";
|
|
14489
14555
|
import { existsSync as existsSync3, readFileSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync, openSync, closeSync, constants } from "fs";
|
|
14490
14556
|
import { fileURLToPath } from "url";
|
|
14491
14557
|
|
|
@@ -14502,6 +14568,41 @@ function parsePositiveIntEnv(name, fallback, log = () => {}, env = process.env)
|
|
|
14502
14568
|
return parsed;
|
|
14503
14569
|
}
|
|
14504
14570
|
|
|
14571
|
+
// src/process-lifecycle.ts
|
|
14572
|
+
import { execFileSync } from "child_process";
|
|
14573
|
+
function commandForPid(pid) {
|
|
14574
|
+
try {
|
|
14575
|
+
return execFileSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf-8" }).trim();
|
|
14576
|
+
} catch {
|
|
14577
|
+
return null;
|
|
14578
|
+
}
|
|
14579
|
+
}
|
|
14580
|
+
function pidLooksAlive(pid) {
|
|
14581
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
14582
|
+
return false;
|
|
14583
|
+
try {
|
|
14584
|
+
process.kill(pid, 0);
|
|
14585
|
+
return true;
|
|
14586
|
+
} catch (err) {
|
|
14587
|
+
return err?.code === "EPERM";
|
|
14588
|
+
}
|
|
14589
|
+
}
|
|
14590
|
+
var isProcessAlive = pidLooksAlive;
|
|
14591
|
+
function isAgentBridgeDaemon(pid, lookup = commandForPid) {
|
|
14592
|
+
const cmd = lookup(pid);
|
|
14593
|
+
if (cmd === null)
|
|
14594
|
+
return false;
|
|
14595
|
+
const hasDaemonEntry = /(?:^|[\s/\\])[\w.-]*-?daemon\.(?:ts|js)(?:\s|$)/.test(cmd);
|
|
14596
|
+
const hasAgentbridge = cmd.includes("agentbridge") || cmd.includes("agent_bridge");
|
|
14597
|
+
return hasDaemonEntry && hasAgentbridge;
|
|
14598
|
+
}
|
|
14599
|
+
function isAgentBridgeProcess(pid, lookup = commandForPid) {
|
|
14600
|
+
const cmd = lookup(pid);
|
|
14601
|
+
if (cmd === null)
|
|
14602
|
+
return false;
|
|
14603
|
+
return cmd.includes("agentbridge") || cmd.includes("agent_bridge");
|
|
14604
|
+
}
|
|
14605
|
+
|
|
14505
14606
|
// src/daemon-lifecycle.ts
|
|
14506
14607
|
var DEFAULT_DAEMON_ENTRY = import.meta.url.endsWith(".ts") ? "./daemon.ts" : "./daemon.js";
|
|
14507
14608
|
var DAEMON_ENTRY = process.env.AGENTBRIDGE_DAEMON_ENTRY || DEFAULT_DAEMON_ENTRY;
|
|
@@ -14510,6 +14611,48 @@ var REUSE_READY_RETRIES = parsePositiveIntEnv("AGENTBRIDGE_REUSE_READY_RETRIES",
|
|
|
14510
14611
|
var REUSE_READY_DELAY_MS = 250;
|
|
14511
14612
|
var HEALTH_FETCH_TIMEOUT_MS = 500;
|
|
14512
14613
|
var LOCK_IDENTITY_GRACE_MS = parsePositiveIntEnv("AGENTBRIDGE_LOCK_IDENTITY_GRACE_MS", 120000);
|
|
14614
|
+
function isReuseVerdict(verdict) {
|
|
14615
|
+
return verdict === "reuse" || verdict === "reuse-despite-drift";
|
|
14616
|
+
}
|
|
14617
|
+
function classifyDaemon(expectedPairId, status, buildInfo) {
|
|
14618
|
+
if (!status) {
|
|
14619
|
+
return { verdict: "unreachable", reason: "daemon status is unavailable or unparseable" };
|
|
14620
|
+
}
|
|
14621
|
+
const reportedPairId = status.pairId;
|
|
14622
|
+
if (!expectedPairId && reportedPairId != null) {
|
|
14623
|
+
return {
|
|
14624
|
+
verdict: "manual-conflict",
|
|
14625
|
+
reason: `manual mode must not adopt registered pair ${reportedPairId}`
|
|
14626
|
+
};
|
|
14627
|
+
}
|
|
14628
|
+
if (expectedPairId) {
|
|
14629
|
+
if (reportedPairId == null) {
|
|
14630
|
+
return {
|
|
14631
|
+
verdict: "replace-foreign",
|
|
14632
|
+
reason: `pair ${expectedPairId} found daemon without pair identity`
|
|
14633
|
+
};
|
|
14634
|
+
}
|
|
14635
|
+
if (reportedPairId !== expectedPairId) {
|
|
14636
|
+
return {
|
|
14637
|
+
verdict: "replace-foreign",
|
|
14638
|
+
reason: `pair ${expectedPairId} found daemon for pair ${reportedPairId}`
|
|
14639
|
+
};
|
|
14640
|
+
}
|
|
14641
|
+
}
|
|
14642
|
+
if (!sameRuntimeContract(status.build, buildInfo)) {
|
|
14643
|
+
if (compatibleContractVersion(status.build, buildInfo) && status.tuiConnected === true) {
|
|
14644
|
+
return {
|
|
14645
|
+
verdict: "reuse-despite-drift",
|
|
14646
|
+
reason: "runtime build drift has a compatible contract and a live Codex TUI is attached"
|
|
14647
|
+
};
|
|
14648
|
+
}
|
|
14649
|
+
return {
|
|
14650
|
+
verdict: "replace-drifted",
|
|
14651
|
+
reason: `runtime build ${formatBuildInfo(status.build)} does not match launcher ${formatBuildInfo(buildInfo)}`
|
|
14652
|
+
};
|
|
14653
|
+
}
|
|
14654
|
+
return { verdict: "reuse", reason: "daemon pair and runtime contract match" };
|
|
14655
|
+
}
|
|
14513
14656
|
|
|
14514
14657
|
class DaemonLifecycle {
|
|
14515
14658
|
stateDir;
|
|
@@ -14542,52 +14685,37 @@ class DaemonLifecycle {
|
|
|
14542
14685
|
return null;
|
|
14543
14686
|
}
|
|
14544
14687
|
}
|
|
14545
|
-
|
|
14546
|
-
const
|
|
14547
|
-
if (
|
|
14548
|
-
return
|
|
14549
|
-
|
|
14550
|
-
|
|
14551
|
-
const reported = status.pairId;
|
|
14552
|
-
if (reported == null)
|
|
14553
|
-
return true;
|
|
14554
|
-
return reported !== expected;
|
|
14555
|
-
}
|
|
14556
|
-
isRegisteredPairDaemonInManualMode(status) {
|
|
14557
|
-
return !this.expectedPairId && status?.pairId != null;
|
|
14558
|
-
}
|
|
14559
|
-
isBuildDrifted(status) {
|
|
14560
|
-
if (process.env.AGENTBRIDGE_ALLOW_BUILD_DRIFT === "1")
|
|
14561
|
-
return false;
|
|
14562
|
-
const runtime = status?.build;
|
|
14563
|
-
if (!runtime)
|
|
14564
|
-
return true;
|
|
14565
|
-
return !sameRuntimeContract(runtime, BUILD_INFO);
|
|
14688
|
+
classifyDaemon(status) {
|
|
14689
|
+
const classification = classifyDaemon(this.expectedPairId, status, BUILD_INFO);
|
|
14690
|
+
if (process.env.AGENTBRIDGE_ALLOW_BUILD_DRIFT === "1" && (classification.verdict === "replace-drifted" || classification.verdict === "unreachable")) {
|
|
14691
|
+
return { verdict: "reuse", reason: "build drift replacement disabled by AGENTBRIDGE_ALLOW_BUILD_DRIFT" };
|
|
14692
|
+
}
|
|
14693
|
+
return classification;
|
|
14566
14694
|
}
|
|
14567
|
-
|
|
14568
|
-
|
|
14569
|
-
return false;
|
|
14570
|
-
return status?.tuiConnected === true;
|
|
14695
|
+
manualConflictError(status) {
|
|
14696
|
+
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.`);
|
|
14571
14697
|
}
|
|
14572
14698
|
async ensureRunning() {
|
|
14573
14699
|
if (await this.isHealthy()) {
|
|
14574
14700
|
const status = await this.fetchStatus();
|
|
14575
|
-
|
|
14576
|
-
|
|
14577
|
-
|
|
14578
|
-
|
|
14579
|
-
|
|
14580
|
-
|
|
14581
|
-
|
|
14582
|
-
|
|
14583
|
-
|
|
14584
|
-
|
|
14585
|
-
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)`);
|
|
14586
|
-
} else {
|
|
14701
|
+
const classification = this.classifyDaemon(status);
|
|
14702
|
+
switch (classification.verdict) {
|
|
14703
|
+
case "manual-conflict":
|
|
14704
|
+
throw this.manualConflictError(status);
|
|
14705
|
+
case "replace-foreign":
|
|
14706
|
+
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`);
|
|
14707
|
+
await this.replaceUnhealthyDaemon(status?.pid);
|
|
14708
|
+
return;
|
|
14709
|
+
case "replace-drifted":
|
|
14710
|
+
case "unreachable":
|
|
14587
14711
|
this.log(`Daemon on control port ${this.controlPort} is running build ${formatBuildInfo(status?.build)} ` + `but launcher is ${formatBuildInfo(BUILD_INFO)} \u2014 replacing drifted daemon`);
|
|
14588
14712
|
await this.replaceUnhealthyDaemon(status?.pid);
|
|
14589
14713
|
return;
|
|
14590
|
-
|
|
14714
|
+
case "reuse-despite-drift":
|
|
14715
|
+
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)`);
|
|
14716
|
+
break;
|
|
14717
|
+
case "reuse":
|
|
14718
|
+
break;
|
|
14591
14719
|
}
|
|
14592
14720
|
try {
|
|
14593
14721
|
await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
|
|
@@ -14601,7 +14729,7 @@ class DaemonLifecycle {
|
|
|
14601
14729
|
const existingPid = this.readPid();
|
|
14602
14730
|
if (existingPid) {
|
|
14603
14731
|
if (isProcessAlive(existingPid)) {
|
|
14604
|
-
if (
|
|
14732
|
+
if (isAgentBridgeDaemon(existingPid)) {
|
|
14605
14733
|
try {
|
|
14606
14734
|
await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
|
|
14607
14735
|
return;
|
|
@@ -14617,14 +14745,17 @@ class DaemonLifecycle {
|
|
|
14617
14745
|
}
|
|
14618
14746
|
await this.withStartupLockStrict(async (locked) => {
|
|
14619
14747
|
if (!locked) {
|
|
14620
|
-
this.
|
|
14621
|
-
await this.waitForReadyAndOurs();
|
|
14748
|
+
await this.waitForContendedStartupLock();
|
|
14622
14749
|
return;
|
|
14623
14750
|
}
|
|
14624
14751
|
if (await this.isHealthy()) {
|
|
14625
14752
|
const status = await this.fetchStatus();
|
|
14626
|
-
|
|
14627
|
-
|
|
14753
|
+
const classification = this.classifyDaemon(status);
|
|
14754
|
+
if (classification.verdict === "manual-conflict") {
|
|
14755
|
+
throw this.manualConflictError(status);
|
|
14756
|
+
}
|
|
14757
|
+
if (!isReuseVerdict(classification.verdict)) {
|
|
14758
|
+
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`);
|
|
14628
14759
|
await this.kill(3000, status?.pid);
|
|
14629
14760
|
} else {
|
|
14630
14761
|
try {
|
|
@@ -14676,7 +14807,11 @@ class DaemonLifecycle {
|
|
|
14676
14807
|
for (let attempt = 0;attempt < maxRetries; attempt++) {
|
|
14677
14808
|
if (await this.isReady()) {
|
|
14678
14809
|
const status = await this.fetchStatus();
|
|
14679
|
-
|
|
14810
|
+
const classification = this.classifyDaemon(status);
|
|
14811
|
+
if (classification.verdict === "manual-conflict") {
|
|
14812
|
+
throw this.manualConflictError(status);
|
|
14813
|
+
}
|
|
14814
|
+
if (isReuseVerdict(classification.verdict)) {
|
|
14680
14815
|
return;
|
|
14681
14816
|
}
|
|
14682
14817
|
}
|
|
@@ -14758,13 +14893,16 @@ class DaemonLifecycle {
|
|
|
14758
14893
|
async replaceUnhealthyDaemon(statusPid) {
|
|
14759
14894
|
await this.withStartupLockStrict(async (locked) => {
|
|
14760
14895
|
if (!locked) {
|
|
14761
|
-
this.
|
|
14762
|
-
await this.waitForReadyAndOurs();
|
|
14896
|
+
await this.waitForContendedStartupLock();
|
|
14763
14897
|
return;
|
|
14764
14898
|
}
|
|
14765
14899
|
if (await this.isHealthy()) {
|
|
14766
14900
|
const status = await this.fetchStatus();
|
|
14767
|
-
|
|
14901
|
+
const classification = this.classifyDaemon(status);
|
|
14902
|
+
if (classification.verdict === "manual-conflict") {
|
|
14903
|
+
throw this.manualConflictError(status);
|
|
14904
|
+
}
|
|
14905
|
+
if (isReuseVerdict(classification.verdict)) {
|
|
14768
14906
|
try {
|
|
14769
14907
|
await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
|
|
14770
14908
|
return;
|
|
@@ -14777,6 +14915,10 @@ class DaemonLifecycle {
|
|
|
14777
14915
|
await this.waitForReady();
|
|
14778
14916
|
});
|
|
14779
14917
|
}
|
|
14918
|
+
async waitForContendedStartupLock() {
|
|
14919
|
+
this.log("Another process holds the startup lock, waiting for readiness+identity...");
|
|
14920
|
+
await this.waitForReadyAndOurs();
|
|
14921
|
+
}
|
|
14780
14922
|
async withStartupLockStrict(fn) {
|
|
14781
14923
|
const locked = this.acquireLockStrict();
|
|
14782
14924
|
try {
|
|
@@ -14812,7 +14954,7 @@ class DaemonLifecycle {
|
|
|
14812
14954
|
this.releaseLock();
|
|
14813
14955
|
return this.acquireLockStrict(true);
|
|
14814
14956
|
}
|
|
14815
|
-
if (Number.isFinite(holderPid) && this.lockAgeMs() > LOCK_IDENTITY_GRACE_MS && !
|
|
14957
|
+
if (Number.isFinite(holderPid) && this.lockAgeMs() > LOCK_IDENTITY_GRACE_MS && !isAgentBridgeProcess(holderPid)) {
|
|
14816
14958
|
this.log(`Startup lock is ${Math.round(this.lockAgeMs() / 1000)}s old and holder pid ${holderPid} ` + `is an unrelated process (pid recycled), reclaiming`);
|
|
14817
14959
|
this.releaseLock();
|
|
14818
14960
|
return this.acquireLockStrict(true);
|
|
@@ -14833,14 +14975,6 @@ class DaemonLifecycle {
|
|
|
14833
14975
|
return 0;
|
|
14834
14976
|
}
|
|
14835
14977
|
}
|
|
14836
|
-
isAgentBridgeProcess(pid) {
|
|
14837
|
-
try {
|
|
14838
|
-
const cmd = execFileSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf-8" }).trim();
|
|
14839
|
-
return cmd.includes("agentbridge") || cmd.includes("agent_bridge");
|
|
14840
|
-
} catch {
|
|
14841
|
-
return false;
|
|
14842
|
-
}
|
|
14843
|
-
}
|
|
14844
14978
|
releaseLock() {
|
|
14845
14979
|
try {
|
|
14846
14980
|
unlinkSync2(this.stateDir.lockFile);
|
|
@@ -14858,7 +14992,7 @@ class DaemonLifecycle {
|
|
|
14858
14992
|
this.cleanup();
|
|
14859
14993
|
return false;
|
|
14860
14994
|
}
|
|
14861
|
-
if (!
|
|
14995
|
+
if (!isAgentBridgeDaemon(pid)) {
|
|
14862
14996
|
this.log(`Pid ${pid} is alive but is NOT an AgentBridge daemon \u2014 refusing to kill. Cleaning up stale pid file.`);
|
|
14863
14997
|
this.cleanup();
|
|
14864
14998
|
return false;
|
|
@@ -14886,16 +15020,6 @@ class DaemonLifecycle {
|
|
|
14886
15020
|
this.cleanup();
|
|
14887
15021
|
return true;
|
|
14888
15022
|
}
|
|
14889
|
-
isDaemonProcess(pid) {
|
|
14890
|
-
try {
|
|
14891
|
-
const cmd = execFileSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf-8" }).trim();
|
|
14892
|
-
const hasDaemonEntry = /(?:^|[\s/\\])[\w.-]*-?daemon\.(?:ts|js)(?:\s|$)/.test(cmd);
|
|
14893
|
-
const hasAgentbridge = cmd.includes("agentbridge") || cmd.includes("agent_bridge");
|
|
14894
|
-
return hasDaemonEntry && hasAgentbridge;
|
|
14895
|
-
} catch {
|
|
14896
|
-
return false;
|
|
14897
|
-
}
|
|
14898
|
-
}
|
|
14899
15023
|
cleanup() {
|
|
14900
15024
|
this.removePidFile();
|
|
14901
15025
|
this.removeStatusFile();
|
|
@@ -14910,21 +15034,13 @@ async function fetchWithTimeout(url, timeoutMs = HEALTH_FETCH_TIMEOUT_MS) {
|
|
|
14910
15034
|
clearTimeout(timer);
|
|
14911
15035
|
}
|
|
14912
15036
|
}
|
|
14913
|
-
function isProcessAlive(pid) {
|
|
14914
|
-
try {
|
|
14915
|
-
process.kill(pid, 0);
|
|
14916
|
-
return true;
|
|
14917
|
-
} catch {
|
|
14918
|
-
return false;
|
|
14919
|
-
}
|
|
14920
|
-
}
|
|
14921
15037
|
|
|
14922
15038
|
// src/config-service.ts
|
|
14923
15039
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
|
|
14924
15040
|
import { join as join2 } from "path";
|
|
14925
15041
|
var DEFAULT_BUDGET_CONFIG = {
|
|
14926
15042
|
enabled: true,
|
|
14927
|
-
pollSeconds:
|
|
15043
|
+
pollSeconds: 300,
|
|
14928
15044
|
pauseAt: 90,
|
|
14929
15045
|
resumeBelow: 30,
|
|
14930
15046
|
syncDriftPct: 10,
|
|
@@ -14953,9 +15069,52 @@ var DEFAULT_CONFIG = {
|
|
|
14953
15069
|
};
|
|
14954
15070
|
var CONFIG_DIR = ".agentbridge";
|
|
14955
15071
|
var CONFIG_FILE = "config.json";
|
|
15072
|
+
var NOOP_LOGGER = () => {};
|
|
14956
15073
|
function isRecord(value) {
|
|
14957
15074
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14958
15075
|
}
|
|
15076
|
+
function isCoercibleNumber(value) {
|
|
15077
|
+
if (typeof value === "number")
|
|
15078
|
+
return Number.isFinite(value);
|
|
15079
|
+
if (typeof value === "string")
|
|
15080
|
+
return Number.isFinite(Number(value));
|
|
15081
|
+
return false;
|
|
15082
|
+
}
|
|
15083
|
+
function findShapeViolation(raw) {
|
|
15084
|
+
if ("idleShutdownSeconds" in raw && !isCoercibleNumber(raw.idleShutdownSeconds)) {
|
|
15085
|
+
return "idleShutdownSeconds is present but not a number";
|
|
15086
|
+
}
|
|
15087
|
+
if ("budget" in raw) {
|
|
15088
|
+
const budget = raw.budget;
|
|
15089
|
+
if (!isRecord(budget)) {
|
|
15090
|
+
return "budget is present but not an object";
|
|
15091
|
+
}
|
|
15092
|
+
const numericKeys = ["pauseAt", "resumeBelow", "pollSeconds", "syncDriftPct"];
|
|
15093
|
+
for (const key of numericKeys) {
|
|
15094
|
+
if (key in budget && !isCoercibleNumber(budget[key])) {
|
|
15095
|
+
return `budget.${key} is present but not a number`;
|
|
15096
|
+
}
|
|
15097
|
+
}
|
|
15098
|
+
if ("parallel" in budget) {
|
|
15099
|
+
const parallel = budget.parallel;
|
|
15100
|
+
if (!isRecord(parallel)) {
|
|
15101
|
+
return "budget.parallel is present but not an object";
|
|
15102
|
+
}
|
|
15103
|
+
for (const key of ["minRemainingPct", "timeWindowSec"]) {
|
|
15104
|
+
if (key in parallel && !isCoercibleNumber(parallel[key])) {
|
|
15105
|
+
return `budget.parallel.${key} is present but not a number`;
|
|
15106
|
+
}
|
|
15107
|
+
}
|
|
15108
|
+
}
|
|
15109
|
+
}
|
|
15110
|
+
return null;
|
|
15111
|
+
}
|
|
15112
|
+
function hasCustomDecisionValues(config2) {
|
|
15113
|
+
const d = DEFAULT_CONFIG;
|
|
15114
|
+
const b = config2.budget;
|
|
15115
|
+
const db = d.budget;
|
|
15116
|
+
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;
|
|
15117
|
+
}
|
|
14959
15118
|
function normalizeInteger(value, fallback) {
|
|
14960
15119
|
if (typeof value === "number" && Number.isFinite(value))
|
|
14961
15120
|
return value;
|
|
@@ -14991,35 +15150,35 @@ function normalizeCodexOverride(raw) {
|
|
|
14991
15150
|
override.effort = raw.effort.trim();
|
|
14992
15151
|
return Object.keys(override).length > 0 ? override : null;
|
|
14993
15152
|
}
|
|
14994
|
-
function normalizeCodexTiers(raw) {
|
|
15153
|
+
function normalizeCodexTiers(raw, fallback = DEFAULT_BUDGET_CONFIG.codexTiers) {
|
|
14995
15154
|
const tiers = isRecord(raw) ? raw : {};
|
|
14996
15155
|
return {
|
|
14997
15156
|
full: normalizeCodexOverride(tiers.full),
|
|
14998
|
-
balanced: normalizeCodexOverride(tiers.balanced) ??
|
|
14999
|
-
eco: normalizeCodexOverride(tiers.eco) ??
|
|
15157
|
+
balanced: normalizeCodexOverride(tiers.balanced) ?? fallback.balanced,
|
|
15158
|
+
eco: normalizeCodexOverride(tiers.eco) ?? fallback.eco
|
|
15000
15159
|
};
|
|
15001
15160
|
}
|
|
15002
|
-
function normalizeBudgetConfig(raw) {
|
|
15161
|
+
function normalizeBudgetConfig(raw, fallback = DEFAULT_BUDGET_CONFIG) {
|
|
15003
15162
|
const budget = isRecord(raw) ? raw : {};
|
|
15004
15163
|
const parallel = isRecord(budget.parallel) ? budget.parallel : {};
|
|
15005
|
-
const codexTiers = normalizeCodexTiers(budget.codexTiers);
|
|
15006
|
-
let pauseAt = normalizeBoundedInteger(budget.pauseAt,
|
|
15007
|
-
let resumeBelow = normalizeBoundedInteger(budget.resumeBelow,
|
|
15164
|
+
const codexTiers = normalizeCodexTiers(budget.codexTiers, fallback.codexTiers);
|
|
15165
|
+
let pauseAt = normalizeBoundedInteger(budget.pauseAt, fallback.pauseAt, 1, 100);
|
|
15166
|
+
let resumeBelow = normalizeBoundedInteger(budget.resumeBelow, fallback.resumeBelow, 0, 99);
|
|
15008
15167
|
if (pauseAt <= resumeBelow) {
|
|
15009
15168
|
pauseAt = DEFAULT_BUDGET_CONFIG.pauseAt;
|
|
15010
15169
|
resumeBelow = DEFAULT_BUDGET_CONFIG.resumeBelow;
|
|
15011
15170
|
}
|
|
15012
15171
|
return {
|
|
15013
|
-
enabled: normalizeBoolean(budget.enabled,
|
|
15014
|
-
pollSeconds: normalizeBoundedInteger(budget.pollSeconds,
|
|
15172
|
+
enabled: normalizeBoolean(budget.enabled, fallback.enabled),
|
|
15173
|
+
pollSeconds: normalizeBoundedInteger(budget.pollSeconds, fallback.pollSeconds, 5, 3600),
|
|
15015
15174
|
pauseAt,
|
|
15016
15175
|
resumeBelow,
|
|
15017
|
-
syncDriftPct: normalizeBoundedInteger(budget.syncDriftPct,
|
|
15176
|
+
syncDriftPct: normalizeBoundedInteger(budget.syncDriftPct, fallback.syncDriftPct, 1, 100),
|
|
15018
15177
|
parallel: {
|
|
15019
|
-
minRemainingPct: normalizeBoundedInteger(parallel.minRemainingPct,
|
|
15020
|
-
timeWindowSec: normalizeBoundedInteger(parallel.timeWindowSec,
|
|
15178
|
+
minRemainingPct: normalizeBoundedInteger(parallel.minRemainingPct, fallback.parallel.minRemainingPct, 1, 100),
|
|
15179
|
+
timeWindowSec: normalizeBoundedInteger(parallel.timeWindowSec, fallback.parallel.timeWindowSec, 60, 604800)
|
|
15021
15180
|
},
|
|
15022
|
-
codexTierControl: normalizeBoolean(budget.codexTierControl,
|
|
15181
|
+
codexTierControl: normalizeBoolean(budget.codexTierControl, fallback.codexTierControl) && codexTiers.full !== null,
|
|
15023
15182
|
codexTiers
|
|
15024
15183
|
};
|
|
15025
15184
|
}
|
|
@@ -15056,15 +15215,59 @@ class ConfigService {
|
|
|
15056
15215
|
return existsSync4(this.configPath);
|
|
15057
15216
|
}
|
|
15058
15217
|
load() {
|
|
15218
|
+
let raw;
|
|
15059
15219
|
try {
|
|
15060
|
-
|
|
15061
|
-
|
|
15062
|
-
|
|
15063
|
-
|
|
15220
|
+
raw = readFileSync2(this.configPath, "utf-8");
|
|
15221
|
+
} catch (err) {
|
|
15222
|
+
if (err?.code === "ENOENT") {
|
|
15223
|
+
return { state: "absent" };
|
|
15224
|
+
}
|
|
15225
|
+
return { state: "corrupt", reason: `config.json is unreadable: ${err.message}` };
|
|
15064
15226
|
}
|
|
15227
|
+
let parsed;
|
|
15228
|
+
try {
|
|
15229
|
+
parsed = JSON.parse(raw);
|
|
15230
|
+
} catch (err) {
|
|
15231
|
+
return {
|
|
15232
|
+
state: "corrupt",
|
|
15233
|
+
reason: `config.json is not valid JSON: ${err.message}`
|
|
15234
|
+
};
|
|
15235
|
+
}
|
|
15236
|
+
if (!isRecord(parsed)) {
|
|
15237
|
+
return { state: "corrupt", reason: "config.json is not a JSON object" };
|
|
15238
|
+
}
|
|
15239
|
+
const violation = findShapeViolation(parsed);
|
|
15240
|
+
if (violation) {
|
|
15241
|
+
return { state: "corrupt", reason: `config.json is shape-invalid: ${violation}` };
|
|
15242
|
+
}
|
|
15243
|
+
const config2 = normalizeConfig(parsed);
|
|
15244
|
+
if (!config2) {
|
|
15245
|
+
return { state: "corrupt", reason: "config.json could not be normalized" };
|
|
15246
|
+
}
|
|
15247
|
+
return { state: "parsed", config: config2 };
|
|
15248
|
+
}
|
|
15249
|
+
loadOrDefault(log = NOOP_LOGGER) {
|
|
15250
|
+
const result = this.load();
|
|
15251
|
+
if (result.state === "parsed")
|
|
15252
|
+
return result.config;
|
|
15253
|
+
if (result.state === "corrupt") {
|
|
15254
|
+
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.");
|
|
15255
|
+
}
|
|
15256
|
+
return structuredClone(DEFAULT_CONFIG);
|
|
15065
15257
|
}
|
|
15066
|
-
|
|
15067
|
-
|
|
15258
|
+
describeConfig() {
|
|
15259
|
+
const result = this.load();
|
|
15260
|
+
if (result.state === "absent") {
|
|
15261
|
+
return { state: "absent", path: this.configPath, customValues: false };
|
|
15262
|
+
}
|
|
15263
|
+
if (result.state === "corrupt") {
|
|
15264
|
+
return { state: "corrupt", path: this.configPath, reason: result.reason, customValues: false };
|
|
15265
|
+
}
|
|
15266
|
+
return {
|
|
15267
|
+
state: "parsed",
|
|
15268
|
+
path: this.configPath,
|
|
15269
|
+
customValues: hasCustomDecisionValues(result.config)
|
|
15270
|
+
};
|
|
15068
15271
|
}
|
|
15069
15272
|
save(config2) {
|
|
15070
15273
|
this.ensureConfigDir();
|
|
@@ -15297,8 +15500,10 @@ function nonEmpty(value) {
|
|
|
15297
15500
|
}
|
|
15298
15501
|
|
|
15299
15502
|
// src/trace-log.ts
|
|
15300
|
-
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync4 } from "fs";
|
|
15503
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync6, mkdirSync as mkdirSync4, readdirSync as readdirSync2, statSync as statSync4, unlinkSync as unlinkSync4 } from "fs";
|
|
15301
15504
|
import { join as join4 } from "path";
|
|
15505
|
+
var TRACE_RETENTION_DAYS = 7;
|
|
15506
|
+
var TRACE_FILE_RE = /^trace-\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
15302
15507
|
var SECRET_KEY_RE = /(token|secret|password|passwd|api[_-]?key|auth|cookie|session)/i;
|
|
15303
15508
|
var SECRET_ARG_RE = /^--?(?:token|secret|password|passwd|apikey|api-key|api_key|auth|cookie|session)(?:=.*)?$/i;
|
|
15304
15509
|
var RELEVANT_ENV_RE = /^(AGENTBRIDGE_|CODEX_)/;
|
|
@@ -15350,11 +15555,39 @@ function appendTraceEvent(input) {
|
|
|
15350
15555
|
...input.env ? { env: pickRelevantEnv(input.env) } : {},
|
|
15351
15556
|
...input.data ? { data: redactData(input.data) } : {}
|
|
15352
15557
|
};
|
|
15353
|
-
|
|
15558
|
+
const logsDir = join4(input.cwd, ".agentbridge", "logs");
|
|
15559
|
+
const isNewDayFile = !existsSync6(path);
|
|
15560
|
+
mkdirSync4(logsDir, { recursive: true });
|
|
15561
|
+
if (isNewDayFile) {
|
|
15562
|
+
pruneOldTraceLogs(logsDir, path, Date.parse(timestamp));
|
|
15563
|
+
}
|
|
15354
15564
|
appendFileSync2(path, JSON.stringify(event) + `
|
|
15355
15565
|
`, "utf-8");
|
|
15356
15566
|
return path;
|
|
15357
15567
|
}
|
|
15568
|
+
function pruneOldTraceLogs(logsDir, keepPath, nowMs) {
|
|
15569
|
+
if (!Number.isFinite(nowMs))
|
|
15570
|
+
return;
|
|
15571
|
+
const cutoff = nowMs - TRACE_RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
|
15572
|
+
let entries;
|
|
15573
|
+
try {
|
|
15574
|
+
entries = readdirSync2(logsDir);
|
|
15575
|
+
} catch {
|
|
15576
|
+
return;
|
|
15577
|
+
}
|
|
15578
|
+
for (const name of entries) {
|
|
15579
|
+
if (!TRACE_FILE_RE.test(name))
|
|
15580
|
+
continue;
|
|
15581
|
+
const filePath = join4(logsDir, name);
|
|
15582
|
+
if (filePath === keepPath)
|
|
15583
|
+
continue;
|
|
15584
|
+
try {
|
|
15585
|
+
if (statSync4(filePath).mtimeMs < cutoff) {
|
|
15586
|
+
unlinkSync4(filePath);
|
|
15587
|
+
}
|
|
15588
|
+
} catch {}
|
|
15589
|
+
}
|
|
15590
|
+
}
|
|
15358
15591
|
function isEnvSnapshot(key, value) {
|
|
15359
15592
|
return /env$/i.test(key) && !!value && typeof value === "object" && !Array.isArray(value);
|
|
15360
15593
|
}
|
|
@@ -15393,10 +15626,10 @@ var envGuardResult = guardAgentBridgeEnv({
|
|
|
15393
15626
|
});
|
|
15394
15627
|
var stateDir = new StateDirResolver;
|
|
15395
15628
|
stateDir.ensure();
|
|
15629
|
+
var processLogger = createProcessLogger({ component: "AgentBridgeFrontend", logFile: stateDir.logFile });
|
|
15396
15630
|
var configService = new ConfigService;
|
|
15397
|
-
var config2 = configService.loadOrDefault();
|
|
15631
|
+
var config2 = configService.loadOrDefault(processLogger.log);
|
|
15398
15632
|
var CONTROL_PORT = parseInt(process.env.AGENTBRIDGE_CONTROL_PORT ?? "4502", 10);
|
|
15399
|
-
var processLogger = createProcessLogger({ component: "AgentBridgeFrontend", logFile: stateDir.logFile });
|
|
15400
15633
|
var daemonLifecycle = new DaemonLifecycle({ stateDir, controlPort: CONTROL_PORT, log });
|
|
15401
15634
|
var CONTROL_WS_URL = daemonLifecycle.controlWsUrl;
|
|
15402
15635
|
var claude = new ClaudeAdapter(stateDir.logFile);
|
|
@@ -15436,7 +15669,7 @@ if (process.env.AGENTBRIDGE_TRACE === "1") {
|
|
|
15436
15669
|
});
|
|
15437
15670
|
} catch {}
|
|
15438
15671
|
}
|
|
15439
|
-
claude.setReplySender(async (msg, requireReply, onBusy) => {
|
|
15672
|
+
claude.setReplySender(async (msg, requireReply, onBusy, idempotencyKey) => {
|
|
15440
15673
|
if (msg.source !== "claude") {
|
|
15441
15674
|
return { success: false, error: "Invalid message source" };
|
|
15442
15675
|
}
|
|
@@ -15446,7 +15679,10 @@ claude.setReplySender(async (msg, requireReply, onBusy) => {
|
|
|
15446
15679
|
error: disabledReplyError(daemonDisabledReason ?? "killed")
|
|
15447
15680
|
};
|
|
15448
15681
|
}
|
|
15449
|
-
return daemonClient.sendReply(msg, requireReply, onBusy);
|
|
15682
|
+
return daemonClient.sendReply(msg, requireReply, onBusy, idempotencyKey);
|
|
15683
|
+
});
|
|
15684
|
+
daemonClient.on("turnStarted", ({ requestId, idempotencyKey, threadId, turnId }) => {
|
|
15685
|
+
log(`Codex turn started for reply ${requestId} (turn=${turnId}, thread=${threadId}` + `${idempotencyKey ? `, idempotencyKey=${idempotencyKey}` : ""})`);
|
|
15450
15686
|
});
|
|
15451
15687
|
daemonClient.on("codexMessage", (message) => {
|
|
15452
15688
|
log(`Forwarding daemon \u2192 Claude (${message.content.length} chars)`);
|
|
@@ -15589,7 +15825,7 @@ async function notifyIfDaemonKilled(logMessage) {
|
|
|
15589
15825
|
return true;
|
|
15590
15826
|
}
|
|
15591
15827
|
async function notifyIfPairRemoved(logMessage) {
|
|
15592
|
-
if (
|
|
15828
|
+
if (existsSync7(stateDir.dir))
|
|
15593
15829
|
return false;
|
|
15594
15830
|
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`);
|
|
15595
15831
|
return true;
|