@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.
@@ -13662,6 +13662,7 @@ class StdioServerTransport {
13662
13662
  // src/claude-adapter.ts
13663
13663
  import { EventEmitter } from "events";
13664
13664
  import { randomUUID } from "crypto";
13665
+ import { performance } from "perf_hooks";
13665
13666
 
13666
13667
  // src/rotating-log.ts
13667
13668
  import { appendFileSync, existsSync, renameSync, statSync, unlinkSync } from "fs";
@@ -13805,6 +13806,10 @@ function formatError2(error2) {
13805
13806
  import { mkdirSync, existsSync as existsSync2 } from "fs";
13806
13807
  import { join } from "path";
13807
13808
  import { homedir, platform } from "os";
13809
+ function resolveXdgStateBase(rawXdg = process.env.XDG_STATE_HOME) {
13810
+ const xdgState = rawXdg && rawXdg.length > 0 ? rawXdg : join(homedir(), ".local", "state");
13811
+ return join(xdgState, "agentbridge");
13812
+ }
13808
13813
 
13809
13814
  class StateDirResolver {
13810
13815
  stateDir;
@@ -13812,8 +13817,7 @@ class StateDirResolver {
13812
13817
  if (platform() === "darwin") {
13813
13818
  return join(homedir(), "Library", "Application Support", "AgentBridge");
13814
13819
  }
13815
- const xdgState = process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state");
13816
- return join(xdgState, "agentbridge");
13820
+ return resolveXdgStateBase(process.env.XDG_STATE_HOME);
13817
13821
  }
13818
13822
  constructor(envOverride) {
13819
13823
  const override = envOverride ?? process.env.AGENTBRIDGE_STATE_DIR;
@@ -13839,8 +13843,8 @@ class StateDirResolver {
13839
13843
  get statusFile() {
13840
13844
  return join(this.stateDir, "status.json");
13841
13845
  }
13842
- get portsFile() {
13843
- return join(this.stateDir, "ports.json");
13846
+ get daemonRecordFile() {
13847
+ return join(this.stateDir, "daemon.json");
13844
13848
  }
13845
13849
  get currentThreadFile() {
13846
13850
  return join(this.stateDir, "current-thread.json");
@@ -13943,6 +13947,10 @@ function renderBudgetSnapshot(snapshot) {
13943
13947
  var BUDGET_UNAVAILABLE_TEXT = "\u9884\u7B97\u611F\u77E5\u4E0D\u53EF\u7528\uFF1A\u672A\u68C0\u6D4B\u5230 agent-quota-guard \u63A2\u9488\uFF08~/.budget-guard/bin/budget-probe\uFF09\u6216 budget \u529F\u80FD\u5DF2\u7981\u7528\u3002\u534F\u4F5C\u4E0D\u53D7\u5F71\u54CD\u3002";
13944
13948
 
13945
13949
  // src/claude-adapter.ts
13950
+ var DEFAULT_MAX_BUFFERED_MESSAGES = 100;
13951
+ var DEFAULT_MAX_BUFFERED_BYTES = 4 * 1024 * 1024;
13952
+ var DEFAULT_DEDUPE_CAPACITY = 2048;
13953
+ var DEFAULT_DEDUPE_TTL_MS = 20 * 60 * 1000;
13946
13954
  var CLAUDE_INSTRUCTIONS = [
13947
13955
  "Codex is an AI coding agent (OpenAI) running in a separate session on the same machine.",
13948
13956
  "",
@@ -13991,10 +13999,20 @@ class ClaudeAdapter extends EventEmitter {
13991
13999
  logFile;
13992
14000
  logger;
13993
14001
  pendingMessages = [];
14002
+ pendingMessageByteSizes = [];
14003
+ pendingMessageBytes = 0;
13994
14004
  maxBufferedMessages;
14005
+ maxBufferedBytes;
13995
14006
  droppedMessageCount = 0;
14007
+ oversizedMessageCount = 0;
14008
+ oversizedMessageBytes = 0;
14009
+ oversizedMessageSourceCounts = {};
14010
+ dedupeCapacity;
14011
+ dedupeTtlMs;
14012
+ monotonicNow;
14013
+ deliveredMessageIds = new Map;
13996
14014
  budgetSnapshot = null;
13997
- constructor(logFile = new StateDirResolver().logFile) {
14015
+ constructor(logFile = new StateDirResolver().logFile, options = {}) {
13998
14016
  super();
13999
14017
  this.logFile = logFile;
14000
14018
  this.logger = createProcessLogger({ component: "ClaudeAdapter", logFile: this.logFile });
@@ -14005,7 +14023,11 @@ class ClaudeAdapter extends EventEmitter {
14005
14023
  if (process.env.AGENTBRIDGE_MODE) {
14006
14024
  this.log(`AGENTBRIDGE_MODE="${process.env.AGENTBRIDGE_MODE}" is no longer supported \u2014 ` + "pull mode was removed; push delivery (with per-message fallback queue) is always used.");
14007
14025
  }
14008
- this.maxBufferedMessages = parseInt(process.env.AGENTBRIDGE_MAX_BUFFERED_MESSAGES ?? "100", 10);
14026
+ this.maxBufferedMessages = positiveIntegerOr(options.maxBufferedMessages, parsePositiveIntegerEnv("AGENTBRIDGE_MAX_BUFFERED_MESSAGES", DEFAULT_MAX_BUFFERED_MESSAGES));
14027
+ this.maxBufferedBytes = positiveIntegerOr(options.maxBufferedBytes, parsePositiveIntegerEnv("AGENTBRIDGE_MAX_BUFFERED_BYTES", DEFAULT_MAX_BUFFERED_BYTES));
14028
+ this.dedupeCapacity = positiveIntegerOr(options.dedupeCapacity, DEFAULT_DEDUPE_CAPACITY);
14029
+ this.dedupeTtlMs = positiveIntegerOr(options.dedupeTtlMs, DEFAULT_DEDUPE_TTL_MS);
14030
+ this.monotonicNow = options.now ?? (() => performance.now());
14009
14031
  this.server = new Server({ name: "agentbridge", version: "0.1.0" }, {
14010
14032
  capabilities: {
14011
14033
  experimental: { "claude/channel": {} },
@@ -14032,10 +14054,12 @@ class ClaudeAdapter extends EventEmitter {
14032
14054
  }
14033
14055
  async pushNotification(message) {
14034
14056
  this.log(`pushNotification (instance=${this.instanceId}, msgId=${message.id}, len=${message.content.length})`);
14057
+ if (!this.rememberDelivery(message))
14058
+ return;
14035
14059
  await this.pushViaChannel(message);
14036
14060
  }
14037
14061
  async pushViaChannel(message) {
14038
- const msgId = `codex_msg_${this.notificationIdPrefix}_${++this.notificationSeq}`;
14062
+ const deliveryAttemptId = `codex_msg_${this.notificationIdPrefix}_${++this.notificationSeq}`;
14039
14063
  const ts = new Date(message.timestamp).toISOString();
14040
14064
  try {
14041
14065
  await this.server.notification({
@@ -14044,7 +14068,8 @@ class ClaudeAdapter extends EventEmitter {
14044
14068
  content: message.content,
14045
14069
  meta: {
14046
14070
  chat_id: this.sessionId,
14047
- message_id: msgId,
14071
+ message_id: message.id,
14072
+ delivery_attempt_id: deliveryAttemptId,
14048
14073
  user: "Codex",
14049
14074
  user_id: "codex",
14050
14075
  ts,
@@ -14052,39 +14077,93 @@ class ClaudeAdapter extends EventEmitter {
14052
14077
  }
14053
14078
  }
14054
14079
  });
14055
- this.log(`Pushed notification: ${msgId}`);
14080
+ this.log(`Pushed notification: ${message.id} (attempt=${deliveryAttemptId})`);
14056
14081
  } catch (e) {
14057
14082
  this.log(`Push notification failed: ${e.message}`);
14058
14083
  this.queueFallbackMessage(message);
14059
14084
  }
14060
14085
  }
14086
+ rememberDelivery(message) {
14087
+ const now = this.monotonicNow();
14088
+ this.pruneDeliveredMessageIds(now);
14089
+ if (this.deliveredMessageIds.has(message.id)) {
14090
+ this.deliveredMessageIds.delete(message.id);
14091
+ this.deliveredMessageIds.set(message.id, now);
14092
+ this.log(`Duplicate Codex message suppressed (msgId=${message.id}, source=${message.source}, ` + `instance=${this.instanceId})`);
14093
+ return false;
14094
+ }
14095
+ this.deliveredMessageIds.set(message.id, now);
14096
+ while (this.deliveredMessageIds.size > this.dedupeCapacity) {
14097
+ const oldest = this.deliveredMessageIds.keys().next().value;
14098
+ if (oldest === undefined)
14099
+ break;
14100
+ this.deliveredMessageIds.delete(oldest);
14101
+ }
14102
+ return true;
14103
+ }
14104
+ pruneDeliveredMessageIds(now) {
14105
+ for (const [id, seenAt] of this.deliveredMessageIds) {
14106
+ if (now - seenAt <= this.dedupeTtlMs)
14107
+ break;
14108
+ this.deliveredMessageIds.delete(id);
14109
+ }
14110
+ }
14061
14111
  queueFallbackMessage(message) {
14062
- if (this.pendingMessages.length >= this.maxBufferedMessages) {
14063
- this.pendingMessages.shift();
14112
+ const messageBytes = utf8ByteLength(message.content);
14113
+ if (messageBytes > this.maxBufferedBytes) {
14114
+ this.oversizedMessageCount++;
14115
+ this.oversizedMessageBytes += messageBytes;
14116
+ this.oversizedMessageSourceCounts[message.source] = (this.oversizedMessageSourceCounts[message.source] ?? 0) + 1;
14117
+ this.log(`Fallback queue omitted oversized ${message.source} message ` + `(${formatBytes(messageBytes)} > ${formatBytes(this.maxBufferedBytes)}; ` + `total oversized: ${this.oversizedMessageCount})`);
14118
+ return;
14119
+ }
14120
+ let dropped = 0;
14121
+ while (this.pendingMessages.length >= this.maxBufferedMessages || this.pendingMessageBytes + messageBytes > this.maxBufferedBytes) {
14122
+ const droppedMessage = this.pendingMessages.shift();
14123
+ const droppedBytes = this.pendingMessageByteSizes.shift() ?? 0;
14124
+ if (!droppedMessage)
14125
+ break;
14126
+ this.pendingMessageBytes = Math.max(0, this.pendingMessageBytes - droppedBytes);
14064
14127
  this.droppedMessageCount++;
14065
- this.log(`Fallback queue full, dropped oldest message (total dropped: ${this.droppedMessageCount})`);
14128
+ dropped++;
14129
+ }
14130
+ if (dropped > 0) {
14131
+ this.log(`Fallback queue overflow: dropped ${dropped} oldest message${dropped > 1 ? "s" : ""} ` + `(${this.pendingMessages.length} pending, ${formatBytes(this.pendingMessageBytes)} buffered, ` + `${this.droppedMessageCount} dropped since last drain)`);
14066
14132
  }
14067
14133
  this.pendingMessages.push(message);
14068
- this.log(`Queued fallback message (${this.pendingMessages.length} pending, instance=${this.instanceId})`);
14134
+ this.pendingMessageByteSizes.push(messageBytes);
14135
+ this.pendingMessageBytes += messageBytes;
14136
+ this.log(`Queued fallback message (${this.pendingMessages.length} pending, ` + `${formatBytes(this.pendingMessageBytes)} buffered, instance=${this.instanceId})`);
14069
14137
  }
14070
14138
  drainMessages() {
14071
- this.log(`get_messages called (instance=${this.instanceId}, pending=${this.pendingMessages.length}, dropped=${this.droppedMessageCount})`);
14072
- if (this.pendingMessages.length === 0 && this.droppedMessageCount === 0) {
14139
+ this.log(`get_messages called (instance=${this.instanceId}, pending=${this.pendingMessages.length}, ` + `bytes=${this.pendingMessageBytes}, dropped=${this.droppedMessageCount}, oversized=${this.oversizedMessageCount})`);
14140
+ if (this.pendingMessages.length === 0 && this.droppedMessageCount === 0 && this.oversizedMessageCount === 0) {
14073
14141
  return {
14074
14142
  content: [{ type: "text", text: "No new messages from Codex." }]
14075
14143
  };
14076
14144
  }
14077
14145
  const messages = this.pendingMessages;
14078
14146
  this.pendingMessages = [];
14147
+ this.pendingMessageByteSizes = [];
14148
+ this.pendingMessageBytes = 0;
14079
14149
  const dropped = this.droppedMessageCount;
14080
14150
  this.droppedMessageCount = 0;
14151
+ const oversizedSourceCounts = this.oversizedMessageSourceCounts;
14152
+ const oversized = this.oversizedMessageCount;
14153
+ const oversizedBytes = this.oversizedMessageBytes;
14154
+ this.oversizedMessageSourceCounts = {};
14155
+ this.oversizedMessageCount = 0;
14156
+ this.oversizedMessageBytes = 0;
14081
14157
  const count = messages.length;
14082
- let header = `[${count} new message${count > 1 ? "s" : ""} from Codex]`;
14158
+ const notices = [];
14083
14159
  if (dropped > 0) {
14084
- header += ` (${dropped} older message${dropped > 1 ? "s" : ""} were dropped due to queue overflow)`;
14160
+ notices.push(`${dropped} older message${dropped > 1 ? "s" : ""} ` + `${dropped > 1 ? "were" : "was"} dropped due to fallback queue overflow`);
14161
+ }
14162
+ if (oversized > 0) {
14163
+ for (const [source, sourceCount] of Object.entries(oversizedSourceCounts)) {
14164
+ notices.push(`${sourceCount} oversized message${sourceCount === 1 ? "" : "s"} ` + `from ${formatSource(source)} omitted ` + `(>${formatBytes(this.maxBufferedBytes)})`);
14165
+ }
14085
14166
  }
14086
- header += `
14087
- chat_id: ${this.sessionId}`;
14088
14167
  const formatted = messages.map((msg, i) => {
14089
14168
  const ts = new Date(msg.timestamp).toISOString();
14090
14169
  return `---
@@ -14093,14 +14172,25 @@ Codex: ${msg.content}`;
14093
14172
  }).join(`
14094
14173
 
14095
14174
  `);
14096
- this.log(`get_messages returning ${count} message(s) (instance=${this.instanceId}, dropped=${dropped})`);
14175
+ const noticeText = notices.map((notice) => `WARNING: ${notice}`).join(`
14176
+ `);
14177
+ const parts = [];
14178
+ if (count > 0) {
14179
+ parts.push(`[${count} new message${count > 1 ? "s" : ""} from Codex]
14180
+ chat_id: ${this.sessionId}`);
14181
+ }
14182
+ if (noticeText)
14183
+ parts.push(noticeText);
14184
+ if (formatted)
14185
+ parts.push(formatted);
14186
+ this.log(`get_messages returning ${count} message(s) ` + `(instance=${this.instanceId}, dropped=${dropped}, oversized=${oversized}, oversizedBytes=${oversizedBytes})`);
14097
14187
  return {
14098
14188
  content: [
14099
14189
  {
14100
14190
  type: "text",
14101
- text: `${header}
14191
+ text: parts.join(`
14102
14192
 
14103
- ${formatted}`
14193
+ `)
14104
14194
  }
14105
14195
  ]
14106
14196
  };
@@ -14256,11 +14346,37 @@ ${formatted}`
14256
14346
  this.logger.log(msg);
14257
14347
  }
14258
14348
  }
14349
+ function parsePositiveIntegerEnv(name, fallback) {
14350
+ return positiveIntegerOr(parseInt(process.env[name] ?? "", 10), fallback);
14351
+ }
14352
+ function positiveIntegerOr(value, fallback) {
14353
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
14354
+ }
14355
+ function utf8ByteLength(value) {
14356
+ return Buffer.byteLength(value, "utf8");
14357
+ }
14358
+ function formatSource(source) {
14359
+ return source === "codex" ? "Codex" : "Claude";
14360
+ }
14361
+ function formatBytes(bytes) {
14362
+ if (bytes < 1024)
14363
+ return `${bytes}B`;
14364
+ if (bytes % (1024 * 1024) === 0)
14365
+ return `${bytes / (1024 * 1024)}MiB`;
14366
+ if (bytes % 1024 === 0)
14367
+ return `${bytes / 1024}KiB`;
14368
+ return `${bytes}B`;
14369
+ }
14259
14370
 
14260
14371
  // src/contract-version.ts
14261
14372
  var CONTRACT_VERSION = 1;
14262
14373
 
14263
14374
  // src/build-info.ts
14375
+ var CODE_HASH_SENTINEL = "source";
14376
+ function hasValidCodeHash(build) {
14377
+ const hash = build?.codeHash;
14378
+ return typeof hash === "string" && hash.length > 0 && hash !== CODE_HASH_SENTINEL;
14379
+ }
14264
14380
  function defineString(value, fallback) {
14265
14381
  return typeof value === "string" && value.length > 0 ? value : fallback;
14266
14382
  }
@@ -14273,15 +14389,23 @@ function defineNumber(value, fallback) {
14273
14389
  return typeof value === "number" && Number.isFinite(value) ? value : fallback;
14274
14390
  }
14275
14391
  var BUILD_INFO = Object.freeze({
14276
- version: defineString("0.1.12", "0.0.0-source"),
14277
- commit: defineString("eec6018", "source"),
14392
+ version: defineString("0.1.13", "0.0.0-source"),
14393
+ commit: defineString("7a71869", "source"),
14278
14394
  bundle: defineBundle("plugin"),
14279
- contractVersion: defineNumber(1, CONTRACT_VERSION)
14395
+ contractVersion: defineNumber(1, CONTRACT_VERSION),
14396
+ codeHash: defineString("e1fd67d07c62", "source")
14280
14397
  });
14281
14398
  function sameRuntimeContract(a, b) {
14282
14399
  if (!a || !b)
14283
14400
  return false;
14284
- return a.version === b.version && a.commit === b.commit && a.contractVersion === b.contractVersion;
14401
+ if (a.version !== b.version || a.contractVersion !== b.contractVersion)
14402
+ return false;
14403
+ if (hasValidCodeHash(a) && hasValidCodeHash(b))
14404
+ return a.codeHash === b.codeHash;
14405
+ return a.commit === b.commit;
14406
+ }
14407
+ function runtimeContractComparisonBasis(a, b) {
14408
+ return hasValidCodeHash(a) && hasValidCodeHash(b) ? "codeHash" : "commit";
14285
14409
  }
14286
14410
  function compatibleContractVersion(a, b) {
14287
14411
  if (!a || !b)
@@ -14291,7 +14415,8 @@ function compatibleContractVersion(a, b) {
14291
14415
  function formatBuildInfo(build) {
14292
14416
  if (!build)
14293
14417
  return "<unknown>";
14294
- return `${build.version}/${build.commit}/${build.bundle}/contract-v${build.contractVersion}`;
14418
+ const codeHash = hasValidCodeHash(build) ? `/code-${build.codeHash}` : "";
14419
+ return `${build.version}/${build.commit}/${build.bundle}/contract-v${build.contractVersion}${codeHash}`;
14295
14420
  }
14296
14421
 
14297
14422
  // src/daemon-client.ts
@@ -14302,12 +14427,84 @@ var CLOSE_CODE_REPLACED = 4001;
14302
14427
  var CLOSE_CODE_EVICTED_STALE = 4002;
14303
14428
  var CLOSE_CODE_PROBE_IN_PROGRESS = 4003;
14304
14429
  var CLOSE_CODE_PAIR_MISMATCH = 4004;
14430
+ var CLOSE_CODE_TOKEN_MISMATCH = 4005;
14431
+ var CLOSE_CODE_CONTRACT_MISMATCH = 4006;
14305
14432
 
14306
14433
  // src/interrupt-timing.ts
14307
14434
  var CLIENT_REPLY_TIMEOUT_MS = 15000;
14308
14435
  var INTERRUPT_CLIENT_MARGIN_MS = 2000;
14309
14436
  var MAX_INTERRUPT_TIMEOUT_MS = CLIENT_REPLY_TIMEOUT_MS - INTERRUPT_CLIENT_MARGIN_MS;
14310
14437
 
14438
+ // src/pending-request-registry.ts
14439
+ class PendingRequestRegistry {
14440
+ entries = new Map;
14441
+ setTimer;
14442
+ clearTimer;
14443
+ constructor(deps = {}) {
14444
+ this.setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
14445
+ this.clearTimer = deps.clearTimer ?? ((handle) => clearTimeout(handle));
14446
+ }
14447
+ get size() {
14448
+ return this.entries.size;
14449
+ }
14450
+ has(id) {
14451
+ return this.entries.has(id);
14452
+ }
14453
+ register(id, options) {
14454
+ const existing = this.entries.get(id);
14455
+ if (existing) {
14456
+ this.clearTimer(existing.timer);
14457
+ this.entries.delete(id);
14458
+ }
14459
+ return new Promise((resolve, reject) => {
14460
+ const timer = this.setTimer(() => {
14461
+ if (!this.entries.has(id))
14462
+ return;
14463
+ this.entries.delete(id);
14464
+ options.onTimeout({ resolve, reject });
14465
+ }, options.timeoutMs);
14466
+ if (options.unref) {
14467
+ timer.unref?.();
14468
+ }
14469
+ this.entries.set(id, { resolve, reject, timer });
14470
+ });
14471
+ }
14472
+ settle(id, value) {
14473
+ const entry = this.entries.get(id);
14474
+ if (!entry)
14475
+ return false;
14476
+ this.clearTimer(entry.timer);
14477
+ this.entries.delete(id);
14478
+ entry.resolve(value);
14479
+ return true;
14480
+ }
14481
+ reject(id, error2) {
14482
+ const entry = this.entries.get(id);
14483
+ if (!entry)
14484
+ return false;
14485
+ this.clearTimer(entry.timer);
14486
+ this.entries.delete(id);
14487
+ entry.reject(error2);
14488
+ return true;
14489
+ }
14490
+ settleAll(value) {
14491
+ const make = typeof value === "function" ? value : () => value;
14492
+ for (const [id, entry] of this.entries) {
14493
+ this.clearTimer(entry.timer);
14494
+ this.entries.delete(id);
14495
+ entry.resolve(make(id));
14496
+ }
14497
+ }
14498
+ rejectAll(error2) {
14499
+ const make = typeof error2 === "function" ? error2 : () => error2;
14500
+ for (const [id, entry] of this.entries) {
14501
+ this.clearTimer(entry.timer);
14502
+ this.entries.delete(id);
14503
+ entry.reject(make(id));
14504
+ }
14505
+ }
14506
+ }
14507
+
14311
14508
  // src/daemon-client.ts
14312
14509
  var nextSocketId = 0;
14313
14510
 
@@ -14317,7 +14514,8 @@ class DaemonClient extends EventEmitter2 {
14317
14514
  ws = null;
14318
14515
  wsId = 0;
14319
14516
  nextRequestId = 1;
14320
- pendingReplies = new Map;
14517
+ pendingReplies = new PendingRequestRegistry;
14518
+ pendingEventWaiters = new PendingRequestRegistry;
14321
14519
  constructor(url, options = {}) {
14322
14520
  super();
14323
14521
  this.url = url;
@@ -14363,82 +14561,73 @@ class DaemonClient extends EventEmitter2 {
14363
14561
  });
14364
14562
  }
14365
14563
  attachClaude() {
14564
+ const identity = this.resolveIdentity();
14366
14565
  this.send({
14367
14566
  type: "claude_connect",
14368
- ...this.options.identity ? { identity: this.options.identity } : {}
14567
+ ...identity ? { identity } : {}
14369
14568
  });
14370
14569
  }
14570
+ resolveIdentity() {
14571
+ const opt = this.options.identity;
14572
+ return typeof opt === "function" ? opt() : opt;
14573
+ }
14371
14574
  async attachClaudeAndWaitForStatus(timeoutMs = 1000) {
14372
14575
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
14373
14576
  return null;
14374
14577
  }
14375
- return await new Promise((resolve) => {
14376
- let settled = false;
14377
- let timer = null;
14378
- const cleanup = () => {
14379
- if (settled)
14380
- return;
14381
- settled = true;
14382
- if (timer) {
14383
- clearTimeout(timer);
14384
- timer = null;
14385
- }
14386
- this.off("status", onStatus);
14387
- this.off("rejected", onRejected);
14388
- this.off("disconnect", onDisconnect);
14389
- };
14390
- const finish = (value) => {
14391
- cleanup();
14392
- resolve(value);
14393
- };
14394
- const onStatus = (status) => finish(status);
14395
- const onRejected = () => finish(null);
14396
- const onDisconnect = () => finish(null);
14397
- this.on("status", onStatus);
14398
- this.on("rejected", onRejected);
14399
- this.on("disconnect", onDisconnect);
14400
- timer = setTimeout(() => {
14401
- finish(null);
14402
- }, timeoutMs);
14403
- try {
14404
- this.attachClaude();
14405
- } catch {
14406
- finish(null);
14407
- }
14578
+ return this.awaitTypedResponse({
14579
+ key: "status",
14580
+ successEvent: "status",
14581
+ successValue: (status) => status,
14582
+ failValue: null,
14583
+ timeoutMs,
14584
+ send: () => this.attachClaude()
14408
14585
  });
14409
14586
  }
14410
14587
  async probeIncumbent(timeoutMs = 3000) {
14411
14588
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
14412
14589
  return { connected: false, alive: false };
14413
14590
  }
14414
- return await new Promise((resolve) => {
14415
- let settled = false;
14416
- let timer = null;
14417
- const finish = (value) => {
14418
- if (settled)
14419
- return;
14420
- settled = true;
14421
- if (timer)
14422
- clearTimeout(timer);
14423
- this.off("incumbentStatus", onStatus);
14424
- this.off("disconnect", onDisconnect);
14425
- this.off("rejected", onRejected);
14426
- resolve(value);
14427
- };
14428
- const onStatus = (s) => finish(s);
14429
- const onDisconnect = () => finish({ connected: false, alive: false });
14430
- const onRejected = () => finish({ connected: false, alive: false });
14431
- this.on("incumbentStatus", onStatus);
14432
- this.on("disconnect", onDisconnect);
14433
- this.on("rejected", onRejected);
14434
- timer = setTimeout(() => finish({ connected: false, alive: false }), timeoutMs);
14435
- try {
14436
- this.send({ type: "probe_incumbent" });
14437
- } catch {
14438
- finish({ connected: false, alive: false });
14439
- }
14591
+ return this.awaitTypedResponse({
14592
+ key: "incumbent_status",
14593
+ successEvent: "incumbentStatus",
14594
+ successValue: (s) => s,
14595
+ failValue: { connected: false, alive: false },
14596
+ timeoutMs,
14597
+ send: () => this.send({ type: "probe_incumbent" })
14440
14598
  });
14441
14599
  }
14600
+ awaitTypedResponse(opts) {
14601
+ const { key, successEvent, successValue, failValue, timeoutMs, send } = opts;
14602
+ const onSuccess = (payload) => {
14603
+ this.pendingEventWaiters.settle(key, successValue(payload));
14604
+ };
14605
+ const onRejected = () => {
14606
+ this.pendingEventWaiters.settle(key, failValue);
14607
+ };
14608
+ const onDisconnect = () => {
14609
+ this.pendingEventWaiters.settle(key, failValue);
14610
+ };
14611
+ const pending = this.pendingEventWaiters.register(key, {
14612
+ timeoutMs,
14613
+ onTimeout: ({ resolve }) => resolve(failValue)
14614
+ });
14615
+ const cleanup = () => {
14616
+ this.off(successEvent, onSuccess);
14617
+ this.off("rejected", onRejected);
14618
+ this.off("disconnect", onDisconnect);
14619
+ };
14620
+ pending.finally(cleanup);
14621
+ this.on(successEvent, onSuccess);
14622
+ this.on("rejected", onRejected);
14623
+ this.on("disconnect", onDisconnect);
14624
+ try {
14625
+ send();
14626
+ } catch {
14627
+ this.pendingEventWaiters.settle(key, failValue);
14628
+ }
14629
+ return pending;
14630
+ }
14442
14631
  async disconnect() {
14443
14632
  if (!this.ws)
14444
14633
  return;
@@ -14456,21 +14645,19 @@ class DaemonClient extends EventEmitter2 {
14456
14645
  return { success: false, error: "AgentBridge daemon is not connected." };
14457
14646
  }
14458
14647
  const requestId = `reply_${Date.now()}_${this.nextRequestId++}`;
14459
- return new Promise((resolve) => {
14460
- const timer = setTimeout(() => {
14461
- this.pendingReplies.delete(requestId);
14462
- resolve({ success: false, error: "Timed out waiting for AgentBridge daemon reply." });
14463
- }, CLIENT_REPLY_TIMEOUT_MS);
14464
- this.pendingReplies.set(requestId, { resolve, timer });
14465
- this.send({
14466
- type: "claude_to_codex",
14467
- requestId,
14468
- message,
14469
- ...requireReply ? { requireReply: true } : {},
14470
- ...onBusy && onBusy !== "reject" ? { onBusy } : {},
14471
- ...idempotencyKey ? { idempotencyKey } : {}
14472
- });
14648
+ const pending = this.pendingReplies.register(requestId, {
14649
+ timeoutMs: CLIENT_REPLY_TIMEOUT_MS,
14650
+ onTimeout: ({ resolve }) => resolve({ success: false, error: "Timed out waiting for AgentBridge daemon reply." })
14651
+ });
14652
+ this.send({
14653
+ type: "claude_to_codex",
14654
+ requestId,
14655
+ message,
14656
+ ...requireReply ? { requireReply: true } : {},
14657
+ ...onBusy && onBusy !== "reject" ? { onBusy } : {},
14658
+ ...idempotencyKey ? { idempotencyKey } : {}
14473
14659
  });
14660
+ return pending;
14474
14661
  }
14475
14662
  attachSocketHandlers(ws, socketId) {
14476
14663
  ws.onmessage = (event) => {
@@ -14486,12 +14673,7 @@ class DaemonClient extends EventEmitter2 {
14486
14673
  this.emit("codexMessage", message.message);
14487
14674
  return;
14488
14675
  case "claude_to_codex_result": {
14489
- const pending = this.pendingReplies.get(message.requestId);
14490
- if (!pending)
14491
- return;
14492
- clearTimeout(pending.timer);
14493
- this.pendingReplies.delete(message.requestId);
14494
- pending.resolve({
14676
+ this.pendingReplies.settle(message.requestId, {
14495
14677
  success: message.success,
14496
14678
  error: message.error,
14497
14679
  ...message.code !== undefined ? { code: message.code } : {},
@@ -14522,7 +14704,7 @@ class DaemonClient extends EventEmitter2 {
14522
14704
  if (isCurrent) {
14523
14705
  this.ws = null;
14524
14706
  this.rejectPendingReplies("AgentBridge daemon disconnected.");
14525
- if (event.code === CLOSE_CODE_REPLACED || event.code === CLOSE_CODE_EVICTED_STALE || event.code === CLOSE_CODE_PROBE_IN_PROGRESS || event.code === CLOSE_CODE_PAIR_MISMATCH) {
14707
+ if (event.code === CLOSE_CODE_REPLACED || event.code === CLOSE_CODE_EVICTED_STALE || event.code === CLOSE_CODE_PROBE_IN_PROGRESS || event.code === CLOSE_CODE_PAIR_MISMATCH || event.code === CLOSE_CODE_TOKEN_MISMATCH || event.code === CLOSE_CODE_CONTRACT_MISMATCH) {
14526
14708
  this.emit("rejected", event.code);
14527
14709
  } else {
14528
14710
  this.emit("disconnect");
@@ -14532,11 +14714,7 @@ class DaemonClient extends EventEmitter2 {
14532
14714
  ws.onerror = () => {};
14533
14715
  }
14534
14716
  rejectPendingReplies(error2) {
14535
- for (const [requestId, pending] of this.pendingReplies.entries()) {
14536
- clearTimeout(pending.timer);
14537
- pending.resolve({ success: false, error: error2 });
14538
- this.pendingReplies.delete(requestId);
14539
- }
14717
+ this.pendingReplies.settleAll(() => ({ success: false, error: error2 }));
14540
14718
  }
14541
14719
  send(message) {
14542
14720
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
@@ -14552,9 +14730,44 @@ class DaemonClient extends EventEmitter2 {
14552
14730
 
14553
14731
  // src/daemon-lifecycle.ts
14554
14732
  import { spawn } from "child_process";
14555
- import { existsSync as existsSync3, readFileSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync, openSync, closeSync, constants } from "fs";
14733
+ import { existsSync as existsSync3, readFileSync as readFileSync2, statSync as statSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync2, openSync as openSync2, closeSync as closeSync2, constants } from "fs";
14556
14734
  import { fileURLToPath } from "url";
14557
14735
 
14736
+ // src/atomic-json.ts
14737
+ import * as fs from "fs";
14738
+ import { randomUUID as randomUUID2 } from "crypto";
14739
+ import { dirname as dirname2 } from "path";
14740
+ function tmpPathFor(targetPath) {
14741
+ return `${targetPath}.tmp.${process.pid}.${randomUUID2()}`;
14742
+ }
14743
+ function atomicWriteText(path, content, options = {}) {
14744
+ fs.mkdirSync(dirname2(path), { recursive: true });
14745
+ const tmp = tmpPathFor(path);
14746
+ let renamed = false;
14747
+ const fd = fs.openSync(tmp, "w", options.mode ?? 438);
14748
+ try {
14749
+ try {
14750
+ fs.writeFileSync(fd, content, "utf-8");
14751
+ if (options.fsync)
14752
+ fs.fsyncSync(fd);
14753
+ } finally {
14754
+ fs.closeSync(fd);
14755
+ }
14756
+ fs.renameSync(tmp, path);
14757
+ renamed = true;
14758
+ } finally {
14759
+ if (!renamed) {
14760
+ try {
14761
+ fs.unlinkSync(tmp);
14762
+ } catch {}
14763
+ }
14764
+ }
14765
+ }
14766
+ function atomicWriteJson(path, value, options = {}) {
14767
+ atomicWriteText(path, JSON.stringify(value, null, 2) + `
14768
+ `, options);
14769
+ }
14770
+
14558
14771
  // src/env-utils.ts
14559
14772
  function parsePositiveIntEnv(name, fallback, log = () => {}, env = process.env) {
14560
14773
  const raw = env[name];
@@ -14603,12 +14816,144 @@ function isAgentBridgeProcess(pid, lookup = commandForPid) {
14603
14816
  return cmd.includes("agentbridge") || cmd.includes("agent_bridge");
14604
14817
  }
14605
14818
 
14819
+ // src/daemon-record.ts
14820
+ import { readFileSync } from "fs";
14821
+ var defaultRead = (path) => readFileSync(path, "utf-8");
14822
+ function writeDaemonRecord(path, record3) {
14823
+ atomicWriteJson(path, record3);
14824
+ }
14825
+ function sanitizePorts(value) {
14826
+ if (typeof value !== "object" || value === null)
14827
+ return;
14828
+ const raw = value;
14829
+ const ports = {};
14830
+ if (typeof raw.appPort === "number")
14831
+ ports.appPort = raw.appPort;
14832
+ if (typeof raw.proxyPort === "number")
14833
+ ports.proxyPort = raw.proxyPort;
14834
+ if (typeof raw.controlPort === "number")
14835
+ ports.controlPort = raw.controlPort;
14836
+ return Object.keys(ports).length > 0 ? ports : undefined;
14837
+ }
14838
+ function readDaemonRecord(path, read = defaultRead) {
14839
+ let parsed;
14840
+ try {
14841
+ parsed = JSON.parse(read(path));
14842
+ } catch {
14843
+ return null;
14844
+ }
14845
+ if (typeof parsed !== "object" || parsed === null)
14846
+ return null;
14847
+ const obj = parsed;
14848
+ if (typeof obj.pid !== "number" || !Number.isFinite(obj.pid))
14849
+ return null;
14850
+ const phase = obj.phase === "ready" ? "ready" : "booting";
14851
+ const record3 = { pid: obj.pid, phase };
14852
+ if (typeof obj.startedAt === "number")
14853
+ record3.startedAt = obj.startedAt;
14854
+ if (typeof obj.nonce === "string")
14855
+ record3.nonce = obj.nonce;
14856
+ if (obj.pairId === null || typeof obj.pairId === "string")
14857
+ record3.pairId = obj.pairId;
14858
+ if (obj.cwd === null || typeof obj.cwd === "string")
14859
+ record3.cwd = obj.cwd;
14860
+ if (obj.stateDir === null || typeof obj.stateDir === "string")
14861
+ record3.stateDir = obj.stateDir;
14862
+ if (typeof obj.proxyUrl === "string")
14863
+ record3.proxyUrl = obj.proxyUrl;
14864
+ if (typeof obj.appServerUrl === "string")
14865
+ record3.appServerUrl = obj.appServerUrl;
14866
+ const ports = sanitizePorts(obj.ports);
14867
+ if (ports !== undefined)
14868
+ record3.ports = ports;
14869
+ if (typeof obj.build === "object" && obj.build !== null) {
14870
+ record3.build = obj.build;
14871
+ }
14872
+ if (typeof obj.turnPhase === "string")
14873
+ record3.turnPhase = obj.turnPhase;
14874
+ if (typeof obj.turnInProgress === "boolean")
14875
+ record3.turnInProgress = obj.turnInProgress;
14876
+ if (typeof obj.attentionWindowActive === "boolean") {
14877
+ record3.attentionWindowActive = obj.attentionWindowActive;
14878
+ }
14879
+ return record3;
14880
+ }
14881
+ function synthesizeLegacyRecord(pidFilePath, statusFilePath, read = defaultRead) {
14882
+ let pidFromPidFile = null;
14883
+ try {
14884
+ const raw = read(pidFilePath).trim();
14885
+ const n = Number.parseInt(raw, 10);
14886
+ if (Number.isFinite(n))
14887
+ pidFromPidFile = n;
14888
+ } catch {}
14889
+ let status = null;
14890
+ try {
14891
+ const parsed = JSON.parse(read(statusFilePath));
14892
+ if (typeof parsed === "object" && parsed !== null)
14893
+ status = parsed;
14894
+ } catch {}
14895
+ const pidFromStatus = status && typeof status.pid === "number" && Number.isFinite(status.pid) ? status.pid : null;
14896
+ const pid = pidFromPidFile ?? pidFromStatus;
14897
+ if (pid === null)
14898
+ return null;
14899
+ const record3 = {
14900
+ pid,
14901
+ phase: status ? "ready" : "booting"
14902
+ };
14903
+ if (status) {
14904
+ if (typeof status.proxyUrl === "string")
14905
+ record3.proxyUrl = status.proxyUrl;
14906
+ if (typeof status.appServerUrl === "string")
14907
+ record3.appServerUrl = status.appServerUrl;
14908
+ const controlPort = typeof status.controlPort === "number" ? status.controlPort : undefined;
14909
+ const proxyPort = portFromUrl(status.proxyUrl);
14910
+ const appPort = portFromUrl(status.appServerUrl);
14911
+ if (controlPort !== undefined || proxyPort !== undefined || appPort !== undefined) {
14912
+ record3.ports = {};
14913
+ if (appPort !== undefined)
14914
+ record3.ports.appPort = appPort;
14915
+ if (proxyPort !== undefined)
14916
+ record3.ports.proxyPort = proxyPort;
14917
+ if (controlPort !== undefined)
14918
+ record3.ports.controlPort = controlPort;
14919
+ }
14920
+ if (status.pairId === null || typeof status.pairId === "string")
14921
+ record3.pairId = status.pairId;
14922
+ if (status.cwd === null || typeof status.cwd === "string")
14923
+ record3.cwd = status.cwd;
14924
+ if (status.stateDir === null || typeof status.stateDir === "string")
14925
+ record3.stateDir = status.stateDir;
14926
+ if (typeof status.build === "object" && status.build !== null) {
14927
+ record3.build = status.build;
14928
+ }
14929
+ if (typeof status.turnPhase === "string")
14930
+ record3.turnPhase = status.turnPhase;
14931
+ if (typeof status.turnInProgress === "boolean")
14932
+ record3.turnInProgress = status.turnInProgress;
14933
+ if (typeof status.attentionWindowActive === "boolean") {
14934
+ record3.attentionWindowActive = status.attentionWindowActive;
14935
+ }
14936
+ }
14937
+ return record3;
14938
+ }
14939
+ function readUnifiedDaemonRecord(paths, read = defaultRead) {
14940
+ return readDaemonRecord(paths.daemonRecordFile, read) ?? synthesizeLegacyRecord(paths.pidFile, paths.statusFile, read);
14941
+ }
14942
+ function portFromUrl(url) {
14943
+ if (typeof url !== "string")
14944
+ return;
14945
+ const match = url.match(/:(\d+)(?:[/?]|$)/);
14946
+ return match ? Number.parseInt(match[1], 10) : undefined;
14947
+ }
14948
+
14606
14949
  // src/daemon-lifecycle.ts
14607
14950
  var DEFAULT_DAEMON_ENTRY = import.meta.url.endsWith(".ts") ? "./daemon.ts" : "./daemon.js";
14608
14951
  var DAEMON_ENTRY = process.env.AGENTBRIDGE_DAEMON_ENTRY || DEFAULT_DAEMON_ENTRY;
14609
14952
  var DAEMON_PATH = fileURLToPath(new URL(DAEMON_ENTRY, import.meta.url));
14610
14953
  var REUSE_READY_RETRIES = parsePositiveIntEnv("AGENTBRIDGE_REUSE_READY_RETRIES", 12);
14611
14954
  var REUSE_READY_DELAY_MS = 250;
14955
+ var WAIT_READY_RETRIES = 40;
14956
+ var WAIT_READY_DELAY_MS = 250;
14612
14957
  var HEALTH_FETCH_TIMEOUT_MS = 500;
14613
14958
  var LOCK_IDENTITY_GRACE_MS = parsePositiveIntEnv("AGENTBRIDGE_LOCK_IDENTITY_GRACE_MS", 120000);
14614
14959
  function isReuseVerdict(verdict) {
@@ -14646,22 +14991,33 @@ function classifyDaemon(expectedPairId, status, buildInfo) {
14646
14991
  reason: "runtime build drift has a compatible contract and a live Codex TUI is attached"
14647
14992
  };
14648
14993
  }
14994
+ const basis = runtimeContractComparisonBasis(status.build, buildInfo) === "codeHash" ? "compared by codeHash" : "compared by commit stamp; legacy build without codeHash";
14649
14995
  return {
14650
14996
  verdict: "replace-drifted",
14651
- reason: `runtime build ${formatBuildInfo(status.build)} does not match launcher ${formatBuildInfo(buildInfo)}`
14997
+ reason: `runtime build ${formatBuildInfo(status.build)} does not match launcher ` + `${formatBuildInfo(buildInfo)} (${basis})`
14652
14998
  };
14653
14999
  }
14654
15000
  return { verdict: "reuse", reason: "daemon pair and runtime contract match" };
14655
15001
  }
15002
+ function resolveTiming(timing) {
15003
+ return {
15004
+ reuseReadyRetries: timing?.reuseReadyRetries ?? REUSE_READY_RETRIES,
15005
+ reuseReadyDelayMs: timing?.reuseReadyDelayMs ?? REUSE_READY_DELAY_MS,
15006
+ waitReadyRetries: timing?.waitReadyRetries ?? WAIT_READY_RETRIES,
15007
+ waitReadyDelayMs: timing?.waitReadyDelayMs ?? WAIT_READY_DELAY_MS
15008
+ };
15009
+ }
14656
15010
 
14657
15011
  class DaemonLifecycle {
14658
15012
  stateDir;
14659
15013
  controlPort;
14660
15014
  log;
15015
+ timing;
14661
15016
  constructor(opts) {
14662
15017
  this.stateDir = opts.stateDir;
14663
15018
  this.controlPort = opts.controlPort;
14664
15019
  this.log = opts.log;
15020
+ this.timing = resolveTiming(opts.timing);
14665
15021
  }
14666
15022
  get healthUrl() {
14667
15023
  return `http://127.0.0.1:${this.controlPort}/healthz`;
@@ -14718,7 +15074,7 @@ class DaemonLifecycle {
14718
15074
  break;
14719
15075
  }
14720
15076
  try {
14721
- await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
15077
+ await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
14722
15078
  return;
14723
15079
  } catch {
14724
15080
  this.log(`Daemon on control port ${this.controlPort} is healthy but not ready within reuse window \u2014 replacing`);
@@ -14731,7 +15087,7 @@ class DaemonLifecycle {
14731
15087
  if (isProcessAlive(existingPid)) {
14732
15088
  if (isAgentBridgeDaemon(existingPid)) {
14733
15089
  try {
14734
- await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
15090
+ await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
14735
15091
  return;
14736
15092
  } catch {
14737
15093
  this.log(`Existing daemon process ${existingPid} never became ready \u2014 replacing`);
@@ -14759,7 +15115,7 @@ class DaemonLifecycle {
14759
15115
  await this.kill(3000, status?.pid);
14760
15116
  } else {
14761
15117
  try {
14762
- await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
15118
+ await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
14763
15119
  return;
14764
15120
  } catch {
14765
15121
  this.log(`Daemon on control port ${this.controlPort} is healthy but not ready under startup lock \u2014 replacing`);
@@ -14768,7 +15124,7 @@ class DaemonLifecycle {
14768
15124
  }
14769
15125
  }
14770
15126
  this.launch();
14771
- await this.waitForReady();
15127
+ await this.waitForReady(this.timing.waitReadyRetries, this.timing.waitReadyDelayMs);
14772
15128
  });
14773
15129
  }
14774
15130
  async isHealthy() {
@@ -14795,7 +15151,7 @@ class DaemonLifecycle {
14795
15151
  return false;
14796
15152
  }
14797
15153
  }
14798
- async waitForReady(maxRetries = 40, delayMs = 250) {
15154
+ async waitForReady(maxRetries = WAIT_READY_RETRIES, delayMs = WAIT_READY_DELAY_MS) {
14799
15155
  for (let attempt = 0;attempt < maxRetries; attempt++) {
14800
15156
  if (await this.isReady())
14801
15157
  return;
@@ -14803,7 +15159,7 @@ class DaemonLifecycle {
14803
15159
  }
14804
15160
  throw new Error(`Timed out waiting for AgentBridge daemon readiness on ${this.readyUrl}`);
14805
15161
  }
14806
- async waitForReadyAndOurs(maxRetries = 40, delayMs = 250) {
15162
+ async waitForReadyAndOurs(maxRetries = WAIT_READY_RETRIES, delayMs = WAIT_READY_DELAY_MS) {
14807
15163
  for (let attempt = 0;attempt < maxRetries; attempt++) {
14808
15164
  if (await this.isReady()) {
14809
15165
  const status = await this.fetchStatus();
@@ -14819,22 +15175,35 @@ class DaemonLifecycle {
14819
15175
  }
14820
15176
  throw new Error(`Timed out waiting for AgentBridge daemon readiness+identity on ${this.readyUrl} (control port ${this.controlPort})`);
14821
15177
  }
15178
+ readDaemonRecord() {
15179
+ return readUnifiedDaemonRecord({
15180
+ daemonRecordFile: this.stateDir.daemonRecordFile,
15181
+ pidFile: this.stateDir.pidFile,
15182
+ statusFile: this.stateDir.statusFile
15183
+ });
15184
+ }
15185
+ writeDaemonRecord(record3) {
15186
+ writeDaemonRecord(this.stateDir.daemonRecordFile, record3);
15187
+ }
15188
+ removeDaemonRecord() {
15189
+ try {
15190
+ unlinkSync3(this.stateDir.daemonRecordFile);
15191
+ } catch {}
15192
+ }
14822
15193
  readStatus() {
14823
15194
  try {
14824
- const raw = readFileSync(this.stateDir.statusFile, "utf-8");
15195
+ const raw = readFileSync2(this.stateDir.statusFile, "utf-8");
14825
15196
  return JSON.parse(raw);
14826
15197
  } catch {
14827
15198
  return null;
14828
15199
  }
14829
15200
  }
14830
15201
  writeStatus(status) {
14831
- this.stateDir.ensure();
14832
- writeFileSync(this.stateDir.statusFile, JSON.stringify(status, null, 2) + `
14833
- `, "utf-8");
15202
+ atomicWriteJson(this.stateDir.statusFile, status);
14834
15203
  }
14835
15204
  readPid() {
14836
15205
  try {
14837
- const raw = readFileSync(this.stateDir.pidFile, "utf-8").trim();
15206
+ const raw = readFileSync2(this.stateDir.pidFile, "utf-8").trim();
14838
15207
  if (!raw)
14839
15208
  return null;
14840
15209
  const pid = Number.parseInt(raw, 10);
@@ -14844,28 +15213,27 @@ class DaemonLifecycle {
14844
15213
  }
14845
15214
  }
14846
15215
  writePid(pid) {
14847
- this.stateDir.ensure();
14848
- writeFileSync(this.stateDir.pidFile, `${pid ?? process.pid}
14849
- `, "utf-8");
15216
+ atomicWriteText(this.stateDir.pidFile, `${pid ?? process.pid}
15217
+ `);
14850
15218
  }
14851
15219
  removePidFile() {
14852
15220
  try {
14853
- unlinkSync2(this.stateDir.pidFile);
15221
+ unlinkSync3(this.stateDir.pidFile);
14854
15222
  } catch {}
14855
15223
  }
14856
15224
  removeStatusFile() {
14857
15225
  try {
14858
- unlinkSync2(this.stateDir.statusFile);
15226
+ unlinkSync3(this.stateDir.statusFile);
14859
15227
  } catch {}
14860
15228
  }
14861
15229
  markKilled() {
14862
15230
  this.stateDir.ensure();
14863
- writeFileSync(this.stateDir.killedFile, `${Date.now()}
15231
+ writeFileSync2(this.stateDir.killedFile, `${Date.now()}
14864
15232
  `, "utf-8");
14865
15233
  }
14866
15234
  clearKilled() {
14867
15235
  try {
14868
- unlinkSync2(this.stateDir.killedFile);
15236
+ unlinkSync3(this.stateDir.killedFile);
14869
15237
  } catch {}
14870
15238
  }
14871
15239
  wasKilled() {
@@ -14887,8 +15255,10 @@ class DaemonLifecycle {
14887
15255
  daemonProc.unref();
14888
15256
  }
14889
15257
  removeStalePidFile() {
14890
- this.log("Removing stale pid file");
15258
+ this.log("Removing stale daemon identity files");
14891
15259
  this.removePidFile();
15260
+ this.removeStatusFile();
15261
+ this.removeDaemonRecord();
14892
15262
  }
14893
15263
  async replaceUnhealthyDaemon(statusPid) {
14894
15264
  await this.withStartupLockStrict(async (locked) => {
@@ -14904,7 +15274,7 @@ class DaemonLifecycle {
14904
15274
  }
14905
15275
  if (isReuseVerdict(classification.verdict)) {
14906
15276
  try {
14907
- await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
15277
+ await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
14908
15278
  return;
14909
15279
  } catch {}
14910
15280
  }
@@ -14912,12 +15282,12 @@ class DaemonLifecycle {
14912
15282
  this.log(`Killing unhealthy daemon on control port ${this.controlPort} and relaunching`);
14913
15283
  await this.kill(3000, statusPid);
14914
15284
  this.launch();
14915
- await this.waitForReady();
15285
+ await this.waitForReady(this.timing.waitReadyRetries, this.timing.waitReadyDelayMs);
14916
15286
  });
14917
15287
  }
14918
15288
  async waitForContendedStartupLock() {
14919
15289
  this.log("Another process holds the startup lock, waiting for readiness+identity...");
14920
- await this.waitForReadyAndOurs();
15290
+ await this.waitForReadyAndOurs(this.timing.waitReadyRetries, this.timing.waitReadyDelayMs);
14921
15291
  }
14922
15292
  async withStartupLockStrict(fn) {
14923
15293
  const locked = this.acquireLockStrict();
@@ -14932,15 +15302,15 @@ class DaemonLifecycle {
14932
15302
  this.stateDir.ensure();
14933
15303
  let fd = null;
14934
15304
  try {
14935
- fd = openSync(this.stateDir.lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
14936
- writeFileSync(fd, `${process.pid}
15305
+ fd = openSync2(this.stateDir.lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
15306
+ writeFileSync2(fd, `${process.pid}
14937
15307
  `);
14938
- closeSync(fd);
15308
+ closeSync2(fd);
14939
15309
  return true;
14940
15310
  } catch (err) {
14941
15311
  if (fd !== null && err.code !== "EEXIST") {
14942
15312
  try {
14943
- closeSync(fd);
15313
+ closeSync2(fd);
14944
15314
  } catch {}
14945
15315
  this.releaseLock();
14946
15316
  }
@@ -14948,7 +15318,7 @@ class DaemonLifecycle {
14948
15318
  if (reclaimed)
14949
15319
  return false;
14950
15320
  try {
14951
- const holderPid = Number.parseInt(readFileSync(this.stateDir.lockFile, "utf-8").trim(), 10);
15321
+ const holderPid = Number.parseInt(readFileSync2(this.stateDir.lockFile, "utf-8").trim(), 10);
14952
15322
  if (Number.isFinite(holderPid) && !isProcessAlive(holderPid)) {
14953
15323
  this.log(`Stale startup lock from dead process ${holderPid}, reclaiming`);
14954
15324
  this.releaseLock();
@@ -14977,7 +15347,7 @@ class DaemonLifecycle {
14977
15347
  }
14978
15348
  releaseLock() {
14979
15349
  try {
14980
- unlinkSync2(this.stateDir.lockFile);
15350
+ unlinkSync3(this.stateDir.lockFile);
14981
15351
  } catch {}
14982
15352
  }
14983
15353
  async kill(gracefulTimeoutMs = 3000, pidOverride) {
@@ -15023,6 +15393,7 @@ class DaemonLifecycle {
15023
15393
  cleanup() {
15024
15394
  this.removePidFile();
15025
15395
  this.removeStatusFile();
15396
+ this.removeDaemonRecord();
15026
15397
  }
15027
15398
  }
15028
15399
  async function fetchWithTimeout(url, timeoutMs = HEALTH_FETCH_TIMEOUT_MS) {
@@ -15036,7 +15407,7 @@ async function fetchWithTimeout(url, timeoutMs = HEALTH_FETCH_TIMEOUT_MS) {
15036
15407
  }
15037
15408
 
15038
15409
  // src/config-service.ts
15039
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
15410
+ import { readFileSync as readFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
15040
15411
  import { join as join2 } from "path";
15041
15412
  var DEFAULT_BUDGET_CONFIG = {
15042
15413
  enabled: true,
@@ -15192,13 +15563,13 @@ function normalizeConfig(raw) {
15192
15563
  return {
15193
15564
  version: typeof config2.version === "string" ? config2.version : DEFAULT_CONFIG.version,
15194
15565
  codex: {
15195
- appPort: normalizeInteger(codex.appPort ?? daemon.port, DEFAULT_CONFIG.codex.appPort),
15196
- proxyPort: normalizeInteger(codex.proxyPort ?? daemon.proxyPort, DEFAULT_CONFIG.codex.proxyPort)
15566
+ appPort: normalizeBoundedInteger(codex.appPort ?? daemon.port, DEFAULT_CONFIG.codex.appPort, 1, 65535),
15567
+ proxyPort: normalizeBoundedInteger(codex.proxyPort ?? daemon.proxyPort, DEFAULT_CONFIG.codex.proxyPort, 1, 65535)
15197
15568
  },
15198
15569
  turnCoordination: {
15199
- attentionWindowSeconds: normalizeInteger(turnCoordination.attentionWindowSeconds, DEFAULT_CONFIG.turnCoordination.attentionWindowSeconds)
15570
+ attentionWindowSeconds: normalizeBoundedInteger(turnCoordination.attentionWindowSeconds, DEFAULT_CONFIG.turnCoordination.attentionWindowSeconds, 0, Number.MAX_SAFE_INTEGER)
15200
15571
  },
15201
- idleShutdownSeconds: normalizeInteger(config2.idleShutdownSeconds, DEFAULT_CONFIG.idleShutdownSeconds),
15572
+ idleShutdownSeconds: normalizeBoundedInteger(config2.idleShutdownSeconds, DEFAULT_CONFIG.idleShutdownSeconds, 1, Number.MAX_SAFE_INTEGER),
15202
15573
  budget: normalizeBudgetConfig(config2.budget)
15203
15574
  };
15204
15575
  }
@@ -15217,7 +15588,7 @@ class ConfigService {
15217
15588
  load() {
15218
15589
  let raw;
15219
15590
  try {
15220
- raw = readFileSync2(this.configPath, "utf-8");
15591
+ raw = readFileSync3(this.configPath, "utf-8");
15221
15592
  } catch (err) {
15222
15593
  if (err?.code === "ENOENT") {
15223
15594
  return { state: "absent" };
@@ -15270,9 +15641,7 @@ class ConfigService {
15270
15641
  };
15271
15642
  }
15272
15643
  save(config2) {
15273
- this.ensureConfigDir();
15274
- writeFileSync2(this.configPath, JSON.stringify(config2, null, 2) + `
15275
- `, "utf-8");
15644
+ atomicWriteJson(this.configPath, config2);
15276
15645
  }
15277
15646
  initDefaults() {
15278
15647
  this.ensureConfigDir();
@@ -15288,34 +15657,46 @@ class ConfigService {
15288
15657
  }
15289
15658
  ensureConfigDir() {
15290
15659
  if (!existsSync4(this.configDir)) {
15291
- mkdirSync2(this.configDir, { recursive: true });
15660
+ mkdirSync3(this.configDir, { recursive: true });
15292
15661
  }
15293
15662
  }
15294
15663
  }
15295
15664
 
15665
+ // src/cli-invocation.ts
15666
+ import { basename } from "path";
15667
+ var CLI_NAMES = ["abg", "agentbridge"];
15668
+ var DEFAULT_CLI_NAME = "abg";
15669
+ function cliInvocationName(argv = process.argv) {
15670
+ const raw = argv[1];
15671
+ if (typeof raw !== "string" || raw.length === 0)
15672
+ return DEFAULT_CLI_NAME;
15673
+ const name = basename(raw).replace(/\.(ts|js|mjs|cjs)$/, "");
15674
+ return isCliName(name) ? name : DEFAULT_CLI_NAME;
15675
+ }
15676
+ function isCliName(value) {
15677
+ return CLI_NAMES.includes(value);
15678
+ }
15679
+
15296
15680
  // src/pair-registry.ts
15297
15681
  import {
15298
- closeSync as closeSync2,
15299
15682
  existsSync as existsSync5,
15300
- fsyncSync,
15301
15683
  linkSync,
15302
15684
  lstatSync,
15303
- mkdirSync as mkdirSync3,
15304
- openSync as openSync2,
15685
+ mkdirSync as mkdirSync4,
15305
15686
  readdirSync,
15306
- readFileSync as readFileSync3,
15687
+ readFileSync as readFileSync4,
15307
15688
  realpathSync,
15308
- renameSync as renameSync2,
15309
15689
  rmSync,
15310
15690
  statSync as statSync3,
15311
- unlinkSync as unlinkSync3,
15691
+ unlinkSync as unlinkSync4,
15312
15692
  writeFileSync as writeFileSync3
15313
15693
  } from "fs";
15314
- import { createHash, randomUUID as randomUUID2 } from "crypto";
15315
- import { basename, join as join3, resolve, sep } from "path";
15694
+ import { createHash, randomUUID as randomUUID3 } from "crypto";
15695
+ import { basename as basename2, join as join3, resolve, sep } from "path";
15316
15696
  var PAIR_BASE_PORT = 4500;
15317
15697
  var PAIR_SLOT_STRIDE = 10;
15318
15698
  var PAIR_ID_REGEX = /^[A-Za-z0-9._-]{1,64}$/;
15699
+ var RECLAIMABLE_MIN_AGE_MS = 24 * 60 * 60 * 1000;
15319
15700
  var REGISTRY_FILE_NAME = "registry.json";
15320
15701
  class PairError extends Error {
15321
15702
  code;
@@ -15351,7 +15732,7 @@ function readRegistry(base) {
15351
15732
  return { version: 1, pairs: [] };
15352
15733
  let parsed;
15353
15734
  try {
15354
- parsed = JSON.parse(readFileSync3(path, "utf-8"));
15735
+ parsed = JSON.parse(readFileSync4(path, "utf-8"));
15355
15736
  } catch (err) {
15356
15737
  throw new PairError("PAIR_REGISTRY_CORRUPT", `Registry JSON is not parseable at ${path}: ${err.message}`, {
15357
15738
  path
@@ -15392,10 +15773,10 @@ function findPair(base, pairId) {
15392
15773
  }
15393
15774
 
15394
15775
  // src/pair-command.ts
15395
- function pairScopedCommand(cmd) {
15776
+ function pairScopedCommand(cmd, name = cliInvocationName()) {
15396
15777
  const pairId = process.env.AGENTBRIDGE_PAIR_ID;
15397
15778
  if (!pairId)
15398
- return `agentbridge ${cmd}`;
15779
+ return `${name} ${cmd}`;
15399
15780
  let selector = process.env.AGENTBRIDGE_PAIR_NAME;
15400
15781
  if (!selector) {
15401
15782
  try {
@@ -15404,10 +15785,13 @@ function pairScopedCommand(cmd) {
15404
15785
  selector = pairId;
15405
15786
  }
15406
15787
  }
15407
- return `agentbridge --pair ${selector} ${cmd}`;
15788
+ return `${name} --pair ${selector} ${cmd}`;
15408
15789
  }
15409
15790
 
15410
15791
  // src/bridge-disabled-state.ts
15792
+ function shouldEmitReconnectSuccess(state) {
15793
+ return !state.daemonDisabled;
15794
+ }
15411
15795
  function disabledReplyError(reason) {
15412
15796
  const claudeCmd = pairScopedCommand("claude");
15413
15797
  switch (reason) {
@@ -15420,7 +15804,7 @@ function disabledReplyError(reason) {
15420
15804
  case "auto_recovery_exhausted":
15421
15805
  return `AgentBridge auto-recovery gave up after exhausting its retry budget for the in-flight liveness probe contention. Retry manually with \`${claudeCmd}\`.`;
15422
15806
  case "killed":
15423
- return `AgentBridge is disabled by \`agentbridge kill\`. Restart Claude Code (\`${claudeCmd}\`), switch to a new conversation, or run \`/resume\` to reconnect.`;
15807
+ return `AgentBridge is disabled by \`${pairScopedCommand("kill")}\`. Restart Claude Code (\`${claudeCmd}\`), switch to a new conversation, or run \`/resume\` to reconnect.`;
15424
15808
  }
15425
15809
  }
15426
15810
 
@@ -15499,9 +15883,25 @@ function nonEmpty(value) {
15499
15883
  return value && value.length > 0 ? value : null;
15500
15884
  }
15501
15885
 
15502
- // src/trace-log.ts
15503
- import { appendFileSync as appendFileSync2, existsSync as existsSync6, mkdirSync as mkdirSync4, readdirSync as readdirSync2, statSync as statSync4, unlinkSync as unlinkSync4 } from "fs";
15886
+ // src/control-token.ts
15887
+ import { chmodSync, readFileSync as readFileSync5 } from "fs";
15504
15888
  import { join as join4 } from "path";
15889
+ var CONTROL_TOKEN_FILENAME = "control-token";
15890
+ function resolveControlTokenPath(stateDir) {
15891
+ return join4(stateDir, CONTROL_TOKEN_FILENAME);
15892
+ }
15893
+ function readControlToken(path) {
15894
+ try {
15895
+ const raw = readFileSync5(path, "utf-8").trim();
15896
+ return raw.length > 0 ? raw : null;
15897
+ } catch {
15898
+ return null;
15899
+ }
15900
+ }
15901
+
15902
+ // src/trace-log.ts
15903
+ import { appendFileSync as appendFileSync2, existsSync as existsSync6, mkdirSync as mkdirSync5, readdirSync as readdirSync2, statSync as statSync4, unlinkSync as unlinkSync5 } from "fs";
15904
+ import { join as join5 } from "path";
15505
15905
  var TRACE_RETENTION_DAYS = 7;
15506
15906
  var TRACE_FILE_RE = /^trace-\d{4}-\d{2}-\d{2}\.jsonl$/;
15507
15907
  var SECRET_KEY_RE = /(token|secret|password|passwd|api[_-]?key|auth|cookie|session)/i;
@@ -15541,7 +15941,7 @@ function redactArgv(argv) {
15541
15941
  }
15542
15942
  function traceLogPath(cwd, timestamp) {
15543
15943
  const day = timestamp.slice(0, 10);
15544
- return join4(cwd, ".agentbridge", "logs", `trace-${day}.jsonl`);
15944
+ return join5(cwd, ".agentbridge", "logs", `trace-${day}.jsonl`);
15545
15945
  }
15546
15946
  function appendTraceEvent(input) {
15547
15947
  const timestamp = input.timestamp ?? new Date().toISOString();
@@ -15555,9 +15955,9 @@ function appendTraceEvent(input) {
15555
15955
  ...input.env ? { env: pickRelevantEnv(input.env) } : {},
15556
15956
  ...input.data ? { data: redactData(input.data) } : {}
15557
15957
  };
15558
- const logsDir = join4(input.cwd, ".agentbridge", "logs");
15958
+ const logsDir = join5(input.cwd, ".agentbridge", "logs");
15559
15959
  const isNewDayFile = !existsSync6(path);
15560
- mkdirSync4(logsDir, { recursive: true });
15960
+ mkdirSync5(logsDir, { recursive: true });
15561
15961
  if (isNewDayFile) {
15562
15962
  pruneOldTraceLogs(logsDir, path, Date.parse(timestamp));
15563
15963
  }
@@ -15578,12 +15978,12 @@ function pruneOldTraceLogs(logsDir, keepPath, nowMs) {
15578
15978
  for (const name of entries) {
15579
15979
  if (!TRACE_FILE_RE.test(name))
15580
15980
  continue;
15581
- const filePath = join4(logsDir, name);
15981
+ const filePath = join5(logsDir, name);
15582
15982
  if (filePath === keepPath)
15583
15983
  continue;
15584
15984
  try {
15585
15985
  if (statSync4(filePath).mtimeMs < cutoff) {
15586
- unlinkSync4(filePath);
15986
+ unlinkSync5(filePath);
15587
15987
  }
15588
15988
  } catch {}
15589
15989
  }
@@ -15633,7 +16033,7 @@ var CONTROL_PORT = parseInt(process.env.AGENTBRIDGE_CONTROL_PORT ?? "4502", 10);
15633
16033
  var daemonLifecycle = new DaemonLifecycle({ stateDir, controlPort: CONTROL_PORT, log });
15634
16034
  var CONTROL_WS_URL = daemonLifecycle.controlWsUrl;
15635
16035
  var claude = new ClaudeAdapter(stateDir.logFile);
15636
- var daemonClient = new DaemonClient(CONTROL_WS_URL, { identity: currentClientIdentity() });
16036
+ var daemonClient = new DaemonClient(CONTROL_WS_URL, { identity: currentClientIdentity });
15637
16037
  var shuttingDown = false;
15638
16038
  var daemonDisabled = false;
15639
16039
  var daemonDisabledReason = null;
@@ -15646,6 +16046,7 @@ var lastReconnectNotifyTs = 0;
15646
16046
  var disabledRecoveryTimer = null;
15647
16047
  var disabledRecoveryInFlight = false;
15648
16048
  var disabledRecoveryAttempts = 0;
16049
+ var nextSystemMessageId = 0;
15649
16050
  var DISABLED_RECOVERY_MAX_ATTEMPTS = 6;
15650
16051
  var DISABLED_RECOVERY_CONFIRM_TIMEOUT_MS = 1000;
15651
16052
  if (process.env.AGENTBRIDGE_TRACE === "1") {
@@ -15740,6 +16141,16 @@ daemonClient.on("rejected", async (code) => {
15740
16141
  notificationId = "system_bridge_pair_mismatch";
15741
16142
  notificationContent = `\u26A0\uFE0F AgentBridge daemon rejected this session \u2014 pair/cwd identity mismatch (this daemon belongs to a different pair or directory). Do NOT kill it; start Claude Code from the pair's own directory, or pick another pair name with \`agentbridge --pair <name> claude\`. AgentBridge \u62D2\u7EDD\u4E86\u6B64\u4F1A\u8BDD\u2014\u2014pair/\u76EE\u5F55\u8EAB\u4EFD\u4E0D\u5339\u914D\uFF08\u8BE5 daemon \u5C5E\u4E8E\u5176\u4ED6 pair \u6216\u76EE\u5F55\uFF09\u3002\u65E0\u9700 kill\uFF1B\u8BF7\u5230\u5BF9\u5E94\u76EE\u5F55\u542F\u52A8\uFF0C\u6216\u6362\u4E00\u4E2A pair \u540D\uFF1A\`agentbridge --pair <\u540D\u5B57> claude\`\u3002`;
15742
16143
  break;
16144
+ case CLOSE_CODE_TOKEN_MISMATCH:
16145
+ reason = "rejected";
16146
+ notificationId = "system_bridge_token_mismatch";
16147
+ notificationContent = `\u26A0\uFE0F AgentBridge daemon rejected this session \u2014 control token mismatch (the daemon likely restarted and rotated its token). Start a fresh session with \`${pairScopedCommand("claude")}\` to pick up the current token. AgentBridge \u62D2\u7EDD\u4E86\u6B64\u4F1A\u8BDD\u2014\u2014\u63A7\u5236\u4EE4\u724C\u4E0D\u5339\u914D\uFF08daemon \u53EF\u80FD\u5DF2\u91CD\u542F\u5E76\u8F6E\u6362\u4EE4\u724C\uFF09\u3002\u8BF7\u7528 \`${pairScopedCommand("claude")}\` \u91CD\u65B0\u542F\u52A8\u4EE5\u83B7\u53D6\u6700\u65B0\u4EE4\u724C\u3002`;
16148
+ break;
16149
+ case CLOSE_CODE_CONTRACT_MISMATCH:
16150
+ reason = "rejected";
16151
+ notificationId = "system_bridge_contract_mismatch";
16152
+ notificationContent = `\u26A0\uFE0F AgentBridge daemon rejected this session \u2014 protocol contract mismatch. The installed plugin and the running daemon are built from out-of-sync protocol versions. Run \`bun run install:global\` to rebuild + reinstall, then close and reopen Claude Code. Do NOT kill other pairs \u2014 this is local build skew, not a session conflict. AgentBridge \u62D2\u7EDD\u4E86\u6B64\u4F1A\u8BDD\u2014\u2014\u534F\u8BAE\u5951\u7EA6\u7248\u672C\u4E0D\u5339\u914D\u3002\u5DF2\u5B89\u88C5\u7684\u63D2\u4EF6\u4E0E\u8FD0\u884C\u4E2D\u7684 daemon \u534F\u8BAE\u7248\u672C\u4E0D\u4E00\u81F4\u3002\u8BF7\u8FD0\u884C \`bun run install:global\` \u91CD\u65B0\u7F16\u8BD1\u5E76\u5B89\u88C5\uFF0C\u7136\u540E\u5173\u95ED\u5E76\u91CD\u65B0\u6253\u5F00 Claude Code\u3002\u8BF7\u52FF kill \u5176\u5B83 pair\u2014\u2014\u8FD9\u662F\u672C\u5730\u6784\u5EFA\u7248\u672C\u6F02\u79FB\uFF0C\u4E0D\u662F\u4F1A\u8BDD\u51B2\u7A81\u3002`;
16153
+ break;
15743
16154
  default:
15744
16155
  reason = "rejected";
15745
16156
  notificationId = "system_bridge_replaced";
@@ -15759,7 +16170,7 @@ daemonClient.on("rejected", async (code) => {
15759
16170
  claude.on("ready", async () => {
15760
16171
  log("MCP server ready (push delivery) \u2014 ensuring AgentBridge daemon...");
15761
16172
  if (daemonLifecycle.wasKilled()) {
15762
- await enterDisabledState("Killed sentinel found \u2014 bridge staying idle", `\u26D4 AgentBridge was stopped by \`agentbridge kill\`. Bridge is staying idle. Restart Claude Code (\`${pairScopedCommand("claude")}\`), switch to a new conversation, or run \`/resume\` to reconnect.`);
16173
+ await enterDisabledState("Killed sentinel found \u2014 bridge staying idle", `\u26D4 AgentBridge was stopped by \`${pairScopedCommand("kill")}\`. Bridge is staying idle. Restart Claude Code (\`${pairScopedCommand("claude")}\`), switch to a new conversation, or run \`/resume\` to reconnect.`);
15763
16174
  return;
15764
16175
  }
15765
16176
  try {
@@ -15821,7 +16232,7 @@ var reconnectTask = null;
15821
16232
  async function notifyIfDaemonKilled(logMessage) {
15822
16233
  if (!daemonLifecycle.wasKilled())
15823
16234
  return false;
15824
- await enterDisabledState(logMessage, `\u26D4 AgentBridge was stopped by \`agentbridge kill\`. Bridge is staying idle. Restart Claude Code (\`${pairScopedCommand("claude")}\`), switch to a new conversation, or run \`/resume\` to reconnect.`);
16235
+ await enterDisabledState(logMessage, `\u26D4 AgentBridge was stopped by \`${pairScopedCommand("kill")}\`. Bridge is staying idle. Restart Claude Code (\`${pairScopedCommand("claude")}\`), switch to a new conversation, or run \`/resume\` to reconnect.`);
15825
16236
  return true;
15826
16237
  }
15827
16238
  async function notifyIfPairRemoved(logMessage) {
@@ -15858,6 +16269,9 @@ function reconnectToDaemon() {
15858
16269
  }
15859
16270
  try {
15860
16271
  await connectToDaemon(true);
16272
+ if (!shouldEmitReconnectSuccess({ daemonDisabled })) {
16273
+ return;
16274
+ }
15861
16275
  log("Reconnected to AgentBridge daemon successfully");
15862
16276
  const now = Date.now();
15863
16277
  if (now - lastReconnectNotifyTs >= RECONNECT_NOTIFY_COOLDOWN_MS) {
@@ -15976,13 +16390,14 @@ async function pollDisabledRecovery() {
15976
16390
  }
15977
16391
  function systemMessage(idPrefix, content) {
15978
16392
  return {
15979
- id: `${idPrefix}_${Date.now()}`,
16393
+ id: `${idPrefix}_${++nextSystemMessageId}`,
15980
16394
  source: "codex",
15981
16395
  content,
15982
16396
  timestamp: Date.now()
15983
16397
  };
15984
16398
  }
15985
16399
  function currentClientIdentity() {
16400
+ const controlToken = readControlToken(resolveControlTokenPath(stateDir.dir));
15986
16401
  return {
15987
16402
  pairId: process.env.AGENTBRIDGE_PAIR_ID ?? null,
15988
16403
  pairName: process.env.AGENTBRIDGE_PAIR_NAME ?? null,
@@ -15990,7 +16405,8 @@ function currentClientIdentity() {
15990
16405
  baseDir: process.env.AGENTBRIDGE_BASE_DIR ?? null,
15991
16406
  stateDir: stateDir.dir,
15992
16407
  clientPid: process.pid,
15993
- contractVersion: BUILD_INFO.contractVersion
16408
+ contractVersion: BUILD_INFO.contractVersion,
16409
+ ...controlToken ? { controlToken } : {}
15994
16410
  };
15995
16411
  }
15996
16412
  function shutdown(reason) {