@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/dist/daemon.js CHANGED
@@ -1,10 +1,19 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
3
 
4
+ // src/daemon.ts
5
+ import { rmSync as rmSync2 } from "fs";
6
+ import { randomUUID as randomUUID4 } from "crypto";
7
+
4
8
  // src/contract-version.ts
5
9
  var CONTRACT_VERSION = 1;
6
10
 
7
11
  // src/build-info.ts
12
+ var CODE_HASH_SENTINEL = "source";
13
+ function hasValidCodeHash(build) {
14
+ const hash = build?.codeHash;
15
+ return typeof hash === "string" && hash.length > 0 && hash !== CODE_HASH_SENTINEL;
16
+ }
8
17
  function defineString(value, fallback) {
9
18
  return typeof value === "string" && value.length > 0 ? value : fallback;
10
19
  }
@@ -17,10 +26,11 @@ function defineNumber(value, fallback) {
17
26
  return typeof value === "number" && Number.isFinite(value) ? value : fallback;
18
27
  }
19
28
  var BUILD_INFO = Object.freeze({
20
- version: defineString("0.1.12", "0.0.0-source"),
21
- commit: defineString("eec6018", "source"),
29
+ version: defineString("0.1.13", "0.0.0-source"),
30
+ commit: defineString("7a71869", "source"),
22
31
  bundle: defineBundle("dist"),
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
- return a.version === b.version && a.commit === b.commit && a.contractVersion === b.contractVersion;
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
- return `${build.version}/${build.commit}/${build.bundle}/contract-v${build.contractVersion}`;
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
- const xdgState = process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state");
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
- mkdirSync(this.stateDir, { recursive: true });
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 portsFile() {
88
- return join(this.stateDir, "ports.json");
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(dirname(path)))
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 mkdirSync2, rmSync, chmodSync } from "fs";
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
- mkdirSync2(dir, { recursive: true, mode: 448 });
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 Map;
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
- this.resolveTransport();
741
- const listen = codexListenArg(this.transport, this.appPort, this.socketPath ?? "");
742
- if (this.transport === "unix" && this.socketPath) {
743
- ensureSocketDir(this.socketPath);
744
- removeSocketFile(this.socketPath);
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
- this.log(`Spawning codex app-server (transport=${this.transport}) --listen ${listen}`);
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
- if (this.transport === "unix" && this.socketPath) {
761
- await waitForUnixWsReady(this.socketPath);
762
- this.relay = new TcpToUnixRelay("127.0.0.1", this.appPort, this.socketPath, (m) => this.log(`[relay] ${m}`));
763
- await this.relay.start();
764
- this.log(`Transport relay ready: ws://127.0.0.1:${this.appPort} \u2192 unix://${this.socketPath}`);
765
- } else {
766
- await this.waitForHealthy();
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
- await this.connectToAppServer();
769
- this.startProxy();
770
- this.log(`Proxy ready on ${this.proxyUrl}`);
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.proxyServer?.stop();
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
- return new Promise((resolve, reject) => {
1229
- const timer = setTimeout(() => {
1230
- this.replayPending.delete(id);
1231
- reject(new Error(`replay timeout (${CodexAdapter.SESSION_REPLAY_TIMEOUT_MS}ms) for ${method} id=${JSON.stringify(id)}`));
1232
- }, CodexAdapter.SESSION_REPLAY_TIMEOUT_MS);
1233
- this.replayPending.set(id, { method, resolve, reject, timer });
1234
- try {
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 pending = this.replayPending.get(id);
1249
- if (!pending)
1552
+ const key = id;
1553
+ if (!this.replayPending.has(key))
1250
1554
  return false;
1251
- clearTimeout(pending.timer);
1252
- this.replayPending.delete(id);
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
- pending.reject(new Error(`${pending.method} rejected: ${errMsg}`));
1559
+ this.replayPending.reject(key, new Error(`${method} rejected: ${errMsg}`));
1256
1560
  } else {
1257
- pending.resolve(payload);
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
- if (errMsg.includes("rate limits") || errMsg.includes("rateLimits")) {
1758
- this.log(`Patching rateLimits error \u2192 mock success (id: ${parsed.id})`);
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 (!input.expectedPairId)
2198
- return { ok: true };
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_${Date.now()}`,
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 unlinkSync2, writeFileSync, openSync, closeSync, constants } from "fs";
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 ${formatBuildInfo(buildInfo)}`
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(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
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(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
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(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
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 = 40, delayMs = 250) {
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 = 40, delayMs = 250) {
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 = readFileSync(this.stateDir.statusFile, "utf-8");
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.ensure();
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 = readFileSync(this.stateDir.pidFile, "utf-8").trim();
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.ensure();
2681
- writeFileSync(this.stateDir.pidFile, `${pid ?? process.pid}
2682
- `, "utf-8");
3193
+ atomicWriteText(this.stateDir.pidFile, `${pid ?? process.pid}
3194
+ `);
2683
3195
  }
2684
3196
  removePidFile() {
2685
3197
  try {
2686
- unlinkSync2(this.stateDir.pidFile);
3198
+ unlinkSync3(this.stateDir.pidFile);
2687
3199
  } catch {}
2688
3200
  }
2689
3201
  removeStatusFile() {
2690
3202
  try {
2691
- unlinkSync2(this.stateDir.statusFile);
3203
+ unlinkSync3(this.stateDir.statusFile);
2692
3204
  } catch {}
2693
3205
  }
2694
3206
  markKilled() {
2695
3207
  this.stateDir.ensure();
2696
- writeFileSync(this.stateDir.killedFile, `${Date.now()}
3208
+ writeFileSync2(this.stateDir.killedFile, `${Date.now()}
2697
3209
  `, "utf-8");
2698
3210
  }
2699
3211
  clearKilled() {
2700
3212
  try {
2701
- unlinkSync2(this.stateDir.killedFile);
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 pid file");
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(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
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 = openSync(this.stateDir.lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
2769
- writeFileSync(fd, `${process.pid}
3282
+ fd = openSync2(this.stateDir.lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
3283
+ writeFileSync2(fd, `${process.pid}
2770
3284
  `);
2771
- closeSync(fd);
3285
+ closeSync2(fd);
2772
3286
  return true;
2773
3287
  } catch (err) {
2774
3288
  if (fd !== null && err.code !== "EEXIST") {
2775
3289
  try {
2776
- closeSync(fd);
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(readFileSync(this.stateDir.lockFile, "utf-8").trim(), 10);
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
- unlinkSync2(this.stateDir.lockFile);
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 readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
2873
- import { join as join3 } from "path";
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: normalizeInteger(codex.appPort ?? daemon.port, DEFAULT_CONFIG.codex.appPort),
3045
- proxyPort: normalizeInteger(codex.proxyPort ?? daemon.proxyPort, DEFAULT_CONFIG.codex.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: normalizeInteger(turnCoordination.attentionWindowSeconds, DEFAULT_CONFIG.turnCoordination.attentionWindowSeconds)
3563
+ attentionWindowSeconds: normalizeBoundedInteger(turnCoordination.attentionWindowSeconds, DEFAULT_CONFIG.turnCoordination.attentionWindowSeconds, 0, Number.MAX_SAFE_INTEGER)
3049
3564
  },
3050
- idleShutdownSeconds: normalizeInteger(config.idleShutdownSeconds, DEFAULT_CONFIG.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 = join3(root, CONFIG_DIR);
3061
- this.configPath = join3(this.configDir, CONFIG_FILE);
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 = readFileSync2(this.configPath, "utf-8");
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.ensureConfigDir();
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
- mkdirSync3(this.configDir, { recursive: true });
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-coordinator.ts
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 AGENT_LABEL2 = {
4059
+ var AGENT_LABEL3 = {
3390
4060
  claude: "Claude",
3391
4061
  codex: "Codex"
3392
4062
  };
3393
- function pct2(value) {
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 `${AGENT_LABEL2[agent]} \u672A\u77E5`;
3399
- return `${AGENT_LABEL2[agent]} gate=${pct2(usage.gateUtil)} warn=${pct2(usage.warnUtil)}`;
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
- activeSides = new Set;
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.activeSides.size > 0;
4165
+ return this.fpState.side !== null;
3507
4166
  }
3508
4167
  isGateClosed() {
3509
- return this.activeSides.has("codex");
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.latestSnapshot = null;
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.latestSnapshot = this.toSnapshot(state);
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 previousSide = this.pauseSide();
3576
- this.updateActiveSides(state);
3577
- const currentSide = this.pauseSide();
3578
- if (currentSide) {
3579
- this.pauseReason = this.interventionReason(state);
3580
- const nextResumeAfterEpoch = this.resumeAfterEpoch(state);
3581
- this.pauseResumeAfterEpoch = previousSide === currentSide ? nextResumeAfterEpoch ?? this.pauseResumeAfterEpoch : nextResumeAfterEpoch;
3582
- const fingerprint2 = previousSide === currentSide && this.activeSideProbeUncertain(state) && this.lastDirectiveFingerprint ? this.lastDirectiveFingerprint : this.directiveFingerprint(state, currentSide);
3583
- if (!previousSide) {
3584
- this.onPauseChange(true);
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
- if (!previousSide || previousSide !== currentSide || fingerprint2 !== this.lastDirectiveFingerprint) {
3587
- this.emitDirective(this.interventionPrefix(currentSide), this.interventionDirective(state, currentSide));
4250
+ case "exit": {
4251
+ this.onPauseChange(false);
4252
+ this.emitDirective(this.recoveryPrefix(effect.previousSide), this.recoveryDirective(state, effect.previousSide));
4253
+ return;
3588
4254
  }
3589
- this.lastDirectiveFingerprint = fingerprint2;
3590
- return;
3591
- }
3592
- if (previousSide) {
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, this.pauseReason ?? "\u9884\u7B97\u63A5\u8FD1\u8017\u5C3D", this.pauseResumeAfterEpoch, this.config);
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 ${pct2(this.config.resumeBelow)}\uFF0C\u4E14\u6CA1\u6709\u6709\u6548 rate_limit\u3002`,
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 ${pct2(this.config.resumeBelow)}\uFF0C\u4E14\u6CA1\u6709\u6709\u6548 rate_limit\u3002`,
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 ${pct2(this.config.resumeBelow)}\uFF0C\u4E14\u6CA1\u6709\u6709\u6548 rate_limit\u3002`,
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.pauseSide(),
3795
- pauseReason: paused ? this.pauseReason ?? state.pause.reason : null,
3796
- resumeAfterEpoch: paused ? this.pauseResumeAfterEpoch ?? state.pause.resumeAfterEpoch : null,
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 join4 } from "path";
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 = join4(this.homeDir, ".budget-guard/bin");
4057
- const installedBudgetProbe = join4(binDir, "budget-probe");
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 = join4(binDir, "probe.mjs");
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 readFileSync3,
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, dirname as dirname2, join as join5 } from "path";
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 : join5(homedir3(), ".codex");
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(readFileSync3(stateDir.currentThreadFile, "utf-8"));
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 = join5(codexHome(env), "sessions");
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 = join5(dir, entry.name);
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 result = classifyMessage(msg.content, FILTER_MODE);
4629
- if (replyTracker.isArmed) {
4630
- log(`Codex \u2192 Claude [${result.marker}/force-forward-reply-required] (${msg.content.length} chars)`);
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 (inAttentionWindow && result.marker === "status") {
4639
- log(`Codex \u2192 Claude [${result.marker}/buffer-attention] (${msg.content.length} chars)`);
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
- log(`Codex \u2192 Claude [${result.marker}/${result.action}] (${msg.content.length} chars)`);
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 (result.marker === "important") {
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
- 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.`));
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
- armBootDeadline();
5346
+ if (wasBootstrapped) {
5347
+ armBootDeadline();
5348
+ }
4736
5349
  });
4737
5350
  function startControlServer() {
4738
- controlServer = Bun.serve({
4739
- port: CONTROL_PORT,
4740
- hostname: "127.0.0.1",
4741
- fetch(req, server) {
4742
- const url = new URL(req.url);
4743
- if (url.pathname === "/healthz") {
4744
- return Response.json(currentStatus());
4745
- }
4746
- if (url.pathname === "/readyz") {
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 (server.upgrade(req, { data: { clientId: 0, attached: false, lastPongAt: Date.now(), pongCount: 0, pendingBackpressure: [] } })) {
4755
- return;
5361
+ if (url.pathname === "/readyz") {
5362
+ return Response.json(currentStatus(), { status: codexBootstrapped ? 200 : 503 });
4756
5363
  }
4757
- }
4758
- return new Response("AgentBridge daemon");
4759
- },
4760
- websocket: {
4761
- idleTimeout: 960,
4762
- sendPings: true,
4763
- open: (ws) => {
4764
- ws.data.clientId = ++nextControlClientId;
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
- message: (ws, raw) => {
4776
- handleControlMessage(ws, raw);
4777
- },
4778
- pong: (ws) => {
4779
- ws.data.lastPongAt = Date.now();
4780
- ws.data.pongCount++;
4781
- },
4782
- drain: (ws) => {
4783
- if (ws.data.pendingBackpressure.length > 0 && ws.getBufferedAmount() === 0) {
4784
- ws.data.pendingBackpressure = [];
4785
- }
4786
- if (ws === attachedClaude && bufferedMessages.length > 0) {
4787
- flushBufferedMessages(ws);
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 resumeAfterEpoch2 = budgetCoordinator?.getSnapshot()?.resumeAfterEpoch ?? null;
4920
- const retryAfterMs = resumeAfterEpoch2 !== null ? Math.max(0, resumeAfterEpoch2 * 1000 - Date.now()) : undefined;
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
- bufferedMessages.unshift(...ws.data.pendingBackpressure);
5110
- log(`Re-buffered ${ws.data.pendingBackpressure.length} backpressured message(s) for redelivery on reconnect`);
5111
- ws.data.pendingBackpressure = [];
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.splice(0, bufferedMessages.length);
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.unshift(...remaining);
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();