@raysonmeng/agentbridge 0.1.11 → 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 +683 -216
- package/dist/daemon.js +932 -212
- package/package.json +1 -1
- package/plugins/agentbridge/.claude-plugin/plugin.json +1 -1
- package/plugins/agentbridge/server/bridge-server.js +336 -109
- package/plugins/agentbridge/server/daemon.js +932 -212
|
@@ -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;
|
|
@@ -14545,6 +14611,48 @@ var REUSE_READY_RETRIES = parsePositiveIntEnv("AGENTBRIDGE_REUSE_READY_RETRIES",
|
|
|
14545
14611
|
var REUSE_READY_DELAY_MS = 250;
|
|
14546
14612
|
var HEALTH_FETCH_TIMEOUT_MS = 500;
|
|
14547
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
|
+
}
|
|
14548
14656
|
|
|
14549
14657
|
class DaemonLifecycle {
|
|
14550
14658
|
stateDir;
|
|
@@ -14577,52 +14685,37 @@ class DaemonLifecycle {
|
|
|
14577
14685
|
return null;
|
|
14578
14686
|
}
|
|
14579
14687
|
}
|
|
14580
|
-
|
|
14581
|
-
const
|
|
14582
|
-
if (
|
|
14583
|
-
return
|
|
14584
|
-
|
|
14585
|
-
|
|
14586
|
-
const reported = status.pairId;
|
|
14587
|
-
if (reported == null)
|
|
14588
|
-
return true;
|
|
14589
|
-
return reported !== expected;
|
|
14590
|
-
}
|
|
14591
|
-
isRegisteredPairDaemonInManualMode(status) {
|
|
14592
|
-
return !this.expectedPairId && status?.pairId != null;
|
|
14593
|
-
}
|
|
14594
|
-
isBuildDrifted(status) {
|
|
14595
|
-
if (process.env.AGENTBRIDGE_ALLOW_BUILD_DRIFT === "1")
|
|
14596
|
-
return false;
|
|
14597
|
-
const runtime = status?.build;
|
|
14598
|
-
if (!runtime)
|
|
14599
|
-
return true;
|
|
14600
|
-
return !sameRuntimeContract(runtime, BUILD_INFO);
|
|
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;
|
|
14601
14694
|
}
|
|
14602
|
-
|
|
14603
|
-
|
|
14604
|
-
return false;
|
|
14605
|
-
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.`);
|
|
14606
14697
|
}
|
|
14607
14698
|
async ensureRunning() {
|
|
14608
14699
|
if (await this.isHealthy()) {
|
|
14609
14700
|
const status = await this.fetchStatus();
|
|
14610
|
-
|
|
14611
|
-
|
|
14612
|
-
|
|
14613
|
-
|
|
14614
|
-
|
|
14615
|
-
|
|
14616
|
-
|
|
14617
|
-
|
|
14618
|
-
|
|
14619
|
-
|
|
14620
|
-
this.log(`Daemon on control port ${this.controlPort} is running build ${formatBuildInfo(status?.build)} ` + `(launcher ${formatBuildInfo(BUILD_INFO)}) but a live Codex TUI is attached \u2014 reusing instead of ` + `replacing; the new build is picked up at the next restart (abg kill, then relaunch)`);
|
|
14621
|
-
} else {
|
|
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":
|
|
14622
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`);
|
|
14623
14712
|
await this.replaceUnhealthyDaemon(status?.pid);
|
|
14624
14713
|
return;
|
|
14625
|
-
|
|
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;
|
|
14626
14719
|
}
|
|
14627
14720
|
try {
|
|
14628
14721
|
await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
|
|
@@ -14652,14 +14745,17 @@ class DaemonLifecycle {
|
|
|
14652
14745
|
}
|
|
14653
14746
|
await this.withStartupLockStrict(async (locked) => {
|
|
14654
14747
|
if (!locked) {
|
|
14655
|
-
this.
|
|
14656
|
-
await this.waitForReadyAndOurs();
|
|
14748
|
+
await this.waitForContendedStartupLock();
|
|
14657
14749
|
return;
|
|
14658
14750
|
}
|
|
14659
14751
|
if (await this.isHealthy()) {
|
|
14660
14752
|
const status = await this.fetchStatus();
|
|
14661
|
-
|
|
14662
|
-
|
|
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`);
|
|
14663
14759
|
await this.kill(3000, status?.pid);
|
|
14664
14760
|
} else {
|
|
14665
14761
|
try {
|
|
@@ -14711,7 +14807,11 @@ class DaemonLifecycle {
|
|
|
14711
14807
|
for (let attempt = 0;attempt < maxRetries; attempt++) {
|
|
14712
14808
|
if (await this.isReady()) {
|
|
14713
14809
|
const status = await this.fetchStatus();
|
|
14714
|
-
|
|
14810
|
+
const classification = this.classifyDaemon(status);
|
|
14811
|
+
if (classification.verdict === "manual-conflict") {
|
|
14812
|
+
throw this.manualConflictError(status);
|
|
14813
|
+
}
|
|
14814
|
+
if (isReuseVerdict(classification.verdict)) {
|
|
14715
14815
|
return;
|
|
14716
14816
|
}
|
|
14717
14817
|
}
|
|
@@ -14793,13 +14893,16 @@ class DaemonLifecycle {
|
|
|
14793
14893
|
async replaceUnhealthyDaemon(statusPid) {
|
|
14794
14894
|
await this.withStartupLockStrict(async (locked) => {
|
|
14795
14895
|
if (!locked) {
|
|
14796
|
-
this.
|
|
14797
|
-
await this.waitForReadyAndOurs();
|
|
14896
|
+
await this.waitForContendedStartupLock();
|
|
14798
14897
|
return;
|
|
14799
14898
|
}
|
|
14800
14899
|
if (await this.isHealthy()) {
|
|
14801
14900
|
const status = await this.fetchStatus();
|
|
14802
|
-
|
|
14901
|
+
const classification = this.classifyDaemon(status);
|
|
14902
|
+
if (classification.verdict === "manual-conflict") {
|
|
14903
|
+
throw this.manualConflictError(status);
|
|
14904
|
+
}
|
|
14905
|
+
if (isReuseVerdict(classification.verdict)) {
|
|
14803
14906
|
try {
|
|
14804
14907
|
await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
|
|
14805
14908
|
return;
|
|
@@ -14812,6 +14915,10 @@ class DaemonLifecycle {
|
|
|
14812
14915
|
await this.waitForReady();
|
|
14813
14916
|
});
|
|
14814
14917
|
}
|
|
14918
|
+
async waitForContendedStartupLock() {
|
|
14919
|
+
this.log("Another process holds the startup lock, waiting for readiness+identity...");
|
|
14920
|
+
await this.waitForReadyAndOurs();
|
|
14921
|
+
}
|
|
14815
14922
|
async withStartupLockStrict(fn) {
|
|
14816
14923
|
const locked = this.acquireLockStrict();
|
|
14817
14924
|
try {
|
|
@@ -14933,7 +15040,7 @@ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSy
|
|
|
14933
15040
|
import { join as join2 } from "path";
|
|
14934
15041
|
var DEFAULT_BUDGET_CONFIG = {
|
|
14935
15042
|
enabled: true,
|
|
14936
|
-
pollSeconds:
|
|
15043
|
+
pollSeconds: 300,
|
|
14937
15044
|
pauseAt: 90,
|
|
14938
15045
|
resumeBelow: 30,
|
|
14939
15046
|
syncDriftPct: 10,
|
|
@@ -14962,9 +15069,52 @@ var DEFAULT_CONFIG = {
|
|
|
14962
15069
|
};
|
|
14963
15070
|
var CONFIG_DIR = ".agentbridge";
|
|
14964
15071
|
var CONFIG_FILE = "config.json";
|
|
15072
|
+
var NOOP_LOGGER = () => {};
|
|
14965
15073
|
function isRecord(value) {
|
|
14966
15074
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14967
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
|
+
}
|
|
14968
15118
|
function normalizeInteger(value, fallback) {
|
|
14969
15119
|
if (typeof value === "number" && Number.isFinite(value))
|
|
14970
15120
|
return value;
|
|
@@ -15000,35 +15150,35 @@ function normalizeCodexOverride(raw) {
|
|
|
15000
15150
|
override.effort = raw.effort.trim();
|
|
15001
15151
|
return Object.keys(override).length > 0 ? override : null;
|
|
15002
15152
|
}
|
|
15003
|
-
function normalizeCodexTiers(raw) {
|
|
15153
|
+
function normalizeCodexTiers(raw, fallback = DEFAULT_BUDGET_CONFIG.codexTiers) {
|
|
15004
15154
|
const tiers = isRecord(raw) ? raw : {};
|
|
15005
15155
|
return {
|
|
15006
15156
|
full: normalizeCodexOverride(tiers.full),
|
|
15007
|
-
balanced: normalizeCodexOverride(tiers.balanced) ??
|
|
15008
|
-
eco: normalizeCodexOverride(tiers.eco) ??
|
|
15157
|
+
balanced: normalizeCodexOverride(tiers.balanced) ?? fallback.balanced,
|
|
15158
|
+
eco: normalizeCodexOverride(tiers.eco) ?? fallback.eco
|
|
15009
15159
|
};
|
|
15010
15160
|
}
|
|
15011
|
-
function normalizeBudgetConfig(raw) {
|
|
15161
|
+
function normalizeBudgetConfig(raw, fallback = DEFAULT_BUDGET_CONFIG) {
|
|
15012
15162
|
const budget = isRecord(raw) ? raw : {};
|
|
15013
15163
|
const parallel = isRecord(budget.parallel) ? budget.parallel : {};
|
|
15014
|
-
const codexTiers = normalizeCodexTiers(budget.codexTiers);
|
|
15015
|
-
let pauseAt = normalizeBoundedInteger(budget.pauseAt,
|
|
15016
|
-
let resumeBelow = normalizeBoundedInteger(budget.resumeBelow,
|
|
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);
|
|
15017
15167
|
if (pauseAt <= resumeBelow) {
|
|
15018
15168
|
pauseAt = DEFAULT_BUDGET_CONFIG.pauseAt;
|
|
15019
15169
|
resumeBelow = DEFAULT_BUDGET_CONFIG.resumeBelow;
|
|
15020
15170
|
}
|
|
15021
15171
|
return {
|
|
15022
|
-
enabled: normalizeBoolean(budget.enabled,
|
|
15023
|
-
pollSeconds: normalizeBoundedInteger(budget.pollSeconds,
|
|
15172
|
+
enabled: normalizeBoolean(budget.enabled, fallback.enabled),
|
|
15173
|
+
pollSeconds: normalizeBoundedInteger(budget.pollSeconds, fallback.pollSeconds, 5, 3600),
|
|
15024
15174
|
pauseAt,
|
|
15025
15175
|
resumeBelow,
|
|
15026
|
-
syncDriftPct: normalizeBoundedInteger(budget.syncDriftPct,
|
|
15176
|
+
syncDriftPct: normalizeBoundedInteger(budget.syncDriftPct, fallback.syncDriftPct, 1, 100),
|
|
15027
15177
|
parallel: {
|
|
15028
|
-
minRemainingPct: normalizeBoundedInteger(parallel.minRemainingPct,
|
|
15029
|
-
timeWindowSec: normalizeBoundedInteger(parallel.timeWindowSec,
|
|
15178
|
+
minRemainingPct: normalizeBoundedInteger(parallel.minRemainingPct, fallback.parallel.minRemainingPct, 1, 100),
|
|
15179
|
+
timeWindowSec: normalizeBoundedInteger(parallel.timeWindowSec, fallback.parallel.timeWindowSec, 60, 604800)
|
|
15030
15180
|
},
|
|
15031
|
-
codexTierControl: normalizeBoolean(budget.codexTierControl,
|
|
15181
|
+
codexTierControl: normalizeBoolean(budget.codexTierControl, fallback.codexTierControl) && codexTiers.full !== null,
|
|
15032
15182
|
codexTiers
|
|
15033
15183
|
};
|
|
15034
15184
|
}
|
|
@@ -15065,15 +15215,59 @@ class ConfigService {
|
|
|
15065
15215
|
return existsSync4(this.configPath);
|
|
15066
15216
|
}
|
|
15067
15217
|
load() {
|
|
15218
|
+
let raw;
|
|
15068
15219
|
try {
|
|
15069
|
-
|
|
15070
|
-
|
|
15071
|
-
|
|
15072
|
-
|
|
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}` };
|
|
15073
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 };
|
|
15074
15248
|
}
|
|
15075
|
-
loadOrDefault() {
|
|
15076
|
-
|
|
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);
|
|
15257
|
+
}
|
|
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
|
+
};
|
|
15077
15271
|
}
|
|
15078
15272
|
save(config2) {
|
|
15079
15273
|
this.ensureConfigDir();
|
|
@@ -15306,8 +15500,10 @@ function nonEmpty(value) {
|
|
|
15306
15500
|
}
|
|
15307
15501
|
|
|
15308
15502
|
// src/trace-log.ts
|
|
15309
|
-
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";
|
|
15310
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$/;
|
|
15311
15507
|
var SECRET_KEY_RE = /(token|secret|password|passwd|api[_-]?key|auth|cookie|session)/i;
|
|
15312
15508
|
var SECRET_ARG_RE = /^--?(?:token|secret|password|passwd|apikey|api-key|api_key|auth|cookie|session)(?:=.*)?$/i;
|
|
15313
15509
|
var RELEVANT_ENV_RE = /^(AGENTBRIDGE_|CODEX_)/;
|
|
@@ -15359,11 +15555,39 @@ function appendTraceEvent(input) {
|
|
|
15359
15555
|
...input.env ? { env: pickRelevantEnv(input.env) } : {},
|
|
15360
15556
|
...input.data ? { data: redactData(input.data) } : {}
|
|
15361
15557
|
};
|
|
15362
|
-
|
|
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
|
+
}
|
|
15363
15564
|
appendFileSync2(path, JSON.stringify(event) + `
|
|
15364
15565
|
`, "utf-8");
|
|
15365
15566
|
return path;
|
|
15366
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
|
+
}
|
|
15367
15591
|
function isEnvSnapshot(key, value) {
|
|
15368
15592
|
return /env$/i.test(key) && !!value && typeof value === "object" && !Array.isArray(value);
|
|
15369
15593
|
}
|
|
@@ -15402,10 +15626,10 @@ var envGuardResult = guardAgentBridgeEnv({
|
|
|
15402
15626
|
});
|
|
15403
15627
|
var stateDir = new StateDirResolver;
|
|
15404
15628
|
stateDir.ensure();
|
|
15629
|
+
var processLogger = createProcessLogger({ component: "AgentBridgeFrontend", logFile: stateDir.logFile });
|
|
15405
15630
|
var configService = new ConfigService;
|
|
15406
|
-
var config2 = configService.loadOrDefault();
|
|
15631
|
+
var config2 = configService.loadOrDefault(processLogger.log);
|
|
15407
15632
|
var CONTROL_PORT = parseInt(process.env.AGENTBRIDGE_CONTROL_PORT ?? "4502", 10);
|
|
15408
|
-
var processLogger = createProcessLogger({ component: "AgentBridgeFrontend", logFile: stateDir.logFile });
|
|
15409
15633
|
var daemonLifecycle = new DaemonLifecycle({ stateDir, controlPort: CONTROL_PORT, log });
|
|
15410
15634
|
var CONTROL_WS_URL = daemonLifecycle.controlWsUrl;
|
|
15411
15635
|
var claude = new ClaudeAdapter(stateDir.logFile);
|
|
@@ -15445,7 +15669,7 @@ if (process.env.AGENTBRIDGE_TRACE === "1") {
|
|
|
15445
15669
|
});
|
|
15446
15670
|
} catch {}
|
|
15447
15671
|
}
|
|
15448
|
-
claude.setReplySender(async (msg, requireReply, onBusy) => {
|
|
15672
|
+
claude.setReplySender(async (msg, requireReply, onBusy, idempotencyKey) => {
|
|
15449
15673
|
if (msg.source !== "claude") {
|
|
15450
15674
|
return { success: false, error: "Invalid message source" };
|
|
15451
15675
|
}
|
|
@@ -15455,7 +15679,10 @@ claude.setReplySender(async (msg, requireReply, onBusy) => {
|
|
|
15455
15679
|
error: disabledReplyError(daemonDisabledReason ?? "killed")
|
|
15456
15680
|
};
|
|
15457
15681
|
}
|
|
15458
|
-
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}` : ""})`);
|
|
15459
15686
|
});
|
|
15460
15687
|
daemonClient.on("codexMessage", (message) => {
|
|
15461
15688
|
log(`Forwarding daemon \u2192 Claude (${message.content.length} chars)`);
|
|
@@ -15598,7 +15825,7 @@ async function notifyIfDaemonKilled(logMessage) {
|
|
|
15598
15825
|
return true;
|
|
15599
15826
|
}
|
|
15600
15827
|
async function notifyIfPairRemoved(logMessage) {
|
|
15601
|
-
if (
|
|
15828
|
+
if (existsSync7(stateDir.dir))
|
|
15602
15829
|
return false;
|
|
15603
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`);
|
|
15604
15831
|
return true;
|