@raysonmeng/agentbridge 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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");
@@ -13859,7 +13863,48 @@ class StateDirResolver {
13859
13863
  }
13860
13864
  }
13861
13865
 
13866
+ // src/budget/types.ts
13867
+ var STALE_MAX_AGE_SEC = 600;
13868
+
13869
+ // src/budget/budget-state.ts
13870
+ function isDecisionGrade(usage, now) {
13871
+ if (!usage)
13872
+ return false;
13873
+ const freshWindow = usage.fiveHour !== null && usage.fiveHour.resetEpoch > now || usage.weekly !== null && usage.weekly.resetEpoch > now;
13874
+ if (!freshWindow)
13875
+ return false;
13876
+ if (usage.fetchedAt > 0 && now - usage.fetchedAt > STALE_MAX_AGE_SEC)
13877
+ return false;
13878
+ return true;
13879
+ }
13880
+
13881
+ // src/budget/burn-view.ts
13882
+ function agentWeeklyFiveHourWindowsLeft(usage, now) {
13883
+ if (!usage || usage.stale || !usage.ok)
13884
+ return null;
13885
+ if (!isDecisionGrade(usage, now))
13886
+ return null;
13887
+ const weekly = usage.weekly;
13888
+ if (!weekly || weekly.resetEpoch <= now)
13889
+ return null;
13890
+ if (weekly.burnConfident !== true)
13891
+ return null;
13892
+ if (weekly.runwaySeconds === undefined)
13893
+ return null;
13894
+ return weekly.fiveHourWindowsLeft ?? null;
13895
+ }
13896
+
13862
13897
  // src/budget/render.ts
13898
+ var DEFAULT_GUARD_HARD_PCT = 92;
13899
+ function resolveGuardHardHint(env = process.env) {
13900
+ const raw = env.AGENTBRIDGE_GUARD_HARD_HINT;
13901
+ if (raw === undefined || raw.trim() === "")
13902
+ return DEFAULT_GUARD_HARD_PCT;
13903
+ const parsed = Number(raw);
13904
+ if (!Number.isFinite(parsed) || parsed < 1 || parsed > 100)
13905
+ return DEFAULT_GUARD_HARD_PCT;
13906
+ return parsed;
13907
+ }
13863
13908
  function formatEpoch(epochSeconds) {
13864
13909
  if (!epochSeconds || epochSeconds <= 0)
13865
13910
  return "\u672A\u77E5";
@@ -13893,17 +13938,98 @@ function formatAgent(name, usage, snapshotAt) {
13893
13938
  }
13894
13939
  return `${name}\uFF1A${parts.join(" \xB7 ")}`;
13895
13940
  }
13941
+ var WINDOW_LABELS = {
13942
+ fiveHour: "5h \u7A97\u53E3",
13943
+ weekly: "\u5468\u7A97\u53E3"
13944
+ };
13945
+ var RESET_TRUNCATION_EPSILON_SEC = 60;
13946
+ function formatDuration(seconds) {
13947
+ const totalMinutes = Math.max(0, Math.round(seconds / 60));
13948
+ const hours = Math.floor(totalMinutes / 60);
13949
+ const minutes = totalMinutes % 60;
13950
+ if (hours === 0)
13951
+ return `${minutes}\u5206\u949F`;
13952
+ return `${hours}\u5C0F\u65F6${minutes}\u5206\u949F`;
13953
+ }
13954
+ function formatClockTime(epochSeconds) {
13955
+ const date4 = new Date(epochSeconds * 1000);
13956
+ const hh = String(date4.getHours()).padStart(2, "0");
13957
+ const mm = String(date4.getMinutes()).padStart(2, "0");
13958
+ return `${hh}:${mm}`;
13959
+ }
13960
+ function formatWindowRate(label, rate) {
13961
+ if (!rate)
13962
+ return null;
13963
+ if (!rate.confident)
13964
+ return `${label} \u91C7\u6837\u4E2D`;
13965
+ return `${label} \u2248${rate.pctPerHour.toFixed(2)}%/h`;
13966
+ }
13967
+ function formatRunwaySegment(runway, basisWindow, snapshotAt) {
13968
+ const truncatedByReset = basisWindow !== null && basisWindow.resetEpoch > 0 && snapshotAt + runway.seconds >= basisWindow.resetEpoch - RESET_TRUNCATION_EPSILON_SEC;
13969
+ const clock = runway.depletedAtEpoch ? formatClockTime(runway.depletedAtEpoch) : null;
13970
+ let clockNote;
13971
+ if (clock) {
13972
+ clockNote = truncatedByReset ? `\u81F3 ${clock} \u7A97\u53E3\u5237\u65B0\u5373\u622A\u65AD\uFF0C` : `\u81F3 ${clock}\uFF0C`;
13973
+ } else {
13974
+ clockNote = truncatedByReset ? "\u7A97\u53E3\u5237\u65B0\u5373\u622A\u65AD\uFF0C" : "";
13975
+ }
13976
+ return `\u7EA6\u53EF\u518D\u5DE5\u4F5C ${formatDuration(runway.seconds)}\uFF08${clockNote}${WINDOW_LABELS[runway.basis]}\u4E3A\u7EA6\u675F\uFF09`;
13977
+ }
13978
+ function formatBurnRateLine(name, usage, rates, runway, snapshotAt, guardHardPct) {
13979
+ const parts = [
13980
+ formatWindowRate("5h", rates.fiveHour),
13981
+ formatWindowRate("\u5468", rates.weekly)
13982
+ ].filter((part) => part !== null);
13983
+ if (parts.length === 0 && !runway)
13984
+ return null;
13985
+ if (runway) {
13986
+ const basisWindow = usage ? usage[runway.basis] : null;
13987
+ parts.push(formatRunwaySegment(runway, basisWindow, snapshotAt));
13988
+ }
13989
+ if (guardHardPct !== null) {
13990
+ parts.push(`\u5916\u5C42 guard \u786C\u7EBF ${guardHardPct}%\uFF08v3 \u4E0D\u53EF\u8D8A\u8FC7\uFF1Brunway \u4E3A\u4E2D\u6027\u53E3\u5F84\uFF0CClaude \u4F1A\u5148\u5728\u786C\u7EBF\u88AB\u5916\u5C42\u505C\u4F4F\uFF09`);
13991
+ }
13992
+ return `${name} \u71C3\u5C3D\u7387\uFF1A${parts.join(" \xB7 ")}`;
13993
+ }
13994
+ function formatFiveHourWindowsLeftLine(snapshot) {
13995
+ const values = [];
13996
+ const claude = agentWeeklyFiveHourWindowsLeft(snapshot.claude, snapshot.updatedAt);
13997
+ const codex = agentWeeklyFiveHourWindowsLeft(snapshot.codex, snapshot.updatedAt);
13998
+ if (claude !== null)
13999
+ values.push(["Claude", claude]);
14000
+ if (codex !== null)
14001
+ values.push(["Codex", codex]);
14002
+ if (values.length === 0)
14003
+ return null;
14004
+ const unique = [...new Set(values.map(([, value]) => value.toFixed(1)))];
14005
+ if (unique.length === 1)
14006
+ return `\u6309\u5F53\u524D\u8282\u594F\uFF0C\u5468\u989D\u5EA6\u8FD8\u591F ~${unique[0]} \u4E2A 5h \u7A97\u53E3`;
14007
+ const byAgent = values.map(([name, value]) => `${name} ~${value.toFixed(1)}`).join(" / ");
14008
+ return `\u6309\u5F53\u524D\u8282\u594F\uFF0C\u5468\u989D\u5EA6\u8FD8\u591F ${byAgent} \u4E2A 5h \u7A97\u53E3`;
14009
+ }
13896
14010
  var PHASE_LABELS = {
13897
14011
  normal: "normal\uFF08\u6B63\u5E38\uFF09",
13898
14012
  balance: "balance\uFF08\u9700\u5747\u8861\uFF09",
13899
14013
  parallel: "parallel\uFF08\u5EFA\u8BAE\u5E76\u884C\u63D0\u901F\uFF09",
13900
14014
  paused: "paused\uFF08\u9884\u7B97\u5E72\u9884\u4E2D\uFF09"
13901
14015
  };
