@raysonmeng/agentbridge 0.1.12 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +1 -1
- package/dist/cli.js +951 -412
- package/dist/daemon.js +1117 -422
- package/package.json +3 -1
- package/plugins/agentbridge/.claude-plugin/plugin.json +1 -1
- package/plugins/agentbridge/server/bridge-server.js +596 -180
- package/plugins/agentbridge/server/daemon.js +1117 -422
|
@@ -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.13", "0.0.0-source"),
|
|
30
|
+
commit: defineString("7a71869", "source"),
|
|
22
31
|
bundle: defineBundle("plugin"),
|
|
23
|
-
contractVersion: defineNumber(1, CONTRACT_VERSION)
|
|
32
|
+
contractVersion: defineNumber(1, CONTRACT_VERSION),
|
|
33
|
+
codeHash: defineString("e1fd67d07c62", "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,
|
|
@@ -3041,13 +3556,13 @@ function normalizeConfig(raw) {
|
|
|
3041
3556
|
return {
|
|
3042
3557
|
version: typeof config.version === "string" ? config.version : DEFAULT_CONFIG.version,
|
|
3043
3558
|
codex: {
|
|
3044
|
-
appPort:
|
|
3045
|
-
proxyPort:
|
|
3559
|
+
appPort: normalizeBoundedInteger(codex.appPort ?? daemon.port, DEFAULT_CONFIG.codex.appPort, 1, 65535),
|
|
3560
|
+
proxyPort: normalizeBoundedInteger(codex.proxyPort ?? daemon.proxyPort, DEFAULT_CONFIG.codex.proxyPort, 1, 65535)
|
|
3046
3561
|
},
|
|
3047
3562
|
turnCoordination: {
|
|
3048
|
-
attentionWindowSeconds:
|
|
3563
|
+
attentionWindowSeconds: normalizeBoundedInteger(turnCoordination.attentionWindowSeconds, DEFAULT_CONFIG.turnCoordination.attentionWindowSeconds, 0, Number.MAX_SAFE_INTEGER)
|
|
3049
3564
|
},
|
|
3050
|
-
idleShutdownSeconds:
|
|
3565
|
+
idleShutdownSeconds: normalizeBoundedInteger(config.idleShutdownSeconds, DEFAULT_CONFIG.idleShutdownSeconds, 1, Number.MAX_SAFE_INTEGER),
|
|
3051
3566
|
budget: normalizeBudgetConfig(config.budget)
|
|
3052
3567
|
};
|
|
3053
3568
|
}
|
|
@@ -3057,8 +3572,8 @@ class ConfigService {
|
|
|
3057
3572
|
configPath;
|
|
3058
3573
|
constructor(projectRoot) {
|
|
3059
3574
|
const root = projectRoot ?? process.cwd();
|
|
3060
|
-
this.configDir =
|
|
3061
|
-
this.configPath =
|
|
3575
|
+
this.configDir = join4(root, CONFIG_DIR);
|
|
3576
|
+
this.configPath = join4(this.configDir, CONFIG_FILE);
|
|
3062
3577
|
}
|
|
3063
3578
|
hasConfig() {
|
|
3064
3579
|
return existsSync4(this.configPath);
|
|
@@ -3066,7 +3581,7 @@ class ConfigService {
|
|
|
3066
3581
|
load() {
|
|
3067
3582
|
let raw;
|
|
3068
3583
|
try {
|
|
3069
|
-
raw =
|
|
3584
|
+
raw = readFileSync4(this.configPath, "utf-8");
|
|
3070
3585
|
} catch (err) {
|
|
3071
3586
|
if (err?.code === "ENOENT") {
|
|
3072
3587
|
return { state: "absent" };
|
|
@@ -3119,9 +3634,7 @@ class ConfigService {
|
|
|
3119
3634
|
};
|
|
3120
3635
|
}
|
|
3121
3636
|
save(config) {
|
|
3122
|
-
this.
|
|
3123
|
-
writeFileSync2(this.configPath, JSON.stringify(config, null, 2) + `
|
|
3124
|
-
`, "utf-8");
|
|
3637
|
+
atomicWriteJson(this.configPath, config);
|
|
3125
3638
|
}
|
|
3126
3639
|
initDefaults() {
|
|
3127
3640
|
this.ensureConfigDir();
|
|
@@ -3137,11 +3650,32 @@ class ConfigService {
|
|
|
3137
3650
|
}
|
|
3138
3651
|
ensureConfigDir() {
|
|
3139
3652
|
if (!existsSync4(this.configDir)) {
|
|
3140
|
-
|
|
3653
|
+
mkdirSync4(this.configDir, { recursive: true });
|
|
3141
3654
|
}
|
|
3142
3655
|
}
|
|
3143
3656
|
}
|
|
3144
3657
|
|
|
3658
|
+
// src/budget/budget-gate.ts
|
|
3659
|
+
function matchingGateReset(usage) {
|
|
3660
|
+
if (!usage)
|
|
3661
|
+
return 0;
|
|
3662
|
+
const windows = [usage.fiveHour, usage.weekly].filter((window) => !!window && window.resetEpoch > 0);
|
|
3663
|
+
const matching = windows.filter((window) => Math.abs(window.util - usage.gateUtil) < 0.0001);
|
|
3664
|
+
const candidates = matching.length > 0 ? matching : windows;
|
|
3665
|
+
if (candidates.length === 0)
|
|
3666
|
+
return 0;
|
|
3667
|
+
return Math.min(...candidates.map((window) => window.resetEpoch));
|
|
3668
|
+
}
|
|
3669
|
+
function resumeBlockingEpoch(usage, cfg, now) {
|
|
3670
|
+
if (!usage)
|
|
3671
|
+
return 0;
|
|
3672
|
+
if (usage.rateLimitedUntil > now)
|
|
3673
|
+
return usage.rateLimitedUntil;
|
|
3674
|
+
if (usage.gateUtil >= cfg.resumeBelow)
|
|
3675
|
+
return matchingGateReset(usage);
|
|
3676
|
+
return 0;
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3145
3679
|
// src/budget/types.ts
|
|
3146
3680
|
var STALE_MAX_AGE_SEC = 600;
|
|
3147
3681
|
|
|
@@ -3166,25 +3700,6 @@ function usageSummary(name, usage) {
|
|
|
3166
3700
|
return `${AGENT_LABEL[name]} \u672A\u77E5`;
|
|
3167
3701
|
return `${AGENT_LABEL[name]} gate=${pct(usage.gateUtil)} warn=${pct(usage.warnUtil)} 5h\u91CD\u7F6E=${formatEpoch(usage.fiveHour?.resetEpoch ?? 0)}`;
|
|
3168
3702
|
}
|
|
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
3703
|
function resumeAfterEpoch(claude, codex, cfg, now) {
|
|
3189
3704
|
const epochs = [
|
|
3190
3705
|
resumeBlockingEpoch(claude, cfg, now),
|
|
@@ -3367,8 +3882,163 @@ function computeBudgetState(claude, codex, cfg, now) {
|
|
|
3367
3882
|
};
|
|
3368
3883
|
}
|
|
3369
3884
|
|
|
3370
|
-
// src/budget/budget-
|
|
3885
|
+
// src/budget/budget-fingerprint.ts
|
|
3371
3886
|
var RESET_FINGERPRINT_BUCKET_SEC = 600;
|
|
3887
|
+
var AGENT_LABEL2 = {
|
|
3888
|
+
claude: "Claude",
|
|
3889
|
+
codex: "Codex"
|
|
3890
|
+
};
|
|
3891
|
+
function pct2(value) {
|
|
3892
|
+
return `${Math.round(value * 10) / 10}%`;
|
|
3893
|
+
}
|
|
3894
|
+
function formatEpoch2(epoch) {
|
|
3895
|
+
return new Date(epoch * 1000).toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z");
|
|
3896
|
+
}
|
|
3897
|
+
var INITIAL_FINGERPRINT_STATE = {
|
|
3898
|
+
side: null,
|
|
3899
|
+
fingerprint: null,
|
|
3900
|
+
resumeEpoch: null,
|
|
3901
|
+
reason: null
|
|
3902
|
+
};
|
|
3903
|
+
function sideToAgents(side) {
|
|
3904
|
+
if (side === "both")
|
|
3905
|
+
return ["claude", "codex"];
|
|
3906
|
+
if (side === "claude")
|
|
3907
|
+
return ["claude"];
|
|
3908
|
+
if (side === "codex")
|
|
3909
|
+
return ["codex"];
|
|
3910
|
+
return [];
|
|
3911
|
+
}
|
|
3912
|
+
function agentsToSide(agents) {
|
|
3913
|
+
const claude = agents.has("claude");
|
|
3914
|
+
const codex = agents.has("codex");
|
|
3915
|
+
if (claude && codex)
|
|
3916
|
+
return "both";
|
|
3917
|
+
if (claude)
|
|
3918
|
+
return "claude";
|
|
3919
|
+
if (codex)
|
|
3920
|
+
return "codex";
|
|
3921
|
+
return null;
|
|
3922
|
+
}
|
|
3923
|
+
function shouldEnter(usage, cfg, now) {
|
|
3924
|
+
if (!isDecisionGrade(usage, now))
|
|
3925
|
+
return false;
|
|
3926
|
+
return usage.gateUtil >= cfg.pauseAt;
|
|
3927
|
+
}
|
|
3928
|
+
function canAgentResume(usage, cfg, now) {
|
|
3929
|
+
if (!isDecisionGrade(usage, now))
|
|
3930
|
+
return false;
|
|
3931
|
+
if (usage.rateLimitedUntil > now)
|
|
3932
|
+
return false;
|
|
3933
|
+
return usage.gateUtil < cfg.resumeBelow;
|
|
3934
|
+
}
|
|
3935
|
+
function nextActiveSide(prevSide, state, cfg) {
|
|
3936
|
+
const active = new Set(sideToAgents(prevSide));
|
|
3937
|
+
for (const agent of ["claude", "codex"]) {
|
|
3938
|
+
const usage = state.perAgent[agent];
|
|
3939
|
+
if (shouldEnter(usage, cfg, state.now)) {
|
|
3940
|
+
active.add(agent);
|
|
3941
|
+
} else if (active.has(agent) && canAgentResume(usage, cfg, state.now)) {
|
|
3942
|
+
active.delete(agent);
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
return agentsToSide(active);
|
|
3946
|
+
}
|
|
3947
|
+
function activeSideReason(agent, usage, cfg, now) {
|
|
3948
|
+
if (!usage)
|
|
3949
|
+
return `${AGENT_LABEL2[agent]} \u63A2\u6D4B\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u4FDD\u6301\u4E0A\u4E00\u8F6E\u9884\u7B97\u5E72\u9884`;
|
|
3950
|
+
if (usage.rateLimitedUntil > now) {
|
|
3951
|
+
return `${AGENT_LABEL2[agent]} \u63A2\u9488\u88AB\u9650\u6D41\u81F3 ${formatEpoch2(usage.rateLimitedUntil)}`;
|
|
3952
|
+
}
|
|
3953
|
+
if (usage.gateUtil >= cfg.pauseAt) {
|
|
3954
|
+
return `${AGENT_LABEL2[agent]} gateUtil ${pct2(usage.gateUtil)} \u2265 pauseAt ${pct2(cfg.pauseAt)}`;
|
|
3955
|
+
}
|
|
3956
|
+
return `${AGENT_LABEL2[agent]} gateUtil ${pct2(usage.gateUtil)} \u5C1A\u672A\u4F4E\u4E8E resumeBelow ${pct2(cfg.resumeBelow)}`;
|
|
3957
|
+
}
|
|
3958
|
+
function interventionReason(side, state, cfg) {
|
|
3959
|
+
return sideToAgents(side).map((agent) => activeSideReason(agent, state.perAgent[agent], cfg, state.now)).join("\uFF1B");
|
|
3960
|
+
}
|
|
3961
|
+
function resumeAfterEpoch2(side, state, cfg) {
|
|
3962
|
+
const epochs = sideToAgents(side).map((agent) => resumeBlockingEpoch(state.perAgent[agent], cfg, state.now)).filter((epoch) => epoch > 0);
|
|
3963
|
+
if (epochs.length === 0)
|
|
3964
|
+
return null;
|
|
3965
|
+
return Math.max(...epochs);
|
|
3966
|
+
}
|
|
3967
|
+
function activeSideProbeUncertain(side, state) {
|
|
3968
|
+
return sideToAgents(side).some((agent) => {
|
|
3969
|
+
const usage = state.perAgent[agent];
|
|
3970
|
+
return usage === null || usage.rateLimitedUntil > state.now || !isDecisionGrade(usage, state.now);
|
|
3971
|
+
});
|
|
3972
|
+
}
|
|
3973
|
+
function directiveFingerprint(state, activeSide) {
|
|
3974
|
+
const side = activeSide ?? (state.phase === "balance" ? state.drift.lighter ?? "none" : state.pause.side ?? "none");
|
|
3975
|
+
let reset = 0;
|
|
3976
|
+
if (activeSide === "claude") {
|
|
3977
|
+
reset = state.pause.resetEpochs.claude;
|
|
3978
|
+
} else if (activeSide === "codex") {
|
|
3979
|
+
reset = state.pause.resetEpochs.codex;
|
|
3980
|
+
} else if (activeSide === "both") {
|
|
3981
|
+
reset = Math.max(state.pause.resetEpochs.claude, state.pause.resetEpochs.codex);
|
|
3982
|
+
} else if (state.phase === "balance" && state.drift.lighter) {
|
|
3983
|
+
reset = state.perAgent[state.drift.lighter]?.fiveHour?.resetEpoch ?? 0;
|
|
3984
|
+
}
|
|
3985
|
+
const heavier = activeSide ? "" : state.drift.heavier ?? "none";
|
|
3986
|
+
return [
|
|
3987
|
+
activeSide ? "paused" : state.phase,
|
|
3988
|
+
heavier,
|
|
3989
|
+
side,
|
|
3990
|
+
Math.round(reset / RESET_FINGERPRINT_BUCKET_SEC)
|
|
3991
|
+
].join("|");
|
|
3992
|
+
}
|
|
3993
|
+
function classifyPoll(prev, state, cfg) {
|
|
3994
|
+
const previousSide = prev.side;
|
|
3995
|
+
const currentSide = nextActiveSide(previousSide, state, cfg);
|
|
3996
|
+
if (currentSide) {
|
|
3997
|
+
const reason = interventionReason(currentSide, state, cfg);
|
|
3998
|
+
const nextResumeRaw = resumeAfterEpoch2(currentSide, state, cfg);
|
|
3999
|
+
const resumeEpoch = previousSide === currentSide ? nextResumeRaw ?? prev.resumeEpoch : nextResumeRaw;
|
|
4000
|
+
const uncertain = previousSide === currentSide && activeSideProbeUncertain(currentSide, state) && prev.fingerprint;
|
|
4001
|
+
const fingerprint2 = uncertain ? prev.fingerprint : directiveFingerprint(state, currentSide);
|
|
4002
|
+
const pauseChanged = !previousSide;
|
|
4003
|
+
const emit = !previousSide || previousSide !== currentSide || fingerprint2 !== prev.fingerprint;
|
|
4004
|
+
return {
|
|
4005
|
+
next: { side: currentSide, fingerprint: fingerprint2, resumeEpoch, reason },
|
|
4006
|
+
effect: {
|
|
4007
|
+
kind: uncertain ? "hold-uncertain" : "enter",
|
|
4008
|
+
side: currentSide,
|
|
4009
|
+
reason,
|
|
4010
|
+
resumeEpoch,
|
|
4011
|
+
emit,
|
|
4012
|
+
pauseChanged
|
|
4013
|
+
}
|
|
4014
|
+
};
|
|
4015
|
+
}
|
|
4016
|
+
if (previousSide) {
|
|
4017
|
+
return {
|
|
4018
|
+
next: { side: null, fingerprint: null, resumeEpoch: null, reason: null },
|
|
4019
|
+
effect: { kind: "exit", previousSide }
|
|
4020
|
+
};
|
|
4021
|
+
}
|
|
4022
|
+
if (!isDecisionGrade(state.perAgent.claude, state.now) || !isDecisionGrade(state.perAgent.codex, state.now)) {
|
|
4023
|
+
return { next: prev, effect: { kind: "none" } };
|
|
4024
|
+
}
|
|
4025
|
+
if (!state.directiveToClaude) {
|
|
4026
|
+
return {
|
|
4027
|
+
next: { side: null, fingerprint: null, resumeEpoch: null, reason: null },
|
|
4028
|
+
effect: { kind: "none" }
|
|
4029
|
+
};
|
|
4030
|
+
}
|
|
4031
|
+
const fingerprint = directiveFingerprint(state);
|
|
4032
|
+
if (fingerprint !== prev.fingerprint) {
|
|
4033
|
+
return {
|
|
4034
|
+
next: { side: null, fingerprint, resumeEpoch: null, reason: null },
|
|
4035
|
+
effect: { kind: "advise", phase: state.phase }
|
|
4036
|
+
};
|
|
4037
|
+
}
|
|
4038
|
+
return { next: prev, effect: { kind: "none" } };
|
|
4039
|
+
}
|
|
4040
|
+
|
|
4041
|
+
// src/budget/budget-coordinator.ts
|
|
3372
4042
|
var LOW_UTIL_PCT = 50;
|
|
3373
4043
|
var NEAR_PAUSE_MARGIN_PCT = 10;
|
|
3374
4044
|
var NEAR_WARN_UTIL_PCT = 75;
|
|
@@ -3386,27 +4056,17 @@ var REAL_BUDGET_POLL_SCHEDULER = {
|
|
|
3386
4056
|
clearTimeout(timer);
|
|
3387
4057
|
}
|
|
3388
4058
|
};
|
|
3389
|
-
var
|
|
4059
|
+
var AGENT_LABEL3 = {
|
|
3390
4060
|
claude: "Claude",
|
|
3391
4061
|
codex: "Codex"
|
|
3392
4062
|
};
|
|
3393
|
-
function
|
|
4063
|
+
function pct3(value) {
|
|
3394
4064
|
return `${Math.round(value * 10) / 10}%`;
|
|
3395
4065
|
}
|
|
3396
4066
|
function usageLine(agent, usage) {
|
|
3397
4067
|
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));
|
|
4068
|
+
return `${AGENT_LABEL3[agent]} \u672A\u77E5`;
|
|
4069
|
+
return `${AGENT_LABEL3[agent]} gate=${pct3(usage.gateUtil)} warn=${pct3(usage.warnUtil)}`;
|
|
3410
4070
|
}
|
|
3411
4071
|
function maxPollDelayMs(config) {
|
|
3412
4072
|
return Math.max(0, config.pollSeconds * 1000);
|
|
@@ -3463,16 +4123,14 @@ class BudgetCoordinator {
|
|
|
3463
4123
|
config;
|
|
3464
4124
|
emit;
|
|
3465
4125
|
onPauseChange;
|
|
4126
|
+
onSnapshot;
|
|
3466
4127
|
now;
|
|
3467
4128
|
scheduler;
|
|
3468
4129
|
log;
|
|
3469
4130
|
timer = null;
|
|
3470
4131
|
running = false;
|
|
3471
|
-
|
|
3472
|
-
lastDirectiveFingerprint = null;
|
|
4132
|
+
fpState = INITIAL_FINGERPRINT_STATE;
|
|
3473
4133
|
latestSnapshot = null;
|
|
3474
|
-
pauseReason = null;
|
|
3475
|
-
pauseResumeAfterEpoch = null;
|
|
3476
4134
|
pendingOverrideTier = null;
|
|
3477
4135
|
pendingOverrides = null;
|
|
3478
4136
|
lastAppliedTier = "full";
|
|
@@ -3483,6 +4141,7 @@ class BudgetCoordinator {
|
|
|
3483
4141
|
this.config = options.config;
|
|
3484
4142
|
this.emit = options.emit;
|
|
3485
4143
|
this.onPauseChange = options.onPauseChange;
|
|
4144
|
+
this.onSnapshot = options.onSnapshot ?? (() => {});
|
|
3486
4145
|
this.now = options.now ?? (() => Math.floor(Date.now() / 1000));
|
|
3487
4146
|
this.scheduler = options.scheduler ?? REAL_BUDGET_POLL_SCHEDULER;
|
|
3488
4147
|
this.log = options.log ?? (() => {});
|
|
@@ -3503,10 +4162,10 @@ class BudgetCoordinator {
|
|
|
3503
4162
|
}
|
|
3504
4163
|
}
|
|
3505
4164
|
isPaused() {
|
|
3506
|
-
return this.
|
|
4165
|
+
return this.fpState.side !== null;
|
|
3507
4166
|
}
|
|
3508
4167
|
isGateClosed() {
|
|
3509
|
-
return this.
|
|
4168
|
+
return this.fpState.side === "codex" || this.fpState.side === "both";
|
|
3510
4169
|
}
|
|
3511
4170
|
getSnapshot() {
|
|
3512
4171
|
return this.latestSnapshot;
|
|
@@ -3560,7 +4219,7 @@ class BudgetCoordinator {
|
|
|
3560
4219
|
}
|
|
3561
4220
|
if (!usage) {
|
|
3562
4221
|
if (!this.isPaused())
|
|
3563
|
-
this.
|
|
4222
|
+
this.setSnapshot(null);
|
|
3564
4223
|
return;
|
|
3565
4224
|
}
|
|
3566
4225
|
if (!this.running) {
|
|
@@ -3569,85 +4228,39 @@ class BudgetCoordinator {
|
|
|
3569
4228
|
const state = computeBudgetState(usage.claude, usage.codex, this.config, this.now());
|
|
3570
4229
|
this.updatePendingOverrides(state.effort.codexTier);
|
|
3571
4230
|
this.applyState(state);
|
|
3572
|
-
this.
|
|
4231
|
+
this.setSnapshot(this.toSnapshot(state));
|
|
4232
|
+
}
|
|
4233
|
+
setSnapshot(snapshot) {
|
|
4234
|
+
this.latestSnapshot = snapshot;
|
|
4235
|
+
this.onSnapshot(snapshot);
|
|
3573
4236
|
}
|
|
3574
4237
|
applyState(state) {
|
|
3575
|
-
const
|
|
3576
|
-
this.
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
4238
|
+
const { next, effect } = classifyPoll(this.fpState, state, this.config);
|
|
4239
|
+
this.fpState = next;
|
|
4240
|
+
switch (effect.kind) {
|
|
4241
|
+
case "enter":
|
|
4242
|
+
case "hold-uncertain": {
|
|
4243
|
+
if (effect.pauseChanged)
|
|
4244
|
+
this.onPauseChange(true);
|
|
4245
|
+
if (effect.emit) {
|
|
4246
|
+
this.emitDirective(this.interventionPrefix(effect.side), this.interventionDirective(state, effect.side, effect.reason, effect.resumeEpoch));
|
|
4247
|
+
}
|
|
4248
|
+
return;
|
|
3585
4249
|
}
|
|
3586
|
-
|
|
3587
|
-
this.
|
|
4250
|
+
case "exit": {
|
|
4251
|
+
this.onPauseChange(false);
|
|
4252
|
+
this.emitDirective(this.recoveryPrefix(effect.previousSide), this.recoveryDirective(state, effect.previousSide));
|
|
4253
|
+
return;
|
|
3588
4254
|
}
|
|
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);
|
|
4255
|
+
case "advise": {
|
|
4256
|
+
const prefix = effect.phase === "balance" ? "system_budget_balance" : "system_budget_parallel";
|
|
4257
|
+
this.emitDirective(prefix, state.directiveToClaude);
|
|
4258
|
+
return;
|
|
3621
4259
|
}
|
|
4260
|
+
case "none":
|
|
4261
|
+
return;
|
|
3622
4262
|
}
|
|
3623
4263
|
}
|
|
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
4264
|
tierControlEnabled() {
|
|
3652
4265
|
if (!this.config.codexTierControl)
|
|
3653
4266
|
return false;
|
|
@@ -3681,82 +4294,24 @@ class BudgetCoordinator {
|
|
|
3681
4294
|
this.pendingOverrideTier = tier;
|
|
3682
4295
|
this.pendingOverrides = { ...overrides };
|
|
3683
4296
|
}
|
|
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
4297
|
emitDirective(prefix, content) {
|
|
3710
4298
|
this.emit(`${prefix}_${this.sequence++}`, content);
|
|
3711
4299
|
}
|
|
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
4300
|
interventionPrefix(side) {
|
|
3724
4301
|
return side === "claude" ? "system_budget_handoff" : "system_budget_pause";
|
|
3725
4302
|
}
|
|
3726
4303
|
recoveryPrefix(previousSide) {
|
|
3727
4304
|
return previousSide === "claude" ? "system_budget_claude_recovered" : "system_budget_resume";
|
|
3728
4305
|
}
|
|
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)}`;
|
|
4306
|
+
interventionDirective(state, side, reason, resumeEpoch) {
|
|
4307
|
+
return renderBudgetInterventionDirective(state.perAgent.claude, state.perAgent.codex, side, reason || "\u9884\u7B97\u63A5\u8FD1\u8017\u5C3D", resumeEpoch, this.config);
|
|
3753
4308
|
}
|
|
3754
4309
|
recoveryDirective(state, previousSide) {
|
|
3755
4310
|
if (previousSide === "claude") {
|
|
3756
4311
|
return [
|
|
3757
4312
|
"\u3010\u9884\u7B97\u534F\u8C03 \xB7 \u8D26\u53F7\u7EA7\u3011Claude \u4FA7\u9884\u7B97\u5DF2\u6062\u590D\u3002",
|
|
3758
4313
|
`${usageLine("claude", state.perAgent.claude)}\uFF1B${usageLine("codex", state.perAgent.codex)}\u3002`,
|
|
3759
|
-
`Claude gateUtil \u5DF2\u4F4E\u4E8E ${
|
|
4314
|
+
`Claude gateUtil \u5DF2\u4F4E\u4E8E ${pct3(this.config.resumeBelow)}\uFF0C\u4E14\u6CA1\u6709\u6709\u6548 rate_limit\u3002`,
|
|
3760
4315
|
"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
4316
|
].join(`
|
|
3762
4317
|
`);
|
|
@@ -3765,7 +4320,7 @@ class BudgetCoordinator {
|
|
|
3765
4320
|
return [
|
|
3766
4321
|
"\u3010\u9884\u7B97\u534F\u8C03 \xB7 \u8D26\u53F7\u7EA7\u3011Codex \u4FA7\u9884\u7B97\u95F8\u95E8\u89E3\u9664\u3002",
|
|
3767
4322
|
`${usageLine("claude", state.perAgent.claude)}\uFF1B${usageLine("codex", state.perAgent.codex)}\u3002`,
|
|
3768
|
-
`\u95F8\u95E8\u5DF2\u653E\u5F00\uFF1ACodex gateUtil \u4F4E\u4E8E ${
|
|
4323
|
+
`\u95F8\u95E8\u5DF2\u653E\u5F00\uFF1ACodex gateUtil \u4F4E\u4E8E ${pct3(this.config.resumeBelow)}\uFF0C\u4E14\u6CA1\u6709\u6709\u6548 rate_limit\u3002`,
|
|
3769
4324
|
"\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
4325
|
].join(`
|
|
3771
4326
|
`);
|
|
@@ -3773,14 +4328,11 @@ class BudgetCoordinator {
|
|
|
3773
4328
|
return [
|
|
3774
4329
|
"\u3010\u9884\u7B97\u534F\u8C03 \xB7 \u8D26\u53F7\u7EA7\u3011\u8054\u5408\u6682\u505C\u89E3\u9664\u3002",
|
|
3775
4330
|
`${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 ${
|
|
4331
|
+
`\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
4332
|
"\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
4333
|
].join(`
|
|
3779
4334
|
`);
|
|
3780
4335
|
}
|
|
3781
|
-
formatEpoch(epoch) {
|
|
3782
|
-
return new Date(epoch * 1000).toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z");
|
|
3783
|
-
}
|
|
3784
4336
|
toSnapshot(state) {
|
|
3785
4337
|
const paused = this.isPaused();
|
|
3786
4338
|
return {
|
|
@@ -3791,9 +4343,9 @@ class BudgetCoordinator {
|
|
|
3791
4343
|
driftPct: state.drift.pct,
|
|
3792
4344
|
paused,
|
|
3793
4345
|
gateClosed: this.isGateClosed(),
|
|
3794
|
-
pauseSide: this.
|
|
3795
|
-
pauseReason: paused ? this.
|
|
3796
|
-
resumeAfterEpoch: paused ? this.
|
|
4346
|
+
pauseSide: this.fpState.side,
|
|
4347
|
+
pauseReason: paused ? this.fpState.reason ?? state.pause.reason : null,
|
|
4348
|
+
resumeAfterEpoch: paused ? this.fpState.resumeEpoch ?? state.pause.resumeAfterEpoch : null,
|
|
3797
4349
|
parallelRecommended: paused ? false : state.parallel.recommended,
|
|
3798
4350
|
codexTier: state.effort.codexTier,
|
|
3799
4351
|
claudeAdvice: state.effort.claudeAdvice
|
|
@@ -3805,7 +4357,7 @@ class BudgetCoordinator {
|
|
|
3805
4357
|
import { execFile } from "child_process";
|
|
3806
4358
|
import { existsSync as existsSync5 } from "fs";
|
|
3807
4359
|
import { homedir as homedir2 } from "os";
|
|
3808
|
-
import { basename, join as
|
|
4360
|
+
import { basename, join as join5 } from "path";
|
|
3809
4361
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
3810
4362
|
var MAX_BUFFER = 1024 * 1024;
|
|
3811
4363
|
function defaultRunner(command, args, options) {
|
|
@@ -4053,11 +4605,11 @@ class QuotaSource {
|
|
|
4053
4605
|
add(command, commandKind(command));
|
|
4054
4606
|
return candidates;
|
|
4055
4607
|
}
|
|
4056
|
-
const binDir =
|
|
4057
|
-
const installedBudgetProbe =
|
|
4608
|
+
const binDir = join5(this.homeDir, ".budget-guard/bin");
|
|
4609
|
+
const installedBudgetProbe = join5(binDir, "budget-probe");
|
|
4058
4610
|
if (existsSync5(installedBudgetProbe))
|
|
4059
4611
|
add(installedBudgetProbe, "budget-probe");
|
|
4060
|
-
const installedProbeMjs =
|
|
4612
|
+
const installedProbeMjs = join5(binDir, "probe.mjs");
|
|
4061
4613
|
if (existsSync5(installedProbeMjs))
|
|
4062
4614
|
add(installedProbeMjs, "probe-mjs");
|
|
4063
4615
|
return candidates;
|
|
@@ -4120,6 +4672,27 @@ function createQuotaSource(options) {
|
|
|
4120
4672
|
return new QuotaSource(options);
|
|
4121
4673
|
}
|
|
4122
4674
|
|
|
4675
|
+
// src/daemon-identity-ownership.ts
|
|
4676
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
4677
|
+
var defaultRead2 = (path) => readFileSync5(path, "utf-8");
|
|
4678
|
+
function pidFileOwnedByUs(pidFilePath, ourPid, read = defaultRead2) {
|
|
4679
|
+
let raw;
|
|
4680
|
+
try {
|
|
4681
|
+
raw = read(pidFilePath);
|
|
4682
|
+
} catch {
|
|
4683
|
+
return false;
|
|
4684
|
+
}
|
|
4685
|
+
const trimmed = raw.trim();
|
|
4686
|
+
if (trimmed.length === 0)
|
|
4687
|
+
return false;
|
|
4688
|
+
if (!/^[+-]?\d+$/.test(trimmed))
|
|
4689
|
+
return false;
|
|
4690
|
+
const pid = Number.parseInt(trimmed, 10);
|
|
4691
|
+
if (!Number.isFinite(pid))
|
|
4692
|
+
return false;
|
|
4693
|
+
return pid === ourPid;
|
|
4694
|
+
}
|
|
4695
|
+
|
|
4123
4696
|
// src/idempotency-tracker.ts
|
|
4124
4697
|
var DEFAULT_TOMBSTONE_TTL_MS = 20 * 60 * 1000;
|
|
4125
4698
|
|
|
@@ -4270,14 +4843,11 @@ class ReplyRequiredTracker {
|
|
|
4270
4843
|
// src/thread-state.ts
|
|
4271
4844
|
import {
|
|
4272
4845
|
existsSync as existsSync6,
|
|
4273
|
-
mkdirSync as mkdirSync4,
|
|
4274
4846
|
readdirSync,
|
|
4275
|
-
readFileSync as
|
|
4276
|
-
renameSync as renameSync2,
|
|
4277
|
-
writeFileSync as writeFileSync3
|
|
4847
|
+
readFileSync as readFileSync6
|
|
4278
4848
|
} from "fs";
|
|
4279
4849
|
import { homedir as homedir3 } from "os";
|
|
4280
|
-
import { basename as basename2,
|
|
4850
|
+
import { basename as basename2, join as join6 } from "path";
|
|
4281
4851
|
function nowIso() {
|
|
4282
4852
|
return new Date().toISOString();
|
|
4283
4853
|
}
|
|
@@ -4286,18 +4856,11 @@ function threadTag(identity) {
|
|
|
4286
4856
|
return `abg:${name}:${identity.cwd}`;
|
|
4287
4857
|
}
|
|
4288
4858
|
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);
|
|
4859
|
+
return env.CODEX_HOME && env.CODEX_HOME.length > 0 ? env.CODEX_HOME : join6(homedir3(), ".codex");
|
|
4297
4860
|
}
|
|
4298
4861
|
function readRawCurrentThread(stateDir) {
|
|
4299
4862
|
try {
|
|
4300
|
-
const parsed = JSON.parse(
|
|
4863
|
+
const parsed = JSON.parse(readFileSync6(stateDir.currentThreadFile, "utf-8"));
|
|
4301
4864
|
if (parsed?.version === 1 && typeof parsed.threadId === "string" && parsed.threadId.length > 0 && (parsed.status === "pending" || parsed.status === "current") && typeof parsed.cwd === "string") {
|
|
4302
4865
|
return parsed;
|
|
4303
4866
|
}
|
|
@@ -4305,7 +4868,7 @@ function readRawCurrentThread(stateDir) {
|
|
|
4305
4868
|
return null;
|
|
4306
4869
|
}
|
|
4307
4870
|
function findCodexRolloutFile(threadId, env = process.env, maxEntries = 20000) {
|
|
4308
|
-
const sessionsDir =
|
|
4871
|
+
const sessionsDir = join6(codexHome(env), "sessions");
|
|
4309
4872
|
if (!threadId || !existsSync6(sessionsDir))
|
|
4310
4873
|
return null;
|
|
4311
4874
|
const exactName = `rollout-${threadId}.jsonl`;
|
|
@@ -4321,7 +4884,7 @@ function findCodexRolloutFile(threadId, env = process.env, maxEntries = 20000) {
|
|
|
4321
4884
|
}
|
|
4322
4885
|
for (const entry of entries) {
|
|
4323
4886
|
visited++;
|
|
4324
|
-
const path =
|
|
4887
|
+
const path = join6(dir, entry.name);
|
|
4325
4888
|
if (entry.isDirectory()) {
|
|
4326
4889
|
stack.push(path);
|
|
4327
4890
|
continue;
|
|
@@ -4415,6 +4978,7 @@ function formatWaitingForCodexTuiMessage(options) {
|
|
|
4415
4978
|
// src/pair-registry.ts
|
|
4416
4979
|
var PAIR_BASE_PORT = 4500;
|
|
4417
4980
|
var PAIR_SLOT_STRIDE = 10;
|
|
4981
|
+
var RECLAIMABLE_MIN_AGE_MS = 24 * 60 * 60 * 1000;
|
|
4418
4982
|
var MAX_PAIR_SLOT = Math.floor((65535 - 2 - PAIR_BASE_PORT) / PAIR_SLOT_STRIDE);
|
|
4419
4983
|
|
|
4420
4984
|
// src/liveness-probe.ts
|
|
@@ -4445,10 +5009,55 @@ async function probeLiveness(target, options) {
|
|
|
4445
5009
|
return target.pongCount > baseline;
|
|
4446
5010
|
}
|
|
4447
5011
|
|
|
5012
|
+
// src/delivery-buffer.ts
|
|
5013
|
+
class BoundedMessageBuffer {
|
|
5014
|
+
messages = [];
|
|
5015
|
+
cap;
|
|
5016
|
+
overflowLabel;
|
|
5017
|
+
overflowNoun;
|
|
5018
|
+
log;
|
|
5019
|
+
constructor(options) {
|
|
5020
|
+
this.cap = options.cap;
|
|
5021
|
+
this.overflowLabel = options.overflowLabel;
|
|
5022
|
+
this.overflowNoun = options.overflowNoun ?? "message(s)";
|
|
5023
|
+
this.log = options.log;
|
|
5024
|
+
}
|
|
5025
|
+
get length() {
|
|
5026
|
+
return this.messages.length;
|
|
5027
|
+
}
|
|
5028
|
+
push(message) {
|
|
5029
|
+
this.messages.push(message);
|
|
5030
|
+
this.enforceCap();
|
|
5031
|
+
}
|
|
5032
|
+
unshiftMany(messages) {
|
|
5033
|
+
if (messages.length === 0)
|
|
5034
|
+
return;
|
|
5035
|
+
this.messages.unshift(...messages);
|
|
5036
|
+
this.enforceCap();
|
|
5037
|
+
}
|
|
5038
|
+
drainAll() {
|
|
5039
|
+
return this.messages.splice(0, this.messages.length);
|
|
5040
|
+
}
|
|
5041
|
+
clear() {
|
|
5042
|
+
this.messages.length = 0;
|
|
5043
|
+
}
|
|
5044
|
+
enforceCap() {
|
|
5045
|
+
if (this.messages.length > this.cap) {
|
|
5046
|
+
const dropped = this.messages.length - this.cap;
|
|
5047
|
+
this.messages.splice(0, dropped);
|
|
5048
|
+
this.log(`${this.overflowLabel}: dropped ${dropped} oldest ${this.overflowNoun}, ${this.cap} remaining`);
|
|
5049
|
+
}
|
|
5050
|
+
}
|
|
5051
|
+
}
|
|
5052
|
+
|
|
4448
5053
|
// src/daemon.ts
|
|
4449
5054
|
var stateDir = new StateDirResolver;
|
|
4450
5055
|
stateDir.ensure();
|
|
4451
5056
|
var processLogger = createProcessLogger({ component: "AgentBridgeDaemon", logFile: stateDir.logFile });
|
|
5057
|
+
var controlTokenPath = resolveControlTokenPath(stateDir.dir);
|
|
5058
|
+
var controlToken = generateControlToken();
|
|
5059
|
+
var weWroteToken = false;
|
|
5060
|
+
var weWrotePid = false;
|
|
4452
5061
|
var configService = new ConfigService;
|
|
4453
5062
|
var config = configService.loadOrDefault(processLogger.log);
|
|
4454
5063
|
var CODEX_APP_PORT = parseInt(process.env.CODEX_WS_PORT ?? String(config.codex.appPort), 10);
|
|
@@ -4465,12 +5074,16 @@ var CODEX_BOOT_RETRIES = parsePositiveIntEnv("AGENTBRIDGE_CODEX_BOOT_RETRIES", 2
|
|
|
4465
5074
|
var ALLOW_IDENTITYLESS_CLIENT = process.env.AGENTBRIDGE_COMPAT_IDENTITYLESS === "1";
|
|
4466
5075
|
var BUDGET_CONFIG = applyBudgetEnvOverrides(config.budget);
|
|
4467
5076
|
var daemonLifecycle = new DaemonLifecycle({ stateDir, controlPort: CONTROL_PORT, log });
|
|
5077
|
+
var DAEMON_NONCE = randomUUID4();
|
|
5078
|
+
var DAEMON_STARTED_AT = Date.now();
|
|
4468
5079
|
var codex = new CodexAdapter(CODEX_APP_PORT, CODEX_PROXY_PORT, stateDir.logFile);
|
|
4469
5080
|
var attachCmd = `codex --enable tui_app_server --remote ${codex.proxyUrl}`;
|
|
4470
5081
|
var controlServer = null;
|
|
5082
|
+
var boundControlPort = false;
|
|
4471
5083
|
var attachedClaude = null;
|
|
4472
5084
|
var nextControlClientId = 0;
|
|
4473
5085
|
var nextSystemMessageId = 0;
|
|
5086
|
+
var SYSTEM_MSG_SALT = randomUUID4().slice(0, 8);
|
|
4474
5087
|
var codexBootstrapped = false;
|
|
4475
5088
|
var attentionWindowTimer = null;
|
|
4476
5089
|
var inAttentionWindow = false;
|
|
@@ -4488,9 +5101,20 @@ var ATTACH_STATUS_COOLDOWN_MS = 30000;
|
|
|
4488
5101
|
var LIVENESS_PROBE_TIMEOUT_MS = parsePositiveIntEnv("AGENTBRIDGE_LIVENESS_PROBE_TIMEOUT_MS", 3000, log);
|
|
4489
5102
|
var LIVENESS_PROBE_POLL_MS = 50;
|
|
4490
5103
|
var challengeInProgress = false;
|
|
4491
|
-
var bufferedMessages =
|
|
5104
|
+
var bufferedMessages = new BoundedMessageBuffer({
|
|
5105
|
+
cap: MAX_BUFFERED_MESSAGES,
|
|
5106
|
+
overflowLabel: "Message buffer overflow",
|
|
5107
|
+
log
|
|
5108
|
+
});
|
|
5109
|
+
function createPendingBackpressureBuffer() {
|
|
5110
|
+
return new BoundedMessageBuffer({
|
|
5111
|
+
cap: MAX_BUFFERED_MESSAGES,
|
|
5112
|
+
overflowLabel: "Backpressure overflow",
|
|
5113
|
+
overflowNoun: "tracked message(s)",
|
|
5114
|
+
log
|
|
5115
|
+
});
|
|
5116
|
+
}
|
|
4492
5117
|
var budgetCoordinator = null;
|
|
4493
|
-
var budgetStatusTimer = null;
|
|
4494
5118
|
function ensureBudgetCoordinatorStarted() {
|
|
4495
5119
|
if (!BUDGET_CONFIG.enabled)
|
|
4496
5120
|
return;
|
|
@@ -4501,27 +5125,18 @@ function ensureBudgetCoordinatorStarted() {
|
|
|
4501
5125
|
config: BUDGET_CONFIG,
|
|
4502
5126
|
emit: (id, content) => {
|
|
4503
5127
|
emitToClaude(systemMessage(id, content));
|
|
4504
|
-
queueMicrotask(() => broadcastStatus());
|
|
4505
5128
|
},
|
|
4506
5129
|
onPauseChange: (paused) => {
|
|
4507
5130
|
log(`Budget intervention ${paused ? "ACTIVE" : "CLEARED"} ` + `(gate ${budgetCoordinator?.isGateClosed() ? "CLOSED" : "OPEN"})`);
|
|
4508
|
-
queueMicrotask(() => broadcastStatus());
|
|
4509
5131
|
},
|
|
5132
|
+
onSnapshot: () => broadcastStatus(),
|
|
4510
5133
|
log
|
|
4511
5134
|
});
|
|
4512
5135
|
}
|
|
4513
5136
|
budgetCoordinator.start();
|
|
4514
|
-
if (!budgetStatusTimer) {
|
|
4515
|
-
budgetStatusTimer = setInterval(() => broadcastStatus(), BUDGET_CONFIG.pollSeconds * 1000);
|
|
4516
|
-
budgetStatusTimer.unref?.();
|
|
4517
|
-
}
|
|
4518
5137
|
}
|
|
4519
5138
|
function stopBudgetCoordinator() {
|
|
4520
5139
|
budgetCoordinator?.stop();
|
|
4521
|
-
if (budgetStatusTimer) {
|
|
4522
|
-
clearInterval(budgetStatusTimer);
|
|
4523
|
-
budgetStatusTimer = null;
|
|
4524
|
-
}
|
|
4525
5140
|
}
|
|
4526
5141
|
function budgetPauseGateError() {
|
|
4527
5142
|
const snapshot = budgetCoordinator?.getSnapshot() ?? null;
|
|
@@ -4625,29 +5240,22 @@ codex.on("turnStarted", () => {
|
|
|
4625
5240
|
codex.on("agentMessage", (msg) => {
|
|
4626
5241
|
if (msg.source !== "codex")
|
|
4627
5242
|
return;
|
|
4628
|
-
const
|
|
4629
|
-
|
|
4630
|
-
|
|
5243
|
+
const route = routeCodexMessage(msg.content, {
|
|
5244
|
+
mode: FILTER_MODE,
|
|
5245
|
+
replyArmed: replyTracker.isArmed,
|
|
5246
|
+
inAttentionWindow
|
|
5247
|
+
});
|
|
5248
|
+
log(`Codex \u2192 Claude [${route.marker}/${route.reason}] (${msg.content.length} chars)`);
|
|
5249
|
+
if (route.noteReplyForwarded) {
|
|
4631
5250
|
replyTracker.noteForwarded();
|
|
4632
|
-
if (statusBuffer.size > 0) {
|
|
4633
|
-
statusBuffer.flush("reply-required message arrived");
|
|
4634
|
-
}
|
|
4635
|
-
emitToClaude(msg);
|
|
4636
|
-
return;
|
|
4637
5251
|
}
|
|
4638
|
-
if (
|
|
4639
|
-
|
|
4640
|
-
statusBuffer.add(msg);
|
|
4641
|
-
return;
|
|
5252
|
+
if (route.flushStatusBuffer) {
|
|
5253
|
+
statusBuffer.flush(route.noteReplyForwarded ? "reply-required message arrived" : "important message arrived");
|
|
4642
5254
|
}
|
|
4643
|
-
|
|
4644
|
-
switch (result.action) {
|
|
5255
|
+
switch (route.action) {
|
|
4645
5256
|
case "forward":
|
|
4646
|
-
if (result.marker === "important" && statusBuffer.size > 0) {
|
|
4647
|
-
statusBuffer.flush("important message arrived");
|
|
4648
|
-
}
|
|
4649
5257
|
emitToClaude(msg);
|
|
4650
|
-
if (
|
|
5258
|
+
if (route.startAttentionWindow) {
|
|
4651
5259
|
startAttentionWindow();
|
|
4652
5260
|
}
|
|
4653
5261
|
break;
|
|
@@ -4722,6 +5330,7 @@ codex.on("error", (err) => {
|
|
|
4722
5330
|
});
|
|
4723
5331
|
codex.on("exit", (code) => {
|
|
4724
5332
|
log(`Codex process exited (code ${code})`);
|
|
5333
|
+
const wasBootstrapped = codexBootstrapped;
|
|
4725
5334
|
codexBootstrapped = false;
|
|
4726
5335
|
replyTracker.reset();
|
|
4727
5336
|
idempotencyTracker.terminateAll("aborted");
|
|
@@ -4730,65 +5339,77 @@ codex.on("exit", (code) => {
|
|
|
4730
5339
|
statusBuffer.flush("codex exited");
|
|
4731
5340
|
tuiConnectionState.handleCodexExit();
|
|
4732
5341
|
clearPendingClaudeDisconnect("Codex process exited");
|
|
4733
|
-
|
|
5342
|
+
if (wasBootstrapped) {
|
|
5343
|
+
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.`));
|
|
5344
|
+
}
|
|
4734
5345
|
broadcastStatus();
|
|
4735
|
-
|
|
5346
|
+
if (wasBootstrapped) {
|
|
5347
|
+
armBootDeadline();
|
|
5348
|
+
}
|
|
4736
5349
|
});
|
|
4737
5350
|
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();
|
|
5351
|
+
let server;
|
|
5352
|
+
try {
|
|
5353
|
+
server = Bun.serve({
|
|
5354
|
+
port: CONTROL_PORT,
|
|
5355
|
+
hostname: "127.0.0.1",
|
|
5356
|
+
fetch(req, server2) {
|
|
5357
|
+
const url = new URL(req.url);
|
|
5358
|
+
if (url.pathname === "/healthz") {
|
|
5359
|
+
return Response.json(currentStatus());
|
|
4753
5360
|
}
|
|
4754
|
-
if (
|
|
4755
|
-
return;
|
|
5361
|
+
if (url.pathname === "/readyz") {
|
|
5362
|
+
return Response.json(currentStatus(), { status: codexBootstrapped ? 200 : 503 });
|
|
4756
5363
|
}
|
|
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");
|
|
5364
|
+
if (url.pathname === "/ws") {
|
|
5365
|
+
if (!isAllowedWsUpgrade(req)) {
|
|
5366
|
+
log("Rejected WS upgrade on control port: Origin header present (possible CSWSH)");
|
|
5367
|
+
return wsOriginRejectedResponse();
|
|
5368
|
+
}
|
|
5369
|
+
if (server2.upgrade(req, { data: { clientId: 0, attached: false, lastPongAt: Date.now(), pongCount: 0, pendingBackpressure: createPendingBackpressureBuffer() } })) {
|
|
5370
|
+
return;
|
|
5371
|
+
}
|
|
4773
5372
|
}
|
|
5373
|
+
return new Response("AgentBridge daemon");
|
|
4774
5374
|
},
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
5375
|
+
websocket: {
|
|
5376
|
+
idleTimeout: 960,
|
|
5377
|
+
sendPings: true,
|
|
5378
|
+
open: (ws) => {
|
|
5379
|
+
ws.data.clientId = ++nextControlClientId;
|
|
5380
|
+
ws.data.lastPongAt = Date.now();
|
|
5381
|
+
ws.data.pendingBackpressure = createPendingBackpressureBuffer();
|
|
5382
|
+
log(`Frontend socket opened (#${ws.data.clientId})`);
|
|
5383
|
+
},
|
|
5384
|
+
close: (ws, code, reason) => {
|
|
5385
|
+
log(`Frontend socket closed (#${ws.data.clientId}, code=${code}, reason=${reason || "none"}, wasAttached=${attachedClaude === ws})`);
|
|
5386
|
+
if (attachedClaude === ws) {
|
|
5387
|
+
detachClaude(ws, "frontend socket closed");
|
|
5388
|
+
}
|
|
5389
|
+
},
|
|
5390
|
+
message: (ws, raw) => {
|
|
5391
|
+
handleControlMessage(ws, raw);
|
|
5392
|
+
},
|
|
5393
|
+
pong: (ws) => {
|
|
5394
|
+
ws.data.lastPongAt = Date.now();
|
|
5395
|
+
ws.data.pongCount++;
|
|
5396
|
+
},
|
|
5397
|
+
drain: (ws) => {
|
|
5398
|
+
if (ws.data.pendingBackpressure.length > 0 && ws.getBufferedAmount() === 0) {
|
|
5399
|
+
ws.data.pendingBackpressure.clear();
|
|
5400
|
+
}
|
|
5401
|
+
if (ws === attachedClaude && bufferedMessages.length > 0) {
|
|
5402
|
+
flushBufferedMessages(ws);
|
|
5403
|
+
}
|
|
4788
5404
|
}
|
|
4789
5405
|
}
|
|
4790
|
-
}
|
|
4791
|
-
})
|
|
5406
|
+
});
|
|
5407
|
+
} catch (err) {
|
|
5408
|
+
log(`Control port ${CONTROL_PORT} bind failed (${err?.code ?? err?.message ?? err}) \u2014 ` + `another daemon owns it; exiting without touching shared identity files`);
|
|
5409
|
+
process.exit(0);
|
|
5410
|
+
}
|
|
5411
|
+
controlServer = server;
|
|
5412
|
+
boundControlPort = true;
|
|
4792
5413
|
}
|
|
4793
5414
|
function handleControlMessage(ws, raw) {
|
|
4794
5415
|
let message;
|
|
@@ -4805,7 +5426,9 @@ function handleControlMessage(ws, raw) {
|
|
|
4805
5426
|
expectedPairId: process.env.AGENTBRIDGE_PAIR_ID ?? null,
|
|
4806
5427
|
daemonCwd: process.cwd(),
|
|
4807
5428
|
identity: message.identity,
|
|
4808
|
-
allowIdentityless: ALLOW_IDENTITYLESS_CLIENT
|
|
5429
|
+
allowIdentityless: ALLOW_IDENTITYLESS_CLIENT,
|
|
5430
|
+
expectedControlToken: controlToken,
|
|
5431
|
+
expectedContractVersion: BUILD_INFO.contractVersion
|
|
4809
5432
|
});
|
|
4810
5433
|
if (!admission.ok) {
|
|
4811
5434
|
log(`Rejecting Claude frontend #${ws.data.clientId}: ${admission.reason}`);
|
|
@@ -4884,6 +5507,16 @@ function waitForInterruptOutcome(turnIds) {
|
|
|
4884
5507
|
});
|
|
4885
5508
|
}
|
|
4886
5509
|
async function handleClaudeToCodex(ws, message) {
|
|
5510
|
+
const attachGuard = evaluateInjectionAttachGuard(attachedClaude, ws);
|
|
5511
|
+
if (!attachGuard.allowed) {
|
|
5512
|
+
log(`Rejecting claude_to_codex from non-attached socket #${ws.data.clientId} ` + `(request ${message.requestId}, attached=${attachedClaude ? "#" + attachedClaude.data.clientId : "none"})`);
|
|
5513
|
+
sendClaudeToCodexResult(ws, message.requestId, {
|
|
5514
|
+
success: false,
|
|
5515
|
+
code: attachGuard.code,
|
|
5516
|
+
error: attachGuard.reason
|
|
5517
|
+
});
|
|
5518
|
+
return;
|
|
5519
|
+
}
|
|
4887
5520
|
if (message.message.source !== "claude") {
|
|
4888
5521
|
sendClaudeToCodexResult(ws, message.requestId, {
|
|
4889
5522
|
success: false,
|
|
@@ -4916,8 +5549,8 @@ async function handleClaudeToCodex(ws, message) {
|
|
|
4916
5549
|
if (budgetCoordinator?.isGateClosed()) {
|
|
4917
5550
|
const reason = budgetPauseGateError();
|
|
4918
5551
|
log(`Injection rejected by budget pause gate`);
|
|
4919
|
-
const
|
|
4920
|
-
const retryAfterMs =
|
|
5552
|
+
const resumeAfterEpoch3 = budgetCoordinator?.getSnapshot()?.resumeAfterEpoch ?? null;
|
|
5553
|
+
const retryAfterMs = resumeAfterEpoch3 !== null ? Math.max(0, resumeAfterEpoch3 * 1000 - Date.now()) : undefined;
|
|
4921
5554
|
sendClaudeToCodexResult(ws, message.requestId, {
|
|
4922
5555
|
success: false,
|
|
4923
5556
|
code: "budget_paused",
|
|
@@ -4999,6 +5632,17 @@ async function handleClaudeToCodex(ws, message) {
|
|
|
4999
5632
|
return;
|
|
5000
5633
|
}
|
|
5001
5634
|
log("Interrupt reached terminal boundary \u2014 injecting the message as a new turn");
|
|
5635
|
+
const postWaitAttachGuard = evaluateInjectionAttachGuard(attachedClaude, ws);
|
|
5636
|
+
if (!postWaitAttachGuard.allowed) {
|
|
5637
|
+
releaseInterruptKey();
|
|
5638
|
+
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"})`);
|
|
5639
|
+
sendClaudeToCodexResult(ws, message.requestId, {
|
|
5640
|
+
success: false,
|
|
5641
|
+
code: "not_attached",
|
|
5642
|
+
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."
|
|
5643
|
+
});
|
|
5644
|
+
return;
|
|
5645
|
+
}
|
|
5002
5646
|
if (interruptThreadId && codex.activeThreadId !== interruptThreadId) {
|
|
5003
5647
|
releaseInterruptKey();
|
|
5004
5648
|
}
|
|
@@ -5106,14 +5750,9 @@ function detachClaude(ws, reason) {
|
|
|
5106
5750
|
ws.data.attached = false;
|
|
5107
5751
|
log(`Claude frontend detached (#${ws.data.clientId}, ${reason})`);
|
|
5108
5752
|
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
|
-
}
|
|
5753
|
+
const reBuffered = ws.data.pendingBackpressure.drainAll();
|
|
5754
|
+
log(`Re-buffered ${reBuffered.length} backpressured message(s) for redelivery on reconnect`);
|
|
5755
|
+
bufferedMessages.unshiftMany(reBuffered);
|
|
5117
5756
|
}
|
|
5118
5757
|
scheduleClaudeDisconnectNotification(ws.data.clientId);
|
|
5119
5758
|
scheduleIdleShutdown();
|
|
@@ -5236,11 +5875,6 @@ function emitToClaude(message) {
|
|
|
5236
5875
|
log("Send to Claude failed, buffering message for retry on reconnect");
|
|
5237
5876
|
}
|
|
5238
5877
|
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
5878
|
}
|
|
5245
5879
|
function trySendBridgeMessage(ws, message) {
|
|
5246
5880
|
try {
|
|
@@ -5251,11 +5885,6 @@ function trySendBridgeMessage(ws, message) {
|
|
|
5251
5885
|
}
|
|
5252
5886
|
if (typeof result === "number" && result === -1) {
|
|
5253
5887
|
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
5888
|
}
|
|
5260
5889
|
return true;
|
|
5261
5890
|
} catch (err) {
|
|
@@ -5264,11 +5893,11 @@ function trySendBridgeMessage(ws, message) {
|
|
|
5264
5893
|
}
|
|
5265
5894
|
}
|
|
5266
5895
|
function flushBufferedMessages(ws) {
|
|
5267
|
-
const messages = bufferedMessages.
|
|
5896
|
+
const messages = bufferedMessages.drainAll();
|
|
5268
5897
|
for (let i = 0;i < messages.length; i++) {
|
|
5269
5898
|
if (!trySendBridgeMessage(ws, messages[i])) {
|
|
5270
5899
|
const remaining = messages.slice(i);
|
|
5271
|
-
bufferedMessages.
|
|
5900
|
+
bufferedMessages.unshiftMany(remaining);
|
|
5272
5901
|
log(`Flush interrupted: re-buffered ${remaining.length} message(s) after send failure`);
|
|
5273
5902
|
return;
|
|
5274
5903
|
}
|
|
@@ -5312,7 +5941,8 @@ function currentStatus() {
|
|
|
5312
5941
|
budget: budgetCoordinator?.getSnapshot() ?? undefined,
|
|
5313
5942
|
turnInProgress: codex.turnInProgress,
|
|
5314
5943
|
turnPhase: codex.turnPhase,
|
|
5315
|
-
attentionWindowActive: inAttentionWindow
|
|
5944
|
+
attentionWindowActive: inAttentionWindow,
|
|
5945
|
+
appServerInfo: codex.capturedAppServerInfo
|
|
5316
5946
|
};
|
|
5317
5947
|
}
|
|
5318
5948
|
function currentWaitingMessage() {
|
|
@@ -5333,7 +5963,7 @@ function currentReadyMessage() {
|
|
|
5333
5963
|
}
|
|
5334
5964
|
function systemMessage(idPrefix, content) {
|
|
5335
5965
|
return {
|
|
5336
|
-
id: `${idPrefix}_${++nextSystemMessageId}`,
|
|
5966
|
+
id: `${idPrefix}_${SYSTEM_MSG_SALT}_${++nextSystemMessageId}`,
|
|
5337
5967
|
source: "codex",
|
|
5338
5968
|
content,
|
|
5339
5969
|
timestamp: Date.now()
|
|
@@ -5341,9 +5971,47 @@ function systemMessage(idPrefix, content) {
|
|
|
5341
5971
|
}
|
|
5342
5972
|
function writePidFile() {
|
|
5343
5973
|
daemonLifecycle.writePid();
|
|
5974
|
+
daemonLifecycle.writeDaemonRecord(buildDaemonRecord("booting"));
|
|
5975
|
+
weWrotePid = true;
|
|
5976
|
+
}
|
|
5977
|
+
function writeControlTokenPostBind() {
|
|
5978
|
+
if (controlToken === null)
|
|
5979
|
+
return;
|
|
5980
|
+
try {
|
|
5981
|
+
writeControlToken(controlTokenPath, controlToken);
|
|
5982
|
+
weWroteToken = true;
|
|
5983
|
+
} catch (err) {
|
|
5984
|
+
controlToken = null;
|
|
5985
|
+
processLogger.log(`Failed to write control token (${controlTokenPath}): ${err?.message ?? err} \u2014 ` + `token layer DISABLED for this daemon (attach guard + Origin guard still active)`);
|
|
5986
|
+
}
|
|
5344
5987
|
}
|
|
5345
5988
|
function removePidFile() {
|
|
5989
|
+
if (!weWrotePid || !pidFileOwnedByUs(stateDir.pidFile, process.pid))
|
|
5990
|
+
return;
|
|
5346
5991
|
daemonLifecycle.removePidFile();
|
|
5992
|
+
daemonLifecycle.removeDaemonRecord();
|
|
5993
|
+
}
|
|
5994
|
+
function buildDaemonRecord(phase) {
|
|
5995
|
+
return {
|
|
5996
|
+
pid: process.pid,
|
|
5997
|
+
phase,
|
|
5998
|
+
startedAt: DAEMON_STARTED_AT,
|
|
5999
|
+
nonce: DAEMON_NONCE,
|
|
6000
|
+
pairId: process.env.AGENTBRIDGE_PAIR_ID ?? null,
|
|
6001
|
+
cwd: process.cwd(),
|
|
6002
|
+
stateDir: stateDir.dir,
|
|
6003
|
+
proxyUrl: codex.proxyUrl,
|
|
6004
|
+
appServerUrl: codex.appServerUrl,
|
|
6005
|
+
ports: {
|
|
6006
|
+
appPort: portFromUrl(codex.appServerUrl) ?? CODEX_APP_PORT,
|
|
6007
|
+
proxyPort: portFromUrl(codex.proxyUrl) ?? CODEX_PROXY_PORT,
|
|
6008
|
+
controlPort: CONTROL_PORT
|
|
6009
|
+
},
|
|
6010
|
+
build: daemonStatusBuildInfo(),
|
|
6011
|
+
turnInProgress: codex.turnInProgress,
|
|
6012
|
+
turnPhase: codex.turnPhase,
|
|
6013
|
+
attentionWindowActive: inAttentionWindow
|
|
6014
|
+
};
|
|
5347
6015
|
}
|
|
5348
6016
|
function writeStatusFile() {
|
|
5349
6017
|
daemonLifecycle.writeStatus({
|
|
@@ -5357,11 +6025,16 @@ function writeStatusFile() {
|
|
|
5357
6025
|
build: daemonStatusBuildInfo(),
|
|
5358
6026
|
turnInProgress: codex.turnInProgress,
|
|
5359
6027
|
turnPhase: codex.turnPhase,
|
|
5360
|
-
attentionWindowActive: inAttentionWindow
|
|
6028
|
+
attentionWindowActive: inAttentionWindow,
|
|
6029
|
+
appServerInfo: codex.capturedAppServerInfo
|
|
5361
6030
|
});
|
|
6031
|
+
daemonLifecycle.writeDaemonRecord(buildDaemonRecord("ready"));
|
|
5362
6032
|
}
|
|
5363
6033
|
function removeStatusFile() {
|
|
6034
|
+
if (!boundControlPort)
|
|
6035
|
+
return;
|
|
5364
6036
|
daemonLifecycle.removeStatusFile();
|
|
6037
|
+
daemonLifecycle.removeDaemonRecord();
|
|
5365
6038
|
}
|
|
5366
6039
|
function armBootDeadline() {
|
|
5367
6040
|
if (bootDeadlineTimer)
|
|
@@ -5434,20 +6107,41 @@ function shutdown(reason, exitCode = 0) {
|
|
|
5434
6107
|
codex.stop();
|
|
5435
6108
|
removePidFile();
|
|
5436
6109
|
removeStatusFile();
|
|
6110
|
+
removeControlToken();
|
|
5437
6111
|
process.exit(exitCode);
|
|
5438
6112
|
}
|
|
6113
|
+
function removeControlToken() {
|
|
6114
|
+
if (!weWroteToken)
|
|
6115
|
+
return;
|
|
6116
|
+
try {
|
|
6117
|
+
rmSync2(controlTokenPath, { force: true });
|
|
6118
|
+
} catch {}
|
|
6119
|
+
}
|
|
5439
6120
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
5440
6121
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
5441
6122
|
process.on("exit", () => {
|
|
5442
6123
|
codex.forceKillAppServerSync();
|
|
5443
6124
|
removePidFile();
|
|
5444
6125
|
removeStatusFile();
|
|
6126
|
+
removeControlToken();
|
|
5445
6127
|
});
|
|
5446
6128
|
process.on("uncaughtException", (err) => {
|
|
5447
|
-
processLogger.fatal("UNCAUGHT EXCEPTION", err);
|
|
6129
|
+
processLogger.fatal("UNCAUGHT EXCEPTION \u2014 auto-shutting down daemon", err);
|
|
6130
|
+
try {
|
|
6131
|
+
shutdown("uncaught exception", 1);
|
|
6132
|
+
} catch (shutdownErr) {
|
|
6133
|
+
processLogger.fatal("shutdown during uncaughtException failed", shutdownErr);
|
|
6134
|
+
}
|
|
6135
|
+
process.exit(1);
|
|
5448
6136
|
});
|
|
5449
6137
|
process.on("unhandledRejection", (reason) => {
|
|
5450
|
-
processLogger.fatal("UNHANDLED REJECTION", reason);
|
|
6138
|
+
processLogger.fatal("UNHANDLED REJECTION \u2014 auto-shutting down daemon", reason);
|
|
6139
|
+
try {
|
|
6140
|
+
shutdown("unhandled rejection", 1);
|
|
6141
|
+
} catch (shutdownErr) {
|
|
6142
|
+
processLogger.fatal("shutdown during unhandledRejection failed", shutdownErr);
|
|
6143
|
+
}
|
|
6144
|
+
process.exit(1);
|
|
5451
6145
|
});
|
|
5452
6146
|
function log(msg) {
|
|
5453
6147
|
processLogger.log(msg);
|
|
@@ -5456,7 +6150,8 @@ if (daemonLifecycle.wasKilled()) {
|
|
|
5456
6150
|
log("Killed sentinel found \u2014 daemon was intentionally stopped. Exiting immediately.");
|
|
5457
6151
|
process.exit(0);
|
|
5458
6152
|
}
|
|
5459
|
-
writePidFile();
|
|
5460
6153
|
startControlServer();
|
|
6154
|
+
writePidFile();
|
|
6155
|
+
writeControlTokenPostBind();
|
|
5461
6156
|
armBootDeadline();
|
|
5462
6157
|
bootCodex();
|