@raysonmeng/agentbridge 0.1.12 → 0.1.14
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 +1235 -503
- package/dist/daemon.js +1262 -432
- package/package.json +3 -1
- package/plugins/agentbridge/.claude-plugin/plugin.json +1 -1
- package/plugins/agentbridge/server/bridge-server.js +726 -183
- package/plugins/agentbridge/server/daemon.js +1262 -432
package/dist/daemon.js
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
|
+
// src/daemon.ts
|
|
5
|
+
import { rmSync as rmSync2 } from "fs";
|
|
6
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
7
|
+
|
|
4
8
|
// src/contract-version.ts
|
|
5
9
|
var CONTRACT_VERSION = 1;
|
|
6
10
|
|
|
7
11
|
// src/build-info.ts
|
|
12
|
+
var CODE_HASH_SENTINEL = "source";
|
|
13
|
+
function hasValidCodeHash(build) {
|
|
14
|
+
const hash = build?.codeHash;
|
|
15
|
+
return typeof hash === "string" && hash.length > 0 && hash !== CODE_HASH_SENTINEL;
|
|
16
|
+
}
|
|
8
17
|
function defineString(value, fallback) {
|
|
9
18
|
return typeof value === "string" && value.length > 0 ? value : fallback;
|
|
10
19
|
}
|
|
@@ -17,10 +26,11 @@ function defineNumber(value, fallback) {
|
|
|
17
26
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
18
27
|
}
|
|
19
28
|
var BUILD_INFO = Object.freeze({
|
|
20
|
-
version: defineString("0.1.
|
|
21
|
-
commit: defineString("
|
|
29
|
+
version: defineString("0.1.14", "0.0.0-source"),
|
|
30
|
+
commit: defineString("f5a9562", "source"),
|
|
22
31
|
bundle: defineBundle("dist"),
|
|
23
|
-
contractVersion: defineNumber(1, CONTRACT_VERSION)
|
|
32
|
+
contractVersion: defineNumber(1, CONTRACT_VERSION),
|
|
33
|
+
codeHash: defineString("e05d18c3cc72", "source")
|
|
24
34
|
});
|
|
25
35
|
function daemonStatusBuildInfo() {
|
|
26
36
|
return { ...BUILD_INFO };
|
|
@@ -28,7 +38,14 @@ function daemonStatusBuildInfo() {
|
|
|
28
38
|
function sameRuntimeContract(a, b) {
|
|
29
39
|
if (!a || !b)
|
|
30
40
|
return false;
|
|
31
|
-
|
|
41
|
+
if (a.version !== b.version || a.contractVersion !== b.contractVersion)
|
|
42
|
+
return false;
|
|
43
|
+
if (hasValidCodeHash(a) && hasValidCodeHash(b))
|
|
44
|
+
return a.codeHash === b.codeHash;
|
|
45
|
+
return a.commit === b.commit;
|
|
46
|
+
}
|
|
47
|
+
function runtimeContractComparisonBasis(a, b) {
|
|
48
|
+
return hasValidCodeHash(a) && hasValidCodeHash(b) ? "codeHash" : "commit";
|
|
32
49
|
}
|
|
33
50
|
function compatibleContractVersion(a, b) {
|
|
34
51
|
if (!a || !b)
|
|
@@ -38,7 +55,175 @@ function compatibleContractVersion(a, b) {
|
|
|
38
55
|
function formatBuildInfo(build) {
|
|
39
56
|
if (!build)
|
|
40
57
|
return "<unknown>";
|
|
41
|
-
|
|
58
|
+
const codeHash = hasValidCodeHash(build) ? `/code-${build.codeHash}` : "";
|
|
59
|
+
return `${build.version}/${build.commit}/${build.bundle}/contract-v${build.contractVersion}${codeHash}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/daemon-record.ts
|
|
63
|
+
import { readFileSync } from "fs";
|
|
64
|
+
|
|
65
|
+
// src/atomic-json.ts
|
|
66
|
+
import * as fs from "fs";
|
|
67
|
+
import { randomUUID } from "crypto";
|
|
68
|
+
import { dirname } from "path";
|
|
69
|
+
function tmpPathFor(targetPath) {
|
|
70
|
+
return `${targetPath}.tmp.${process.pid}.${randomUUID()}`;
|
|
71
|
+
}
|
|
72
|
+
function atomicWriteText(path, content, options = {}) {
|
|
73
|
+
fs.mkdirSync(dirname(path), { recursive: true });
|
|
74
|
+
const tmp = tmpPathFor(path);
|
|
75
|
+
let renamed = false;
|
|
76
|
+
const fd = fs.openSync(tmp, "w", options.mode ?? 438);
|
|
77
|
+
try {
|
|
78
|
+
try {
|
|
79
|
+
fs.writeFileSync(fd, content, "utf-8");
|
|
80
|
+
if (options.fsync)
|
|
81
|
+
fs.fsyncSync(fd);
|
|
82
|
+
} finally {
|
|
83
|
+
fs.closeSync(fd);
|
|
84
|
+
}
|
|
85
|
+
fs.renameSync(tmp, path);
|
|
86
|
+
renamed = true;
|
|
87
|
+
} finally {
|
|
88
|
+
if (!renamed) {
|
|
89
|
+
try {
|
|
90
|
+
fs.unlinkSync(tmp);
|
|
91
|
+
} catch {}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function atomicWriteJson(path, value, options = {}) {
|
|
96
|
+
atomicWriteText(path, JSON.stringify(value, null, 2) + `
|
|
97
|
+
`, options);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/daemon-record.ts
|
|
101
|
+
var defaultRead = (path) => readFileSync(path, "utf-8");
|
|
102
|
+
function writeDaemonRecord(path, record) {
|
|
103
|
+
atomicWriteJson(path, record);
|
|
104
|
+
}
|
|
105
|
+
function sanitizePorts(value) {
|
|
106
|
+
if (typeof value !== "object" || value === null)
|
|
107
|
+
return;
|
|
108
|
+
const raw = value;
|
|
109
|
+
const ports = {};
|
|
110
|
+
if (typeof raw.appPort === "number")
|
|
111
|
+
ports.appPort = raw.appPort;
|
|
112
|
+
if (typeof raw.proxyPort === "number")
|
|
113
|
+
ports.proxyPort = raw.proxyPort;
|
|
114
|
+
if (typeof raw.controlPort === "number")
|
|
115
|
+
ports.controlPort = raw.controlPort;
|
|
116
|
+
return Object.keys(ports).length > 0 ? ports : undefined;
|
|
117
|
+
}
|
|
118
|
+
function readDaemonRecord(path, read = defaultRead) {
|
|
119
|
+
let parsed;
|
|
120
|
+
try {
|
|
121
|
+
parsed = JSON.parse(read(path));
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
126
|
+
return null;
|
|
127
|
+
const obj = parsed;
|
|
128
|
+
if (typeof obj.pid !== "number" || !Number.isFinite(obj.pid))
|
|
129
|
+
return null;
|
|
130
|
+
const phase = obj.phase === "ready" ? "ready" : "booting";
|
|
131
|
+
const record = { pid: obj.pid, phase };
|
|
132
|
+
if (typeof obj.startedAt === "number")
|
|
133
|
+
record.startedAt = obj.startedAt;
|
|
134
|
+
if (typeof obj.nonce === "string")
|
|
135
|
+
record.nonce = obj.nonce;
|
|
136
|
+
if (obj.pairId === null || typeof obj.pairId === "string")
|
|
137
|
+
record.pairId = obj.pairId;
|
|
138
|
+
if (obj.cwd === null || typeof obj.cwd === "string")
|
|
139
|
+
record.cwd = obj.cwd;
|
|
140
|
+
if (obj.stateDir === null || typeof obj.stateDir === "string")
|
|
141
|
+
record.stateDir = obj.stateDir;
|
|
142
|
+
if (typeof obj.proxyUrl === "string")
|
|
143
|
+
record.proxyUrl = obj.proxyUrl;
|
|
144
|
+
if (typeof obj.appServerUrl === "string")
|
|
145
|
+
record.appServerUrl = obj.appServerUrl;
|
|
146
|
+
const ports = sanitizePorts(obj.ports);
|
|
147
|
+
if (ports !== undefined)
|
|
148
|
+
record.ports = ports;
|
|
149
|
+
if (typeof obj.build === "object" && obj.build !== null) {
|
|
150
|
+
record.build = obj.build;
|
|
151
|
+
}
|
|
152
|
+
if (typeof obj.turnPhase === "string")
|
|
153
|
+
record.turnPhase = obj.turnPhase;
|
|
154
|
+
if (typeof obj.turnInProgress === "boolean")
|
|
155
|
+
record.turnInProgress = obj.turnInProgress;
|
|
156
|
+
if (typeof obj.attentionWindowActive === "boolean") {
|
|
157
|
+
record.attentionWindowActive = obj.attentionWindowActive;
|
|
158
|
+
}
|
|
159
|
+
return record;
|
|
160
|
+
}
|
|
161
|
+
function synthesizeLegacyRecord(pidFilePath, statusFilePath, read = defaultRead) {
|
|
162
|
+
let pidFromPidFile = null;
|
|
163
|
+
try {
|
|
164
|
+
const raw = read(pidFilePath).trim();
|
|
165
|
+
const n = Number.parseInt(raw, 10);
|
|
166
|
+
if (Number.isFinite(n))
|
|
167
|
+
pidFromPidFile = n;
|
|
168
|
+
} catch {}
|
|
169
|
+
let status = null;
|
|
170
|
+
try {
|
|
171
|
+
const parsed = JSON.parse(read(statusFilePath));
|
|
172
|
+
if (typeof parsed === "object" && parsed !== null)
|
|
173
|
+
status = parsed;
|
|
174
|
+
} catch {}
|
|
175
|
+
const pidFromStatus = status && typeof status.pid === "number" && Number.isFinite(status.pid) ? status.pid : null;
|
|
176
|
+
const pid = pidFromPidFile ?? pidFromStatus;
|
|
177
|
+
if (pid === null)
|
|
178
|
+
return null;
|
|
179
|
+
const record = {
|
|
180
|
+
pid,
|
|
181
|
+
phase: status ? "ready" : "booting"
|
|
182
|
+
};
|
|
183
|
+
if (status) {
|
|
184
|
+
if (typeof status.proxyUrl === "string")
|
|
185
|
+
record.proxyUrl = status.proxyUrl;
|
|
186
|
+
if (typeof status.appServerUrl === "string")
|
|
187
|
+
record.appServerUrl = status.appServerUrl;
|
|
188
|
+
const controlPort = typeof status.controlPort === "number" ? status.controlPort : undefined;
|
|
189
|
+
const proxyPort = portFromUrl(status.proxyUrl);
|
|
190
|
+
const appPort = portFromUrl(status.appServerUrl);
|
|
191
|
+
if (controlPort !== undefined || proxyPort !== undefined || appPort !== undefined) {
|
|
192
|
+
record.ports = {};
|
|
193
|
+
if (appPort !== undefined)
|
|
194
|
+
record.ports.appPort = appPort;
|
|
195
|
+
if (proxyPort !== undefined)
|
|
196
|
+
record.ports.proxyPort = proxyPort;
|
|
197
|
+
if (controlPort !== undefined)
|
|
198
|
+
record.ports.controlPort = controlPort;
|
|
199
|
+
}
|
|
200
|
+
if (status.pairId === null || typeof status.pairId === "string")
|
|
201
|
+
record.pairId = status.pairId;
|
|
202
|
+
if (status.cwd === null || typeof status.cwd === "string")
|
|
203
|
+
record.cwd = status.cwd;
|
|
204
|
+
if (status.stateDir === null || typeof status.stateDir === "string")
|
|
205
|
+
record.stateDir = status.stateDir;
|
|
206
|
+
if (typeof status.build === "object" && status.build !== null) {
|
|
207
|
+
record.build = status.build;
|
|
208
|
+
}
|
|
209
|
+
if (typeof status.turnPhase === "string")
|
|
210
|
+
record.turnPhase = status.turnPhase;
|
|
211
|
+
if (typeof status.turnInProgress === "boolean")
|
|
212
|
+
record.turnInProgress = status.turnInProgress;
|
|
213
|
+
if (typeof status.attentionWindowActive === "boolean") {
|
|
214
|
+
record.attentionWindowActive = status.attentionWindowActive;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return record;
|
|
218
|
+
}
|
|
219
|
+
function readUnifiedDaemonRecord(paths, read = defaultRead) {
|
|
220
|
+
return readDaemonRecord(paths.daemonRecordFile, read) ?? synthesizeLegacyRecord(paths.pidFile, paths.statusFile, read);
|
|
221
|
+
}
|
|
222
|
+
function portFromUrl(url) {
|
|
223
|
+
if (typeof url !== "string")
|
|
224
|
+
return;
|
|
225
|
+
const match = url.match(/:(\d+)(?:[/?]|$)/);
|
|
226
|
+
return match ? Number.parseInt(match[1], 10) : undefined;
|
|
42
227
|
}
|
|
43
228
|
|
|
44
229
|
// src/codex-adapter.ts
|
|
@@ -47,9 +232,13 @@ import { createInterface } from "readline";
|
|
|
47
232
|
import { EventEmitter } from "events";
|
|
48
233
|
|
|
49
234
|
// src/state-dir.ts
|
|
50
|
-
import { mkdirSync, existsSync } from "fs";
|
|
235
|
+
import { mkdirSync as mkdirSync2, existsSync } from "fs";
|
|
51
236
|
import { join } from "path";
|
|
52
237
|
import { homedir, platform } from "os";
|
|
238
|
+
function resolveXdgStateBase(rawXdg = process.env.XDG_STATE_HOME) {
|
|
239
|
+
const xdgState = rawXdg && rawXdg.length > 0 ? rawXdg : join(homedir(), ".local", "state");
|
|
240
|
+
return join(xdgState, "agentbridge");
|
|
241
|
+
}
|
|
53
242
|
|
|
54
243
|
class StateDirResolver {
|
|
55
244
|
stateDir;
|
|
@@ -57,8 +246,7 @@ class StateDirResolver {
|
|
|
57
246
|
if (platform() === "darwin") {
|
|
58
247
|
return join(homedir(), "Library", "Application Support", "AgentBridge");
|
|
59
248
|
}
|
|
60
|
-
|
|
61
|
-
return join(xdgState, "agentbridge");
|
|
249
|
+
return resolveXdgStateBase(process.env.XDG_STATE_HOME);
|
|
62
250
|
}
|
|
63
251
|
constructor(envOverride) {
|
|
64
252
|
const override = envOverride ?? process.env.AGENTBRIDGE_STATE_DIR;
|
|
@@ -66,7 +254,7 @@ class StateDirResolver {
|
|
|
66
254
|
}
|
|
67
255
|
ensure() {
|
|
68
256
|
if (!existsSync(this.stateDir)) {
|
|
69
|
-
|
|
257
|
+
mkdirSync2(this.stateDir, { recursive: true });
|
|
70
258
|
}
|
|
71
259
|
}
|
|
72
260
|
get dir() {
|
|
@@ -84,8 +272,8 @@ class StateDirResolver {
|
|
|
84
272
|
get statusFile() {
|
|
85
273
|
return join(this.stateDir, "status.json");
|
|
86
274
|
}
|
|
87
|
-
get
|
|
88
|
-
return join(this.stateDir, "
|
|
275
|
+
get daemonRecordFile() {
|
|
276
|
+
return join(this.stateDir, "daemon.json");
|
|
89
277
|
}
|
|
90
278
|
get currentThreadFile() {
|
|
91
279
|
return join(this.stateDir, "current-thread.json");
|
|
@@ -205,15 +393,15 @@ async function cleanupPorts(options) {
|
|
|
205
393
|
}
|
|
206
394
|
|
|
207
395
|
// src/rotating-log.ts
|
|
208
|
-
import { appendFileSync, existsSync as existsSync2, renameSync, statSync, unlinkSync } from "fs";
|
|
209
|
-
import { dirname } from "path";
|
|
396
|
+
import { appendFileSync, existsSync as existsSync2, renameSync as renameSync2, statSync, unlinkSync as unlinkSync2 } from "fs";
|
|
397
|
+
import { dirname as dirname2 } from "path";
|
|
210
398
|
var DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
|
|
211
399
|
var DEFAULT_KEEP = 3;
|
|
212
|
-
var REAL_FS_OPS = { statSync, renameSync, unlinkSync, appendFileSync, existsSync: existsSync2 };
|
|
400
|
+
var REAL_FS_OPS = { statSync, renameSync: renameSync2, unlinkSync: unlinkSync2, appendFileSync, existsSync: existsSync2 };
|
|
213
401
|
function appendRotatingLog(path, content, options = {}, fsOps = REAL_FS_OPS) {
|
|
214
402
|
const maxBytes = options.maxBytes ?? positiveIntFromEnv("AGENTBRIDGE_LOG_MAX_BYTES", DEFAULT_MAX_BYTES);
|
|
215
403
|
const keep = options.keep ?? positiveIntFromEnv("AGENTBRIDGE_LOG_ROTATE_KEEP", DEFAULT_KEEP);
|
|
216
|
-
if (!fsOps.existsSync(
|
|
404
|
+
if (!fsOps.existsSync(dirname2(path)))
|
|
217
405
|
return;
|
|
218
406
|
rotateIfNeeded(path, Buffer.byteLength(content), maxBytes, keep, fsOps);
|
|
219
407
|
fsOps.appendFileSync(path, content, "utf-8");
|
|
@@ -363,6 +551,16 @@ var APP_SERVER_NOTIFICATION_METHODS = [
|
|
|
363
551
|
var TRACKED_REQUEST_METHOD_SET = new Set(APP_SERVER_TRACKED_REQUEST_METHODS);
|
|
364
552
|
var SERVER_REQUEST_METHOD_SET = new Set(APP_SERVER_SERVER_REQUEST_METHODS);
|
|
365
553
|
var NOTIFICATION_METHOD_SET = new Set(APP_SERVER_NOTIFICATION_METHODS);
|
|
554
|
+
function parseAppServerVersion(userAgent) {
|
|
555
|
+
if (typeof userAgent !== "string")
|
|
556
|
+
return null;
|
|
557
|
+
const match = userAgent.match(/\/([^\s]+)/);
|
|
558
|
+
return match ? match[1] : null;
|
|
559
|
+
}
|
|
560
|
+
var APP_SERVER_RATE_LIMIT_ERROR_CODES = new Set([
|
|
561
|
+
-32603,
|
|
562
|
+
-32600
|
|
563
|
+
]);
|
|
366
564
|
function isObjectRecord(value) {
|
|
367
565
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
368
566
|
}
|
|
@@ -410,7 +608,7 @@ function clampInterruptTimeoutMs(requested) {
|
|
|
410
608
|
// src/codex-transport.ts
|
|
411
609
|
import { createServer, connect } from "net";
|
|
412
610
|
import { spawnSync } from "child_process";
|
|
413
|
-
import { mkdirSync as
|
|
611
|
+
import { mkdirSync as mkdirSync3, rmSync, chmodSync } from "fs";
|
|
414
612
|
import { join as join2 } from "path";
|
|
415
613
|
import { tmpdir } from "os";
|
|
416
614
|
var CODEX_TRANSPORT_ENV = "AGENTBRIDGE_CODEX_TRANSPORT";
|
|
@@ -471,7 +669,7 @@ function ensureSocketDir(socketPath) {
|
|
|
471
669
|
const dir = socketPath.slice(0, socketPath.lastIndexOf("/"));
|
|
472
670
|
if (!dir)
|
|
473
671
|
return;
|
|
474
|
-
|
|
672
|
+
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
475
673
|
try {
|
|
476
674
|
chmodSync(dir, 448);
|
|
477
675
|
} catch (err) {
|
|
@@ -601,10 +799,13 @@ async function waitForUnixWsReady(socketPath, maxRetries = 40, delayMs = 250) {
|
|
|
601
799
|
function attemptUnixWsUpgrade(socketPath) {
|
|
602
800
|
return new Promise((resolve) => {
|
|
603
801
|
let settled = false;
|
|
802
|
+
let timeout;
|
|
604
803
|
const done = (ok) => {
|
|
605
804
|
if (settled)
|
|
606
805
|
return;
|
|
607
806
|
settled = true;
|
|
807
|
+
if (timeout !== undefined)
|
|
808
|
+
clearTimeout(timeout);
|
|
608
809
|
try {
|
|
609
810
|
socket.destroy();
|
|
610
811
|
} catch {}
|
|
@@ -629,7 +830,8 @@ Sec-WebSocket-Version: 13\r
|
|
|
629
830
|
});
|
|
630
831
|
socket.on("error", () => done(false));
|
|
631
832
|
socket.on("close", () => done(false));
|
|
632
|
-
setTimeout(() => done(false), 1500);
|
|
833
|
+
timeout = setTimeout(() => done(false), 1500);
|
|
834
|
+
timeout.unref?.();
|
|
633
835
|
});
|
|
634
836
|
}
|
|
635
837
|
|
|
@@ -666,6 +868,76 @@ function wsOriginRejectedResponse() {
|
|
|
666
868
|
return new Response("Forbidden: WebSocket Origin not allowed", { status: 403 });
|
|
667
869
|
}
|
|
668
870
|
|
|
871
|
+
// src/pending-request-registry.ts
|
|
872
|
+
class PendingRequestRegistry {
|
|
873
|
+
entries = new Map;
|
|
874
|
+
setTimer;
|
|
875
|
+
clearTimer;
|
|
876
|
+
constructor(deps = {}) {
|
|
877
|
+
this.setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
|
|
878
|
+
this.clearTimer = deps.clearTimer ?? ((handle) => clearTimeout(handle));
|
|
879
|
+
}
|
|
880
|
+
get size() {
|
|
881
|
+
return this.entries.size;
|
|
882
|
+
}
|
|
883
|
+
has(id) {
|
|
884
|
+
return this.entries.has(id);
|
|
885
|
+
}
|
|
886
|
+
register(id, options) {
|
|
887
|
+
const existing = this.entries.get(id);
|
|
888
|
+
if (existing) {
|
|
889
|
+
this.clearTimer(existing.timer);
|
|
890
|
+
this.entries.delete(id);
|
|
891
|
+
}
|
|
892
|
+
return new Promise((resolve, reject) => {
|
|
893
|
+
const timer = this.setTimer(() => {
|
|
894
|
+
if (!this.entries.has(id))
|
|
895
|
+
return;
|
|
896
|
+
this.entries.delete(id);
|
|
897
|
+
options.onTimeout({ resolve, reject });
|
|
898
|
+
}, options.timeoutMs);
|
|
899
|
+
if (options.unref) {
|
|
900
|
+
timer.unref?.();
|
|
901
|
+
}
|
|
902
|
+
this.entries.set(id, { resolve, reject, timer });
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
settle(id, value) {
|
|
906
|
+
const entry = this.entries.get(id);
|
|
907
|
+
if (!entry)
|
|
908
|
+
return false;
|
|
909
|
+
this.clearTimer(entry.timer);
|
|
910
|
+
this.entries.delete(id);
|
|
911
|
+
entry.resolve(value);
|
|
912
|
+
return true;
|
|
913
|
+
}
|
|
914
|
+
reject(id, error) {
|
|
915
|
+
const entry = this.entries.get(id);
|
|
916
|
+
if (!entry)
|
|
917
|
+
return false;
|
|
918
|
+
this.clearTimer(entry.timer);
|
|
919
|
+
this.entries.delete(id);
|
|
920
|
+
entry.reject(error);
|
|
921
|
+
return true;
|
|
922
|
+
}
|
|
923
|
+
settleAll(value) {
|
|
924
|
+
const make = typeof value === "function" ? value : () => value;
|
|
925
|
+
for (const [id, entry] of this.entries) {
|
|
926
|
+
this.clearTimer(entry.timer);
|
|
927
|
+
this.entries.delete(id);
|
|
928
|
+
entry.resolve(make(id));
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
rejectAll(error) {
|
|
932
|
+
const make = typeof error === "function" ? error : () => error;
|
|
933
|
+
for (const [id, entry] of this.entries) {
|
|
934
|
+
this.clearTimer(entry.timer);
|
|
935
|
+
this.entries.delete(id);
|
|
936
|
+
entry.reject(make(id));
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
669
941
|
// src/codex-adapter.ts
|
|
670
942
|
class CodexAdapter extends EventEmitter {
|
|
671
943
|
static RESPONSE_TRACKING_TTL_MS = 30000;
|
|
@@ -715,8 +987,13 @@ class CodexAdapter extends EventEmitter {
|
|
|
715
987
|
static OUTAGE_TIMEOUT_MS = 1e4;
|
|
716
988
|
lastInitializeRaw = null;
|
|
717
989
|
lastInitializedRaw = null;
|
|
990
|
+
pendingInitializeProxyIds = new Set;
|
|
991
|
+
appServerInfo = null;
|
|
992
|
+
warnedAppServerVersions = new Set;
|
|
993
|
+
warnedFragileRateLimitMessages = new Set;
|
|
718
994
|
sessionRestoreInProgress = false;
|
|
719
|
-
replayPending = new
|
|
995
|
+
replayPending = new PendingRequestRegistry;
|
|
996
|
+
replayMethods = new Map;
|
|
720
997
|
static SESSION_REPLAY_TIMEOUT_MS = 5000;
|
|
721
998
|
constructor(appPort = 4500, proxyPort = 4501, logFile = new StateDirResolver().logFile) {
|
|
722
999
|
super();
|
|
@@ -734,16 +1011,40 @@ class CodexAdapter extends EventEmitter {
|
|
|
734
1011
|
get activeThreadId() {
|
|
735
1012
|
return this.threadId;
|
|
736
1013
|
}
|
|
1014
|
+
get capturedAppServerInfo() {
|
|
1015
|
+
return this.appServerInfo;
|
|
1016
|
+
}
|
|
737
1017
|
async start() {
|
|
738
1018
|
this.intentionalDisconnect = false;
|
|
739
1019
|
await this.checkPorts();
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
1020
|
+
try {
|
|
1021
|
+
this.resolveTransport();
|
|
1022
|
+
const listen = codexListenArg(this.transport, this.appPort, this.socketPath ?? "");
|
|
1023
|
+
if (this.transport === "unix" && this.socketPath) {
|
|
1024
|
+
ensureSocketDir(this.socketPath);
|
|
1025
|
+
removeSocketFile(this.socketPath);
|
|
1026
|
+
}
|
|
1027
|
+
this.log(`Spawning codex app-server (transport=${this.transport}) --listen ${listen}`);
|
|
1028
|
+
this.spawnAppServer(listen);
|
|
1029
|
+
if (this.transport === "unix" && this.socketPath) {
|
|
1030
|
+
await waitForUnixWsReady(this.socketPath);
|
|
1031
|
+
this.relay = new TcpToUnixRelay("127.0.0.1", this.appPort, this.socketPath, (m) => this.log(`[relay] ${m}`));
|
|
1032
|
+
await this.relay.start();
|
|
1033
|
+
this.log(`Transport relay ready: ws://127.0.0.1:${this.appPort} \u2192 unix://${this.socketPath}`);
|
|
1034
|
+
} else {
|
|
1035
|
+
await this.waitForHealthy();
|
|
1036
|
+
}
|
|
1037
|
+
await this.connectToAppServer();
|
|
1038
|
+
this.startProxy();
|
|
1039
|
+
this.log(`Proxy ready on ${this.proxyUrl}`);
|
|
1040
|
+
} catch (err) {
|
|
1041
|
+
const m = err instanceof Error ? err.message : String(err);
|
|
1042
|
+
this.log(`start() failed (${m}) \u2014 tearing down partial transport before rethrow`);
|
|
1043
|
+
this.cleanupAfterFailedStart();
|
|
1044
|
+
throw err;
|
|
745
1045
|
}
|
|
746
|
-
|
|
1046
|
+
}
|
|
1047
|
+
spawnAppServer(listen) {
|
|
747
1048
|
this.proc = spawn("codex", ["app-server", "--listen", listen], {
|
|
748
1049
|
stdio: ["pipe", "pipe", "pipe"]
|
|
749
1050
|
});
|
|
@@ -757,17 +1058,25 @@ class CodexAdapter extends EventEmitter {
|
|
|
757
1058
|
stderrRl.on("line", (l) => this.log(`[codex-server] ${l}`));
|
|
758
1059
|
const stdoutRl = createInterface({ input: this.proc.stdout });
|
|
759
1060
|
stdoutRl.on("line", (l) => this.log(`[codex-stdout] ${l}`));
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
1061
|
+
}
|
|
1062
|
+
teardownTransport() {
|
|
1063
|
+
this.proxyServer?.stop();
|
|
1064
|
+
this.proxyServer = null;
|
|
1065
|
+
if (this.relay) {
|
|
1066
|
+
this.relay.stop();
|
|
1067
|
+
this.relay = null;
|
|
767
1068
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1069
|
+
if (this.socketPath)
|
|
1070
|
+
removeSocketFile(this.socketPath);
|
|
1071
|
+
}
|
|
1072
|
+
cleanupAfterFailedStart() {
|
|
1073
|
+
try {
|
|
1074
|
+
this.teardownTransport();
|
|
1075
|
+
} catch (e) {
|
|
1076
|
+
this.log(`cleanupAfterFailedStart: teardownTransport error: ${e.message}`);
|
|
1077
|
+
}
|
|
1078
|
+
this.forceKillAppServerSync();
|
|
1079
|
+
this.proc = null;
|
|
771
1080
|
}
|
|
772
1081
|
resolveTransport() {
|
|
773
1082
|
const mode = parseTransportMode(process.env[CODEX_TRANSPORT_ENV]);
|
|
@@ -791,14 +1100,7 @@ class CodexAdapter extends EventEmitter {
|
|
|
791
1100
|
} catch {}
|
|
792
1101
|
this.secondaryConnections.delete(id);
|
|
793
1102
|
}
|
|
794
|
-
this.
|
|
795
|
-
this.proxyServer = null;
|
|
796
|
-
if (this.relay) {
|
|
797
|
-
this.relay.stop();
|
|
798
|
-
this.relay = null;
|
|
799
|
-
}
|
|
800
|
-
if (this.socketPath)
|
|
801
|
-
removeSocketFile(this.socketPath);
|
|
1103
|
+
this.teardownTransport();
|
|
802
1104
|
this.clearResponseTrackingState();
|
|
803
1105
|
this.resetTurnState(ADAPTER_DISCONNECT_REASON);
|
|
804
1106
|
}
|
|
@@ -1225,36 +1527,38 @@ class CodexAdapter extends EventEmitter {
|
|
|
1225
1527
|
const m = e instanceof Error ? e.message : String(e);
|
|
1226
1528
|
return Promise.reject(new Error(`replay parse failed for ${method}: ${m}`));
|
|
1227
1529
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
this.appServerWs.send(raw);
|
|
1236
|
-
} catch (e) {
|
|
1237
|
-
clearTimeout(timer);
|
|
1238
|
-
this.replayPending.delete(id);
|
|
1239
|
-
const m = e instanceof Error ? e.message : String(e);
|
|
1240
|
-
reject(new Error(`replay send failed for ${method}: ${m}`));
|
|
1530
|
+
const timeoutMs = CodexAdapter.SESSION_REPLAY_TIMEOUT_MS;
|
|
1531
|
+
this.replayMethods.set(id, method);
|
|
1532
|
+
const pending = this.replayPending.register(id, {
|
|
1533
|
+
timeoutMs,
|
|
1534
|
+
onTimeout: ({ reject }) => {
|
|
1535
|
+
this.replayMethods.delete(id);
|
|
1536
|
+
reject(new Error(`replay timeout (${timeoutMs}ms) for ${method} id=${JSON.stringify(id)}`));
|
|
1241
1537
|
}
|
|
1242
1538
|
});
|
|
1539
|
+
try {
|
|
1540
|
+
this.appServerWs.send(raw);
|
|
1541
|
+
} catch (e) {
|
|
1542
|
+
this.replayMethods.delete(id);
|
|
1543
|
+
const m = e instanceof Error ? e.message : String(e);
|
|
1544
|
+
this.replayPending.reject(id, new Error(`replay send failed for ${method}: ${m}`));
|
|
1545
|
+
}
|
|
1546
|
+
return pending;
|
|
1243
1547
|
}
|
|
1244
1548
|
tryConsumeReplayResponse(payload) {
|
|
1245
1549
|
const id = payload.id;
|
|
1246
1550
|
if (id === undefined)
|
|
1247
1551
|
return false;
|
|
1248
|
-
const
|
|
1249
|
-
if (!
|
|
1552
|
+
const key = id;
|
|
1553
|
+
if (!this.replayPending.has(key))
|
|
1250
1554
|
return false;
|
|
1251
|
-
|
|
1252
|
-
this.
|
|
1555
|
+
const method = this.replayMethods.get(key) ?? "replay";
|
|
1556
|
+
this.replayMethods.delete(key);
|
|
1253
1557
|
if (payload.error !== undefined) {
|
|
1254
1558
|
const errMsg = typeof payload.error === "object" && payload.error !== null && "message" in payload.error ? String(payload.error.message ?? "unknown") : JSON.stringify(payload.error);
|
|
1255
|
-
|
|
1559
|
+
this.replayPending.reject(key, new Error(`${method} rejected: ${errMsg}`));
|
|
1256
1560
|
} else {
|
|
1257
|
-
|
|
1561
|
+
this.replayPending.settle(key, payload);
|
|
1258
1562
|
}
|
|
1259
1563
|
return true;
|
|
1260
1564
|
}
|
|
@@ -1549,6 +1853,9 @@ class CodexAdapter extends EventEmitter {
|
|
|
1549
1853
|
const proxyId = this.nextProxyId++;
|
|
1550
1854
|
this.upstreamToClient.set(proxyId, { connId, clientId: parsed.id });
|
|
1551
1855
|
this.trackPendingRequest(parsed, connId, proxyId);
|
|
1856
|
+
if (parsed.method === "initialize") {
|
|
1857
|
+
this.pendingInitializeProxyIds.add(proxyId);
|
|
1858
|
+
}
|
|
1552
1859
|
parsed.id = proxyId;
|
|
1553
1860
|
forwarded = JSON.stringify(parsed);
|
|
1554
1861
|
} else {
|
|
@@ -1701,6 +2008,9 @@ class CodexAdapter extends EventEmitter {
|
|
|
1701
2008
|
const mapping = !isNaN(numericId) ? this.upstreamToClient.get(numericId) : undefined;
|
|
1702
2009
|
if (mapping) {
|
|
1703
2010
|
this.upstreamToClient.delete(numericId);
|
|
2011
|
+
if (!isNaN(numericId) && this.pendingInitializeProxyIds.delete(numericId)) {
|
|
2012
|
+
this.captureAppServerInfo(parsed.result);
|
|
2013
|
+
}
|
|
1704
2014
|
if (mapping.connId !== this.tuiConnId) {
|
|
1705
2015
|
this.log(`Dropping stale response (upstream id ${responseId}, from conn #${mapping.connId}, current #${this.tuiConnId})`);
|
|
1706
2016
|
return null;
|
|
@@ -1751,11 +2061,40 @@ class CodexAdapter extends EventEmitter {
|
|
|
1751
2061
|
this.log(`Dropping unmatched app-server response id ${String(responseId)}`);
|
|
1752
2062
|
return null;
|
|
1753
2063
|
}
|
|
2064
|
+
captureAppServerInfo(result) {
|
|
2065
|
+
const init = typeof result === "object" && result !== null ? result : {};
|
|
2066
|
+
const userAgent = typeof init.userAgent === "string" ? init.userAgent : null;
|
|
2067
|
+
const version = parseAppServerVersion(userAgent);
|
|
2068
|
+
const info = {
|
|
2069
|
+
version,
|
|
2070
|
+
userAgent,
|
|
2071
|
+
platformFamily: typeof init.platformFamily === "string" ? init.platformFamily : null,
|
|
2072
|
+
platformOs: typeof init.platformOs === "string" ? init.platformOs : null
|
|
2073
|
+
};
|
|
2074
|
+
this.appServerInfo = info;
|
|
2075
|
+
this.log(`Captured app-server initialize: version=${version ?? "unknown"} ` + `userAgent=${userAgent ?? "none"} platform=${info.platformOs ?? "?"}/${info.platformFamily ?? "?"}`);
|
|
2076
|
+
if (version === null) {
|
|
2077
|
+
const dedupKey = userAgent ?? "<missing-userAgent>";
|
|
2078
|
+
if (!this.warnedAppServerVersions.has(dedupKey)) {
|
|
2079
|
+
this.warnedAppServerVersions.add(dedupKey);
|
|
2080
|
+
this.log(`WARNING: app-server initialize response carried no parseable version ` + `(userAgent=${userAgent ?? "missing"}). The proxy's intercept points assume a ` + `known protocol snapshot \u2014 verify the version-coupling checklist if Codex was upgraded.`);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
1754
2084
|
patchResponse(parsed, raw) {
|
|
1755
2085
|
if (isAppServerResponseMessage(parsed) && parsed.error && parsed.id !== undefined) {
|
|
1756
2086
|
const errMsg = parsed.error.message ?? "";
|
|
1757
|
-
|
|
1758
|
-
|
|
2087
|
+
const errCode = parsed.error.code;
|
|
2088
|
+
const textMatchesRateLimit = errMsg.includes("rate limits") || errMsg.includes("rateLimits");
|
|
2089
|
+
const codeRecognized = typeof errCode === "number" && APP_SERVER_RATE_LIMIT_ERROR_CODES.has(errCode);
|
|
2090
|
+
const structuredMatch = codeRecognized && textMatchesRateLimit;
|
|
2091
|
+
if (structuredMatch || textMatchesRateLimit) {
|
|
2092
|
+
if (structuredMatch) {
|
|
2093
|
+
this.log(`Patching rateLimits error \u2192 mock success via structured code ${errCode} (id: ${parsed.id})`);
|
|
2094
|
+
} else {
|
|
2095
|
+
this.warnFragileRateLimitMatch(errMsg, errCode);
|
|
2096
|
+
this.log(`Patching rateLimits error \u2192 mock success via fragile text fallback (id: ${parsed.id})`);
|
|
2097
|
+
}
|
|
1759
2098
|
return JSON.stringify({
|
|
1760
2099
|
id: parsed.id,
|
|
1761
2100
|
result: {
|
|
@@ -1774,6 +2113,12 @@ class CodexAdapter extends EventEmitter {
|
|
|
1774
2113
|
}
|
|
1775
2114
|
return raw;
|
|
1776
2115
|
}
|
|
2116
|
+
warnFragileRateLimitMatch(errMsg, errCode) {
|
|
2117
|
+
if (this.warnedFragileRateLimitMessages.has(errMsg))
|
|
2118
|
+
return;
|
|
2119
|
+
this.warnedFragileRateLimitMessages.add(errMsg);
|
|
2120
|
+
this.log(`WARNING: fragile-match \u2014 patched a rate-limit error by human-readable text ` + `(code=${errCode ?? "none"} not in the recognized set). If Codex changed the ` + `error wording or code, update patchResponse / APP_SERVER_RATE_LIMIT_ERROR_CODES. ` + `Message: ${errMsg.slice(0, 120)}`);
|
|
2121
|
+
}
|
|
1777
2122
|
interceptServerMessage(msg, connId) {
|
|
1778
2123
|
this.handleTrackedResponse(msg, connId);
|
|
1779
2124
|
if ("method" in msg && typeof msg.method === "string" && isAppServerNotification(msg)) {
|
|
@@ -1977,15 +2322,27 @@ class CodexAdapter extends EventEmitter {
|
|
|
1977
2322
|
markTurnCompleted(turnId) {
|
|
1978
2323
|
const completedId = typeof turnId === "string" && turnId.length > 0 ? turnId : null;
|
|
1979
2324
|
if (completedId !== null) {
|
|
2325
|
+
const idWasTracked = this.activeTurnIds.has(completedId);
|
|
1980
2326
|
this.activeTurnIds.delete(completedId);
|
|
1981
2327
|
this.clearTurnWatchdog(completedId);
|
|
1982
2328
|
this.stalledTurnIds.delete(completedId);
|
|
1983
2329
|
this.currentlyStalledTurnIds.delete(completedId);
|
|
2330
|
+
if (!idWasTracked) {
|
|
2331
|
+
const placeholders = [...this.activeTurnIds].filter((id) => id.startsWith("unknown:"));
|
|
2332
|
+
if (placeholders.length === 1) {
|
|
2333
|
+
const placeholder = placeholders[0];
|
|
2334
|
+
this.activeTurnIds.delete(placeholder);
|
|
2335
|
+
this.clearTurnWatchdog(placeholder);
|
|
2336
|
+
this.stalledTurnIds.delete(placeholder);
|
|
2337
|
+
this.currentlyStalledTurnIds.delete(placeholder);
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
1984
2340
|
} else {
|
|
1985
2341
|
this.activeTurnIds.clear();
|
|
1986
2342
|
this.clearAllTurnWatchdogs();
|
|
1987
2343
|
this.stalledTurnIds.clear();
|
|
1988
2344
|
this.currentlyStalledTurnIds.clear();
|
|
2345
|
+
this.agentMessageBuffers.clear();
|
|
1989
2346
|
}
|
|
1990
2347
|
this.lastTurnEndedAbnormally = false;
|
|
1991
2348
|
this.turnInProgress = this.activeTurnIds.size > 0;
|
|
@@ -2048,6 +2405,7 @@ class CodexAdapter extends EventEmitter {
|
|
|
2048
2405
|
this.clearAllTurnWatchdogs();
|
|
2049
2406
|
this.stalledTurnIds.clear();
|
|
2050
2407
|
this.currentlyStalledTurnIds.clear();
|
|
2408
|
+
this.agentMessageBuffers.clear();
|
|
2051
2409
|
this.turnInProgress = false;
|
|
2052
2410
|
if (wasInProgress) {
|
|
2053
2411
|
this.lastTurnEndedAbnormally = !emitCompleted;
|
|
@@ -2136,6 +2494,7 @@ class CodexAdapter extends EventEmitter {
|
|
|
2136
2494
|
clearTransientResponseTrackingState() {
|
|
2137
2495
|
this.pendingRequests.clear();
|
|
2138
2496
|
this.upstreamToClient.clear();
|
|
2497
|
+
this.pendingInitializeProxyIds.clear();
|
|
2139
2498
|
for (const timer of this.staleProxyIds.values()) {
|
|
2140
2499
|
clearTimeout(timer);
|
|
2141
2500
|
}
|
|
@@ -2191,11 +2550,65 @@ var CLOSE_CODE_REPLACED = 4001;
|
|
|
2191
2550
|
var CLOSE_CODE_EVICTED_STALE = 4002;
|
|
2192
2551
|
var CLOSE_CODE_PROBE_IN_PROGRESS = 4003;
|
|
2193
2552
|
var CLOSE_CODE_PAIR_MISMATCH = 4004;
|
|
2553
|
+
var CLOSE_CODE_TOKEN_MISMATCH = 4005;
|
|
2554
|
+
var CLOSE_CODE_CONTRACT_MISMATCH = 4006;
|
|
2555
|
+
|
|
2556
|
+
// src/control-token.ts
|
|
2557
|
+
import { chmodSync as chmodSync2, readFileSync as readFileSync2 } from "fs";
|
|
2558
|
+
import { join as join3 } from "path";
|
|
2559
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2560
|
+
var CONTROL_TOKEN_FILENAME = "control-token";
|
|
2561
|
+
function resolveControlTokenPath(stateDir) {
|
|
2562
|
+
return join3(stateDir, CONTROL_TOKEN_FILENAME);
|
|
2563
|
+
}
|
|
2564
|
+
function generateControlToken() {
|
|
2565
|
+
return randomUUID2();
|
|
2566
|
+
}
|
|
2567
|
+
function writeControlToken(path, token) {
|
|
2568
|
+
atomicWriteText(path, token, { mode: 384 });
|
|
2569
|
+
chmodSync2(path, 384);
|
|
2570
|
+
}
|
|
2571
|
+
function validateControlToken(input) {
|
|
2572
|
+
const { expectedToken } = input;
|
|
2573
|
+
if (expectedToken == null || expectedToken.length === 0) {
|
|
2574
|
+
return { ok: true };
|
|
2575
|
+
}
|
|
2576
|
+
const provided = input.providedToken;
|
|
2577
|
+
if (provided == null || provided.length === 0) {
|
|
2578
|
+
return { ok: false, reason: "missing control token" };
|
|
2579
|
+
}
|
|
2580
|
+
if (!constantTimeEquals(provided, expectedToken)) {
|
|
2581
|
+
return { ok: false, reason: "control token mismatch" };
|
|
2582
|
+
}
|
|
2583
|
+
return { ok: true };
|
|
2584
|
+
}
|
|
2585
|
+
function constantTimeEquals(a, b) {
|
|
2586
|
+
const len = Math.max(a.length, b.length);
|
|
2587
|
+
let diff = a.length ^ b.length;
|
|
2588
|
+
for (let i = 0;i < len; i++) {
|
|
2589
|
+
diff |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
|
|
2590
|
+
}
|
|
2591
|
+
return diff === 0;
|
|
2592
|
+
}
|
|
2194
2593
|
|
|
2195
2594
|
// src/daemon-identity.ts
|
|
2196
2595
|
function validateClaudeClientIdentity(input) {
|
|
2197
|
-
if (
|
|
2198
|
-
|
|
2596
|
+
if (input.expectedControlToken && input.identity) {
|
|
2597
|
+
const tokenResult = validateControlToken({
|
|
2598
|
+
expectedToken: input.expectedControlToken,
|
|
2599
|
+
providedToken: input.identity.controlToken
|
|
2600
|
+
});
|
|
2601
|
+
if (!tokenResult.ok) {
|
|
2602
|
+
return {
|
|
2603
|
+
ok: false,
|
|
2604
|
+
closeCode: CLOSE_CODE_TOKEN_MISMATCH,
|
|
2605
|
+
reason: tokenResult.reason
|
|
2606
|
+
};
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
if (!input.expectedPairId) {
|
|
2610
|
+
return input.identity ? validateContractVersion(input) : { ok: true };
|
|
2611
|
+
}
|
|
2199
2612
|
if (!input.identity) {
|
|
2200
2613
|
return input.allowIdentityless ? { ok: true } : { ok: false, closeCode: CLOSE_CODE_PAIR_MISMATCH, reason: "missing client identity" };
|
|
2201
2614
|
}
|
|
@@ -2213,10 +2626,43 @@ function validateClaudeClientIdentity(input) {
|
|
|
2213
2626
|
reason: `cwd mismatch: expected ${input.daemonCwd}, got ${input.identity.cwd ?? "<none>"}`
|
|
2214
2627
|
};
|
|
2215
2628
|
}
|
|
2629
|
+
return validateContractVersion(input);
|
|
2630
|
+
}
|
|
2631
|
+
function validateContractVersion(input) {
|
|
2632
|
+
if (input.expectedContractVersion === undefined)
|
|
2633
|
+
return { ok: true };
|
|
2634
|
+
const provided = input.identity?.contractVersion;
|
|
2635
|
+
if (provided === undefined || provided === null) {
|
|
2636
|
+
return {
|
|
2637
|
+
ok: false,
|
|
2638
|
+
closeCode: CLOSE_CODE_CONTRACT_MISMATCH,
|
|
2639
|
+
reason: `missing contract version: daemon speaks contract v${input.expectedContractVersion}`
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
if (provided !== input.expectedContractVersion) {
|
|
2643
|
+
return {
|
|
2644
|
+
ok: false,
|
|
2645
|
+
closeCode: CLOSE_CODE_CONTRACT_MISMATCH,
|
|
2646
|
+
reason: `contract version mismatch: daemon v${input.expectedContractVersion}, client v${provided}`
|
|
2647
|
+
};
|
|
2648
|
+
}
|
|
2216
2649
|
return { ok: true };
|
|
2217
2650
|
}
|
|
2651
|
+
function evaluateInjectionAttachGuard(attachedSocket, requestingSocket) {
|
|
2652
|
+
if (attachedSocket != null && attachedSocket === requestingSocket) {
|
|
2653
|
+
return { allowed: true };
|
|
2654
|
+
}
|
|
2655
|
+
return {
|
|
2656
|
+
allowed: false,
|
|
2657
|
+
code: "not_attached",
|
|
2658
|
+
reason: "This socket is not the attached Claude session. Send `claude_connect` " + "(with a valid control token) and win the attach slot before injecting a turn."
|
|
2659
|
+
};
|
|
2660
|
+
}
|
|
2218
2661
|
|
|
2219
2662
|
// src/message-filter.ts
|
|
2663
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
2664
|
+
var STATUS_SUMMARY_SALT = randomUUID3().slice(0, 8);
|
|
2665
|
+
var statusSummaryCounter = 0;
|
|
2220
2666
|
var MARKER_REGEX = /^\s*\[(IMPORTANT|STATUS|FYI)\]\s*/i;
|
|
2221
2667
|
function parseMarker(content) {
|
|
2222
2668
|
const match = content.match(MARKER_REGEX);
|
|
@@ -2242,6 +2688,37 @@ function classifyMessage(content, mode) {
|
|
|
2242
2688
|
return { action: "forward", marker };
|
|
2243
2689
|
}
|
|
2244
2690
|
}
|
|
2691
|
+
function routeCodexMessage(content, ctx) {
|
|
2692
|
+
const result = classifyMessage(content, ctx.mode);
|
|
2693
|
+
if (ctx.replyArmed) {
|
|
2694
|
+
return {
|
|
2695
|
+
action: "forward",
|
|
2696
|
+
marker: result.marker,
|
|
2697
|
+
reason: "force-forward-reply-required",
|
|
2698
|
+
flushStatusBuffer: true,
|
|
2699
|
+
noteReplyForwarded: true
|
|
2700
|
+
};
|
|
2701
|
+
}
|
|
2702
|
+
if (ctx.inAttentionWindow && result.marker === "status") {
|
|
2703
|
+
return {
|
|
2704
|
+
action: "buffer",
|
|
2705
|
+
marker: result.marker,
|
|
2706
|
+
reason: "buffer-attention"
|
|
2707
|
+
};
|
|
2708
|
+
}
|
|
2709
|
+
if (result.action === "forward" && result.marker === "important") {
|
|
2710
|
+
return {
|
|
2711
|
+
...result,
|
|
2712
|
+
reason: "forward",
|
|
2713
|
+
flushStatusBuffer: true,
|
|
2714
|
+
startAttentionWindow: true
|
|
2715
|
+
};
|
|
2716
|
+
}
|
|
2717
|
+
return {
|
|
2718
|
+
...result,
|
|
2719
|
+
reason: result.action
|
|
2720
|
+
};
|
|
2721
|
+
}
|
|
2245
2722
|
var REPLY_REQUIRED_INSTRUCTION = `
|
|
2246
2723
|
|
|
2247
2724
|
[\u26A0\uFE0F REPLY REQUIRED] Claude has explicitly requested a reply. You MUST send an agentMessage with [IMPORTANT] marker containing your response. This is a mandatory requirement \u2014 do not skip or use [STATUS]/[FYI] markers for this reply.`;
|
|
@@ -2251,11 +2728,14 @@ class StatusBuffer {
|
|
|
2251
2728
|
flushTimer = null;
|
|
2252
2729
|
flushThreshold;
|
|
2253
2730
|
flushTimeoutMs;
|
|
2731
|
+
maxBuffered;
|
|
2254
2732
|
paused = false;
|
|
2733
|
+
droppedCount = 0;
|
|
2255
2734
|
constructor(onFlush, options) {
|
|
2256
2735
|
this.onFlush = onFlush;
|
|
2257
2736
|
this.flushThreshold = options?.flushThreshold ?? 3;
|
|
2258
2737
|
this.flushTimeoutMs = options?.flushTimeoutMs ?? 15000;
|
|
2738
|
+
this.maxBuffered = options?.maxBuffered ?? 200;
|
|
2259
2739
|
}
|
|
2260
2740
|
get size() {
|
|
2261
2741
|
return this.buffer.length;
|
|
@@ -2275,6 +2755,10 @@ class StatusBuffer {
|
|
|
2275
2755
|
}
|
|
2276
2756
|
add(message) {
|
|
2277
2757
|
this.buffer.push(message);
|
|
2758
|
+
while (this.buffer.length > this.maxBuffered) {
|
|
2759
|
+
this.buffer.shift();
|
|
2760
|
+
this.droppedCount++;
|
|
2761
|
+
}
|
|
2278
2762
|
if (this.paused)
|
|
2279
2763
|
return;
|
|
2280
2764
|
this.resetTimer();
|
|
@@ -2289,19 +2773,22 @@ class StatusBuffer {
|
|
|
2289
2773
|
const combined = this.buffer.map((m) => parseMarker(m.content).body).join(`
|
|
2290
2774
|
---
|
|
2291
2775
|
`);
|
|
2776
|
+
const droppedNote = this.droppedCount > 0 ? `, ${this.droppedCount} older dropped` : "";
|
|
2292
2777
|
const summary = {
|
|
2293
|
-
id: `status_summary_${
|
|
2778
|
+
id: `status_summary_${STATUS_SUMMARY_SALT}_${++statusSummaryCounter}`,
|
|
2294
2779
|
source: "codex",
|
|
2295
|
-
content: `[STATUS summary \u2014 ${this.buffer.length} update(s), flushed: ${reason}]
|
|
2780
|
+
content: `[STATUS summary \u2014 ${this.buffer.length} update(s)${droppedNote}, flushed: ${reason}]
|
|
2296
2781
|
${combined}`,
|
|
2297
2782
|
timestamp: Date.now()
|
|
2298
2783
|
};
|
|
2299
2784
|
this.onFlush(summary);
|
|
2300
2785
|
this.buffer = [];
|
|
2786
|
+
this.droppedCount = 0;
|
|
2301
2787
|
}
|
|
2302
2788
|
dispose() {
|
|
2303
2789
|
this.clearTimer();
|
|
2304
2790
|
this.buffer = [];
|
|
2791
|
+
this.droppedCount = 0;
|
|
2305
2792
|
}
|
|
2306
2793
|
clearTimer() {
|
|
2307
2794
|
if (this.flushTimer) {
|
|
@@ -2398,7 +2885,7 @@ class TuiConnectionState {
|
|
|
2398
2885
|
|
|
2399
2886
|
// src/daemon-lifecycle.ts
|
|
2400
2887
|
import { spawn as spawn2 } from "child_process";
|
|
2401
|
-
import { existsSync as existsSync3, readFileSync, statSync as statSync2, unlinkSync as
|
|
2888
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, statSync as statSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync2, openSync as openSync2, closeSync as closeSync2, constants } from "fs";
|
|
2402
2889
|
import { fileURLToPath } from "url";
|
|
2403
2890
|
|
|
2404
2891
|
// src/process-lifecycle.ts
|
|
@@ -2442,6 +2929,8 @@ var DAEMON_ENTRY = process.env.AGENTBRIDGE_DAEMON_ENTRY || DEFAULT_DAEMON_ENTRY;
|
|
|
2442
2929
|
var DAEMON_PATH = fileURLToPath(new URL(DAEMON_ENTRY, import.meta.url));
|
|
2443
2930
|
var REUSE_READY_RETRIES = parsePositiveIntEnv("AGENTBRIDGE_REUSE_READY_RETRIES", 12);
|
|
2444
2931
|
var REUSE_READY_DELAY_MS = 250;
|
|
2932
|
+
var WAIT_READY_RETRIES = 40;
|
|
2933
|
+
var WAIT_READY_DELAY_MS = 250;
|
|
2445
2934
|
var HEALTH_FETCH_TIMEOUT_MS = 500;
|
|
2446
2935
|
var LOCK_IDENTITY_GRACE_MS = parsePositiveIntEnv("AGENTBRIDGE_LOCK_IDENTITY_GRACE_MS", 120000);
|
|
2447
2936
|
function isReuseVerdict(verdict) {
|
|
@@ -2479,22 +2968,33 @@ function classifyDaemon(expectedPairId, status, buildInfo) {
|
|
|
2479
2968
|
reason: "runtime build drift has a compatible contract and a live Codex TUI is attached"
|
|
2480
2969
|
};
|
|
2481
2970
|
}
|
|
2971
|
+
const basis = runtimeContractComparisonBasis(status.build, buildInfo) === "codeHash" ? "compared by codeHash" : "compared by commit stamp; legacy build without codeHash";
|
|
2482
2972
|
return {
|
|
2483
2973
|
verdict: "replace-drifted",
|
|
2484
|
-
reason: `runtime build ${formatBuildInfo(status.build)} does not match launcher
|
|
2974
|
+
reason: `runtime build ${formatBuildInfo(status.build)} does not match launcher ` + `${formatBuildInfo(buildInfo)} (${basis})`
|
|
2485
2975
|
};
|
|
2486
2976
|
}
|
|
2487
2977
|
return { verdict: "reuse", reason: "daemon pair and runtime contract match" };
|
|
2488
2978
|
}
|
|
2979
|
+
function resolveTiming(timing) {
|
|
2980
|
+
return {
|
|
2981
|
+
reuseReadyRetries: timing?.reuseReadyRetries ?? REUSE_READY_RETRIES,
|
|
2982
|
+
reuseReadyDelayMs: timing?.reuseReadyDelayMs ?? REUSE_READY_DELAY_MS,
|
|
2983
|
+
waitReadyRetries: timing?.waitReadyRetries ?? WAIT_READY_RETRIES,
|
|
2984
|
+
waitReadyDelayMs: timing?.waitReadyDelayMs ?? WAIT_READY_DELAY_MS
|
|
2985
|
+
};
|
|
2986
|
+
}
|
|
2489
2987
|
|
|
2490
2988
|
class DaemonLifecycle {
|
|
2491
2989
|
stateDir;
|
|
2492
2990
|
controlPort;
|
|
2493
2991
|
log;
|
|
2992
|
+
timing;
|
|
2494
2993
|
constructor(opts) {
|
|
2495
2994
|
this.stateDir = opts.stateDir;
|
|
2496
2995
|
this.controlPort = opts.controlPort;
|
|
2497
2996
|
this.log = opts.log;
|
|
2997
|
+
this.timing = resolveTiming(opts.timing);
|
|
2498
2998
|
}
|
|
2499
2999
|
get healthUrl() {
|
|
2500
3000
|
return `http://127.0.0.1:${this.controlPort}/healthz`;
|
|
@@ -2551,7 +3051,7 @@ class DaemonLifecycle {
|
|
|
2551
3051
|
break;
|
|
2552
3052
|
}
|
|
2553
3053
|
try {
|
|
2554
|
-
await this.waitForReady(
|
|
3054
|
+
await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
|
|
2555
3055
|
return;
|
|
2556
3056
|
} catch {
|
|
2557
3057
|
this.log(`Daemon on control port ${this.controlPort} is healthy but not ready within reuse window \u2014 replacing`);
|
|
@@ -2564,7 +3064,7 @@ class DaemonLifecycle {
|
|
|
2564
3064
|
if (isProcessAlive(existingPid)) {
|
|
2565
3065
|
if (isAgentBridgeDaemon(existingPid)) {
|
|
2566
3066
|
try {
|
|
2567
|
-
await this.waitForReady(
|
|
3067
|
+
await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
|
|
2568
3068
|
return;
|
|
2569
3069
|
} catch {
|
|
2570
3070
|
this.log(`Existing daemon process ${existingPid} never became ready \u2014 replacing`);
|
|
@@ -2592,7 +3092,7 @@ class DaemonLifecycle {
|
|
|
2592
3092
|
await this.kill(3000, status?.pid);
|
|
2593
3093
|
} else {
|
|
2594
3094
|
try {
|
|
2595
|
-
await this.waitForReady(
|
|
3095
|
+
await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
|
|
2596
3096
|
return;
|
|
2597
3097
|
} catch {
|
|
2598
3098
|
this.log(`Daemon on control port ${this.controlPort} is healthy but not ready under startup lock \u2014 replacing`);
|
|
@@ -2601,7 +3101,7 @@ class DaemonLifecycle {
|
|
|
2601
3101
|
}
|
|
2602
3102
|
}
|
|
2603
3103
|
this.launch();
|
|
2604
|
-
await this.waitForReady();
|
|
3104
|
+
await this.waitForReady(this.timing.waitReadyRetries, this.timing.waitReadyDelayMs);
|
|
2605
3105
|
});
|
|
2606
3106
|
}
|
|
2607
3107
|
async isHealthy() {
|
|
@@ -2628,7 +3128,7 @@ class DaemonLifecycle {
|
|
|
2628
3128
|
return false;
|
|
2629
3129
|
}
|
|
2630
3130
|
}
|
|
2631
|
-
async waitForReady(maxRetries =
|
|
3131
|
+
async waitForReady(maxRetries = WAIT_READY_RETRIES, delayMs = WAIT_READY_DELAY_MS) {
|
|
2632
3132
|
for (let attempt = 0;attempt < maxRetries; attempt++) {
|
|
2633
3133
|
if (await this.isReady())
|
|
2634
3134
|
return;
|
|
@@ -2636,7 +3136,7 @@ class DaemonLifecycle {
|
|
|
2636
3136
|
}
|
|
2637
3137
|
throw new Error(`Timed out waiting for AgentBridge daemon readiness on ${this.readyUrl}`);
|
|
2638
3138
|
}
|
|
2639
|
-
async waitForReadyAndOurs(maxRetries =
|
|
3139
|
+
async waitForReadyAndOurs(maxRetries = WAIT_READY_RETRIES, delayMs = WAIT_READY_DELAY_MS) {
|
|
2640
3140
|
for (let attempt = 0;attempt < maxRetries; attempt++) {
|
|
2641
3141
|
if (await this.isReady()) {
|
|
2642
3142
|
const status = await this.fetchStatus();
|
|
@@ -2652,22 +3152,35 @@ class DaemonLifecycle {
|
|
|
2652
3152
|
}
|
|
2653
3153
|
throw new Error(`Timed out waiting for AgentBridge daemon readiness+identity on ${this.readyUrl} (control port ${this.controlPort})`);
|
|
2654
3154
|
}
|
|
3155
|
+
readDaemonRecord() {
|
|
3156
|
+
return readUnifiedDaemonRecord({
|
|
3157
|
+
daemonRecordFile: this.stateDir.daemonRecordFile,
|
|
3158
|
+
pidFile: this.stateDir.pidFile,
|
|
3159
|
+
statusFile: this.stateDir.statusFile
|
|
3160
|
+
});
|
|
3161
|
+
}
|
|
3162
|
+
writeDaemonRecord(record) {
|
|
3163
|
+
writeDaemonRecord(this.stateDir.daemonRecordFile, record);
|
|
3164
|
+
}
|
|
3165
|
+
removeDaemonRecord() {
|
|
3166
|
+
try {
|
|
3167
|
+
unlinkSync3(this.stateDir.daemonRecordFile);
|
|
3168
|
+
} catch {}
|
|
3169
|
+
}
|
|
2655
3170
|
readStatus() {
|
|
2656
3171
|
try {
|
|
2657
|
-
const raw =
|
|
3172
|
+
const raw = readFileSync3(this.stateDir.statusFile, "utf-8");
|
|
2658
3173
|
return JSON.parse(raw);
|
|
2659
3174
|
} catch {
|
|
2660
3175
|
return null;
|
|
2661
3176
|
}
|
|
2662
3177
|
}
|
|
2663
3178
|
writeStatus(status) {
|
|
2664
|
-
this.stateDir.
|
|
2665
|
-
writeFileSync(this.stateDir.statusFile, JSON.stringify(status, null, 2) + `
|
|
2666
|
-
`, "utf-8");
|
|
3179
|
+
atomicWriteJson(this.stateDir.statusFile, status);
|
|
2667
3180
|
}
|
|
2668
3181
|
readPid() {
|
|
2669
3182
|
try {
|
|
2670
|
-
const raw =
|
|
3183
|
+
const raw = readFileSync3(this.stateDir.pidFile, "utf-8").trim();
|
|
2671
3184
|
if (!raw)
|
|
2672
3185
|
return null;
|
|
2673
3186
|
const pid = Number.parseInt(raw, 10);
|
|
@@ -2677,28 +3190,27 @@ class DaemonLifecycle {
|
|
|
2677
3190
|
}
|
|
2678
3191
|
}
|
|
2679
3192
|
writePid(pid) {
|
|
2680
|
-
this.stateDir.
|
|
2681
|
-
|
|
2682
|
-
`, "utf-8");
|
|
3193
|
+
atomicWriteText(this.stateDir.pidFile, `${pid ?? process.pid}
|
|
3194
|
+
`);
|
|
2683
3195
|
}
|
|
2684
3196
|
removePidFile() {
|
|
2685
3197
|
try {
|
|
2686
|
-
|
|
3198
|
+
unlinkSync3(this.stateDir.pidFile);
|
|
2687
3199
|
} catch {}
|
|
2688
3200
|
}
|
|
2689
3201
|
removeStatusFile() {
|
|
2690
3202
|
try {
|
|
2691
|
-
|
|
3203
|
+
unlinkSync3(this.stateDir.statusFile);
|
|
2692
3204
|
} catch {}
|
|
2693
3205
|
}
|
|
2694
3206
|
markKilled() {
|
|
2695
3207
|
this.stateDir.ensure();
|
|
2696
|
-
|
|
3208
|
+
writeFileSync2(this.stateDir.killedFile, `${Date.now()}
|
|
2697
3209
|
`, "utf-8");
|
|
2698
3210
|
}
|
|
2699
3211
|
clearKilled() {
|
|
2700
3212
|
try {
|
|
2701
|
-
|
|
3213
|
+
unlinkSync3(this.stateDir.killedFile);
|
|
2702
3214
|
} catch {}
|
|
2703
3215
|
}
|
|
2704
3216
|
wasKilled() {
|
|
@@ -2720,8 +3232,10 @@ class DaemonLifecycle {
|
|
|
2720
3232
|
daemonProc.unref();
|
|
2721
3233
|
}
|
|
2722
3234
|
removeStalePidFile() {
|
|
2723
|
-
this.log("Removing stale
|
|
3235
|
+
this.log("Removing stale daemon identity files");
|
|
2724
3236
|
this.removePidFile();
|
|
3237
|
+
this.removeStatusFile();
|
|
3238
|
+
this.removeDaemonRecord();
|
|
2725
3239
|
}
|
|
2726
3240
|
async replaceUnhealthyDaemon(statusPid) {
|
|
2727
3241
|
await this.withStartupLockStrict(async (locked) => {
|
|
@@ -2737,7 +3251,7 @@ class DaemonLifecycle {
|
|
|
2737
3251
|
}
|
|
2738
3252
|
if (isReuseVerdict(classification.verdict)) {
|
|
2739
3253
|
try {
|
|
2740
|
-
await this.waitForReady(
|
|
3254
|
+
await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
|
|
2741
3255
|
return;
|
|
2742
3256
|
} catch {}
|
|
2743
3257
|
}
|
|
@@ -2745,12 +3259,12 @@ class DaemonLifecycle {
|
|
|
2745
3259
|
this.log(`Killing unhealthy daemon on control port ${this.controlPort} and relaunching`);
|
|
2746
3260
|
await this.kill(3000, statusPid);
|
|
2747
3261
|
this.launch();
|
|
2748
|
-
await this.waitForReady();
|
|
3262
|
+
await this.waitForReady(this.timing.waitReadyRetries, this.timing.waitReadyDelayMs);
|
|
2749
3263
|
});
|
|
2750
3264
|
}
|
|
2751
3265
|
async waitForContendedStartupLock() {
|
|
2752
3266
|
this.log("Another process holds the startup lock, waiting for readiness+identity...");
|
|
2753
|
-
await this.waitForReadyAndOurs();
|
|
3267
|
+
await this.waitForReadyAndOurs(this.timing.waitReadyRetries, this.timing.waitReadyDelayMs);
|
|
2754
3268
|
}
|
|
2755
3269
|
async withStartupLockStrict(fn) {
|
|
2756
3270
|
const locked = this.acquireLockStrict();
|
|
@@ -2765,15 +3279,15 @@ class DaemonLifecycle {
|
|
|
2765
3279
|
this.stateDir.ensure();
|
|
2766
3280
|
let fd = null;
|
|
2767
3281
|
try {
|
|
2768
|
-
fd =
|
|
2769
|
-
|
|
3282
|
+
fd = openSync2(this.stateDir.lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
|
|
3283
|
+
writeFileSync2(fd, `${process.pid}
|
|
2770
3284
|
`);
|
|
2771
|
-
|
|
3285
|
+
closeSync2(fd);
|
|
2772
3286
|
return true;
|
|
2773
3287
|
} catch (err) {
|
|
2774
3288
|
if (fd !== null && err.code !== "EEXIST") {
|
|
2775
3289
|
try {
|
|
2776
|
-
|
|
3290
|
+
closeSync2(fd);
|
|
2777
3291
|
} catch {}
|
|
2778
3292
|
this.releaseLock();
|
|
2779
3293
|
}
|
|
@@ -2781,7 +3295,7 @@ class DaemonLifecycle {
|
|
|
2781
3295
|
if (reclaimed)
|
|
2782
3296
|
return false;
|
|
2783
3297
|
try {
|
|
2784
|
-
const holderPid = Number.parseInt(
|
|
3298
|
+
const holderPid = Number.parseInt(readFileSync3(this.stateDir.lockFile, "utf-8").trim(), 10);
|
|
2785
3299
|
if (Number.isFinite(holderPid) && !isProcessAlive(holderPid)) {
|
|
2786
3300
|
this.log(`Stale startup lock from dead process ${holderPid}, reclaiming`);
|
|
2787
3301
|
this.releaseLock();
|
|
@@ -2810,7 +3324,7 @@ class DaemonLifecycle {
|
|
|
2810
3324
|
}
|
|
2811
3325
|
releaseLock() {
|
|
2812
3326
|
try {
|
|
2813
|
-
|
|
3327
|
+
unlinkSync3(this.stateDir.lockFile);
|
|
2814
3328
|
} catch {}
|
|
2815
3329
|
}
|
|
2816
3330
|
async kill(gracefulTimeoutMs = 3000, pidOverride) {
|
|
@@ -2856,6 +3370,7 @@ class DaemonLifecycle {
|
|
|
2856
3370
|
cleanup() {
|
|
2857
3371
|
this.removePidFile();
|
|
2858
3372
|
this.removeStatusFile();
|
|
3373
|
+
this.removeDaemonRecord();
|
|
2859
3374
|
}
|
|
2860
3375
|
}
|
|
2861
3376
|
async function fetchWithTimeout(url, timeoutMs = HEALTH_FETCH_TIMEOUT_MS) {
|
|
@@ -2869,8 +3384,8 @@ async function fetchWithTimeout(url, timeoutMs = HEALTH_FETCH_TIMEOUT_MS) {
|
|
|
2869
3384
|
}
|
|
2870
3385
|
|
|
2871
3386
|
// src/config-service.ts
|
|
2872
|
-
import { readFileSync as
|
|
2873
|
-
import { join as
|
|
3387
|
+
import { readFileSync as readFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync4 } from "fs";
|
|
3388
|
+
import { join as join4 } from "path";
|
|
2874
3389
|
var DEFAULT_BUDGET_CONFIG = {
|
|
2875
3390
|
enabled: true,
|
|
2876
3391
|
pollSeconds: 300,
|
|
@@ -2886,7 +3401,8 @@ var DEFAULT_BUDGET_CONFIG = {
|
|
|
2886
3401
|
full: null,
|
|
2887
3402
|
balanced: { effort: "medium" },
|
|
2888
3403
|
eco: { effort: "low" }
|
|
2889
|
-
}
|
|
3404
|
+
},
|
|
3405
|
+
strategy: "conserve"
|
|
2890
3406
|
};
|
|
2891
3407
|
var DEFAULT_CONFIG = {
|
|
2892
3408
|
version: "1.0",
|
|
@@ -2964,6 +3480,9 @@ function normalizeBoundedInteger(value, fallback, min, max) {
|
|
|
2964
3480
|
return fallback;
|
|
2965
3481
|
return parsed;
|
|
2966
3482
|
}
|
|
3483
|
+
function normalizeStrategy(value, fallback) {
|
|
3484
|
+
return value === "conserve" || value === "maximize" ? value : fallback;
|
|
3485
|
+
}
|
|
2967
3486
|
function normalizeBoolean(value, fallback) {
|
|
2968
3487
|
if (typeof value === "boolean")
|
|
2969
3488
|
return value;
|
|
@@ -3012,7 +3531,8 @@ function normalizeBudgetConfig(raw, fallback = DEFAULT_BUDGET_CONFIG) {
|
|
|
3012
3531
|
timeWindowSec: normalizeBoundedInteger(parallel.timeWindowSec, fallback.parallel.timeWindowSec, 60, 604800)
|
|
3013
3532
|
},
|
|
3014
3533
|
codexTierControl: normalizeBoolean(budget.codexTierControl, fallback.codexTierControl) && codexTiers.full !== null,
|
|
3015
|
-
codexTiers
|
|
3534
|
+
codexTiers,
|
|
3535
|
+
strategy: normalizeStrategy(budget.strategy, fallback.strategy)
|
|
3016
3536
|
};
|
|
3017
3537
|
}
|
|
3018
3538
|
function applyBudgetEnvOverrides(budget, env = process.env) {
|
|
@@ -3027,7 +3547,8 @@ function applyBudgetEnvOverrides(budget, env = process.env) {
|
|
|
3027
3547
|
timeWindowSec: env.AGENTBRIDGE_BUDGET_PARALLEL_TIME_WINDOW_SEC ?? budget.parallel.timeWindowSec
|
|
3028
3548
|
},
|
|
3029
3549
|
codexTierControl: env.AGENTBRIDGE_BUDGET_CODEX_TIER_CONTROL ?? budget.codexTierControl,
|
|
3030
|
-
codexTiers: budget.codexTiers
|
|
3550
|
+
codexTiers: budget.codexTiers,
|
|
3551
|
+
strategy: env.AGENTBRIDGE_BUDGET_STRATEGY ?? budget.strategy
|
|
3031
3552
|
};
|
|
3032
3553
|
return normalizeBudgetConfig(overlay, budget);
|
|
3033
3554
|
}
|
|
@@ -3041,13 +3562,13 @@ function normalizeConfig(raw) {
|
|
|
3041
3562
|
return {
|
|
3042
3563
|
version: typeof config.version === "string" ? config.version : DEFAULT_CONFIG.version,
|
|
3043
3564
|
codex: {
|
|
3044
|
-
appPort:
|
|
3045
|
-
proxyPort:
|
|
3565
|
+
appPort: normalizeBoundedInteger(codex.appPort ?? daemon.port, DEFAULT_CONFIG.codex.appPort, 1, 65535),
|
|
3566
|
+
proxyPort: normalizeBoundedInteger(codex.proxyPort ?? daemon.proxyPort, DEFAULT_CONFIG.codex.proxyPort, 1, 65535)
|
|
3046
3567
|
},
|
|
3047
3568
|
turnCoordination: {
|
|
3048
|
-
attentionWindowSeconds:
|
|
3569
|
+
attentionWindowSeconds: normalizeBoundedInteger(turnCoordination.attentionWindowSeconds, DEFAULT_CONFIG.turnCoordination.attentionWindowSeconds, 0, Number.MAX_SAFE_INTEGER)
|
|
3049
3570
|
},
|
|
3050
|
-
idleShutdownSeconds:
|
|
3571
|
+
idleShutdownSeconds: normalizeBoundedInteger(config.idleShutdownSeconds, DEFAULT_CONFIG.idleShutdownSeconds, 1, Number.MAX_SAFE_INTEGER),
|
|
3051
3572
|
budget: normalizeBudgetConfig(config.budget)
|
|
3052
3573
|
};
|
|
3053
3574
|
}
|
|
@@ -3057,8 +3578,8 @@ class ConfigService {
|
|
|
3057
3578
|
configPath;
|
|
3058
3579
|
constructor(projectRoot) {
|
|
3059
3580
|
const root = projectRoot ?? process.cwd();
|
|
3060
|
-
this.configDir =
|
|
3061
|
-
this.configPath =
|
|
3581
|
+
this.configDir = join4(root, CONFIG_DIR);
|
|
3582
|
+
this.configPath = join4(this.configDir, CONFIG_FILE);
|
|
3062
3583
|
}
|
|
3063
3584
|
hasConfig() {
|
|
3064
3585
|
return existsSync4(this.configPath);
|
|
@@ -3066,7 +3587,7 @@ class ConfigService {
|
|
|
3066
3587
|
load() {
|
|
3067
3588
|
let raw;
|
|
3068
3589
|
try {
|
|
3069
|
-
raw =
|
|
3590
|
+
raw = readFileSync4(this.configPath, "utf-8");
|
|
3070
3591
|
} catch (err) {
|
|
3071
3592
|
if (err?.code === "ENOENT") {
|
|
3072
3593
|
return { state: "absent" };
|
|
@@ -3119,9 +3640,7 @@ class ConfigService {
|
|
|
3119
3640
|
};
|
|
3120
3641
|
}
|
|
3121
3642
|
save(config) {
|
|
3122
|
-
this.
|
|
3123
|
-
writeFileSync2(this.configPath, JSON.stringify(config, null, 2) + `
|
|
3124
|
-
`, "utf-8");
|
|
3643
|
+
atomicWriteJson(this.configPath, config);
|
|
3125
3644
|
}
|
|
3126
3645
|
initDefaults() {
|
|
3127
3646
|
this.ensureConfigDir();
|
|
@@ -3137,11 +3656,32 @@ class ConfigService {
|
|
|
3137
3656
|
}
|
|
3138
3657
|
ensureConfigDir() {
|
|
3139
3658
|
if (!existsSync4(this.configDir)) {
|
|
3140
|
-
|
|
3659
|
+
mkdirSync4(this.configDir, { recursive: true });
|
|
3141
3660
|
}
|
|
3142
3661
|
}
|
|
3143
3662
|
}
|
|
3144
3663
|
|
|
3664
|
+
// src/budget/budget-gate.ts
|
|
3665
|
+
function matchingGateReset(usage) {
|
|
3666
|
+
if (!usage)
|
|
3667
|
+
return 0;
|
|
3668
|
+
const windows = [usage.fiveHour, usage.weekly].filter((window) => !!window && window.resetEpoch > 0);
|
|
3669
|
+
const matching = windows.filter((window) => Math.abs(window.util - usage.gateUtil) < 0.0001);
|
|
3670
|
+
const candidates = matching.length > 0 ? matching : windows;
|
|
3671
|
+
if (candidates.length === 0)
|
|
3672
|
+
return 0;
|
|
3673
|
+
return Math.min(...candidates.map((window) => window.resetEpoch));
|
|
3674
|
+
}
|
|
3675
|
+
function resumeBlockingEpoch(usage, cfg, now) {
|
|
3676
|
+
if (!usage)
|
|
3677
|
+
return 0;
|
|
3678
|
+
if (usage.rateLimitedUntil > now)
|
|
3679
|
+
return usage.rateLimitedUntil;
|
|
3680
|
+
if (usage.gateUtil >= cfg.resumeBelow)
|
|
3681
|
+
return matchingGateReset(usage);
|
|
3682
|
+
return 0;
|
|
3683
|
+
}
|
|
3684
|
+
|
|
3145
3685
|
// src/budget/types.ts
|
|
3146
3686
|
var STALE_MAX_AGE_SEC = 600;
|
|
3147
3687
|
|
|
@@ -3166,25 +3706,6 @@ function usageSummary(name, usage) {
|
|
|
3166
3706
|
return `${AGENT_LABEL[name]} \u672A\u77E5`;
|
|
3167
3707
|
return `${AGENT_LABEL[name]} gate=${pct(usage.gateUtil)} warn=${pct(usage.warnUtil)} 5h\u91CD\u7F6E=${formatEpoch(usage.fiveHour?.resetEpoch ?? 0)}`;
|
|
3168
3708
|
}
|
|
3169
|
-
function matchingGateReset(usage) {
|
|
3170
|
-
if (!usage)
|
|
3171
|
-
return 0;
|
|
3172
|
-
const windows = [usage.fiveHour, usage.weekly].filter((window) => !!window && window.resetEpoch > 0);
|
|
3173
|
-
const matching = windows.filter((window) => Math.abs(window.util - usage.gateUtil) < 0.0001);
|
|
3174
|
-
const candidates = matching.length > 0 ? matching : windows;
|
|
3175
|
-
if (candidates.length === 0)
|
|
3176
|
-
return 0;
|
|
3177
|
-
return Math.min(...candidates.map((window) => window.resetEpoch));
|
|
3178
|
-
}
|
|
3179
|
-
function resumeBlockingEpoch(usage, cfg, now) {
|
|
3180
|
-
if (!usage)
|
|
3181
|
-
return 0;
|
|
3182
|
-
if (usage.rateLimitedUntil > now)
|
|
3183
|
-
return usage.rateLimitedUntil;
|
|
3184
|
-
if (usage.gateUtil >= cfg.resumeBelow)
|
|
3185
|
-
return matchingGateReset(usage);
|
|
3186
|
-
return 0;
|
|
3187
|
-
}
|
|
3188
3709
|
function resumeAfterEpoch(claude, codex, cfg, now) {
|
|
3189
3710
|
const epochs = [
|
|
3190
3711
|
resumeBlockingEpoch(claude, cfg, now),
|
|
@@ -3367,8 +3888,211 @@ function computeBudgetState(claude, codex, cfg, now) {
|
|
|
3367
3888
|
};
|
|
3368
3889
|
}
|
|
3369
3890
|
|
|
3370
|
-
// src/budget/budget-
|
|
3891
|
+
// src/budget/budget-fingerprint.ts
|
|
3371
3892
|
var RESET_FINGERPRINT_BUCKET_SEC = 600;
|
|
3893
|
+
var AGENT_LABEL2 = {
|
|
3894
|
+
claude: "Claude",
|
|
3895
|
+
codex: "Codex"
|
|
3896
|
+
};
|
|
3897
|
+
function pct2(value) {
|
|
3898
|
+
return `${Math.round(value * 10) / 10}%`;
|
|
3899
|
+
}
|
|
3900
|
+
function formatEpoch2(epoch) {
|
|
3901
|
+
return new Date(epoch * 1000).toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z");
|
|
3902
|
+
}
|
|
3903
|
+
var INITIAL_FINGERPRINT_STATE = {
|
|
3904
|
+
side: null,
|
|
3905
|
+
fingerprint: null,
|
|
3906
|
+
resumeEpoch: null,
|
|
3907
|
+
reason: null
|
|
3908
|
+
};
|
|
3909
|
+
function sideToAgents(side) {
|
|
3910
|
+
if (side === "both")
|
|
3911
|
+
return ["claude", "codex"];
|
|
3912
|
+
if (side === "claude")
|
|
3913
|
+
return ["claude"];
|
|
3914
|
+
if (side === "codex")
|
|
3915
|
+
return ["codex"];
|
|
3916
|
+
return [];
|
|
3917
|
+
}
|
|
3918
|
+
function agentsToSide(agents) {
|
|
3919
|
+
const claude = agents.has("claude");
|
|
3920
|
+
const codex = agents.has("codex");
|
|
3921
|
+
if (claude && codex)
|
|
3922
|
+
return "both";
|
|
3923
|
+
if (claude)
|
|
3924
|
+
return "claude";
|
|
3925
|
+
if (codex)
|
|
3926
|
+
return "codex";
|
|
3927
|
+
return null;
|
|
3928
|
+
}
|
|
3929
|
+
function shouldEnter(usage, cfg, now) {
|
|
3930
|
+
if (!isDecisionGrade(usage, now))
|
|
3931
|
+
return false;
|
|
3932
|
+
return usage.gateUtil >= cfg.pauseAt;
|
|
3933
|
+
}
|
|
3934
|
+
function canAgentResume(usage, cfg, now) {
|
|
3935
|
+
if (!isDecisionGrade(usage, now))
|
|
3936
|
+
return false;
|
|
3937
|
+
if (usage.rateLimitedUntil > now)
|
|
3938
|
+
return false;
|
|
3939
|
+
return usage.gateUtil < cfg.resumeBelow;
|
|
3940
|
+
}
|
|
3941
|
+
function nextActiveSide(prevSide, state, cfg) {
|
|
3942
|
+
const active = new Set(sideToAgents(prevSide));
|
|
3943
|
+
for (const agent of ["claude", "codex"]) {
|
|
3944
|
+
const usage = state.perAgent[agent];
|
|
3945
|
+
if (shouldEnter(usage, cfg, state.now)) {
|
|
3946
|
+
active.add(agent);
|
|
3947
|
+
} else if (active.has(agent) && canAgentResume(usage, cfg, state.now)) {
|
|
3948
|
+
active.delete(agent);
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
return agentsToSide(active);
|
|
3952
|
+
}
|
|
3953
|
+
function activeSideReason(agent, usage, cfg, now) {
|
|
3954
|
+
if (!usage)
|
|
3955
|
+
return `${AGENT_LABEL2[agent]} \u63A2\u6D4B\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u4FDD\u6301\u4E0A\u4E00\u8F6E\u9884\u7B97\u5E72\u9884`;
|
|
3956
|
+
if (usage.rateLimitedUntil > now) {
|
|
3957
|
+
return `${AGENT_LABEL2[agent]} \u63A2\u9488\u88AB\u9650\u6D41\u81F3 ${formatEpoch2(usage.rateLimitedUntil)}`;
|
|
3958
|
+
}
|
|
3959
|
+
if (usage.gateUtil >= cfg.pauseAt) {
|
|
3960
|
+
return `${AGENT_LABEL2[agent]} gateUtil ${pct2(usage.gateUtil)} \u2265 pauseAt ${pct2(cfg.pauseAt)}`;
|
|
3961
|
+
}
|
|
3962
|
+
return `${AGENT_LABEL2[agent]} gateUtil ${pct2(usage.gateUtil)} \u5C1A\u672A\u4F4E\u4E8E resumeBelow ${pct2(cfg.resumeBelow)}`;
|
|
3963
|
+
}
|
|
3964
|
+
function interventionReason(side, state, cfg) {
|
|
3965
|
+
return sideToAgents(side).map((agent) => activeSideReason(agent, state.perAgent[agent], cfg, state.now)).join("\uFF1B");
|
|
3966
|
+
}
|
|
3967
|
+
function resumeAfterEpoch2(side, state, cfg) {
|
|
3968
|
+
const epochs = sideToAgents(side).map((agent) => resumeBlockingEpoch(state.perAgent[agent], cfg, state.now)).filter((epoch) => epoch > 0);
|
|
3969
|
+
if (epochs.length === 0)
|
|
3970
|
+
return null;
|
|
3971
|
+
return Math.max(...epochs);
|
|
3972
|
+
}
|
|
3973
|
+
function activeSideProbeUncertain(side, state) {
|
|
3974
|
+
return sideToAgents(side).some((agent) => {
|
|
3975
|
+
const usage = state.perAgent[agent];
|
|
3976
|
+
return usage === null || usage.rateLimitedUntil > state.now || !isDecisionGrade(usage, state.now);
|
|
3977
|
+
});
|
|
3978
|
+
}
|
|
3979
|
+
function directiveFingerprint(state, activeSide) {
|
|
3980
|
+
const side = activeSide ?? (state.phase === "balance" ? state.drift.lighter ?? "none" : state.pause.side ?? "none");
|
|
3981
|
+
let reset = 0;
|
|
3982
|
+
if (activeSide === "claude") {
|
|
3983
|
+
reset = state.pause.resetEpochs.claude;
|
|
3984
|
+
} else if (activeSide === "codex") {
|
|
3985
|
+
reset = state.pause.resetEpochs.codex;
|
|
3986
|
+
} else if (activeSide === "both") {
|
|
3987
|
+
reset = Math.max(state.pause.resetEpochs.claude, state.pause.resetEpochs.codex);
|
|
3988
|
+
} else if (state.phase === "balance" && state.drift.lighter) {
|
|
3989
|
+
reset = state.perAgent[state.drift.lighter]?.fiveHour?.resetEpoch ?? 0;
|
|
3990
|
+
}
|
|
3991
|
+
const heavier = activeSide ? "" : state.drift.heavier ?? "none";
|
|
3992
|
+
return [
|
|
3993
|
+
activeSide ? "paused" : state.phase,
|
|
3994
|
+
heavier,
|
|
3995
|
+
side,
|
|
3996
|
+
Math.round(reset / RESET_FINGERPRINT_BUCKET_SEC)
|
|
3997
|
+
].join("|");
|
|
3998
|
+
}
|
|
3999
|
+
function classifyPoll(prev, state, cfg) {
|
|
4000
|
+
const previousSide = prev.side;
|
|
4001
|
+
const currentSide = nextActiveSide(previousSide, state, cfg);
|
|
4002
|
+
if (currentSide) {
|
|
4003
|
+
const reason = interventionReason(currentSide, state, cfg);
|
|
4004
|
+
const nextResumeRaw = resumeAfterEpoch2(currentSide, state, cfg);
|
|
4005
|
+
const resumeEpoch = previousSide === currentSide ? nextResumeRaw ?? prev.resumeEpoch : nextResumeRaw;
|
|
4006
|
+
const uncertain = previousSide === currentSide && activeSideProbeUncertain(currentSide, state) && prev.fingerprint;
|
|
4007
|
+
const fingerprint2 = uncertain ? prev.fingerprint : directiveFingerprint(state, currentSide);
|
|
4008
|
+
const pauseChanged = !previousSide;
|
|
4009
|
+
const emit = !previousSide || previousSide !== currentSide || fingerprint2 !== prev.fingerprint;
|
|
4010
|
+
return {
|
|
4011
|
+
next: { side: currentSide, fingerprint: fingerprint2, resumeEpoch, reason },
|
|
4012
|
+
effect: {
|
|
4013
|
+
kind: uncertain ? "hold-uncertain" : "enter",
|
|
4014
|
+
side: currentSide,
|
|
4015
|
+
reason,
|
|
4016
|
+
resumeEpoch,
|
|
4017
|
+
emit,
|
|
4018
|
+
pauseChanged
|
|
4019
|
+
}
|
|
4020
|
+
};
|
|
4021
|
+
}
|
|
4022
|
+
if (previousSide) {
|
|
4023
|
+
return {
|
|
4024
|
+
next: { side: null, fingerprint: null, resumeEpoch: null, reason: null },
|
|
4025
|
+
effect: { kind: "exit", previousSide }
|
|
4026
|
+
};
|
|
4027
|
+
}
|
|
4028
|
+
if (!isDecisionGrade(state.perAgent.claude, state.now) || !isDecisionGrade(state.perAgent.codex, state.now)) {
|
|
4029
|
+
return { next: prev, effect: { kind: "none" } };
|
|
4030
|
+
}
|
|
4031
|
+
if (!state.directiveToClaude) {
|
|
4032
|
+
return {
|
|
4033
|
+
next: { side: null, fingerprint: null, resumeEpoch: null, reason: null },
|
|
4034
|
+
effect: { kind: "none" }
|
|
4035
|
+
};
|
|
4036
|
+
}
|
|
4037
|
+
const fingerprint = directiveFingerprint(state);
|
|
4038
|
+
if (fingerprint !== prev.fingerprint) {
|
|
4039
|
+
return {
|
|
4040
|
+
next: { side: null, fingerprint, resumeEpoch: null, reason: null },
|
|
4041
|
+
effect: { kind: "advise", phase: state.phase }
|
|
4042
|
+
};
|
|
4043
|
+
}
|
|
4044
|
+
return { next: prev, effect: { kind: "none" } };
|
|
4045
|
+
}
|
|
4046
|
+
|
|
4047
|
+
// src/budget/burn-view.ts
|
|
4048
|
+
function windowBurnRate(window) {
|
|
4049
|
+
if (!window || window.burnRate === undefined)
|
|
4050
|
+
return null;
|
|
4051
|
+
return {
|
|
4052
|
+
pctPerHour: window.burnRate,
|
|
4053
|
+
confident: window.burnConfident === true
|
|
4054
|
+
};
|
|
4055
|
+
}
|
|
4056
|
+
function agentBurnRates(usage) {
|
|
4057
|
+
if (!usage)
|
|
4058
|
+
return { fiveHour: null, weekly: null };
|
|
4059
|
+
return {
|
|
4060
|
+
fiveHour: windowBurnRate(usage.fiveHour),
|
|
4061
|
+
weekly: windowBurnRate(usage.weekly)
|
|
4062
|
+
};
|
|
4063
|
+
}
|
|
4064
|
+
function agentRunway(usage, now) {
|
|
4065
|
+
if (!usage || usage.stale || !usage.ok)
|
|
4066
|
+
return null;
|
|
4067
|
+
if (!isDecisionGrade(usage, now))
|
|
4068
|
+
return null;
|
|
4069
|
+
let best = null;
|
|
4070
|
+
const candidates = [
|
|
4071
|
+
["fiveHour", usage.fiveHour],
|
|
4072
|
+
["weekly", usage.weekly]
|
|
4073
|
+
];
|
|
4074
|
+
for (const [basis, window] of candidates) {
|
|
4075
|
+
if (!window || window.resetEpoch <= now)
|
|
4076
|
+
continue;
|
|
4077
|
+
if (window.burnConfident !== true)
|
|
4078
|
+
continue;
|
|
4079
|
+
if (window.runwaySeconds === undefined)
|
|
4080
|
+
continue;
|
|
4081
|
+
if (best === null || window.runwaySeconds < best.seconds) {
|
|
4082
|
+
best = {
|
|
4083
|
+
seconds: window.runwaySeconds,
|
|
4084
|
+
basis,
|
|
4085
|
+
depletedAtEpoch: window.depletedAtEpoch ?? null
|
|
4086
|
+
};
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
return best;
|
|
4090
|
+
}
|
|
4091
|
+
function hasAnyBurnSignal(rates, runway) {
|
|
4092
|
+
return rates.claude.fiveHour !== null || rates.claude.weekly !== null || rates.codex.fiveHour !== null || rates.codex.weekly !== null || runway.claude !== null || runway.codex !== null;
|
|
4093
|
+
}
|
|
4094
|
+
|
|
4095
|
+
// src/budget/budget-coordinator.ts
|
|
3372
4096
|
var LOW_UTIL_PCT = 50;
|
|
3373
4097
|
var NEAR_PAUSE_MARGIN_PCT = 10;
|
|
3374
4098
|
var NEAR_WARN_UTIL_PCT = 75;
|
|
@@ -3386,27 +4110,17 @@ var REAL_BUDGET_POLL_SCHEDULER = {
|
|
|
3386
4110
|
clearTimeout(timer);
|
|
3387
4111
|
}
|
|
3388
4112
|
};
|
|
3389
|
-
var
|
|
4113
|
+
var AGENT_LABEL3 = {
|
|
3390
4114
|
claude: "Claude",
|
|
3391
4115
|
codex: "Codex"
|
|
3392
4116
|
};
|
|
3393
|
-
function
|
|
4117
|
+
function pct3(value) {
|
|
3394
4118
|
return `${Math.round(value * 10) / 10}%`;
|
|
3395
4119
|
}
|
|
3396
4120
|
function usageLine(agent, usage) {
|
|
3397
4121
|
if (!usage)
|
|
3398
|
-
return `${
|
|
3399
|
-
return `${
|
|
3400
|
-
}
|
|
3401
|
-
function matchingGateReset2(usage) {
|
|
3402
|
-
if (!usage)
|
|
3403
|
-
return 0;
|
|
3404
|
-
const windows = [usage.fiveHour, usage.weekly].filter((window) => !!window && window.resetEpoch > 0);
|
|
3405
|
-
const matching = windows.filter((window) => Math.abs(window.util - usage.gateUtil) < 0.0001);
|
|
3406
|
-
const candidates = matching.length > 0 ? matching : windows;
|
|
3407
|
-
if (candidates.length === 0)
|
|
3408
|
-
return 0;
|
|
3409
|
-
return Math.min(...candidates.map((window) => window.resetEpoch));
|
|
4122
|
+
return `${AGENT_LABEL3[agent]} \u672A\u77E5`;
|
|
4123
|
+
return `${AGENT_LABEL3[agent]} gate=${pct3(usage.gateUtil)} warn=${pct3(usage.warnUtil)}`;
|
|
3410
4124
|
}
|
|
3411
4125
|
function maxPollDelayMs(config) {
|
|
3412
4126
|
return Math.max(0, config.pollSeconds * 1000);
|
|
@@ -3463,16 +4177,14 @@ class BudgetCoordinator {
|
|
|
3463
4177
|
config;
|
|
3464
4178
|
emit;
|
|
3465
4179
|
onPauseChange;
|
|
4180
|
+
onSnapshot;
|
|
3466
4181
|
now;
|
|
3467
4182
|
scheduler;
|
|
3468
4183
|
log;
|
|
3469
4184
|
timer = null;
|
|
3470
4185
|
running = false;
|
|
3471
|
-
|
|
3472
|
-
lastDirectiveFingerprint = null;
|
|
4186
|
+
fpState = INITIAL_FINGERPRINT_STATE;
|
|
3473
4187
|
latestSnapshot = null;
|
|
3474
|
-
pauseReason = null;
|
|
3475
|
-
pauseResumeAfterEpoch = null;
|
|
3476
4188
|
pendingOverrideTier = null;
|
|
3477
4189
|
pendingOverrides = null;
|
|
3478
4190
|
lastAppliedTier = "full";
|
|
@@ -3483,6 +4195,7 @@ class BudgetCoordinator {
|
|
|
3483
4195
|
this.config = options.config;
|
|
3484
4196
|
this.emit = options.emit;
|
|
3485
4197
|
this.onPauseChange = options.onPauseChange;
|
|
4198
|
+
this.onSnapshot = options.onSnapshot ?? (() => {});
|
|
3486
4199
|
this.now = options.now ?? (() => Math.floor(Date.now() / 1000));
|
|
3487
4200
|
this.scheduler = options.scheduler ?? REAL_BUDGET_POLL_SCHEDULER;
|
|
3488
4201
|
this.log = options.log ?? (() => {});
|
|
@@ -3503,10 +4216,10 @@ class BudgetCoordinator {
|
|
|
3503
4216
|
}
|
|
3504
4217
|
}
|
|
3505
4218
|
isPaused() {
|
|
3506
|
-
return this.
|
|
4219
|
+
return this.fpState.side !== null;
|
|
3507
4220
|
}
|
|
3508
4221
|
isGateClosed() {
|
|
3509
|
-
return this.
|
|
4222
|
+
return this.fpState.side === "codex" || this.fpState.side === "both";
|
|
3510
4223
|
}
|
|
3511
4224
|
getSnapshot() {
|
|
3512
4225
|
return this.latestSnapshot;
|
|
@@ -3560,7 +4273,7 @@ class BudgetCoordinator {
|
|
|
3560
4273
|
}
|
|
3561
4274
|
if (!usage) {
|
|
3562
4275
|
if (!this.isPaused())
|
|
3563
|
-
this.
|
|
4276
|
+
this.setSnapshot(null);
|
|
3564
4277
|
return;
|
|
3565
4278
|
}
|
|
3566
4279
|
if (!this.running) {
|
|
@@ -3569,85 +4282,39 @@ class BudgetCoordinator {
|
|
|
3569
4282
|
const state = computeBudgetState(usage.claude, usage.codex, this.config, this.now());
|
|
3570
4283
|
this.updatePendingOverrides(state.effort.codexTier);
|
|
3571
4284
|
this.applyState(state);
|
|
3572
|
-
this.
|
|
4285
|
+
this.setSnapshot(this.toSnapshot(state));
|
|
4286
|
+
}
|
|
4287
|
+
setSnapshot(snapshot) {
|
|
4288
|
+
this.latestSnapshot = snapshot;
|
|
4289
|
+
this.onSnapshot(snapshot);
|
|
3573
4290
|
}
|
|
3574
4291
|
applyState(state) {
|
|
3575
|
-
const
|
|
3576
|
-
this.
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
4292
|
+
const { next, effect } = classifyPoll(this.fpState, state, this.config);
|
|
4293
|
+
this.fpState = next;
|
|
4294
|
+
switch (effect.kind) {
|
|
4295
|
+
case "enter":
|
|
4296
|
+
case "hold-uncertain": {
|
|
4297
|
+
if (effect.pauseChanged)
|
|
4298
|
+
this.onPauseChange(true);
|
|
4299
|
+
if (effect.emit) {
|
|
4300
|
+
this.emitDirective(this.interventionPrefix(effect.side), this.interventionDirective(state, effect.side, effect.reason, effect.resumeEpoch));
|
|
4301
|
+
}
|
|
4302
|
+
return;
|
|
3585
4303
|
}
|
|
3586
|
-
|
|
3587
|
-
this.
|
|
4304
|
+
case "exit": {
|
|
4305
|
+
this.onPauseChange(false);
|
|
4306
|
+
this.emitDirective(this.recoveryPrefix(effect.previousSide), this.recoveryDirective(state, effect.previousSide));
|
|
4307
|
+
return;
|
|
3588
4308
|
}
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
this.pauseReason = null;
|
|
3594
|
-
this.pauseResumeAfterEpoch = null;
|
|
3595
|
-
this.lastDirectiveFingerprint = null;
|
|
3596
|
-
this.onPauseChange(false);
|
|
3597
|
-
this.emitDirective(this.recoveryPrefix(previousSide), this.recoveryDirective(state, previousSide));
|
|
3598
|
-
return;
|
|
3599
|
-
}
|
|
3600
|
-
if (!isDecisionGrade(state.perAgent.claude, state.now) || !isDecisionGrade(state.perAgent.codex, state.now)) {
|
|
3601
|
-
return;
|
|
3602
|
-
}
|
|
3603
|
-
if (!state.directiveToClaude) {
|
|
3604
|
-
this.lastDirectiveFingerprint = null;
|
|
3605
|
-
return;
|
|
3606
|
-
}
|
|
3607
|
-
const fingerprint = this.directiveFingerprint(state);
|
|
3608
|
-
if (fingerprint !== this.lastDirectiveFingerprint) {
|
|
3609
|
-
const prefix = state.phase === "balance" ? "system_budget_balance" : "system_budget_parallel";
|
|
3610
|
-
this.emitDirective(prefix, state.directiveToClaude);
|
|
3611
|
-
this.lastDirectiveFingerprint = fingerprint;
|
|
3612
|
-
}
|
|
3613
|
-
}
|
|
3614
|
-
updateActiveSides(state) {
|
|
3615
|
-
for (const agent of ["claude", "codex"]) {
|
|
3616
|
-
const usage = state.perAgent[agent];
|
|
3617
|
-
if (this.shouldEnter(usage, state.now)) {
|
|
3618
|
-
this.activeSides.add(agent);
|
|
3619
|
-
} else if (this.activeSides.has(agent) && this.canAgentResume(usage, state.now)) {
|
|
3620
|
-
this.activeSides.delete(agent);
|
|
4309
|
+
case "advise": {
|
|
4310
|
+
const prefix = effect.phase === "balance" ? "system_budget_balance" : "system_budget_parallel";
|
|
4311
|
+
this.emitDirective(prefix, state.directiveToClaude);
|
|
4312
|
+
return;
|
|
3621
4313
|
}
|
|
4314
|
+
case "none":
|
|
4315
|
+
return;
|
|
3622
4316
|
}
|
|
3623
4317
|
}
|
|
3624
|
-
shouldEnter(usage, now) {
|
|
3625
|
-
if (!isDecisionGrade(usage, now))
|
|
3626
|
-
return false;
|
|
3627
|
-
return usage.gateUtil >= this.config.pauseAt;
|
|
3628
|
-
}
|
|
3629
|
-
canAgentResume(usage, now) {
|
|
3630
|
-
if (!isDecisionGrade(usage, now))
|
|
3631
|
-
return false;
|
|
3632
|
-
if (usage.rateLimitedUntil > now)
|
|
3633
|
-
return false;
|
|
3634
|
-
return usage.gateUtil < this.config.resumeBelow;
|
|
3635
|
-
}
|
|
3636
|
-
resumeAfterEpoch(state) {
|
|
3637
|
-
const epochs = ["claude", "codex"].filter((agent) => this.activeSides.has(agent)).map((agent) => this.resumeBlockingEpoch(state.perAgent[agent], state.now)).filter((epoch) => epoch > 0);
|
|
3638
|
-
if (epochs.length === 0)
|
|
3639
|
-
return null;
|
|
3640
|
-
return Math.max(...epochs);
|
|
3641
|
-
}
|
|
3642
|
-
resumeBlockingEpoch(usage, now) {
|
|
3643
|
-
if (!usage)
|
|
3644
|
-
return 0;
|
|
3645
|
-
if (usage.rateLimitedUntil > now)
|
|
3646
|
-
return usage.rateLimitedUntil;
|
|
3647
|
-
if (usage.gateUtil >= this.config.resumeBelow)
|
|
3648
|
-
return matchingGateReset2(usage);
|
|
3649
|
-
return 0;
|
|
3650
|
-
}
|
|
3651
4318
|
tierControlEnabled() {
|
|
3652
4319
|
if (!this.config.codexTierControl)
|
|
3653
4320
|
return false;
|
|
@@ -3681,82 +4348,24 @@ class BudgetCoordinator {
|
|
|
3681
4348
|
this.pendingOverrideTier = tier;
|
|
3682
4349
|
this.pendingOverrides = { ...overrides };
|
|
3683
4350
|
}
|
|
3684
|
-
directiveFingerprint(state, activeSide) {
|
|
3685
|
-
const side = activeSide ?? (state.phase === "balance" ? state.drift.lighter ?? "none" : state.pause.side ?? "none");
|
|
3686
|
-
let reset = 0;
|
|
3687
|
-
if (activeSide === "claude") {
|
|
3688
|
-
reset = state.pause.resetEpochs.claude;
|
|
3689
|
-
} else if (activeSide === "codex") {
|
|
3690
|
-
reset = state.pause.resetEpochs.codex;
|
|
3691
|
-
} else if (activeSide === "both") {
|
|
3692
|
-
reset = Math.max(state.pause.resetEpochs.claude, state.pause.resetEpochs.codex);
|
|
3693
|
-
} else if (state.phase === "balance" && state.drift.lighter) {
|
|
3694
|
-
reset = state.perAgent[state.drift.lighter]?.fiveHour?.resetEpoch ?? 0;
|
|
3695
|
-
} else if (side === "claude") {
|
|
3696
|
-
reset = state.pause.resetEpochs.claude;
|
|
3697
|
-
} else if (side === "codex") {
|
|
3698
|
-
reset = state.pause.resetEpochs.codex;
|
|
3699
|
-
} else if (side === "both") {
|
|
3700
|
-
reset = Math.max(state.pause.resetEpochs.claude, state.pause.resetEpochs.codex);
|
|
3701
|
-
}
|
|
3702
|
-
return [
|
|
3703
|
-
activeSide ? "paused" : state.phase,
|
|
3704
|
-
state.drift.heavier ?? "none",
|
|
3705
|
-
side,
|
|
3706
|
-
Math.round(reset / RESET_FINGERPRINT_BUCKET_SEC)
|
|
3707
|
-
].join("|");
|
|
3708
|
-
}
|
|
3709
4351
|
emitDirective(prefix, content) {
|
|
3710
4352
|
this.emit(`${prefix}_${this.sequence++}`, content);
|
|
3711
4353
|
}
|
|
3712
|
-
pauseSide() {
|
|
3713
|
-
const claude = this.activeSides.has("claude");
|
|
3714
|
-
const codex = this.activeSides.has("codex");
|
|
3715
|
-
if (claude && codex)
|
|
3716
|
-
return "both";
|
|
3717
|
-
if (claude)
|
|
3718
|
-
return "claude";
|
|
3719
|
-
if (codex)
|
|
3720
|
-
return "codex";
|
|
3721
|
-
return null;
|
|
3722
|
-
}
|
|
3723
4354
|
interventionPrefix(side) {
|
|
3724
4355
|
return side === "claude" ? "system_budget_handoff" : "system_budget_pause";
|
|
3725
4356
|
}
|
|
3726
4357
|
recoveryPrefix(previousSide) {
|
|
3727
4358
|
return previousSide === "claude" ? "system_budget_claude_recovered" : "system_budget_resume";
|
|
3728
4359
|
}
|
|
3729
|
-
interventionDirective(state, side) {
|
|
3730
|
-
return renderBudgetInterventionDirective(state.perAgent.claude, state.perAgent.codex, side,
|
|
3731
|
-
}
|
|
3732
|
-
interventionReason(state) {
|
|
3733
|
-
return ["claude", "codex"].filter((agent) => this.activeSides.has(agent)).map((agent) => this.activeSideReason(agent, state.perAgent[agent], state.now)).join("\uFF1B");
|
|
3734
|
-
}
|
|
3735
|
-
activeSideProbeUncertain(state) {
|
|
3736
|
-
return ["claude", "codex"].some((agent) => {
|
|
3737
|
-
if (!this.activeSides.has(agent))
|
|
3738
|
-
return false;
|
|
3739
|
-
const usage = state.perAgent[agent];
|
|
3740
|
-
return usage === null || usage.rateLimitedUntil > state.now || !isDecisionGrade(usage, state.now);
|
|
3741
|
-
});
|
|
3742
|
-
}
|
|
3743
|
-
activeSideReason(agent, usage, now) {
|
|
3744
|
-
if (!usage)
|
|
3745
|
-
return `${AGENT_LABEL2[agent]} \u63A2\u6D4B\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u4FDD\u6301\u4E0A\u4E00\u8F6E\u9884\u7B97\u5E72\u9884`;
|
|
3746
|
-
if (usage.rateLimitedUntil > now) {
|
|
3747
|
-
return `${AGENT_LABEL2[agent]} \u63A2\u9488\u88AB\u9650\u6D41\u81F3 ${this.formatEpoch(usage.rateLimitedUntil)}`;
|
|
3748
|
-
}
|
|
3749
|
-
if (usage.gateUtil >= this.config.pauseAt) {
|
|
3750
|
-
return `${AGENT_LABEL2[agent]} gateUtil ${pct2(usage.gateUtil)} \u2265 pauseAt ${pct2(this.config.pauseAt)}`;
|
|
3751
|
-
}
|
|
3752
|
-
return `${AGENT_LABEL2[agent]} gateUtil ${pct2(usage.gateUtil)} \u5C1A\u672A\u4F4E\u4E8E resumeBelow ${pct2(this.config.resumeBelow)}`;
|
|
4360
|
+
interventionDirective(state, side, reason, resumeEpoch) {
|
|
4361
|
+
return renderBudgetInterventionDirective(state.perAgent.claude, state.perAgent.codex, side, reason || "\u9884\u7B97\u63A5\u8FD1\u8017\u5C3D", resumeEpoch, this.config);
|
|
3753
4362
|
}
|
|
3754
4363
|
recoveryDirective(state, previousSide) {
|
|
3755
4364
|
if (previousSide === "claude") {
|
|
3756
4365
|
return [
|
|
3757
4366
|
"\u3010\u9884\u7B97\u534F\u8C03 \xB7 \u8D26\u53F7\u7EA7\u3011Claude \u4FA7\u9884\u7B97\u5DF2\u6062\u590D\u3002",
|
|
3758
4367
|
`${usageLine("claude", state.perAgent.claude)}\uFF1B${usageLine("codex", state.perAgent.codex)}\u3002`,
|
|
3759
|
-
`Claude gateUtil \u5DF2\u4F4E\u4E8E ${
|
|
4368
|
+
`Claude gateUtil \u5DF2\u4F4E\u4E8E ${pct3(this.config.resumeBelow)}\uFF0C\u4E14\u6CA1\u6709\u6709\u6548 rate_limit\u3002`,
|
|
3760
4369
|
"Claude \u53EF\u6062\u590D orchestrator \u89D2\u8272\uFF1B\u540E\u7EED\u5206\u914D\u524D\u8BF7\u91CD\u65B0\u67E5\u8BE2\u5B9E\u65F6\u989D\u5EA6\uFF0C\u4E0D\u8981\u4F9D\u8D56\u65E7\u6570\u5B57\u3002"
|
|
3761
4370
|
].join(`
|
|
3762
4371
|
`);
|
|
@@ -3765,7 +4374,7 @@ class BudgetCoordinator {
|
|
|
3765
4374
|
return [
|
|
3766
4375
|
"\u3010\u9884\u7B97\u534F\u8C03 \xB7 \u8D26\u53F7\u7EA7\u3011Codex \u4FA7\u9884\u7B97\u95F8\u95E8\u89E3\u9664\u3002",
|
|
3767
4376
|
`${usageLine("claude", state.perAgent.claude)}\uFF1B${usageLine("codex", state.perAgent.codex)}\u3002`,
|
|
3768
|
-
`\u95F8\u95E8\u5DF2\u653E\u5F00\uFF1ACodex gateUtil \u4F4E\u4E8E ${
|
|
4377
|
+
`\u95F8\u95E8\u5DF2\u653E\u5F00\uFF1ACodex gateUtil \u4F4E\u4E8E ${pct3(this.config.resumeBelow)}\uFF0C\u4E14\u6CA1\u6709\u6709\u6548 rate_limit\u3002`,
|
|
3769
4378
|
"\u5EFA\u8BAE Claude \u7528 reply \u5E26\u4E0A\u5F53\u524D\u76EE\u6807\u3001checkpoint \u548C\u4E0B\u4E00\u6B65\uFF0C\u5524\u9192 Codex \u63A5\u7EED\u6267\u884C\u3002"
|
|
3770
4379
|
].join(`
|
|
3771
4380
|
`);
|
|
@@ -3773,14 +4382,11 @@ class BudgetCoordinator {
|
|
|
3773
4382
|
return [
|
|
3774
4383
|
"\u3010\u9884\u7B97\u534F\u8C03 \xB7 \u8D26\u53F7\u7EA7\u3011\u8054\u5408\u6682\u505C\u89E3\u9664\u3002",
|
|
3775
4384
|
`${usageLine("claude", state.perAgent.claude)}\uFF1B${usageLine("codex", state.perAgent.codex)}\u3002`,
|
|
3776
|
-
`\u95F8\u95E8\u5DF2\u653E\u5F00\uFF1A\u53CC\u65B9 gateUtil \u5747\u4F4E\u4E8E ${
|
|
4385
|
+
`\u95F8\u95E8\u5DF2\u653E\u5F00\uFF1A\u53CC\u65B9 gateUtil \u5747\u4F4E\u4E8E ${pct3(this.config.resumeBelow)}\uFF0C\u4E14\u6CA1\u6709\u6709\u6548 rate_limit\u3002`,
|
|
3777
4386
|
"\u5EFA\u8BAE Claude \u7528 reply \u5E26\u4E0A\u5F53\u524D\u76EE\u6807\u3001checkpoint \u548C\u4E0B\u4E00\u6B65\uFF0C\u5524\u9192 Codex \u63A5\u7EED\u6267\u884C\u3002"
|
|
3778
4387
|
].join(`
|
|
3779
4388
|
`);
|
|
3780
4389
|
}
|
|
3781
|
-
formatEpoch(epoch) {
|
|
3782
|
-
return new Date(epoch * 1000).toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z");
|
|
3783
|
-
}
|
|
3784
4390
|
toSnapshot(state) {
|
|
3785
4391
|
const paused = this.isPaused();
|
|
3786
4392
|
return {
|
|
@@ -3791,13 +4397,27 @@ class BudgetCoordinator {
|
|
|
3791
4397
|
driftPct: state.drift.pct,
|
|
3792
4398
|
paused,
|
|
3793
4399
|
gateClosed: this.isGateClosed(),
|
|
3794
|
-
pauseSide: this.
|
|
3795
|
-
pauseReason: paused ? this.
|
|
3796
|
-
resumeAfterEpoch: paused ? this.
|
|
4400
|
+
pauseSide: this.fpState.side,
|
|
4401
|
+
pauseReason: paused ? this.fpState.reason ?? state.pause.reason : null,
|
|
4402
|
+
resumeAfterEpoch: paused ? this.fpState.resumeEpoch ?? state.pause.resumeAfterEpoch : null,
|
|
3797
4403
|
parallelRecommended: paused ? false : state.parallel.recommended,
|
|
3798
4404
|
codexTier: state.effort.codexTier,
|
|
3799
|
-
claudeAdvice: state.effort.claudeAdvice
|
|
4405
|
+
claudeAdvice: state.effort.claudeAdvice,
|
|
4406
|
+
...this.burnRateSnapshotFields(state)
|
|
4407
|
+
};
|
|
4408
|
+
}
|
|
4409
|
+
burnRateSnapshotFields(state) {
|
|
4410
|
+
const rates = {
|
|
4411
|
+
claude: agentBurnRates(state.perAgent.claude),
|
|
4412
|
+
codex: agentBurnRates(state.perAgent.codex)
|
|
4413
|
+
};
|
|
4414
|
+
const runway = {
|
|
4415
|
+
claude: agentRunway(state.perAgent.claude, state.now),
|
|
4416
|
+
codex: agentRunway(state.perAgent.codex, state.now)
|
|
3800
4417
|
};
|
|
4418
|
+
if (!hasAnyBurnSignal(rates, runway))
|
|
4419
|
+
return {};
|
|
4420
|
+
return { burnRate: rates, runway };
|
|
3801
4421
|
}
|
|
3802
4422
|
}
|
|
3803
4423
|
|
|
@@ -3805,7 +4425,58 @@ class BudgetCoordinator {
|
|
|
3805
4425
|
import { execFile } from "child_process";
|
|
3806
4426
|
import { existsSync as existsSync5 } from "fs";
|
|
3807
4427
|
import { homedir as homedir2 } from "os";
|
|
3808
|
-
import { basename, join as
|
|
4428
|
+
import { basename, join as join5 } from "path";
|
|
4429
|
+
function parseBurnFields(record) {
|
|
4430
|
+
const group = {};
|
|
4431
|
+
let any = false;
|
|
4432
|
+
const takeNumber = (value, min) => {
|
|
4433
|
+
if (value === undefined)
|
|
4434
|
+
return "absent";
|
|
4435
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
4436
|
+
return "invalid";
|
|
4437
|
+
if (min === "zero" && value < 0)
|
|
4438
|
+
return "invalid";
|
|
4439
|
+
if (min === "positive" && value <= 0)
|
|
4440
|
+
return "invalid";
|
|
4441
|
+
return value;
|
|
4442
|
+
};
|
|
4443
|
+
const burnRate = takeNumber(record.burn_rate_pct_per_hour ?? record.burnRatePctPerHour, "zero");
|
|
4444
|
+
if (burnRate === "invalid")
|
|
4445
|
+
return null;
|
|
4446
|
+
if (burnRate !== "absent") {
|
|
4447
|
+
group.burnRate = burnRate;
|
|
4448
|
+
any = true;
|
|
4449
|
+
}
|
|
4450
|
+
const confidentRaw = record.burn_confident ?? record.burnConfident;
|
|
4451
|
+
if (confidentRaw !== undefined) {
|
|
4452
|
+
if (typeof confidentRaw !== "boolean")
|
|
4453
|
+
return null;
|
|
4454
|
+
group.burnConfident = confidentRaw;
|
|
4455
|
+
any = true;
|
|
4456
|
+
}
|
|
4457
|
+
const runwaySeconds = takeNumber(record.runway_seconds ?? record.runwaySeconds, "zero");
|
|
4458
|
+
if (runwaySeconds === "invalid")
|
|
4459
|
+
return null;
|
|
4460
|
+
if (runwaySeconds !== "absent") {
|
|
4461
|
+
group.runwaySeconds = runwaySeconds;
|
|
4462
|
+
any = true;
|
|
4463
|
+
}
|
|
4464
|
+
const depletedAtEpoch = takeNumber(record.depleted_at_epoch ?? record.depletedAtEpoch, "positive");
|
|
4465
|
+
if (depletedAtEpoch === "invalid")
|
|
4466
|
+
return null;
|
|
4467
|
+
if (depletedAtEpoch !== "absent") {
|
|
4468
|
+
group.depletedAtEpoch = depletedAtEpoch;
|
|
4469
|
+
any = true;
|
|
4470
|
+
}
|
|
4471
|
+
const fiveHourWindowsLeft = takeNumber(record.five_hour_windows_left ?? record.fiveHourWindowsLeft, "zero");
|
|
4472
|
+
if (fiveHourWindowsLeft === "invalid")
|
|
4473
|
+
return null;
|
|
4474
|
+
if (fiveHourWindowsLeft !== "absent") {
|
|
4475
|
+
group.fiveHourWindowsLeft = fiveHourWindowsLeft;
|
|
4476
|
+
any = true;
|
|
4477
|
+
}
|
|
4478
|
+
return any ? group : null;
|
|
4479
|
+
}
|
|
3809
4480
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
3810
4481
|
var MAX_BUFFER = 1024 * 1024;
|
|
3811
4482
|
function defaultRunner(command, args, options) {
|
|
@@ -3867,7 +4538,8 @@ function normalizeBucket(value, fetchedAt) {
|
|
|
3867
4538
|
id,
|
|
3868
4539
|
util: clamp(util, 0, 100),
|
|
3869
4540
|
resetEpoch: Math.max(0, resetEpoch),
|
|
3870
|
-
resetAfterSeconds: resetAfter === null ? null : Math.max(0, resetAfter)
|
|
4541
|
+
resetAfterSeconds: resetAfter === null ? null : Math.max(0, resetAfter),
|
|
4542
|
+
burn: parseBurnFields(bucket)
|
|
3871
4543
|
};
|
|
3872
4544
|
}
|
|
3873
4545
|
function normalizeTopLevelBucket(record, util, fetchedAt) {
|
|
@@ -3880,13 +4552,27 @@ function normalizeTopLevelBucket(record, util, fetchedAt) {
|
|
|
3880
4552
|
id: "top_level",
|
|
3881
4553
|
util: clamp(util, 0, 100),
|
|
3882
4554
|
resetEpoch: Math.max(0, resetEpoch),
|
|
3883
|
-
resetAfterSeconds: resetAfter === null ? null : Math.max(0, resetAfter)
|
|
4555
|
+
resetAfterSeconds: resetAfter === null ? null : Math.max(0, resetAfter),
|
|
4556
|
+
burn: parseBurnFields(record)
|
|
3884
4557
|
};
|
|
3885
4558
|
}
|
|
3886
4559
|
function toWindow(bucket) {
|
|
3887
4560
|
if (!bucket)
|
|
3888
4561
|
return null;
|
|
3889
|
-
|
|
4562
|
+
const window = { util: bucket.util, resetEpoch: bucket.resetEpoch };
|
|
4563
|
+
if (bucket.burn) {
|
|
4564
|
+
if (bucket.burn.burnRate !== undefined)
|
|
4565
|
+
window.burnRate = bucket.burn.burnRate;
|
|
4566
|
+
if (bucket.burn.burnConfident !== undefined)
|
|
4567
|
+
window.burnConfident = bucket.burn.burnConfident;
|
|
4568
|
+
if (bucket.burn.runwaySeconds !== undefined)
|
|
4569
|
+
window.runwaySeconds = bucket.burn.runwaySeconds;
|
|
4570
|
+
if (bucket.burn.depletedAtEpoch !== undefined)
|
|
4571
|
+
window.depletedAtEpoch = bucket.burn.depletedAtEpoch;
|
|
4572
|
+
if (bucket.burn.fiveHourWindowsLeft !== undefined)
|
|
4573
|
+
window.fiveHourWindowsLeft = bucket.burn.fiveHourWindowsLeft;
|
|
4574
|
+
}
|
|
4575
|
+
return window;
|
|
3890
4576
|
}
|
|
3891
4577
|
function bucketSortKey(bucket) {
|
|
3892
4578
|
if (bucket.resetAfterSeconds !== null)
|
|
@@ -3966,10 +4652,11 @@ function normalizeTolerantProbeRecord(record) {
|
|
|
3966
4652
|
};
|
|
3967
4653
|
}
|
|
3968
4654
|
var PROBE_SCHEMA_PARSERS = {
|
|
3969
|
-
"1": normalizeTolerantProbeRecord
|
|
4655
|
+
"1": normalizeTolerantProbeRecord,
|
|
4656
|
+
"2": normalizeTolerantProbeRecord
|
|
3970
4657
|
};
|
|
3971
4658
|
function schemaVersionKey(record) {
|
|
3972
|
-
const value = record.schema_version ?? record.schemaVersion;
|
|
4659
|
+
const value = record.schema_version ?? record.schemaVersion ?? record.probe_schema ?? record.probeSchema;
|
|
3973
4660
|
if (typeof value === "number" && Number.isFinite(value))
|
|
3974
4661
|
return String(value);
|
|
3975
4662
|
if (typeof value === "string" && value.trim() !== "")
|
|
@@ -4053,11 +4740,11 @@ class QuotaSource {
|
|
|
4053
4740
|
add(command, commandKind(command));
|
|
4054
4741
|
return candidates;
|
|
4055
4742
|
}
|
|
4056
|
-
const binDir =
|
|
4057
|
-
const installedBudgetProbe =
|
|
4743
|
+
const binDir = join5(this.homeDir, ".budget-guard/bin");
|
|
4744
|
+
const installedBudgetProbe = join5(binDir, "budget-probe");
|
|
4058
4745
|
if (existsSync5(installedBudgetProbe))
|
|
4059
4746
|
add(installedBudgetProbe, "budget-probe");
|
|
4060
|
-
const installedProbeMjs =
|
|
4747
|
+
const installedProbeMjs = join5(binDir, "probe.mjs");
|
|
4061
4748
|
if (existsSync5(installedProbeMjs))
|
|
4062
4749
|
add(installedProbeMjs, "probe-mjs");
|
|
4063
4750
|
return candidates;
|
|
@@ -4120,6 +4807,27 @@ function createQuotaSource(options) {
|
|
|
4120
4807
|
return new QuotaSource(options);
|
|
4121
4808
|
}
|
|
4122
4809
|
|
|
4810
|
+
// src/daemon-identity-ownership.ts
|
|
4811
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
4812
|
+
var defaultRead2 = (path) => readFileSync5(path, "utf-8");
|
|
4813
|
+
function pidFileOwnedByUs(pidFilePath, ourPid, read = defaultRead2) {
|
|
4814
|
+
let raw;
|
|
4815
|
+
try {
|
|
4816
|
+
raw = read(pidFilePath);
|
|
4817
|
+
} catch {
|
|
4818
|
+
return false;
|
|
4819
|
+
}
|
|
4820
|
+
const trimmed = raw.trim();
|
|
4821
|
+
if (trimmed.length === 0)
|
|
4822
|
+
return false;
|
|
4823
|
+
if (!/^[+-]?\d+$/.test(trimmed))
|
|
4824
|
+
return false;
|
|
4825
|
+
const pid = Number.parseInt(trimmed, 10);
|
|
4826
|
+
if (!Number.isFinite(pid))
|
|
4827
|
+
return false;
|
|
4828
|
+
return pid === ourPid;
|
|
4829
|
+
}
|
|
4830
|
+
|
|
4123
4831
|
// src/idempotency-tracker.ts
|
|
4124
4832
|
var DEFAULT_TOMBSTONE_TTL_MS = 20 * 60 * 1000;
|
|
4125
4833
|
|
|
@@ -4270,14 +4978,11 @@ class ReplyRequiredTracker {
|
|
|
4270
4978
|
// src/thread-state.ts
|
|
4271
4979
|
import {
|
|
4272
4980
|
existsSync as existsSync6,
|
|
4273
|
-
mkdirSync as mkdirSync4,
|
|
4274
4981
|
readdirSync,
|
|
4275
|
-
readFileSync as
|
|
4276
|
-
renameSync as renameSync2,
|
|
4277
|
-
writeFileSync as writeFileSync3
|
|
4982
|
+
readFileSync as readFileSync6
|
|
4278
4983
|
} from "fs";
|
|
4279
4984
|
import { homedir as homedir3 } from "os";
|
|
4280
|
-
import { basename as basename2,
|
|
4985
|
+
import { basename as basename2, join as join6 } from "path";
|
|
4281
4986
|
function nowIso() {
|
|
4282
4987
|
return new Date().toISOString();
|
|
4283
4988
|
}
|
|
@@ -4286,18 +4991,11 @@ function threadTag(identity) {
|
|
|
4286
4991
|
return `abg:${name}:${identity.cwd}`;
|
|
4287
4992
|
}
|
|
4288
4993
|
function codexHome(env = process.env) {
|
|
4289
|
-
return env.CODEX_HOME && env.CODEX_HOME.length > 0 ? env.CODEX_HOME :
|
|
4290
|
-
}
|
|
4291
|
-
function atomicWriteJson(path, value) {
|
|
4292
|
-
mkdirSync4(dirname2(path), { recursive: true });
|
|
4293
|
-
const tmp = `${path}.tmp.${process.pid}.${Date.now()}`;
|
|
4294
|
-
writeFileSync3(tmp, JSON.stringify(value, null, 2) + `
|
|
4295
|
-
`, "utf-8");
|
|
4296
|
-
renameSync2(tmp, path);
|
|
4994
|
+
return env.CODEX_HOME && env.CODEX_HOME.length > 0 ? env.CODEX_HOME : join6(homedir3(), ".codex");
|
|
4297
4995
|
}
|
|
4298
4996
|
function readRawCurrentThread(stateDir) {
|
|
4299
4997
|
try {
|
|
4300
|
-
const parsed = JSON.parse(
|
|
4998
|
+
const parsed = JSON.parse(readFileSync6(stateDir.currentThreadFile, "utf-8"));
|
|
4301
4999
|
if (parsed?.version === 1 && typeof parsed.threadId === "string" && parsed.threadId.length > 0 && (parsed.status === "pending" || parsed.status === "current") && typeof parsed.cwd === "string") {
|
|
4302
5000
|
return parsed;
|
|
4303
5001
|
}
|
|
@@ -4305,7 +5003,7 @@ function readRawCurrentThread(stateDir) {
|
|
|
4305
5003
|
return null;
|
|
4306
5004
|
}
|
|
4307
5005
|
function findCodexRolloutFile(threadId, env = process.env, maxEntries = 20000) {
|
|
4308
|
-
const sessionsDir =
|
|
5006
|
+
const sessionsDir = join6(codexHome(env), "sessions");
|
|
4309
5007
|
if (!threadId || !existsSync6(sessionsDir))
|
|
4310
5008
|
return null;
|
|
4311
5009
|
const exactName = `rollout-${threadId}.jsonl`;
|
|
@@ -4321,7 +5019,7 @@ function findCodexRolloutFile(threadId, env = process.env, maxEntries = 20000) {
|
|
|
4321
5019
|
}
|
|
4322
5020
|
for (const entry of entries) {
|
|
4323
5021
|
visited++;
|
|
4324
|
-
const path =
|
|
5022
|
+
const path = join6(dir, entry.name);
|
|
4325
5023
|
if (entry.isDirectory()) {
|
|
4326
5024
|
stack.push(path);
|
|
4327
5025
|
continue;
|
|
@@ -4415,6 +5113,7 @@ function formatWaitingForCodexTuiMessage(options) {
|
|
|
4415
5113
|
// src/pair-registry.ts
|
|
4416
5114
|
var PAIR_BASE_PORT = 4500;
|
|
4417
5115
|
var PAIR_SLOT_STRIDE = 10;
|
|
5116
|
+
var RECLAIMABLE_MIN_AGE_MS = 24 * 60 * 60 * 1000;
|
|
4418
5117
|
var MAX_PAIR_SLOT = Math.floor((65535 - 2 - PAIR_BASE_PORT) / PAIR_SLOT_STRIDE);
|
|
4419
5118
|
|
|
4420
5119
|
// src/liveness-probe.ts
|
|
@@ -4445,10 +5144,55 @@ async function probeLiveness(target, options) {
|
|
|
4445
5144
|
return target.pongCount > baseline;
|
|
4446
5145
|
}
|
|
4447
5146
|
|
|
5147
|
+
// src/delivery-buffer.ts
|
|
5148
|
+
class BoundedMessageBuffer {
|
|
5149
|
+
messages = [];
|
|
5150
|
+
cap;
|
|
5151
|
+
overflowLabel;
|
|
5152
|
+
overflowNoun;
|
|
5153
|
+
log;
|
|
5154
|
+
constructor(options) {
|
|
5155
|
+
this.cap = options.cap;
|
|
5156
|
+
this.overflowLabel = options.overflowLabel;
|
|
5157
|
+
this.overflowNoun = options.overflowNoun ?? "message(s)";
|
|
5158
|
+
this.log = options.log;
|
|
5159
|
+
}
|
|
5160
|
+
get length() {
|
|
5161
|
+
return this.messages.length;
|
|
5162
|
+
}
|
|
5163
|
+
push(message) {
|
|
5164
|
+
this.messages.push(message);
|
|
5165
|
+
this.enforceCap();
|
|
5166
|
+
}
|
|
5167
|
+
unshiftMany(messages) {
|
|
5168
|
+
if (messages.length === 0)
|
|
5169
|
+
return;
|
|
5170
|
+
this.messages.unshift(...messages);
|
|
5171
|
+
this.enforceCap();
|
|
5172
|
+
}
|
|
5173
|
+
drainAll() {
|
|
5174
|
+
return this.messages.splice(0, this.messages.length);
|
|
5175
|
+
}
|
|
5176
|
+
clear() {
|
|
5177
|
+
this.messages.length = 0;
|
|
5178
|
+
}
|
|
5179
|
+
enforceCap() {
|
|
5180
|
+
if (this.messages.length > this.cap) {
|
|
5181
|
+
const dropped = this.messages.length - this.cap;
|
|
5182
|
+
this.messages.splice(0, dropped);
|
|
5183
|
+
this.log(`${this.overflowLabel}: dropped ${dropped} oldest ${this.overflowNoun}, ${this.cap} remaining`);
|
|
5184
|
+
}
|
|
5185
|
+
}
|
|
5186
|
+
}
|
|
5187
|
+
|
|
4448
5188
|
// src/daemon.ts
|
|
4449
5189
|
var stateDir = new StateDirResolver;
|
|
4450
5190
|
stateDir.ensure();
|
|
4451
5191
|
var processLogger = createProcessLogger({ component: "AgentBridgeDaemon", logFile: stateDir.logFile });
|
|
5192
|
+
var controlTokenPath = resolveControlTokenPath(stateDir.dir);
|
|
5193
|
+
var controlToken = generateControlToken();
|
|
5194
|
+
var weWroteToken = false;
|
|
5195
|
+
var weWrotePid = false;
|
|
4452
5196
|
var configService = new ConfigService;
|
|
4453
5197
|
var config = configService.loadOrDefault(processLogger.log);
|
|
4454
5198
|
var CODEX_APP_PORT = parseInt(process.env.CODEX_WS_PORT ?? String(config.codex.appPort), 10);
|
|
@@ -4465,12 +5209,16 @@ var CODEX_BOOT_RETRIES = parsePositiveIntEnv("AGENTBRIDGE_CODEX_BOOT_RETRIES", 2
|
|
|
4465
5209
|
var ALLOW_IDENTITYLESS_CLIENT = process.env.AGENTBRIDGE_COMPAT_IDENTITYLESS === "1";
|
|
4466
5210
|
var BUDGET_CONFIG = applyBudgetEnvOverrides(config.budget);
|
|
4467
5211
|
var daemonLifecycle = new DaemonLifecycle({ stateDir, controlPort: CONTROL_PORT, log });
|
|
5212
|
+
var DAEMON_NONCE = randomUUID4();
|
|
5213
|
+
var DAEMON_STARTED_AT = Date.now();
|
|
4468
5214
|
var codex = new CodexAdapter(CODEX_APP_PORT, CODEX_PROXY_PORT, stateDir.logFile);
|
|
4469
5215
|
var attachCmd = `codex --enable tui_app_server --remote ${codex.proxyUrl}`;
|
|
4470
5216
|
var controlServer = null;
|
|
5217
|
+
var boundControlPort = false;
|
|
4471
5218
|
var attachedClaude = null;
|
|
4472
5219
|
var nextControlClientId = 0;
|
|
4473
5220
|
var nextSystemMessageId = 0;
|
|
5221
|
+
var SYSTEM_MSG_SALT = randomUUID4().slice(0, 8);
|
|
4474
5222
|
var codexBootstrapped = false;
|
|
4475
5223
|
var attentionWindowTimer = null;
|
|
4476
5224
|
var inAttentionWindow = false;
|
|
@@ -4488,40 +5236,42 @@ var ATTACH_STATUS_COOLDOWN_MS = 30000;
|
|
|
4488
5236
|
var LIVENESS_PROBE_TIMEOUT_MS = parsePositiveIntEnv("AGENTBRIDGE_LIVENESS_PROBE_TIMEOUT_MS", 3000, log);
|
|
4489
5237
|
var LIVENESS_PROBE_POLL_MS = 50;
|
|
4490
5238
|
var challengeInProgress = false;
|
|
4491
|
-
var bufferedMessages =
|
|
5239
|
+
var bufferedMessages = new BoundedMessageBuffer({
|
|
5240
|
+
cap: MAX_BUFFERED_MESSAGES,
|
|
5241
|
+
overflowLabel: "Message buffer overflow",
|
|
5242
|
+
log
|
|
5243
|
+
});
|
|
5244
|
+
function createPendingBackpressureBuffer() {
|
|
5245
|
+
return new BoundedMessageBuffer({
|
|
5246
|
+
cap: MAX_BUFFERED_MESSAGES,
|
|
5247
|
+
overflowLabel: "Backpressure overflow",
|
|
5248
|
+
overflowNoun: "tracked message(s)",
|
|
5249
|
+
log
|
|
5250
|
+
});
|
|
5251
|
+
}
|
|
4492
5252
|
var budgetCoordinator = null;
|
|
4493
|
-
var budgetStatusTimer = null;
|
|
4494
5253
|
function ensureBudgetCoordinatorStarted() {
|
|
4495
5254
|
if (!BUDGET_CONFIG.enabled)
|
|
4496
5255
|
return;
|
|
4497
5256
|
if (!budgetCoordinator) {
|
|
4498
|
-
log(`Budget coordinator config: pollSeconds=${BUDGET_CONFIG.pollSeconds} pauseAt=${BUDGET_CONFIG.pauseAt} ` + `resumeBelow=${BUDGET_CONFIG.resumeBelow} syncDriftPct=${BUDGET_CONFIG.syncDriftPct} ` + `parallel=${BUDGET_CONFIG.parallel.minRemainingPct}%/${BUDGET_CONFIG.parallel.timeWindowSec}s ` + `codexTierControl=${BUDGET_CONFIG.codexTierControl} ` + `codexTiersFull=${BUDGET_CONFIG.codexTiers.full ? "configured" : "missing"}`);
|
|
5257
|
+
log(`Budget coordinator config: pollSeconds=${BUDGET_CONFIG.pollSeconds} pauseAt=${BUDGET_CONFIG.pauseAt} ` + `resumeBelow=${BUDGET_CONFIG.resumeBelow} syncDriftPct=${BUDGET_CONFIG.syncDriftPct} ` + `parallel=${BUDGET_CONFIG.parallel.minRemainingPct}%/${BUDGET_CONFIG.parallel.timeWindowSec}s ` + `codexTierControl=${BUDGET_CONFIG.codexTierControl} ` + `codexTiersFull=${BUDGET_CONFIG.codexTiers.full ? "configured" : "missing"} ` + `strategy=${BUDGET_CONFIG.strategy}`);
|
|
4499
5258
|
budgetCoordinator = new BudgetCoordinator({
|
|
4500
5259
|
source: createQuotaSource({ log }),
|
|
4501
5260
|
config: BUDGET_CONFIG,
|
|
4502
5261
|
emit: (id, content) => {
|
|
4503
5262
|
emitToClaude(systemMessage(id, content));
|
|
4504
|
-
queueMicrotask(() => broadcastStatus());
|
|
4505
5263
|
},
|
|
4506
5264
|
onPauseChange: (paused) => {
|
|
4507
5265
|
log(`Budget intervention ${paused ? "ACTIVE" : "CLEARED"} ` + `(gate ${budgetCoordinator?.isGateClosed() ? "CLOSED" : "OPEN"})`);
|
|
4508
|
-
queueMicrotask(() => broadcastStatus());
|
|
4509
5266
|
},
|
|
5267
|
+
onSnapshot: () => broadcastStatus(),
|
|
4510
5268
|
log
|
|
4511
5269
|
});
|
|
4512
5270
|
}
|
|
4513
5271
|
budgetCoordinator.start();
|
|
4514
|
-
if (!budgetStatusTimer) {
|
|
4515
|
-
budgetStatusTimer = setInterval(() => broadcastStatus(), BUDGET_CONFIG.pollSeconds * 1000);
|
|
4516
|
-
budgetStatusTimer.unref?.();
|
|
4517
|
-
}
|
|
4518
5272
|
}
|
|
4519
5273
|
function stopBudgetCoordinator() {
|
|
4520
5274
|
budgetCoordinator?.stop();
|
|
4521
|
-
if (budgetStatusTimer) {
|
|
4522
|
-
clearInterval(budgetStatusTimer);
|
|
4523
|
-
budgetStatusTimer = null;
|
|
4524
|
-
}
|
|
4525
5275
|
}
|
|
4526
5276
|
function budgetPauseGateError() {
|
|
4527
5277
|
const snapshot = budgetCoordinator?.getSnapshot() ?? null;
|
|
@@ -4625,29 +5375,22 @@ codex.on("turnStarted", () => {
|
|
|
4625
5375
|
codex.on("agentMessage", (msg) => {
|
|
4626
5376
|
if (msg.source !== "codex")
|
|
4627
5377
|
return;
|
|
4628
|
-
const
|
|
4629
|
-
|
|
4630
|
-
|
|
5378
|
+
const route = routeCodexMessage(msg.content, {
|
|
5379
|
+
mode: FILTER_MODE,
|
|
5380
|
+
replyArmed: replyTracker.isArmed,
|
|
5381
|
+
inAttentionWindow
|
|
5382
|
+
});
|
|
5383
|
+
log(`Codex \u2192 Claude [${route.marker}/${route.reason}] (${msg.content.length} chars)`);
|
|
5384
|
+
if (route.noteReplyForwarded) {
|
|
4631
5385
|
replyTracker.noteForwarded();
|
|
4632
|
-
if (statusBuffer.size > 0) {
|
|
4633
|
-
statusBuffer.flush("reply-required message arrived");
|
|
4634
|
-
}
|
|
4635
|
-
emitToClaude(msg);
|
|
4636
|
-
return;
|
|
4637
5386
|
}
|
|
4638
|
-
if (
|
|
4639
|
-
|
|
4640
|
-
statusBuffer.add(msg);
|
|
4641
|
-
return;
|
|
5387
|
+
if (route.flushStatusBuffer) {
|
|
5388
|
+
statusBuffer.flush(route.noteReplyForwarded ? "reply-required message arrived" : "important message arrived");
|
|
4642
5389
|
}
|
|
4643
|
-
|
|
4644
|
-
switch (result.action) {
|
|
5390
|
+
switch (route.action) {
|
|
4645
5391
|
case "forward":
|
|
4646
|
-
if (result.marker === "important" && statusBuffer.size > 0) {
|
|
4647
|
-
statusBuffer.flush("important message arrived");
|
|
4648
|
-
}
|
|
4649
5392
|
emitToClaude(msg);
|
|
4650
|
-
if (
|
|
5393
|
+
if (route.startAttentionWindow) {
|
|
4651
5394
|
startAttentionWindow();
|
|
4652
5395
|
}
|
|
4653
5396
|
break;
|
|
@@ -4722,6 +5465,7 @@ codex.on("error", (err) => {
|
|
|
4722
5465
|
});
|
|
4723
5466
|
codex.on("exit", (code) => {
|
|
4724
5467
|
log(`Codex process exited (code ${code})`);
|
|
5468
|
+
const wasBootstrapped = codexBootstrapped;
|
|
4725
5469
|
codexBootstrapped = false;
|
|
4726
5470
|
replyTracker.reset();
|
|
4727
5471
|
idempotencyTracker.terminateAll("aborted");
|
|
@@ -4730,65 +5474,77 @@ codex.on("exit", (code) => {
|
|
|
4730
5474
|
statusBuffer.flush("codex exited");
|
|
4731
5475
|
tuiConnectionState.handleCodexExit();
|
|
4732
5476
|
clearPendingClaudeDisconnect("Codex process exited");
|
|
4733
|
-
|
|
5477
|
+
if (wasBootstrapped) {
|
|
5478
|
+
emitToClaude(systemMessage("system_codex_exit", `\u26A0\uFE0F Codex app-server exited (code ${code ?? "unknown"}). AgentBridge daemon is still running. ` + `Restart the Codex side (\`agentbridge codex\`); if it does not come back within ` + `${Math.round(BOOTSTRAP_TIMEOUT_MS / 1000)}s the daemon will self-replace so the next launch starts clean.`));
|
|
5479
|
+
}
|
|
4734
5480
|
broadcastStatus();
|
|
4735
|
-
|
|
5481
|
+
if (wasBootstrapped) {
|
|
5482
|
+
armBootDeadline();
|
|
5483
|
+
}
|
|
4736
5484
|
});
|
|
4737
5485
|
function startControlServer() {
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
return Response.json(currentStatus(), { status: codexBootstrapped ? 200 : 503 });
|
|
4748
|
-
}
|
|
4749
|
-
if (url.pathname === "/ws") {
|
|
4750
|
-
if (!isAllowedWsUpgrade(req)) {
|
|
4751
|
-
log("Rejected WS upgrade on control port: Origin header present (possible CSWSH)");
|
|
4752
|
-
return wsOriginRejectedResponse();
|
|
5486
|
+
let server;
|
|
5487
|
+
try {
|
|
5488
|
+
server = Bun.serve({
|
|
5489
|
+
port: CONTROL_PORT,
|
|
5490
|
+
hostname: "127.0.0.1",
|
|
5491
|
+
fetch(req, server2) {
|
|
5492
|
+
const url = new URL(req.url);
|
|
5493
|
+
if (url.pathname === "/healthz") {
|
|
5494
|
+
return Response.json(currentStatus());
|
|
4753
5495
|
}
|
|
4754
|
-
if (
|
|
4755
|
-
return;
|
|
5496
|
+
if (url.pathname === "/readyz") {
|
|
5497
|
+
return Response.json(currentStatus(), { status: codexBootstrapped ? 200 : 503 });
|
|
4756
5498
|
}
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
ws.data.lastPongAt = Date.now();
|
|
4766
|
-
ws.data.pendingBackpressure = [];
|
|
4767
|
-
log(`Frontend socket opened (#${ws.data.clientId})`);
|
|
4768
|
-
},
|
|
4769
|
-
close: (ws, code, reason) => {
|
|
4770
|
-
log(`Frontend socket closed (#${ws.data.clientId}, code=${code}, reason=${reason || "none"}, wasAttached=${attachedClaude === ws})`);
|
|
4771
|
-
if (attachedClaude === ws) {
|
|
4772
|
-
detachClaude(ws, "frontend socket closed");
|
|
5499
|
+
if (url.pathname === "/ws") {
|
|
5500
|
+
if (!isAllowedWsUpgrade(req)) {
|
|
5501
|
+
log("Rejected WS upgrade on control port: Origin header present (possible CSWSH)");
|
|
5502
|
+
return wsOriginRejectedResponse();
|
|
5503
|
+
}
|
|
5504
|
+
if (server2.upgrade(req, { data: { clientId: 0, attached: false, lastPongAt: Date.now(), pongCount: 0, pendingBackpressure: createPendingBackpressureBuffer() } })) {
|
|
5505
|
+
return;
|
|
5506
|
+
}
|
|
4773
5507
|
}
|
|
5508
|
+
return new Response("AgentBridge daemon");
|
|
4774
5509
|
},
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
5510
|
+
websocket: {
|
|
5511
|
+
idleTimeout: 960,
|
|
5512
|
+
sendPings: true,
|
|
5513
|
+
open: (ws) => {
|
|
5514
|
+
ws.data.clientId = ++nextControlClientId;
|
|
5515
|
+
ws.data.lastPongAt = Date.now();
|
|
5516
|
+
ws.data.pendingBackpressure = createPendingBackpressureBuffer();
|
|
5517
|
+
log(`Frontend socket opened (#${ws.data.clientId})`);
|
|
5518
|
+
},
|
|
5519
|
+
close: (ws, code, reason) => {
|
|
5520
|
+
log(`Frontend socket closed (#${ws.data.clientId}, code=${code}, reason=${reason || "none"}, wasAttached=${attachedClaude === ws})`);
|
|
5521
|
+
if (attachedClaude === ws) {
|
|
5522
|
+
detachClaude(ws, "frontend socket closed");
|
|
5523
|
+
}
|
|
5524
|
+
},
|
|
5525
|
+
message: (ws, raw) => {
|
|
5526
|
+
handleControlMessage(ws, raw);
|
|
5527
|
+
},
|
|
5528
|
+
pong: (ws) => {
|
|
5529
|
+
ws.data.lastPongAt = Date.now();
|
|
5530
|
+
ws.data.pongCount++;
|
|
5531
|
+
},
|
|
5532
|
+
drain: (ws) => {
|
|
5533
|
+
if (ws.data.pendingBackpressure.length > 0 && ws.getBufferedAmount() === 0) {
|
|
5534
|
+
ws.data.pendingBackpressure.clear();
|
|
5535
|
+
}
|
|
5536
|
+
if (ws === attachedClaude && bufferedMessages.length > 0) {
|
|
5537
|
+
flushBufferedMessages(ws);
|
|
5538
|
+
}
|
|
4788
5539
|
}
|
|
4789
5540
|
}
|
|
4790
|
-
}
|
|
4791
|
-
})
|
|
5541
|
+
});
|
|
5542
|
+
} catch (err) {
|
|
5543
|
+
log(`Control port ${CONTROL_PORT} bind failed (${err?.code ?? err?.message ?? err}) \u2014 ` + `another daemon owns it; exiting without touching shared identity files`);
|
|
5544
|
+
process.exit(0);
|
|
5545
|
+
}
|
|
5546
|
+
controlServer = server;
|
|
5547
|
+
boundControlPort = true;
|
|
4792
5548
|
}
|
|
4793
5549
|
function handleControlMessage(ws, raw) {
|
|
4794
5550
|
let message;
|
|
@@ -4805,7 +5561,9 @@ function handleControlMessage(ws, raw) {
|
|
|
4805
5561
|
expectedPairId: process.env.AGENTBRIDGE_PAIR_ID ?? null,
|
|
4806
5562
|
daemonCwd: process.cwd(),
|
|
4807
5563
|
identity: message.identity,
|
|
4808
|
-
allowIdentityless: ALLOW_IDENTITYLESS_CLIENT
|
|
5564
|
+
allowIdentityless: ALLOW_IDENTITYLESS_CLIENT,
|
|
5565
|
+
expectedControlToken: controlToken,
|
|
5566
|
+
expectedContractVersion: BUILD_INFO.contractVersion
|
|
4809
5567
|
});
|
|
4810
5568
|
if (!admission.ok) {
|
|
4811
5569
|
log(`Rejecting Claude frontend #${ws.data.clientId}: ${admission.reason}`);
|
|
@@ -4884,6 +5642,16 @@ function waitForInterruptOutcome(turnIds) {
|
|
|
4884
5642
|
});
|
|
4885
5643
|
}
|
|
4886
5644
|
async function handleClaudeToCodex(ws, message) {
|
|
5645
|
+
const attachGuard = evaluateInjectionAttachGuard(attachedClaude, ws);
|
|
5646
|
+
if (!attachGuard.allowed) {
|
|
5647
|
+
log(`Rejecting claude_to_codex from non-attached socket #${ws.data.clientId} ` + `(request ${message.requestId}, attached=${attachedClaude ? "#" + attachedClaude.data.clientId : "none"})`);
|
|
5648
|
+
sendClaudeToCodexResult(ws, message.requestId, {
|
|
5649
|
+
success: false,
|
|
5650
|
+
code: attachGuard.code,
|
|
5651
|
+
error: attachGuard.reason
|
|
5652
|
+
});
|
|
5653
|
+
return;
|
|
5654
|
+
}
|
|
4887
5655
|
if (message.message.source !== "claude") {
|
|
4888
5656
|
sendClaudeToCodexResult(ws, message.requestId, {
|
|
4889
5657
|
success: false,
|
|
@@ -4916,8 +5684,8 @@ async function handleClaudeToCodex(ws, message) {
|
|
|
4916
5684
|
if (budgetCoordinator?.isGateClosed()) {
|
|
4917
5685
|
const reason = budgetPauseGateError();
|
|
4918
5686
|
log(`Injection rejected by budget pause gate`);
|
|
4919
|
-
const
|
|
4920
|
-
const retryAfterMs =
|
|
5687
|
+
const resumeAfterEpoch3 = budgetCoordinator?.getSnapshot()?.resumeAfterEpoch ?? null;
|
|
5688
|
+
const retryAfterMs = resumeAfterEpoch3 !== null ? Math.max(0, resumeAfterEpoch3 * 1000 - Date.now()) : undefined;
|
|
4921
5689
|
sendClaudeToCodexResult(ws, message.requestId, {
|
|
4922
5690
|
success: false,
|
|
4923
5691
|
code: "budget_paused",
|
|
@@ -4999,6 +5767,17 @@ async function handleClaudeToCodex(ws, message) {
|
|
|
4999
5767
|
return;
|
|
5000
5768
|
}
|
|
5001
5769
|
log("Interrupt reached terminal boundary \u2014 injecting the message as a new turn");
|
|
5770
|
+
const postWaitAttachGuard = evaluateInjectionAttachGuard(attachedClaude, ws);
|
|
5771
|
+
if (!postWaitAttachGuard.allowed) {
|
|
5772
|
+
releaseInterruptKey();
|
|
5773
|
+
log(`Rejecting interrupt-path injection from socket #${ws.data.clientId} that lost the attach ` + `slot during the terminal-boundary wait (request ${message.requestId}, ` + `attached=${attachedClaude ? "#" + attachedClaude.data.clientId : "none"})`);
|
|
5774
|
+
sendClaudeToCodexResult(ws, message.requestId, {
|
|
5775
|
+
success: false,
|
|
5776
|
+
code: "not_attached",
|
|
5777
|
+
error: "The original Claude session disconnected (or was replaced by a newer session) while " + "the interrupt was waiting to take effect. Your message was NOT injected \u2014 this avoids " + "delivering it into a different session's thread. Reconnect and resend if still needed."
|
|
5778
|
+
});
|
|
5779
|
+
return;
|
|
5780
|
+
}
|
|
5002
5781
|
if (interruptThreadId && codex.activeThreadId !== interruptThreadId) {
|
|
5003
5782
|
releaseInterruptKey();
|
|
5004
5783
|
}
|
|
@@ -5106,14 +5885,9 @@ function detachClaude(ws, reason) {
|
|
|
5106
5885
|
ws.data.attached = false;
|
|
5107
5886
|
log(`Claude frontend detached (#${ws.data.clientId}, ${reason})`);
|
|
5108
5887
|
if (ws.data.pendingBackpressure.length > 0) {
|
|
5109
|
-
|
|
5110
|
-
log(`Re-buffered ${
|
|
5111
|
-
|
|
5112
|
-
if (bufferedMessages.length > MAX_BUFFERED_MESSAGES) {
|
|
5113
|
-
const dropped = bufferedMessages.length - MAX_BUFFERED_MESSAGES;
|
|
5114
|
-
bufferedMessages.splice(0, dropped);
|
|
5115
|
-
log(`Message buffer overflow: dropped ${dropped} oldest message(s), ${MAX_BUFFERED_MESSAGES} remaining`);
|
|
5116
|
-
}
|
|
5888
|
+
const reBuffered = ws.data.pendingBackpressure.drainAll();
|
|
5889
|
+
log(`Re-buffered ${reBuffered.length} backpressured message(s) for redelivery on reconnect`);
|
|
5890
|
+
bufferedMessages.unshiftMany(reBuffered);
|
|
5117
5891
|
}
|
|
5118
5892
|
scheduleClaudeDisconnectNotification(ws.data.clientId);
|
|
5119
5893
|
scheduleIdleShutdown();
|
|
@@ -5236,11 +6010,6 @@ function emitToClaude(message) {
|
|
|
5236
6010
|
log("Send to Claude failed, buffering message for retry on reconnect");
|
|
5237
6011
|
}
|
|
5238
6012
|
bufferedMessages.push(message);
|
|
5239
|
-
if (bufferedMessages.length > MAX_BUFFERED_MESSAGES) {
|
|
5240
|
-
const dropped = bufferedMessages.length - MAX_BUFFERED_MESSAGES;
|
|
5241
|
-
bufferedMessages.splice(0, dropped);
|
|
5242
|
-
log(`Message buffer overflow: dropped ${dropped} oldest message(s), ${MAX_BUFFERED_MESSAGES} remaining`);
|
|
5243
|
-
}
|
|
5244
6013
|
}
|
|
5245
6014
|
function trySendBridgeMessage(ws, message) {
|
|
5246
6015
|
try {
|
|
@@ -5251,11 +6020,6 @@ function trySendBridgeMessage(ws, message) {
|
|
|
5251
6020
|
}
|
|
5252
6021
|
if (typeof result === "number" && result === -1) {
|
|
5253
6022
|
ws.data.pendingBackpressure.push(message);
|
|
5254
|
-
if (ws.data.pendingBackpressure.length > MAX_BUFFERED_MESSAGES) {
|
|
5255
|
-
const dropped = ws.data.pendingBackpressure.length - MAX_BUFFERED_MESSAGES;
|
|
5256
|
-
ws.data.pendingBackpressure.splice(0, dropped);
|
|
5257
|
-
log(`Backpressure overflow: dropped ${dropped} oldest tracked message(s), ${MAX_BUFFERED_MESSAGES} remaining`);
|
|
5258
|
-
}
|
|
5259
6023
|
}
|
|
5260
6024
|
return true;
|
|
5261
6025
|
} catch (err) {
|
|
@@ -5264,11 +6028,11 @@ function trySendBridgeMessage(ws, message) {
|
|
|
5264
6028
|
}
|
|
5265
6029
|
}
|
|
5266
6030
|
function flushBufferedMessages(ws) {
|
|
5267
|
-
const messages = bufferedMessages.
|
|
6031
|
+
const messages = bufferedMessages.drainAll();
|
|
5268
6032
|
for (let i = 0;i < messages.length; i++) {
|
|
5269
6033
|
if (!trySendBridgeMessage(ws, messages[i])) {
|
|
5270
6034
|
const remaining = messages.slice(i);
|
|
5271
|
-
bufferedMessages.
|
|
6035
|
+
bufferedMessages.unshiftMany(remaining);
|
|
5272
6036
|
log(`Flush interrupted: re-buffered ${remaining.length} message(s) after send failure`);
|
|
5273
6037
|
return;
|
|
5274
6038
|
}
|
|
@@ -5312,7 +6076,8 @@ function currentStatus() {
|
|
|
5312
6076
|
budget: budgetCoordinator?.getSnapshot() ?? undefined,
|
|
5313
6077
|
turnInProgress: codex.turnInProgress,
|
|
5314
6078
|
turnPhase: codex.turnPhase,
|
|
5315
|
-
attentionWindowActive: inAttentionWindow
|
|
6079
|
+
attentionWindowActive: inAttentionWindow,
|
|
6080
|
+
appServerInfo: codex.capturedAppServerInfo
|
|
5316
6081
|
};
|
|
5317
6082
|
}
|
|
5318
6083
|
function currentWaitingMessage() {
|
|
@@ -5333,7 +6098,7 @@ function currentReadyMessage() {
|
|
|
5333
6098
|
}
|
|
5334
6099
|
function systemMessage(idPrefix, content) {
|
|
5335
6100
|
return {
|
|
5336
|
-
id: `${idPrefix}_${++nextSystemMessageId}`,
|
|
6101
|
+
id: `${idPrefix}_${SYSTEM_MSG_SALT}_${++nextSystemMessageId}`,
|
|
5337
6102
|
source: "codex",
|
|
5338
6103
|
content,
|
|
5339
6104
|
timestamp: Date.now()
|
|
@@ -5341,9 +6106,47 @@ function systemMessage(idPrefix, content) {
|
|
|
5341
6106
|
}
|
|
5342
6107
|
function writePidFile() {
|
|
5343
6108
|
daemonLifecycle.writePid();
|
|
6109
|
+
daemonLifecycle.writeDaemonRecord(buildDaemonRecord("booting"));
|
|
6110
|
+
weWrotePid = true;
|
|
6111
|
+
}
|
|
6112
|
+
function writeControlTokenPostBind() {
|
|
6113
|
+
if (controlToken === null)
|
|
6114
|
+
return;
|
|
6115
|
+
try {
|
|
6116
|
+
writeControlToken(controlTokenPath, controlToken);
|
|
6117
|
+
weWroteToken = true;
|
|
6118
|
+
} catch (err) {
|
|
6119
|
+
controlToken = null;
|
|
6120
|
+
processLogger.log(`Failed to write control token (${controlTokenPath}): ${err?.message ?? err} \u2014 ` + `token layer DISABLED for this daemon (attach guard + Origin guard still active)`);
|
|
6121
|
+
}
|
|
5344
6122
|
}
|
|
5345
6123
|
function removePidFile() {
|
|
6124
|
+
if (!weWrotePid || !pidFileOwnedByUs(stateDir.pidFile, process.pid))
|
|
6125
|
+
return;
|
|
5346
6126
|
daemonLifecycle.removePidFile();
|
|
6127
|
+
daemonLifecycle.removeDaemonRecord();
|
|
6128
|
+
}
|
|
6129
|
+
function buildDaemonRecord(phase) {
|
|
6130
|
+
return {
|
|
6131
|
+
pid: process.pid,
|
|
6132
|
+
phase,
|
|
6133
|
+
startedAt: DAEMON_STARTED_AT,
|
|
6134
|
+
nonce: DAEMON_NONCE,
|
|
6135
|
+
pairId: process.env.AGENTBRIDGE_PAIR_ID ?? null,
|
|
6136
|
+
cwd: process.cwd(),
|
|
6137
|
+
stateDir: stateDir.dir,
|
|
6138
|
+
proxyUrl: codex.proxyUrl,
|
|
6139
|
+
appServerUrl: codex.appServerUrl,
|
|
6140
|
+
ports: {
|
|
6141
|
+
appPort: portFromUrl(codex.appServerUrl) ?? CODEX_APP_PORT,
|
|
6142
|
+
proxyPort: portFromUrl(codex.proxyUrl) ?? CODEX_PROXY_PORT,
|
|
6143
|
+
controlPort: CONTROL_PORT
|
|
6144
|
+
},
|
|
6145
|
+
build: daemonStatusBuildInfo(),
|
|
6146
|
+
turnInProgress: codex.turnInProgress,
|
|
6147
|
+
turnPhase: codex.turnPhase,
|
|
6148
|
+
attentionWindowActive: inAttentionWindow
|
|
6149
|
+
};
|
|
5347
6150
|
}
|
|
5348
6151
|
function writeStatusFile() {
|
|
5349
6152
|
daemonLifecycle.writeStatus({
|
|
@@ -5357,11 +6160,16 @@ function writeStatusFile() {
|
|
|
5357
6160
|
build: daemonStatusBuildInfo(),
|
|
5358
6161
|
turnInProgress: codex.turnInProgress,
|
|
5359
6162
|
turnPhase: codex.turnPhase,
|
|
5360
|
-
attentionWindowActive: inAttentionWindow
|
|
6163
|
+
attentionWindowActive: inAttentionWindow,
|
|
6164
|
+
appServerInfo: codex.capturedAppServerInfo
|
|
5361
6165
|
});
|
|
6166
|
+
daemonLifecycle.writeDaemonRecord(buildDaemonRecord("ready"));
|
|
5362
6167
|
}
|
|
5363
6168
|
function removeStatusFile() {
|
|
6169
|
+
if (!boundControlPort)
|
|
6170
|
+
return;
|
|
5364
6171
|
daemonLifecycle.removeStatusFile();
|
|
6172
|
+
daemonLifecycle.removeDaemonRecord();
|
|
5365
6173
|
}
|
|
5366
6174
|
function armBootDeadline() {
|
|
5367
6175
|
if (bootDeadlineTimer)
|
|
@@ -5434,20 +6242,41 @@ function shutdown(reason, exitCode = 0) {
|
|
|
5434
6242
|
codex.stop();
|
|
5435
6243
|
removePidFile();
|
|
5436
6244
|
removeStatusFile();
|
|
6245
|
+
removeControlToken();
|
|
5437
6246
|
process.exit(exitCode);
|
|
5438
6247
|
}
|
|
6248
|
+
function removeControlToken() {
|
|
6249
|
+
if (!weWroteToken)
|
|
6250
|
+
return;
|
|
6251
|
+
try {
|
|
6252
|
+
rmSync2(controlTokenPath, { force: true });
|
|
6253
|
+
} catch {}
|
|
6254
|
+
}
|
|
5439
6255
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
5440
6256
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
5441
6257
|
process.on("exit", () => {
|
|
5442
6258
|
codex.forceKillAppServerSync();
|
|
5443
6259
|
removePidFile();
|
|
5444
6260
|
removeStatusFile();
|
|
6261
|
+
removeControlToken();
|
|
5445
6262
|
});
|
|
5446
6263
|
process.on("uncaughtException", (err) => {
|
|
5447
|
-
processLogger.fatal("UNCAUGHT EXCEPTION", err);
|
|
6264
|
+
processLogger.fatal("UNCAUGHT EXCEPTION \u2014 auto-shutting down daemon", err);
|
|
6265
|
+
try {
|
|
6266
|
+
shutdown("uncaught exception", 1);
|
|
6267
|
+
} catch (shutdownErr) {
|
|
6268
|
+
processLogger.fatal("shutdown during uncaughtException failed", shutdownErr);
|
|
6269
|
+
}
|
|
6270
|
+
process.exit(1);
|
|
5448
6271
|
});
|
|
5449
6272
|
process.on("unhandledRejection", (reason) => {
|
|
5450
|
-
processLogger.fatal("UNHANDLED REJECTION", reason);
|
|
6273
|
+
processLogger.fatal("UNHANDLED REJECTION \u2014 auto-shutting down daemon", reason);
|
|
6274
|
+
try {
|
|
6275
|
+
shutdown("unhandled rejection", 1);
|
|
6276
|
+
} catch (shutdownErr) {
|
|
6277
|
+
processLogger.fatal("shutdown during unhandledRejection failed", shutdownErr);
|
|
6278
|
+
}
|
|
6279
|
+
process.exit(1);
|
|
5451
6280
|
});
|
|
5452
6281
|
function log(msg) {
|
|
5453
6282
|
processLogger.log(msg);
|
|
@@ -5456,7 +6285,8 @@ if (daemonLifecycle.wasKilled()) {
|
|
|
5456
6285
|
log("Killed sentinel found \u2014 daemon was intentionally stopped. Exiting immediately.");
|
|
5457
6286
|
process.exit(0);
|
|
5458
6287
|
}
|
|
5459
|
-
writePidFile();
|
|
5460
6288
|
startControlServer();
|
|
6289
|
+
writePidFile();
|
|
6290
|
+
writeControlTokenPostBind();
|
|
5461
6291
|
armBootDeadline();
|
|
5462
6292
|
bootCodex();
|