13902
- function renderBudgetSnapshot(snapshot) {
14016
+ function renderBudgetSnapshot(snapshot, options = {}) {
14017
+ const guardHardPct = options.guardHardPct ?? resolveGuardHardHint();
13903
14018
  const lines = [];
13904
14019
  lines.push(`\u3010\u9884\u7B97\u5FEB\u7167 \xB7 \u8D26\u53F7\u7EA7\u3011\u9636\u6BB5\uFF1A${PHASE_LABELS[snapshot.phase]} \xB7 \u66F4\u65B0\u4E8E ${formatEpoch(snapshot.updatedAt)}`);
13905
14020
  lines.push(formatAgent("Claude", snapshot.claude, snapshot.updatedAt));
13906
14021
  lines.push(formatAgent("Codex", snapshot.codex, snapshot.updatedAt));
14022
+ if (snapshot.burnRate) {
14023
+ const claudeLine = formatBurnRateLine("Claude", snapshot.claude, snapshot.burnRate.claude, snapshot.runway?.claude ?? null, snapshot.updatedAt, guardHardPct);
14024
+ if (claudeLine)
14025
+ lines.push(claudeLine);
14026
+ const codexLine = formatBurnRateLine("Codex", snapshot.codex, snapshot.burnRate.codex, snapshot.runway?.codex ?? null, snapshot.updatedAt, null);
14027
+ if (codexLine)
14028
+ lines.push(codexLine);
14029
+ }
14030
+ const fiveHourWindowsLeftLine = formatFiveHourWindowsLeftLine(snapshot);
14031
+ if (fiveHourWindowsLeftLine)
14032
+ lines.push(fiveHourWindowsLeftLine);
13907
14033
  if (snapshot.claude && snapshot.codex) {
13908
14034
  const abs = Math.abs(snapshot.driftPct);
13909
14035
  if (abs > 0) {
@@ -13943,6 +14069,10 @@ function renderBudgetSnapshot(snapshot) {
13943
14069
  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
14070
 
13945
14071
  // src/claude-adapter.ts
14072
+ var DEFAULT_MAX_BUFFERED_MESSAGES = 100;
14073
+ var DEFAULT_MAX_BUFFERED_BYTES = 4 * 1024 * 1024;
14074
+ var DEFAULT_DEDUPE_CAPACITY = 2048;
14075
+ var DEFAULT_DEDUPE_TTL_MS = 20 * 60 * 1000;
13946
14076
  var CLAUDE_INSTRUCTIONS = [
13947
14077
  "Codex is an AI coding agent (OpenAI) running in a separate session on the same machine.",
13948
14078
  "",
@@ -13991,10 +14121,20 @@ class ClaudeAdapter extends EventEmitter {
13991
14121
  logFile;
13992
14122
  logger;
13993
14123
  pendingMessages = [];
14124
+ pendingMessageByteSizes = [];
14125
+ pendingMessageBytes = 0;
13994
14126
  maxBufferedMessages;
14127
+ maxBufferedBytes;
13995
14128
  droppedMessageCount = 0;
14129
+ oversizedMessageCount = 0;
14130
+ oversizedMessageBytes = 0;
14131
+ oversizedMessageSourceCounts = {};
14132
+ dedupeCapacity;
14133
+ dedupeTtlMs;
14134
+ monotonicNow;
14135
+ deliveredMessageIds = new Map;
13996
14136
  budgetSnapshot = null;
13997
- constructor(logFile = new StateDirResolver().logFile) {
14137
+ constructor(logFile = new StateDirResolver().logFile, options = {}) {
13998
14138
  super();
13999
14139
  this.logFile = logFile;
14000
14140
  this.logger = createProcessLogger({ component: "ClaudeAdapter", logFile: this.logFile });
@@ -14005,7 +14145,11 @@ class ClaudeAdapter extends EventEmitter {
14005
14145
  if (process.env.AGENTBRIDGE_MODE) {
14006
14146
  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
14147
  }
14008
- this.maxBufferedMessages = parseInt(process.env.AGENTBRIDGE_MAX_BUFFERED_MESSAGES ?? "100", 10);
14148
+ this.maxBufferedMessages = positiveIntegerOr(options.maxBufferedMessages, parsePositiveIntegerEnv("AGENTBRIDGE_MAX_BUFFERED_MESSAGES", DEFAULT_MAX_BUFFERED_MESSAGES));
14149
+ this.maxBufferedBytes = positiveIntegerOr(options.maxBufferedBytes, parsePositiveIntegerEnv("AGENTBRIDGE_MAX_BUFFERED_BYTES", DEFAULT_MAX_BUFFERED_BYTES));
14150
+ this.dedupeCapacity = positiveIntegerOr(options.dedupeCapacity, DEFAULT_DEDUPE_CAPACITY);
14151
+ this.dedupeTtlMs = positiveIntegerOr(options.dedupeTtlMs, DEFAULT_DEDUPE_TTL_MS);
14152
+ this.monotonicNow = options.now ?? (() => performance.now());
14009
14153
  this.server = new Server({ name: "agentbridge", version: "0.1.0" }, {
14010
14154
  capabilities: {
14011
14155
  experimental: { "claude/channel": {} },
@@ -14032,10 +14176,12 @@ class ClaudeAdapter extends EventEmitter {
14032
14176
  }
14033
14177
  async pushNotification(message) {
14034
14178
  this.log(`pushNotification (instance=${this.instanceId}, msgId=${message.id}, len=${message.content.length})`);
14179
+ if (!this.rememberDelivery(message))
14180
+ return;
14035
14181
  await this.pushViaChannel(message);
14036
14182
  }
14037
14183
  async pushViaChannel(message) {
14038
- const msgId = `codex_msg_${this.notificationIdPrefix}_${++this.notificationSeq}`;
14184
+ const deliveryAttemptId = `codex_msg_${this.notificationIdPrefix}_${++this.notificationSeq}`;
14039
14185
  const ts = new Date(message.timestamp).toISOString();
14040
14186
  try {
14041
14187
  await this.server.notification({
@@ -14044,7 +14190,8 @@ class ClaudeAdapter extends EventEmitter {
14044
14190
  content: message.content,
14045
14191
  meta: {
14046
14192
  chat_id: this.sessionId,
14047
- message_id: msgId,
14193
+ message_id: message.id,
14194
+ delivery_attempt_id: deliveryAttemptId,
14048
14195
  user: "Codex",
14049
14196
  user_id: "codex",
14050
14197
  ts,
@@ -14052,39 +14199,93 @@ class ClaudeAdapter extends EventEmitter {
14052
14199
  }
14053
14200
  }
14054
14201
  });
14055
- this.log(`Pushed notification: ${msgId}`);
14202
+ this.log(`Pushed notification: ${message.id} (attempt=${deliveryAttemptId})`);
14056
14203
  } catch (e) {
14057
14204
  this.log(`Push notification failed: ${e.message}`);
14058
14205
  this.queueFallbackMessage(message);
14059
14206
  }
14060
14207
  }
14208
+ rememberDelivery(message) {
14209
+ const now = this.monotonicNow();
14210
+ this.pruneDeliveredMessageIds(now);
14211
+ if (this.deliveredMessageIds.has(message.id)) {
14212
+ this.deliveredMessageIds.delete(message.id);
14213
+ this.deliveredMessageIds.set(message.id, now);
14214
+ this.log(`Duplicate Codex message suppressed (msgId=${message.id}, source=${message.source}, ` + `instance=${this.instanceId})`);
14215
+ return false;
14216
+ }
14217
+ this.deliveredMessageIds.set(message.id, now);
14218
+ while (this.deliveredMessageIds.size > this.dedupeCapacity) {
14219
+ const oldest = this.deliveredMessageIds.keys().next().value;
14220
+ if (oldest === undefined)
14221
+ break;
14222
+ this.deliveredMessageIds.delete(oldest);
14223
+ }
14224
+ return true;
14225
+ }
14226
+ pruneDeliveredMessageIds(now) {
14227
+ for (const [id, seenAt] of this.deliveredMessageIds) {
14228
+ if (now - seenAt <= this.dedupeTtlMs)
14229
+ break;
14230
+ this.deliveredMessageIds.delete(id);
14231
+ }
14232
+ }
14061
14233
  queueFallbackMessage(message) {
14062
- if (this.pendingMessages.length >= this.maxBufferedMessages) {
14063
- this.pendingMessages.shift();
14234
+ const messageBytes = utf8ByteLength(message.content);
14235
+ if (messageBytes > this.maxBufferedBytes) {
14236
+ this.oversizedMessageCount++;
14237
+ this.oversizedMessageBytes += messageBytes;
14238
+ this.oversizedMessageSourceCounts[message.source] = (this.oversizedMessageSourceCounts[message.source] ?? 0) + 1;
14239
+ this.log(`Fallback queue omitted oversized ${message.source} message ` + `(${formatBytes(messageBytes)} > ${formatBytes(this.maxBufferedBytes)}; ` + `total oversized: ${this.oversizedMessageCount})`);
14240
+ return;
14241
+ }
14242
+ let dropped = 0;
14243
+ while (this.pendingMessages.length >= this.maxBufferedMessages || this.pendingMessageBytes + messageBytes > this.maxBufferedBytes) {
14244
+ const droppedMessage = this.pendingMessages.shift();
14245
+ const droppedBytes = this.pendingMessageByteSizes.shift() ?? 0;
14246
+ if (!droppedMessage)
14247
+ break;
14248
+ this.pendingMessageBytes = Math.max(0, this.pendingMessageBytes - droppedBytes);
14064
14249
  this.droppedMessageCount++;
14065
- this.log(`Fallback queue full, dropped oldest message (total dropped: ${this.droppedMessageCount})`);
14250
+ dropped++;
14251
+ }
14252
+ if (dropped > 0) {
14253
+ 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
14254
  }
14067
14255
  this.pendingMessages.push(message);
14068
- this.log(`Queued fallback message (${this.pendingMessages.length} pending, instance=${this.instanceId})`);
14256
+ this.pendingMessageByteSizes.push(messageBytes);
14257
+ this.pendingMessageBytes += messageBytes;
14258
+ this.log(`Queued fallback message (${this.pendingMessages.length} pending, ` + `${formatBytes(this.pendingMessageBytes)} buffered, instance=${this.instanceId})`);
14069
14259
  }
14070
14260
  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) {
14261
+ this.log(`get_messages called (instance=${this.instanceId}, pending=${this.pendingMessages.length}, ` + `bytes=${this.pendingMessageBytes}, dropped=${this.droppedMessageCount}, oversized=${this.oversizedMessageCount})`);
14262
+ if (this.pendingMessages.length === 0 && this.droppedMessageCount === 0 && this.oversizedMessageCount === 0) {
14073
14263
  return {
14074
14264
  content: [{ type: "text", text: "No new messages from Codex." }]
14075
14265
  };
14076
14266
  }
14077
14267
  const messages = this.pendingMessages;
14078
14268
  this.pendingMessages = [];
14269
+ this.pendingMessageByteSizes = [];
14270
+ this.pendingMessageBytes = 0;
14079
14271
  const dropped = this.droppedMessageCount;
14080
14272
  this.droppedMessageCount = 0;
14273
+ const oversizedSourceCounts = this.oversizedMessageSourceCounts;
14274
+ const oversized = this.oversizedMessageCount;
14275
+ const oversizedBytes = this.oversizedMessageBytes;
14276
+ this.oversizedMessageSourceCounts = {};
14277
+ this.oversizedMessageCount = 0;
14278
+ this.oversizedMessageBytes = 0;
14081
14279
  const count = messages.length;
14082
- let header = `[${count} new message${count > 1 ? "s" : ""} from Codex]`;
14280
+ const notices = [];
14083
14281
  if (dropped > 0) {
14084
- header += ` (${dropped} older message${dropped > 1 ? "s" : ""} were dropped due to queue overflow)`;
14282
+ notices.push(`${dropped} older message${dropped > 1 ? "s" : ""} ` + `${dropped > 1 ? "were" : "was"} dropped due to fallback queue overflow`);
14283
+ }
14284
+ if (oversized > 0) {
14285
+ for (const [source, sourceCount] of Object.entries(oversizedSourceCounts)) {
14286
+ notices.push(`${sourceCount} oversized message${sourceCount === 1 ? "" : "s"} ` + `from ${formatSource(source)} omitted ` + `(>${formatBytes(this.maxBufferedBytes)})`);
14287
+ }
14085
14288
  }
14086
- header += `
14087
- chat_id: ${this.sessionId}`;
14088
14289
  const formatted = messages.map((msg, i) => {
14089
14290
  const ts = new Date(msg.timestamp).toISOString();
14090
14291
  return `---
@@ -14093,14 +14294,25 @@ Codex: ${msg.content}`;
14093
14294
  }).join(`
14094
14295
 
14095
14296
  `);
14096
- this.log(`get_messages returning ${count} message(s) (instance=${this.instanceId}, dropped=${dropped})`);
14297
+ const noticeText = notices.map((notice) => `WARNING: ${notice}`).join(`
14298
+ `);
14299
+ const parts = [];
14300
+ if (count > 0) {
14301
+ parts.push(`[${count} new message${count > 1 ? "s" : ""} from Codex]
14302
+ chat_id: ${this.sessionId}`);
14303
+ }
14304
+ if (noticeText)
14305
+ parts.push(noticeText);
14306
+ if (formatted)
14307
+ parts.push(formatted);
14308
+ this.log(`get_messages returning ${count} message(s) ` + `(instance=${this.instanceId}, dropped=${dropped}, oversized=${oversized}, oversizedBytes=${oversizedBytes})`);
14097
14309
  return {
14098
14310
  content: [
14099
14311
  {
14100
14312
  type: "text",
14101
- text: `${header}
14313
+ text: parts.join(`
14102
14314
 
14103
- ${formatted}`
14315
+ `)
14104
14316
  }
14105
14317
  ]
14106
14318
  };
@@ -14256,11 +14468,37 @@ ${formatted}`
14256
14468
  this.logger.log(msg);
14257
14469
  }
14258
14470
  }
14471
+ function parsePositiveIntegerEnv(name, fallback) {
14472
+ return positiveIntegerOr(parseInt(process.env[name] ?? "", 10), fallback);
14473
+ }
14474
+ function positiveIntegerOr(value, fallback) {
14475
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
14476
+ }
14477
+ function utf8ByteLength(value) {
14478
+ return Buffer.byteLength(value, "utf8");
14479
+ }
14480
+ function formatSource(source) {
14481
+ return source === "codex" ? "Codex" : "Claude";
14482
+ }
14483
+ function formatBytes(bytes) {
14484
+ if (bytes < 1024)
14485
+ return `${bytes}B`;
14486
+ if (bytes % (1024 * 1024) === 0)
14487
+ return `${bytes / (1024 * 1024)}MiB`;
14488
+ if (bytes % 1024 === 0)
14489
+ return `${bytes / 1024}KiB`;
14490
+ return `${bytes}B`;
14491
+ }
14259
14492
 
14260
14493
  // src/contract-version.ts
14261
14494
  var CONTRACT_VERSION = 1;
14262
14495
 
14263
14496
  // src/build-info.ts
14497
+ var CODE_HASH_SENTINEL = "source";
14498
+ function hasValidCodeHash(build) {
14499
+ const hash = build?.codeHash;
14500
+ return typeof hash === "string" && hash.length > 0 && hash !== CODE_HASH_SENTINEL;
14501
+ }
14264
14502
  function defineString(value, fallback) {
14265
14503
  return typeof value === "string" && value.length > 0 ? value : fallback;
14266
14504
  }
@@ -14273,15 +14511,23 @@ function defineNumber(value, fallback) {
14273
14511
  return typeof value === "number" && Number.isFinite(value) ? value : fallback;
14274
14512
  }
14275
14513
  var BUILD_INFO = Object.freeze({
14276
- version: defineString("0.1.12", "0.0.0-source"),
14277
- commit: defineString("eec6018", "source"),
14514
+ version: defineString("0.1.14", "0.0.0-source"),
14515
+ commit: defineString("f5a9562", "source"),
14278
14516
  bundle: defineBundle("plugin"),
14279
- contractVersion: defineNumber(1, CONTRACT_VERSION)
14517
+ contractVersion: defineNumber(1, CONTRACT_VERSION),
14518
+ codeHash: defineString("e05d18c3cc72", "source")
14280
14519
  });
14281
14520
  function sameRuntimeContract(a, b) {
14282
14521
  if (!a || !b)
14283
14522
  return false;
14284
- return a.version === b.version && a.commit === b.commit && a.contractVersion === b.contractVersion;
14523
+ if (a.version !== b.version || a.contractVersion !== b.contractVersion)
14524
+ return false;
14525
+ if (hasValidCodeHash(a) && hasValidCodeHash(b))
14526
+ return a.codeHash === b.codeHash;
14527
+ return a.commit === b.commit;
14528
+ }
14529
+ function runtimeContractComparisonBasis(a, b) {
14530
+ return hasValidCodeHash(a) && hasValidCodeHash(b) ? "codeHash" : "commit";
14285
14531
  }
14286
14532
  function compatibleContractVersion(a, b) {
14287
14533
  if (!a || !b)
@@ -14291,7 +14537,8 @@ function compatibleContractVersion(a, b) {
14291
14537
  function formatBuildInfo(build) {
14292
14538
  if (!build)
14293
14539
  return "<unknown>";
14294
- return `${build.version}/${build.commit}/${build.bundle}/contract-v${build.contractVersion}`;
14540
+ const codeHash = hasValidCodeHash(build) ? `/code-${build.codeHash}` : "";
14541
+ return `${build.version}/${build.commit}/${build.bundle}/contract-v${build.contractVersion}${codeHash}`;
14295
14542
  }
14296
14543
 
14297
14544
  // src/daemon-client.ts
@@ -14302,12 +14549,84 @@ var CLOSE_CODE_REPLACED = 4001;
14302
14549
  var CLOSE_CODE_EVICTED_STALE = 4002;
14303
14550
  var CLOSE_CODE_PROBE_IN_PROGRESS = 4003;
14304
14551
  var CLOSE_CODE_PAIR_MISMATCH = 4004;
14552
+ var CLOSE_CODE_TOKEN_MISMATCH = 4005;
14553
+ var CLOSE_CODE_CONTRACT_MISMATCH = 4006;
14305
14554
 
14306
14555
  // src/interrupt-timing.ts
14307
14556
  var CLIENT_REPLY_TIMEOUT_MS = 15000;
14308
14557
  var INTERRUPT_CLIENT_MARGIN_MS = 2000;
14309
14558
  var MAX_INTERRUPT_TIMEOUT_MS = CLIENT_REPLY_TIMEOUT_MS - INTERRUPT_CLIENT_MARGIN_MS;
14310
14559
 
14560
+ // src/pending-request-registry.ts
14561
+ class PendingRequestRegistry {
14562
+ entries = new Map;
14563
+ setTimer;
14564
+ clearTimer;
14565
+ constructor(deps = {}) {
14566
+ this.setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
14567
+ this.clearTimer = deps.clearTimer ?? ((handle) => clearTimeout(handle));
14568
+ }
14569
+ get size() {
14570
+ return this.entries.size;
14571
+ }
14572
+ has(id) {
14573
+ return this.entries.has(id);
14574
+ }
14575
+ register(id, options) {
14576
+ const existing = this.entries.get(id);
14577
+ if (existing) {
14578
+ this.clearTimer(existing.timer);
14579
+ this.entries.delete(id);
14580
+ }
14581
+ return new Promise((resolve, reject) => {
14582
+ const timer = this.setTimer(() => {
14583
+ if (!this.entries.has(id))
14584
+ return;
14585
+ this.entries.delete(id);
14586
+ options.onTimeout({ resolve, reject });
14587
+ }, options.timeoutMs);
14588
+ if (options.unref) {
14589
+ timer.unref?.();
14590
+ }
14591
+ this.entries.set(id, { resolve, reject, timer });
14592
+ });
14593
+ }
14594
+ settle(id, value) {
14595
+ const entry = this.entries.get(id);
14596
+ if (!entry)
14597
+ return false;
14598
+ this.clearTimer(entry.timer);
14599
+ this.entries.delete(id);
14600
+ entry.resolve(value);
14601
+ return true;
14602
+ }
14603
+ reject(id, error2) {
14604
+ const entry = this.entries.get(id);
14605
+ if (!entry)
14606
+ return false;
14607
+ this.clearTimer(entry.timer);
14608
+ this.entries.delete(id);
14609
+ entry.reject(error2);
14610
+ return true;
14611
+ }
14612
+ settleAll(value) {
14613
+ const make = typeof value === "function" ? value : () => value;
14614
+ for (const [id, entry] of this.entries) {
14615
+ this.clearTimer(entry.timer);
14616
+ this.entries.delete(id);
14617
+ entry.resolve(make(id));
14618
+ }
14619
+ }
14620
+ rejectAll(error2) {
14621
+ const make = typeof error2 === "function" ? error2 : () => error2;
14622
+ for (const [id, entry] of this.entries) {
14623
+ this.clearTimer(entry.timer);
14624
+ this.entries.delete(id);
14625
+ entry.reject(make(id));
14626
+ }
14627
+ }
14628
+ }
14629
+
14311
14630
  // src/daemon-client.ts
14312
14631
  var nextSocketId = 0;
14313
14632
 
@@ -14317,7 +14636,8 @@ class DaemonClient extends EventEmitter2 {
14317
14636
  ws = null;
14318
14637
  wsId = 0;
14319
14638
  nextRequestId = 1;
14320
- pendingReplies = new Map;
14639
+ pendingReplies = new PendingRequestRegistry;
14640
+ pendingEventWaiters = new PendingRequestRegistry;
14321
14641
  constructor(url, options = {}) {
14322
14642
  super();
14323
14643
  this.url = url;
@@ -14363,82 +14683,73 @@ class DaemonClient extends EventEmitter2 {
14363
14683
  });
14364
14684
  }
14365
14685
  attachClaude() {
14686
+ const identity = this.resolveIdentity();
14366
14687
  this.send({
14367
14688
  type: "claude_connect",
14368
- ...this.options.identity ? { identity: this.options.identity } : {}
14689
+ ...identity ? { identity } : {}
14369
14690
  });
14370
14691
  }
14692
+ resolveIdentity() {
14693
+ const opt = this.options.identity;
14694
+ return typeof opt === "function" ? opt() : opt;
14695
+ }
14371
14696
  async attachClaudeAndWaitForStatus(timeoutMs = 1000) {
14372
14697
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
14373
14698
  return null;
14374
14699
  }
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
- }
14700
+ return this.awaitTypedResponse({
14701
+ key: "status",
14702
+ successEvent: "status",
14703
+ successValue: (status) => status,
14704
+ failValue: null,
14705
+ timeoutMs,
14706
+ send: () => this.attachClaude()
14408
14707
  });
14409
14708
  }
14410
14709
  async probeIncumbent(timeoutMs = 3000) {
14411
14710
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
14412
14711
  return { connected: false, alive: false };
14413
14712
  }
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
- }
14713
+ return this.awaitTypedResponse({
14714
+ key: "incumbent_status",
14715
+ successEvent: "incumbentStatus",
14716
+ successValue: (s) => s,
14717
+ failValue: { connected: false, alive: false },
14718
+ timeoutMs,
14719
+ send: () => this.send({ type: "probe_incumbent" })
14440
14720
  });
14441
14721
  }
14722
+ awaitTypedResponse(opts) {
14723
+ const { key, successEvent, successValue, failValue, timeoutMs, send } = opts;
14724
+ const onSuccess = (payload) => {
14725
+ this.pendingEventWaiters.settle(key, successValue(payload));
14726
+ };
14727
+ const onRejected = () => {
14728
+ this.pendingEventWaiters.settle(key, failValue);
14729
+ };
14730
+ const onDisconnect = () => {
14731
+ this.pendingEventWaiters.settle(key, failValue);
14732
+ };
14733
+ const pending = this.pendingEventWaiters.register(key, {
14734
+ timeoutMs,
14735
+ onTimeout: ({ resolve }) => resolve(failValue)
14736
+ });
14737
+ const cleanup = () => {
14738
+ this.off(successEvent, onSuccess);
14739
+ this.off("rejected", onRejected);
14740
+ this.off("disconnect", onDisconnect);
14741
+ };
14742
+ pending.finally(cleanup);
14743
+ this.on(successEvent, onSuccess);
14744
+ this.on("rejected", onRejected);
14745
+ this.on("disconnect", onDisconnect);
14746
+ try {
14747
+ send();
14748
+ } catch {
14749
+ this.pendingEventWaiters.settle(key, failValue);
14750
+ }
14751
+ return pending;
14752
+ }
14442
14753
  async disconnect() {
14443
14754
  if (!this.ws)
14444
14755
  return;
@@ -14456,21 +14767,19 @@ class DaemonClient extends EventEmitter2 {
14456
14767
  return { success: false, error: "AgentBridge daemon is not connected." };
14457
14768
  }
14458
14769
  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
- });
14770
+ const pending = this.pendingReplies.register(requestId, {
14771
+ timeoutMs: CLIENT_REPLY_TIMEOUT_MS,
14772
+ onTimeout: ({ resolve }) => resolve({ success: false, error: "Timed out waiting for AgentBridge daemon reply." })
14773
+ });
14774
+ this.send({
14775
+ type: "claude_to_codex",
14776
+ requestId,
14777
+ message,
14778
+ ...requireReply ? { requireReply: true } : {},
14779
+ ...onBusy && onBusy !== "reject" ? { onBusy } : {},
14780
+ ...idempotencyKey ? { idempotencyKey } : {}
14473
14781
  });
14782
+ return pending;
14474
14783
  }
14475
14784
  attachSocketHandlers(ws, socketId) {
14476
14785
  ws.onmessage = (event) => {
@@ -14486,12 +14795,7 @@ class DaemonClient extends EventEmitter2 {
14486
14795
  this.emit("codexMessage", message.message);
14487
14796
  return;
14488
14797
  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({
14798
+ this.pendingReplies.settle(message.requestId, {
14495
14799
  success: message.success,
14496
14800
  error: message.error,
14497
14801
  ...message.code !== undefined ? { code: message.code } : {},
@@ -14522,7 +14826,7 @@ class DaemonClient extends EventEmitter2 {
14522
14826
  if (isCurrent) {
14523
14827
  this.ws = null;
14524
14828
  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) {
14829
+ 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
14830
  this.emit("rejected", event.code);
14527
14831
  } else {
14528
14832
  this.emit("disconnect");
@@ -14532,11 +14836,7 @@ class DaemonClient extends EventEmitter2 {
14532
14836
  ws.onerror = () => {};
14533
14837
  }
14534
14838
  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
- }
14839
+ this.pendingReplies.settleAll(() => ({ success: false, error: error2 }));
14540
14840
  }
14541
14841
  send(message) {
14542
14842
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
@@ -14552,9 +14852,44 @@ class DaemonClient extends EventEmitter2 {
14552
14852
 
14553
14853
  // src/daemon-lifecycle.ts
14554
14854
  import { spawn } from "child_process";
14555
- import { existsSync as existsSync3, readFileSync, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync, openSync, closeSync, constants } from "fs";
14855
+ 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
14856
  import { fileURLToPath } from "url";
14557
14857
 
14858
+ // src/atomic-json.ts
14859
+ import * as fs from "fs";
14860
+ import { randomUUID as randomUUID2 } from "crypto";
14861
+ import { dirname as dirname2 } from "path";
14862
+ function tmpPathFor(targetPath) {
14863
+ return `${targetPath}.tmp.${process.pid}.${randomUUID2()}`;
14864
+ }
14865
+ function atomicWriteText(path, content, options = {}) {
14866
+ fs.mkdirSync(dirname2(path), { recursive: true });
14867
+ const tmp = tmpPathFor(path);
14868
+ let renamed = false;
14869
+ const fd = fs.openSync(tmp, "w", options.mode ?? 438);
14870
+ try {
14871
+ try {
14872
+ fs.writeFileSync(fd, content, "utf-8");
14873
+ if (options.fsync)
14874
+ fs.fsyncSync(fd);
14875
+ } finally {
14876
+ fs.closeSync(fd);
14877
+ }
14878
+ fs.renameSync(tmp, path);
14879
+ renamed = true;
14880
+ } finally {
14881
+ if (!renamed) {
14882
+ try {
14883
+ fs.unlinkSync(tmp);
14884
+ } catch {}
14885
+ }
14886
+ }
14887
+ }
14888
+ function atomicWriteJson(path, value, options = {}) {
14889
+ atomicWriteText(path, JSON.stringify(value, null, 2) + `
14890
+ `, options);
14891
+ }
14892
+
14558
14893
  // src/env-utils.ts
14559
14894
  function parsePositiveIntEnv(name, fallback, log = () => {}, env = process.env) {
14560
14895
  const raw = env[name];
@@ -14603,12 +14938,144 @@ function isAgentBridgeProcess(pid, lookup = commandForPid) {
14603
14938
  return cmd.includes("agentbridge") || cmd.includes("agent_bridge");
14604
14939
  }
14605
14940
 
14941
+ // src/daemon-record.ts
14942
+ import { readFileSync } from "fs";
14943
+ var defaultRead = (path) => readFileSync(path, "utf-8");
14944
+ function writeDaemonRecord(path, record3) {
14945
+ atomicWriteJson(path, record3);
14946
+ }
14947
+ function sanitizePorts(value) {
14948
+ if (typeof value !== "object" || value === null)
14949
+ return;
14950
+ const raw = value;
14951
+ const ports = {};
14952
+ if (typeof raw.appPort === "number")
14953
+ ports.appPort = raw.appPort;
14954
+ if (typeof raw.proxyPort === "number")
14955
+ ports.proxyPort = raw.proxyPort;
14956
+ if (typeof raw.controlPort === "number")
14957
+ ports.controlPort = raw.controlPort;
14958
+ return Object.keys(ports).length > 0 ? ports : undefined;
14959
+ }
14960
+ function readDaemonRecord(path, read = defaultRead) {
14961
+ let parsed;
14962
+ try {
14963
+ parsed = JSON.parse(read(path));
14964
+ } catch {
14965
+ return null;
14966
+ }
14967
+ if (typeof parsed !== "object" || parsed === null)
14968
+ return null;
14969
+ const obj = parsed;
14970
+ if (typeof obj.pid !== "number" || !Number.isFinite(obj.pid))
14971
+ return null;
14972
+ const phase = obj.phase === "ready" ? "ready" : "booting";
14973
+ const record3 = { pid: obj.pid, phase };
14974
+ if (typeof obj.startedAt === "number")
14975
+ record3.startedAt = obj.startedAt;
14976
+ if (typeof obj.nonce === "string")
14977
+ record3.nonce = obj.nonce;
14978
+ if (obj.pairId === null || typeof obj.pairId === "string")
14979
+ record3.pairId = obj.pairId;
14980
+ if (obj.cwd === null || typeof obj.cwd === "string")
14981
+ record3.cwd = obj.cwd;
14982
+ if (obj.stateDir === null || typeof obj.stateDir === "string")
14983
+ record3.stateDir = obj.stateDir;
14984
+ if (typeof obj.proxyUrl === "string")
14985
+ record3.proxyUrl = obj.proxyUrl;
14986
+ if (typeof obj.appServerUrl === "string")
14987
+ record3.appServerUrl = obj.appServerUrl;
14988
+ const ports = sanitizePorts(obj.ports);
14989
+ if (ports !== undefined)
14990
+ record3.ports = ports;
14991
+ if (typeof obj.build === "object" && obj.build !== null) {
14992
+ record3.build = obj.build;
14993
+ }
14994
+ if (typeof obj.turnPhase === "string")
14995
+ record3.turnPhase = obj.turnPhase;
14996
+ if (typeof obj.turnInProgress === "boolean")
14997
+ record3.turnInProgress = obj.turnInProgress;
14998
+ if (typeof obj.attentionWindowActive === "boolean") {
14999
+ record3.attentionWindowActive = obj.attentionWindowActive;
15000
+ }
15001
+ return record3;
15002
+ }
15003
+ function synthesizeLegacyRecord(pidFilePath, statusFilePath, read = defaultRead) {
15004
+ let pidFromPidFile = null;
15005
+ try {
15006
+ const raw = read(pidFilePath).trim();
15007
+ const n = Number.parseInt(raw, 10);
15008
+ if (Number.isFinite(n))
15009
+ pidFromPidFile = n;
15010
+ } catch {}
15011
+ let status = null;
15012
+ try {
15013
+ const parsed = JSON.parse(read(statusFilePath));
15014
+ if (typeof parsed === "object" && parsed !== null)
15015
+ status = parsed;
15016
+ } catch {}
15017
+ const pidFromStatus = status && typeof status.pid === "number" && Number.isFinite(status.pid) ? status.pid : null;
15018
+ const pid = pidFromPidFile ?? pidFromStatus;
15019
+ if (pid === null)
15020
+ return null;
15021
+ const record3 = {
15022
+ pid,
15023
+ phase: status ? "ready" : "booting"
15024
+ };
15025
+ if (status) {
15026
+ if (typeof status.proxyUrl === "string")
15027
+ record3.proxyUrl = status.proxyUrl;
15028
+ if (typeof status.appServerUrl === "string")
15029
+ record3.appServerUrl = status.appServerUrl;
15030
+ const controlPort = typeof status.controlPort === "number" ? status.controlPort : undefined;
15031
+ const proxyPort = portFromUrl(status.proxyUrl);
15032
+ const appPort = portFromUrl(status.appServerUrl);
15033
+ if (controlPort !== undefined || proxyPort !== undefined || appPort !== undefined) {
15034
+ record3.ports = {};
15035
+ if (appPort !== undefined)
15036
+ record3.ports.appPort = appPort;
15037
+ if (proxyPort !== undefined)
15038
+ record3.ports.proxyPort = proxyPort;
15039
+ if (controlPort !== undefined)
15040
+ record3.ports.controlPort = controlPort;
15041
+ }
15042
+ if (status.pairId === null || typeof status.pairId === "string")
15043
+ record3.pairId = status.pairId;
15044
+ if (status.cwd === null || typeof status.cwd === "string")
15045
+ record3.cwd = status.cwd;
15046
+ if (status.stateDir === null || typeof status.stateDir === "string")
15047
+ record3.stateDir = status.stateDir;
15048
+ if (typeof status.build === "object" && status.build !== null) {
15049
+ record3.build = status.build;
15050
+ }
15051
+ if (typeof status.turnPhase === "string")
15052
+ record3.turnPhase = status.turnPhase;
15053
+ if (typeof status.turnInProgress === "boolean")
15054
+ record3.turnInProgress = status.turnInProgress;
15055
+ if (typeof status.attentionWindowActive === "boolean") {
15056
+ record3.attentionWindowActive = status.attentionWindowActive;
15057
+ }
15058
+ }
15059
+ return record3;
15060
+ }
15061
+ function readUnifiedDaemonRecord(paths, read = defaultRead) {
15062
+ return readDaemonRecord(paths.daemonRecordFile, read) ?? synthesizeLegacyRecord(paths.pidFile, paths.statusFile, read);
15063
+ }
15064
+ function portFromUrl(url) {
15065
+ if (typeof url !== "string")
15066
+ return;
15067
+ const match = url.match(/:(\d+)(?:[/?]|$)/);
15068
+ return match ? Number.parseInt(match[1], 10) : undefined;
15069
+ }
15070
+
14606
15071
  // src/daemon-lifecycle.ts
14607
15072
  var DEFAULT_DAEMON_ENTRY = import.meta.url.endsWith(".ts") ? "./daemon.ts" : "./daemon.js";
14608
15073
  var DAEMON_ENTRY = process.env.AGENTBRIDGE_DAEMON_ENTRY || DEFAULT_DAEMON_ENTRY;
14609
15074
  var DAEMON_PATH = fileURLToPath(new URL(DAEMON_ENTRY, import.meta.url));
14610
15075
  var REUSE_READY_RETRIES = parsePositiveIntEnv("AGENTBRIDGE_REUSE_READY_RETRIES", 12);
14611
15076
  var REUSE_READY_DELAY_MS = 250;
15077
+ var WAIT_READY_RETRIES = 40;
15078
+ var WAIT_READY_DELAY_MS = 250;
14612
15079
  var HEALTH_FETCH_TIMEOUT_MS = 500;
14613
15080
  var LOCK_IDENTITY_GRACE_MS = parsePositiveIntEnv("AGENTBRIDGE_LOCK_IDENTITY_GRACE_MS", 120000);
14614
15081
  function isReuseVerdict(verdict) {
@@ -14646,22 +15113,33 @@ function classifyDaemon(expectedPairId, status, buildInfo) {
14646
15113
  reason: "runtime build drift has a compatible contract and a live Codex TUI is attached"
14647
15114
  };
14648
15115
  }
15116
+ const basis = runtimeContractComparisonBasis(status.build, buildInfo) === "codeHash" ? "compared by codeHash" : "compared by commit stamp; legacy build without codeHash";
14649
15117
  return {
14650
15118
  verdict: "replace-drifted",
14651
- reason: `runtime build ${formatBuildInfo(status.build)} does not match launcher ${formatBuildInfo(buildInfo)}`
15119
+ reason: `runtime build ${formatBuildInfo(status.build)} does not match launcher ` + `${formatBuildInfo(buildInfo)} (${basis})`
14652
15120
  };
14653
15121
  }
14654
15122
  return { verdict: "reuse", reason: "daemon pair and runtime contract match" };
14655
15123
  }
15124
+ function resolveTiming(timing) {
15125
+ return {
15126
+ reuseReadyRetries: timing?.reuseReadyRetries ?? REUSE_READY_RETRIES,
15127
+ reuseReadyDelayMs: timing?.reuseReadyDelayMs ?? REUSE_READY_DELAY_MS,
15128
+ waitReadyRetries: timing?.waitReadyRetries ?? WAIT_READY_RETRIES,
15129
+ waitReadyDelayMs: timing?.waitReadyDelayMs ?? WAIT_READY_DELAY_MS
15130
+ };
15131
+ }
14656
15132
 
14657
15133
  class DaemonLifecycle {
14658
15134
  stateDir;
14659
15135
  controlPort;
14660
15136
  log;
15137
+ timing;
14661
15138
  constructor(opts) {
14662
15139
  this.stateDir = opts.stateDir;
14663
15140
  this.controlPort = opts.controlPort;
14664
15141
  this.log = opts.log;
15142
+ this.timing = resolveTiming(opts.timing);
14665
15143
  }
14666
15144
  get healthUrl() {
14667
15145
  return `http://127.0.0.1:${this.controlPort}/healthz`;
@@ -14718,7 +15196,7 @@ class DaemonLifecycle {
14718
15196
  break;
14719
15197
  }
14720
15198
  try {
14721
- await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
15199
+ await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
14722
15200
  return;
14723
15201
  } catch {
14724
15202
  this.log(`Daemon on control port ${this.controlPort} is healthy but not ready within reuse window \u2014 replacing`);
@@ -14731,7 +15209,7 @@ class DaemonLifecycle {
14731
15209
  if (isProcessAlive(existingPid)) {
14732
15210
  if (isAgentBridgeDaemon(existingPid)) {
14733
15211
  try {
14734
- await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
15212
+ await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
14735
15213
  return;
14736
15214
  } catch {
14737
15215
  this.log(`Existing daemon process ${existingPid} never became ready \u2014 replacing`);
@@ -14759,7 +15237,7 @@ class DaemonLifecycle {
14759
15237
  await this.kill(3000, status?.pid);
14760
15238
  } else {
14761
15239
  try {
14762
- await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
15240
+ await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
14763
15241
  return;
14764
15242
  } catch {
14765
15243
  this.log(`Daemon on control port ${this.controlPort} is healthy but not ready under startup lock \u2014 replacing`);
@@ -14768,7 +15246,7 @@ class DaemonLifecycle {
14768
15246
  }
14769
15247
  }
14770
15248
  this.launch();
14771
- await this.waitForReady();
15249
+ await this.waitForReady(this.timing.waitReadyRetries, this.timing.waitReadyDelayMs);
14772
15250
  });
14773
15251
  }
14774
15252
  async isHealthy() {
@@ -14795,7 +15273,7 @@ class DaemonLifecycle {
14795
15273
  return false;
14796
15274
  }
14797
15275
  }
14798
- async waitForReady(maxRetries = 40, delayMs = 250) {
15276
+ async waitForReady(maxRetries = WAIT_READY_RETRIES, delayMs = WAIT_READY_DELAY_MS) {
14799
15277
  for (let attempt = 0;attempt < maxRetries; attempt++) {
14800
15278
  if (await this.isReady())
14801
15279
  return;
@@ -14803,7 +15281,7 @@ class DaemonLifecycle {
14803
15281
  }
14804
15282
  throw new Error(`Timed out waiting for AgentBridge daemon readiness on ${this.readyUrl}`);
14805
15283
  }
14806
- async waitForReadyAndOurs(maxRetries = 40, delayMs = 250) {
15284
+ async waitForReadyAndOurs(maxRetries = WAIT_READY_RETRIES, delayMs = WAIT_READY_DELAY_MS) {
14807
15285
  for (let attempt = 0;attempt < maxRetries; attempt++) {
14808
15286
  if (await this.isReady()) {
14809
15287
  const status = await this.fetchStatus();
@@ -14819,22 +15297,35 @@ class DaemonLifecycle {
14819
15297
  }
14820
15298
  throw new Error(`Timed out waiting for AgentBridge daemon readiness+identity on ${this.readyUrl} (control port ${this.controlPort})`);
14821
15299
  }
15300
+ readDaemonRecord() {
15301
+ return readUnifiedDaemonRecord({
15302
+ daemonRecordFile: this.stateDir.daemonRecordFile,
15303
+ pidFile: this.stateDir.pidFile,
15304
+ statusFile: this.stateDir.statusFile
15305
+ });
15306
+ }
15307
+ writeDaemonRecord(record3) {
15308
+ writeDaemonRecord(this.stateDir.daemonRecordFile, record3);
15309
+ }
15310
+ removeDaemonRecord() {
15311
+ try {
15312
+ unlinkSync3(this.stateDir.daemonRecordFile);
15313
+ } catch {}
15314
+ }
14822
15315
  readStatus() {
14823
15316
  try {
14824
- const raw = readFileSync(this.stateDir.statusFile, "utf-8");
15317
+ const raw = readFileSync2(this.stateDir.statusFile, "utf-8");
14825
15318
  return JSON.parse(raw);
14826
15319
  } catch {
14827
15320
  return null;
14828
15321
  }
14829
15322
  }
14830
15323
  writeStatus(status) {
14831
- this.stateDir.ensure();
14832
- writeFileSync(this.stateDir.statusFile, JSON.stringify(status, null, 2) + `
14833
- `, "utf-8");
15324
+ atomicWriteJson(this.stateDir.statusFile, status);
14834
15325
  }
14835
15326
  readPid() {
14836
15327
  try {
14837
- const raw = readFileSync(this.stateDir.pidFile, "utf-8").trim();
15328
+ const raw = readFileSync2(this.stateDir.pidFile, "utf-8").trim();
14838
15329
  if (!raw)
14839
15330
  return null;
14840
15331
  const pid = Number.parseInt(raw, 10);
@@ -14844,28 +15335,27 @@ class DaemonLifecycle {
14844
15335
  }
14845
15336
  }
14846
15337
  writePid(pid) {
14847
- this.stateDir.ensure();
14848
- writeFileSync(this.stateDir.pidFile, `${pid ?? process.pid}
14849
- `, "utf-8");
15338
+ atomicWriteText(this.stateDir.pidFile, `${pid ?? process.pid}
15339
+ `);
14850
15340
  }
14851
15341
  removePidFile() {
14852
15342
  try {
14853
- unlinkSync2(this.stateDir.pidFile);
15343
+ unlinkSync3(this.stateDir.pidFile);
14854
15344
  } catch {}
14855
15345
  }
14856
15346
  removeStatusFile() {
14857
15347
  try {
14858
- unlinkSync2(this.stateDir.statusFile);
15348
+ unlinkSync3(this.stateDir.statusFile);
14859
15349
  } catch {}
14860
15350
  }
14861
15351
  markKilled() {
14862
15352
  this.stateDir.ensure();
14863
- writeFileSync(this.stateDir.killedFile, `${Date.now()}
15353
+ writeFileSync2(this.stateDir.killedFile, `${Date.now()}
14864
15354
  `, "utf-8");
14865
15355
  }
14866
15356
  clearKilled() {
14867
15357
  try {
14868
- unlinkSync2(this.stateDir.killedFile);
15358
+ unlinkSync3(this.stateDir.killedFile);
14869
15359
  } catch {}
14870
15360
  }
14871
15361
  wasKilled() {
@@ -14887,8 +15377,10 @@ class DaemonLifecycle {
14887
15377
  daemonProc.unref();
14888
15378
  }
14889
15379
  removeStalePidFile() {
14890
- this.log("Removing stale pid file");
15380
+ this.log("Removing stale daemon identity files");
14891
15381
  this.removePidFile();
15382
+ this.removeStatusFile();
15383
+ this.removeDaemonRecord();
14892
15384
  }
14893
15385
  async replaceUnhealthyDaemon(statusPid) {
14894
15386
  await this.withStartupLockStrict(async (locked) => {
@@ -14904,7 +15396,7 @@ class DaemonLifecycle {
14904
15396
  }
14905
15397
  if (isReuseVerdict(classification.verdict)) {
14906
15398
  try {
14907
- await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
15399
+ await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
14908
15400
  return;
14909
15401
  } catch {}
14910
15402
  }
@@ -14912,12 +15404,12 @@ class DaemonLifecycle {
14912
15404
  this.log(`Killing unhealthy daemon on control port ${this.controlPort} and relaunching`);
14913
15405
  await this.kill(3000, statusPid);
14914
15406
  this.launch();
14915
- await this.waitForReady();
15407
+ await this.waitForReady(this.timing.waitReadyRetries, this.timing.waitReadyDelayMs);
14916
15408
  });
14917
15409
  }
14918
15410
  async waitForContendedStartupLock() {
14919
15411
  this.log("Another process holds the startup lock, waiting for readiness+identity...");
14920
- await this.waitForReadyAndOurs();
15412
+ await this.waitForReadyAndOurs(this.timing.waitReadyRetries, this.timing.waitReadyDelayMs);
14921
15413
  }
14922
15414
  async withStartupLockStrict(fn) {
14923
15415
  const locked = this.acquireLockStrict();
@@ -14932,15 +15424,15 @@ class DaemonLifecycle {
14932
15424
  this.stateDir.ensure();
14933
15425
  let fd = null;
14934
15426
  try {
14935
- fd = openSync(this.stateDir.lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
14936
- writeFileSync(fd, `${process.pid}
15427
+ fd = openSync2(this.stateDir.lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
15428
+ writeFileSync2(fd, `${process.pid}
14937
15429
  `);
14938
- closeSync(fd);
15430
+ closeSync2(fd);
14939
15431
  return true;
14940
15432
  } catch (err) {
14941
15433
  if (fd !== null && err.code !== "EEXIST") {
14942
15434
  try {
14943
- closeSync(fd);
15435
+ closeSync2(fd);
14944
15436
  } catch {}
14945
15437
  this.releaseLock();
14946
15438
  }
@@ -14948,7 +15440,7 @@ class DaemonLifecycle {
14948
15440
  if (reclaimed)
14949
15441
  return false;
14950
15442
  try {
14951
- const holderPid = Number.parseInt(readFileSync(this.stateDir.lockFile, "utf-8").trim(), 10);
15443
+ const holderPid = Number.parseInt(readFileSync2(this.stateDir.lockFile, "utf-8").trim(), 10);
14952
15444
  if (Number.isFinite(holderPid) && !isProcessAlive(holderPid)) {
14953
15445
  this.log(`Stale startup lock from dead process ${holderPid}, reclaiming`);
14954
15446
  this.releaseLock();
@@ -14977,7 +15469,7 @@ class DaemonLifecycle {
14977
15469
  }
14978
15470
  releaseLock() {
14979
15471
  try {
14980
- unlinkSync2(this.stateDir.lockFile);
15472
+ unlinkSync3(this.stateDir.lockFile);
14981
15473
  } catch {}
14982
15474
  }
14983
15475
  async kill(gracefulTimeoutMs = 3000, pidOverride) {
@@ -15023,6 +15515,7 @@ class DaemonLifecycle {
15023
15515
  cleanup() {
15024
15516
  this.removePidFile();
15025
15517
  this.removeStatusFile();
15518
+ this.removeDaemonRecord();
15026
15519
  }
15027
15520
  }
15028
15521
  async function fetchWithTimeout(url, timeoutMs = HEALTH_FETCH_TIMEOUT_MS) {
@@ -15036,7 +15529,7 @@ async function fetchWithTimeout(url, timeoutMs = HEALTH_FETCH_TIMEOUT_MS) {
15036
15529
  }
15037
15530
 
15038
15531
  // src/config-service.ts
15039
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
15532
+ import { readFileSync as readFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
15040
15533
  import { join as join2 } from "path";
15041
15534
  var DEFAULT_BUDGET_CONFIG = {
15042
15535
  enabled: true,
@@ -15053,7 +15546,8 @@ var DEFAULT_BUDGET_CONFIG = {
15053
15546
  full: null,
15054
15547
  balanced: { effort: "medium" },
15055
15548
  eco: { effort: "low" }
15056
- }
15549
+ },
15550
+ strategy: "conserve"
15057
15551
  };
15058
15552
  var DEFAULT_CONFIG = {
15059
15553
  version: "1.0",
@@ -15131,6 +15625,9 @@ function normalizeBoundedInteger(value, fallback, min, max) {
15131
15625
  return fallback;
15132
15626
  return parsed;
15133
15627
  }
15628
+ function normalizeStrategy(value, fallback) {
15629
+ return value === "conserve" || value === "maximize" ? value : fallback;
15630
+ }
15134
15631
  function normalizeBoolean(value, fallback) {
15135
15632
  if (typeof value === "boolean")
15136
15633
  return value;
@@ -15179,7 +15676,8 @@ function normalizeBudgetConfig(raw, fallback = DEFAULT_BUDGET_CONFIG) {
15179
15676
  timeWindowSec: normalizeBoundedInteger(parallel.timeWindowSec, fallback.parallel.timeWindowSec, 60, 604800)
15180
15677
  },
15181
15678
  codexTierControl: normalizeBoolean(budget.codexTierControl, fallback.codexTierControl) && codexTiers.full !== null,
15182
- codexTiers
15679
+ codexTiers,
15680
+ strategy: normalizeStrategy(budget.strategy, fallback.strategy)
15183
15681
  };
15184
15682
  }
15185
15683
  function normalizeConfig(raw) {
@@ -15192,13 +15690,13 @@ function normalizeConfig(raw) {
15192
15690
  return {
15193
15691
  version: typeof config2.version === "string" ? config2.version : DEFAULT_CONFIG.version,
15194
15692
  codex: {
15195
- appPort: normalizeInteger(codex.appPort ?? daemon.port, DEFAULT_CONFIG.codex.appPort),
15196
- proxyPort: normalizeInteger(codex.proxyPort ?? daemon.proxyPort, DEFAULT_CONFIG.codex.proxyPort)
15693
+ appPort: normalizeBoundedInteger(codex.appPort ?? daemon.port, DEFAULT_CONFIG.codex.appPort, 1, 65535),
15694
+ proxyPort: normalizeBoundedInteger(codex.proxyPort ?? daemon.proxyPort, DEFAULT_CONFIG.codex.proxyPort, 1, 65535)
15197
15695
  },
15198
15696
  turnCoordination: {
15199
- attentionWindowSeconds: normalizeInteger(turnCoordination.attentionWindowSeconds, DEFAULT_CONFIG.turnCoordination.attentionWindowSeconds)
15697
+ attentionWindowSeconds: normalizeBoundedInteger(turnCoordination.attentionWindowSeconds, DEFAULT_CONFIG.turnCoordination.attentionWindowSeconds, 0, Number.MAX_SAFE_INTEGER)
15200
15698
  },
15201
- idleShutdownSeconds: normalizeInteger(config2.idleShutdownSeconds, DEFAULT_CONFIG.idleShutdownSeconds),
15699
+ idleShutdownSeconds: normalizeBoundedInteger(config2.idleShutdownSeconds, DEFAULT_CONFIG.idleShutdownSeconds, 1, Number.MAX_SAFE_INTEGER),
15202
15700
  budget: normalizeBudgetConfig(config2.budget)
15203
15701
  };
15204
15702
  }
@@ -15217,7 +15715,7 @@ class ConfigService {
15217
15715
  load() {
15218
15716
  let raw;
15219
15717
  try {
15220
- raw = readFileSync2(this.configPath, "utf-8");
15718
+ raw = readFileSync3(this.configPath, "utf-8");
15221
15719
  } catch (err) {
15222
15720
  if (err?.code === "ENOENT") {
15223
15721
  return { state: "absent" };
@@ -15270,9 +15768,7 @@ class ConfigService {
15270
15768
  };
15271
15769
  }
15272
15770
  save(config2) {
15273
- this.ensureConfigDir();
15274
- writeFileSync2(this.configPath, JSON.stringify(config2, null, 2) + `
15275
- `, "utf-8");
15771
+ atomicWriteJson(this.configPath, config2);
15276
15772
  }
15277
15773
  initDefaults() {
15278
15774
  this.ensureConfigDir();
@@ -15288,34 +15784,46 @@ class ConfigService {
15288
15784
  }
15289
15785
  ensureConfigDir() {
15290
15786
  if (!existsSync4(this.configDir)) {
15291
- mkdirSync2(this.configDir, { recursive: true });
15787
+ mkdirSync3(this.configDir, { recursive: true });
15292
15788
  }
15293
15789
  }
15294
15790
  }
15295
15791
 
15792
+ // src/cli-invocation.ts
15793
+ import { basename } from "path";
15794
+ var CLI_NAMES = ["abg", "agentbridge"];
15795
+ var DEFAULT_CLI_NAME = "abg";
15796
+ function cliInvocationName(argv = process.argv) {
15797
+ const raw = argv[1];
15798
+ if (typeof raw !== "string" || raw.length === 0)
15799
+ return DEFAULT_CLI_NAME;
15800
+ const name = basename(raw).replace(/\.(ts|js|mjs|cjs)$/, "");
15801
+ return isCliName(name) ? name : DEFAULT_CLI_NAME;
15802
+ }
15803
+ function isCliName(value) {
15804
+ return CLI_NAMES.includes(value);
15805
+ }
15806
+
15296
15807
  // src/pair-registry.ts
15297
15808
  import {
15298
- closeSync as closeSync2,
15299
15809
  existsSync as existsSync5,
15300
- fsyncSync,
15301
15810
  linkSync,
15302
15811
  lstatSync,
15303
- mkdirSync as mkdirSync3,
15304
- openSync as openSync2,
15812
+ mkdirSync as mkdirSync4,
15305
15813
  readdirSync,
15306
- readFileSync as readFileSync3,
15814
+ readFileSync as readFileSync4,
15307
15815
  realpathSync,
15308
- renameSync as renameSync2,
15309
15816
  rmSync,
15310
15817
  statSync as statSync3,
15311
- unlinkSync as unlinkSync3,
15818
+ unlinkSync as unlinkSync4,
15312
15819
  writeFileSync as writeFileSync3
15313
15820
  } from "fs";
15314
- import { createHash, randomUUID as randomUUID2 } from "crypto";
15315
- import { basename, join as join3, resolve, sep } from "path";
15821
+ import { createHash, randomUUID as randomUUID3 } from "crypto";
15822
+ import { basename as basename2, join as join3, resolve, sep } from "path";
15316
15823
  var PAIR_BASE_PORT = 4500;
15317
15824
  var PAIR_SLOT_STRIDE = 10;
15318
15825
  var PAIR_ID_REGEX = /^[A-Za-z0-9._-]{1,64}$/;
15826
+ var RECLAIMABLE_MIN_AGE_MS = 24 * 60 * 60 * 1000;
15319
15827
  var REGISTRY_FILE_NAME = "registry.json";
15320
15828
  class PairError extends Error {
15321
15829
  code;
@@ -15351,7 +15859,7 @@ function readRegistry(base) {
15351
15859
  return { version: 1, pairs: [] };
15352
15860
  let parsed;
15353
15861
  try {
15354
- parsed = JSON.parse(readFileSync3(path, "utf-8"));
15862
+ parsed = JSON.parse(readFileSync4(path, "utf-8"));
15355
15863
  } catch (err) {
15356
15864
  throw new PairError("PAIR_REGISTRY_CORRUPT", `Registry JSON is not parseable at ${path}: ${err.message}`, {
15357
15865
  path
@@ -15392,10 +15900,10 @@ function findPair(base, pairId) {
15392
15900
  }
15393
15901
 
15394
15902
  // src/pair-command.ts
15395
- function pairScopedCommand(cmd) {
15903
+ function pairScopedCommand(cmd, name = cliInvocationName()) {
15396
15904
  const pairId = process.env.AGENTBRIDGE_PAIR_ID;
15397
15905
  if (!pairId)
15398
- return `agentbridge ${cmd}`;
15906
+ return `${name} ${cmd}`;
15399
15907
  let selector = process.env.AGENTBRIDGE_PAIR_NAME;
15400
15908
  if (!selector) {
15401
15909
  try {
@@ -15404,10 +15912,13 @@ function pairScopedCommand(cmd) {
15404
15912
  selector = pairId;
15405
15913
  }
15406
15914
  }
15407
- return `agentbridge --pair ${selector} ${cmd}`;
15915
+ return `${name} --pair ${selector} ${cmd}`;
15408
15916
  }
15409
15917
 
15410
15918
  // src/bridge-disabled-state.ts
15919
+ function shouldEmitReconnectSuccess(state) {
15920
+ return !state.daemonDisabled;
15921
+ }
15411
15922
  function disabledReplyError(reason) {
15412
15923
  const claudeCmd = pairScopedCommand("claude");
15413
15924
  switch (reason) {
@@ -15420,7 +15931,7 @@ function disabledReplyError(reason) {
15420
15931
  case "auto_recovery_exhausted":
15421
15932
  return `AgentBridge auto-recovery gave up after exhausting its retry budget for the in-flight liveness probe contention. Retry manually with \`${claudeCmd}\`.`;
15422
15933
  case "killed":
15423
- return `AgentBridge is disabled by \`agentbridge kill\`. Restart Claude Code (\`${claudeCmd}\`), switch to a new conversation, or run \`/resume\` to reconnect.`;
15934
+ return `AgentBridge is disabled by \`${pairScopedCommand("kill")}\`. Restart Claude Code (\`${claudeCmd}\`), switch to a new conversation, or run \`/resume\` to reconnect.`;
15424
15935
  }
15425
15936
  }
15426
15937
 
@@ -15499,9 +16010,25 @@ function nonEmpty(value) {
15499
16010
  return value && value.length > 0 ? value : null;
15500
16011
  }
15501
16012
 
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";
16013
+ // src/control-token.ts
16014
+ import { chmodSync, readFileSync as readFileSync5 } from "fs";
15504
16015
  import { join as join4 } from "path";
16016
+ var CONTROL_TOKEN_FILENAME = "control-token";
16017
+ function resolveControlTokenPath(stateDir) {
16018
+ return join4(stateDir, CONTROL_TOKEN_FILENAME);
16019
+ }
16020
+ function readControlToken(path) {
16021
+ try {
16022
+ const raw = readFileSync5(path, "utf-8").trim();
16023
+ return raw.length > 0 ? raw : null;
16024
+ } catch {
16025
+ return null;
16026
+ }
16027
+ }
16028
+
16029
+ // src/trace-log.ts
16030
+ import { appendFileSync as appendFileSync2, existsSync as existsSync6, mkdirSync as mkdirSync5, readdirSync as readdirSync2, statSync as statSync4, unlinkSync as unlinkSync5 } from "fs";
16031
+ import { join as join5 } from "path";
15505
16032
  var TRACE_RETENTION_DAYS = 7;
15506
16033
  var TRACE_FILE_RE = /^trace-\d{4}-\d{2}-\d{2}\.jsonl$/;
15507
16034
  var SECRET_KEY_RE = /(token|secret|password|passwd|api[_-]?key|auth|cookie|session)/i;
@@ -15541,7 +16068,7 @@ function redactArgv(argv) {
15541
16068
  }
15542
16069
  function traceLogPath(cwd, timestamp) {
15543
16070
  const day = timestamp.slice(0, 10);
15544
- return join4(cwd, ".agentbridge", "logs", `trace-${day}.jsonl`);
16071
+ return join5(cwd, ".agentbridge", "logs", `trace-${day}.jsonl`);
15545
16072
  }
15546
16073
  function appendTraceEvent(input) {
15547
16074
  const timestamp = input.timestamp ?? new Date().toISOString();
@@ -15555,9 +16082,9 @@ function appendTraceEvent(input) {
15555
16082
  ...input.env ? { env: pickRelevantEnv(input.env) } : {},
15556
16083
  ...input.data ? { data: redactData(input.data) } : {}
15557
16084
  };
15558
- const logsDir = join4(input.cwd, ".agentbridge", "logs");
16085
+ const logsDir = join5(input.cwd, ".agentbridge", "logs");
15559
16086
  const isNewDayFile = !existsSync6(path);
15560
- mkdirSync4(logsDir, { recursive: true });
16087
+ mkdirSync5(logsDir, { recursive: true });
15561
16088
  if (isNewDayFile) {
15562
16089
  pruneOldTraceLogs(logsDir, path, Date.parse(timestamp));
15563
16090
  }
@@ -15578,12 +16105,12 @@ function pruneOldTraceLogs(logsDir, keepPath, nowMs) {
15578
16105
  for (const name of entries) {
15579
16106
  if (!TRACE_FILE_RE.test(name))
15580
16107
  continue;
15581
- const filePath = join4(logsDir, name);
16108
+ const filePath = join5(logsDir, name);
15582
16109
  if (filePath === keepPath)
15583
16110
  continue;
15584
16111
  try {
15585
16112
  if (statSync4(filePath).mtimeMs < cutoff) {
15586
- unlinkSync4(filePath);
16113
+ unlinkSync5(filePath);
15587
16114
  }
15588
16115
  } catch {}
15589
16116
  }
@@ -15633,7 +16160,7 @@ var CONTROL_PORT = parseInt(process.env.AGENTBRIDGE_CONTROL_PORT ?? "4502", 10);
15633
16160
  var daemonLifecycle = new DaemonLifecycle({ stateDir, controlPort: CONTROL_PORT, log });
15634
16161
  var CONTROL_WS_URL = daemonLifecycle.controlWsUrl;
15635
16162
  var claude = new ClaudeAdapter(stateDir.logFile);
15636
- var daemonClient = new DaemonClient(CONTROL_WS_URL, { identity: currentClientIdentity() });
16163
+ var daemonClient = new DaemonClient(CONTROL_WS_URL, { identity: currentClientIdentity });
15637
16164
  var shuttingDown = false;
15638
16165
  var daemonDisabled = false;
15639
16166
  var daemonDisabledReason = null;
@@ -15646,6 +16173,7 @@ var lastReconnectNotifyTs = 0;
15646
16173
  var disabledRecoveryTimer = null;
15647
16174
  var disabledRecoveryInFlight = false;
15648
16175
  var disabledRecoveryAttempts = 0;
16176
+ var nextSystemMessageId = 0;
15649
16177
  var DISABLED_RECOVERY_MAX_ATTEMPTS = 6;
15650
16178
  var DISABLED_RECOVERY_CONFIRM_TIMEOUT_MS = 1000;
15651
16179
  if (process.env.AGENTBRIDGE_TRACE === "1") {
@@ -15740,6 +16268,16 @@ daemonClient.on("rejected", async (code) => {
15740
16268
  notificationId = "system_bridge_pair_mismatch";
15741
16269
  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
16270
  break;
16271
+ case CLOSE_CODE_TOKEN_MISMATCH:
16272
+ reason = "rejected";
16273
+ notificationId = "system_bridge_token_mismatch";
16274
+ 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`;
16275
+ break;
16276
+ case CLOSE_CODE_CONTRACT_MISMATCH:
16277
+ reason = "rejected";
16278
+ notificationId = "system_bridge_contract_mismatch";
16279
+ 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`;
16280
+ break;
15743
16281
  default:
15744
16282
  reason = "rejected";
15745
16283
  notificationId = "system_bridge_replaced";
@@ -15759,7 +16297,7 @@ daemonClient.on("rejected", async (code) => {
15759
16297
  claude.on("ready", async () => {
15760
16298
  log("MCP server ready (push delivery) \u2014 ensuring AgentBridge daemon...");
15761
16299
  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.`);
16300
+ 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
16301
  return;
15764
16302
  }
15765
16303
  try {
@@ -15821,7 +16359,7 @@ var reconnectTask = null;
15821
16359
  async function notifyIfDaemonKilled(logMessage) {
15822
16360
  if (!daemonLifecycle.wasKilled())
15823
16361
  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.`);
16362
+ 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
16363
  return true;
15826
16364
  }
15827
16365
  async function notifyIfPairRemoved(logMessage) {
@@ -15858,6 +16396,9 @@ function reconnectToDaemon() {
15858
16396
  }
15859
16397
  try {
15860
16398
  await connectToDaemon(true);
16399
+ if (!shouldEmitReconnectSuccess({ daemonDisabled })) {
16400
+ return;
16401
+ }
15861
16402
  log("Reconnected to AgentBridge daemon successfully");
15862
16403
  const now = Date.now();
15863
16404
  if (now - lastReconnectNotifyTs >= RECONNECT_NOTIFY_COOLDOWN_MS) {
@@ -15976,13 +16517,14 @@ async function pollDisabledRecovery() {
15976
16517
  }
15977
16518
  function systemMessage(idPrefix, content) {
15978
16519
  return {
15979
- id: `${idPrefix}_${Date.now()}`,
16520
+ id: `${idPrefix}_${++nextSystemMessageId}`,
15980
16521
  source: "codex",
15981
16522
  content,
15982
16523
  timestamp: Date.now()
15983
16524
  };
15984
16525
  }
15985
16526
  function currentClientIdentity() {
16527
+ const controlToken = readControlToken(resolveControlTokenPath(stateDir.dir));
15986
16528
  return {
15987
16529
  pairId: process.env.AGENTBRIDGE_PAIR_ID ?? null,
15988
16530
  pairName: process.env.AGENTBRIDGE_PAIR_NAME ?? null,
@@ -15990,7 +16532,8 @@ function currentClientIdentity() {
15990
16532
  baseDir: process.env.AGENTBRIDGE_BASE_DIR ?? null,
15991
16533
  stateDir: stateDir.dir,
15992
16534
  clientPid: process.pid,
15993
- contractVersion: BUILD_INFO.contractVersion
16535
+ contractVersion: BUILD_INFO.contractVersion,
16536
+ ...controlToken ? { controlToken } : {}
15994
16537
  };
15995
16538
  }
15996
16539
  function shutdown(reason) {