@raysonmeng/agentbridge 0.1.12 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -17,10 +17,67 @@ var __export = (target, all) => {
17
17
  };
18
18
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
19
19
 
20
+ // src/cli-invocation.ts
21
+ import { basename } from "path";
22
+ function cliInvocationName(argv = process.argv) {
23
+ const raw = argv[1];
24
+ if (typeof raw !== "string" || raw.length === 0)
25
+ return DEFAULT_CLI_NAME;
26
+ const name = basename(raw).replace(/\.(ts|js|mjs|cjs)$/, "");
27
+ return isCliName(name) ? name : DEFAULT_CLI_NAME;
28
+ }
29
+ function isCliName(value) {
30
+ return CLI_NAMES.includes(value);
31
+ }
32
+ var CLI_NAMES, DEFAULT_CLI_NAME = "abg";
33
+ var init_cli_invocation = __esm(() => {
34
+ CLI_NAMES = ["abg", "agentbridge"];
35
+ });
36
+
37
+ // src/atomic-json.ts
38
+ import * as fs from "fs";
39
+ import { randomUUID } from "crypto";
40
+ import { dirname } from "path";
41
+ function tmpPathFor(targetPath) {
42
+ return `${targetPath}.tmp.${process.pid}.${randomUUID()}`;
43
+ }
44
+ function atomicWriteText(path, content, options = {}) {
45
+ fs.mkdirSync(dirname(path), { recursive: true });
46
+ const tmp = tmpPathFor(path);
47
+ let renamed = false;
48
+ const fd = fs.openSync(tmp, "w", options.mode ?? 438);
49
+ try {
50
+ try {
51
+ fs.writeFileSync(fd, content, "utf-8");
52
+ if (options.fsync)
53
+ fs.fsyncSync(fd);
54
+ } finally {
55
+ fs.closeSync(fd);
56
+ }
57
+ fs.renameSync(tmp, path);
58
+ renamed = true;
59
+ } finally {
60
+ if (!renamed) {
61
+ try {
62
+ fs.unlinkSync(tmp);
63
+ } catch {}
64
+ }
65
+ }
66
+ }
67
+ function atomicWriteJson(path, value, options = {}) {
68
+ atomicWriteText(path, JSON.stringify(value, null, 2) + `
69
+ `, options);
70
+ }
71
+ var init_atomic_json = () => {};
72
+
20
73
  // src/state-dir.ts
21
- import { mkdirSync, existsSync } from "fs";
74
+ import { mkdirSync as mkdirSync2, existsSync } from "fs";
22
75
  import { join } from "path";
23
76
  import { homedir, platform } from "os";
77
+ function resolveXdgStateBase(rawXdg = process.env.XDG_STATE_HOME) {
78
+ const xdgState = rawXdg && rawXdg.length > 0 ? rawXdg : join(homedir(), ".local", "state");
79
+ return join(xdgState, "agentbridge");
80
+ }
24
81
 
25
82
  class StateDirResolver {
26
83
  stateDir;
@@ -28,8 +85,7 @@ class StateDirResolver {
28
85
  if (platform() === "darwin") {
29
86
  return join(homedir(), "Library", "Application Support", "AgentBridge");
30
87
  }
31
- const xdgState = process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state");
32
- return join(xdgState, "agentbridge");
88
+ return resolveXdgStateBase(process.env.XDG_STATE_HOME);
33
89
  }
34
90
  constructor(envOverride) {
35
91
  const override = envOverride ?? process.env.AGENTBRIDGE_STATE_DIR;
@@ -37,7 +93,7 @@ class StateDirResolver {
37
93
  }
38
94
  ensure() {
39
95
  if (!existsSync(this.stateDir)) {
40
- mkdirSync(this.stateDir, { recursive: true });
96
+ mkdirSync2(this.stateDir, { recursive: true });
41
97
  }
42
98
  }
43
99
  get dir() {
@@ -55,8 +111,8 @@ class StateDirResolver {
55
111
  get statusFile() {
56
112
  return join(this.stateDir, "status.json");
57
113
  }
58
- get portsFile() {
59
- return join(this.stateDir, "ports.json");
114
+ get daemonRecordFile() {
115
+ return join(this.stateDir, "daemon.json");
60
116
  }
61
117
  get currentThreadFile() {
62
118
  return join(this.stateDir, "current-thread.json");
@@ -120,7 +176,7 @@ function parsePositiveIntEnv(name, fallback, log = () => {}, env = process.env)
120
176
  var require_package = __commonJS((exports, module) => {
121
177
  module.exports = {
122
178
  name: "@raysonmeng/agentbridge",
123
- version: "0.1.12",
179
+ version: "0.1.13",
124
180
  description: "Bridge between Claude Code and Codex \u2014 bidirectional agent communication via MCP Channel + JSON-RPC",
125
181
  type: "module",
126
182
  packageManager: "bun@1.3.11",
@@ -151,6 +207,8 @@ var require_package = __commonJS((exports, module) => {
151
207
  prepublishOnly: "bun run build:cli && bun run build:plugin && bun run verify:plugin-sync && bun scripts/check-plugin-versions.js",
152
208
  "validate:plugin": "claude plugin validate plugins/agentbridge && claude plugin validate .claude-plugin/marketplace.json",
153
209
  test: "bun test src",
210
+ "test:unit": "bun test src/unit-test",
211
+ "test:integration": "bun test src/integration-test",
154
212
  "e2e:transport": "bun scripts/e2e-codex-transport.mjs",
155
213
  "install:global": "node scripts/install-global.mjs local",
156
214
  "install:global:local": "node scripts/install-global.mjs local",
@@ -199,7 +257,7 @@ __export(exports_update_notifier, {
199
257
  buildUpdateNotice: () => buildUpdateNotice,
200
258
  PACKAGE_NAME: () => PACKAGE_NAME
201
259
  });
202
- import { readFileSync, writeFileSync } from "fs";
260
+ import { readFileSync } from "fs";
203
261
  function getCurrentVersion() {
204
262
  try {
205
263
  return require_package().version;
@@ -235,9 +293,7 @@ function readCache(stateDir) {
235
293
  }
236
294
  function writeCache(stateDir, cache) {
237
295
  try {
238
- stateDir.ensure();
239
- writeFileSync(stateDir.updateCheckFile, JSON.stringify(cache, null, 2) + `
240
- `, "utf-8");
296
+ atomicWriteJson(stateDir.updateCheckFile, cache);
241
297
  } catch {}
242
298
  }
243
299
  function parseLatestFromRegistry(body) {
@@ -311,6 +367,7 @@ function maybeNotifyUpdate(deps = {}) {
311
367
  }
312
368
  var PACKAGE_NAME = "@raysonmeng/agentbridge", REGISTRY_URL, ABBREVIATED_ACCEPT = "application/vnd.npm.install-v1+json", DEFAULT_CHECK_INTERVAL_MS, FETCH_TIMEOUT_MS = 2500, CHECK_INTERVAL_ENV = "AGENTBRIDGE_UPDATE_CHECK_INTERVAL_MS";
313
369
  var init_update_notifier = __esm(() => {
370
+ init_atomic_json();
314
371
  init_state_dir();
315
372
  init_version_utils();
316
373
  REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}`;
@@ -318,7 +375,7 @@ var init_update_notifier = __esm(() => {
318
375
  });
319
376
 
320
377
  // src/config-service.ts
321
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
378
+ import { readFileSync as readFileSync2, mkdirSync as mkdirSync3, existsSync as existsSync2 } from "fs";
322
379
  import { join as join2 } from "path";
323
380
  function isRecord(value) {
324
381
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -442,13 +499,13 @@ function normalizeConfig(raw) {
442
499
  return {
443
500
  version: typeof config.version === "string" ? config.version : DEFAULT_CONFIG.version,
444
501
  codex: {
445
- appPort: normalizeInteger(codex.appPort ?? daemon.port, DEFAULT_CONFIG.codex.appPort),
446
- proxyPort: normalizeInteger(codex.proxyPort ?? daemon.proxyPort, DEFAULT_CONFIG.codex.proxyPort)
502
+ appPort: normalizeBoundedInteger(codex.appPort ?? daemon.port, DEFAULT_CONFIG.codex.appPort, 1, 65535),
503
+ proxyPort: normalizeBoundedInteger(codex.proxyPort ?? daemon.proxyPort, DEFAULT_CONFIG.codex.proxyPort, 1, 65535)
447
504
  },
448
505
  turnCoordination: {
449
- attentionWindowSeconds: normalizeInteger(turnCoordination.attentionWindowSeconds, DEFAULT_CONFIG.turnCoordination.attentionWindowSeconds)
506
+ attentionWindowSeconds: normalizeBoundedInteger(turnCoordination.attentionWindowSeconds, DEFAULT_CONFIG.turnCoordination.attentionWindowSeconds, 0, Number.MAX_SAFE_INTEGER)
450
507
  },
451
- idleShutdownSeconds: normalizeInteger(config.idleShutdownSeconds, DEFAULT_CONFIG.idleShutdownSeconds),
508
+ idleShutdownSeconds: normalizeBoundedInteger(config.idleShutdownSeconds, DEFAULT_CONFIG.idleShutdownSeconds, 1, Number.MAX_SAFE_INTEGER),
452
509
  budget: normalizeBudgetConfig(config.budget)
453
510
  };
454
511
  }
@@ -520,9 +577,7 @@ class ConfigService {
520
577
  };
521
578
  }
522
579
  save(config) {
523
- this.ensureConfigDir();
524
- writeFileSync2(this.configPath, JSON.stringify(config, null, 2) + `
525
- `, "utf-8");
580
+ atomicWriteJson(this.configPath, config);
526
581
  }
527
582
  initDefaults() {
528
583
  this.ensureConfigDir();
@@ -538,12 +593,13 @@ class ConfigService {
538
593
  }
539
594
  ensureConfigDir() {
540
595
  if (!existsSync2(this.configDir)) {
541
- mkdirSync2(this.configDir, { recursive: true });
596
+ mkdirSync3(this.configDir, { recursive: true });
542
597
  }
543
598
  }
544
599
  }
545
600
  var DEFAULT_BUDGET_CONFIG, DEFAULT_CONFIG, CONFIG_DIR = ".agentbridge", CONFIG_FILE = "config.json", NOOP_LOGGER = () => {};
546
601
  var init_config_service = __esm(() => {
602
+ init_atomic_json();
547
603
  DEFAULT_BUDGET_CONFIG = {
548
604
  enabled: true,
549
605
  pollSeconds: 300,
@@ -576,7 +632,7 @@ var init_config_service = __esm(() => {
576
632
  });
577
633
 
578
634
  // src/cli/pkg-root.ts
579
- import { dirname, join as join3 } from "path";
635
+ import { dirname as dirname2, join as join3 } from "path";
580
636
  import { existsSync as existsSync3 } from "fs";
581
637
  import { execFileSync } from "child_process";
582
638
  function findPackageRoot() {
@@ -585,7 +641,7 @@ function findPackageRoot() {
585
641
  if (existsSync3(join3(dir, "package.json"))) {
586
642
  return dir;
587
643
  }
588
- const parent = dirname(dir);
644
+ const parent = dirname2(dir);
589
645
  if (parent === dir) {
590
646
  throw new Error("Could not find package.json in any parent directory");
591
647
  }
@@ -760,18 +816,24 @@ __export(exports_init, {
760
816
  writeCollaborationSections: () => writeCollaborationSections,
761
817
  runInit: () => runInit,
762
818
  pluginInstallFallbackGuidance: () => pluginInstallFallbackGuidance,
819
+ formatDepChecks: () => formatDepChecks,
763
820
  compareVersions: () => compareVersions
764
821
  });
765
822
  import { execSync, execFileSync as execFileSync2 } from "child_process";
766
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
823
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
767
824
  import { join as join5 } from "path";
768
825
  async function runInit() {
769
826
  console.log(`AgentBridge Init
770
827
  `);
828
+ const cli = cliInvocationName();
771
829
  console.log("Checking dependencies...");
772
- checkBun();
773
- checkClaude();
774
- checkCodex();
830
+ const depChecks = [checkBun(), checkClaude(), checkCodex()];
831
+ for (const line of formatDepChecks(depChecks, cli)) {
832
+ console.log(line);
833
+ }
834
+ if (depChecks.some((check) => check.status === "fail")) {
835
+ process.exit(1);
836
+ }
775
837
  console.log("");
776
838
  console.log("Generating project config...");
777
839
  const configService = new ConfigService;
@@ -803,7 +865,7 @@ async function runInit() {
803
865
  pluginInstalled = true;
804
866
  } catch {
805
867
  console.log(" Plugin install skipped (marketplace registration or install failed).");
806
- for (const line of pluginInstallFallbackGuidance(detectRepoCheckout())) {
868
+ for (const line of pluginInstallFallbackGuidance(detectRepoCheckout(), cli)) {
807
869
  console.log(line);
808
870
  }
809
871
  }
@@ -818,8 +880,8 @@ async function runInit() {
818
880
  }
819
881
  console.log("Next steps:");
820
882
  console.log(" 1. If Claude Code is already running, execute /reload-plugins in your session");
821
- console.log(" 2. Start Claude Code: agentbridge claude");
822
- console.log(" 3. Start Codex TUI: agentbridge codex");
883
+ console.log(` 2. Start Claude Code: ${cli} claude`);
884
+ console.log(` 3. Start Codex TUI: ${cli} codex`);
823
885
  }
824
886
  function detectRepoCheckout() {
825
887
  try {
@@ -828,11 +890,11 @@ function detectRepoCheckout() {
828
890
  return false;
829
891
  }
830
892
  }
831
- function pluginInstallFallbackGuidance(insideRepo) {
893
+ function pluginInstallFallbackGuidance(insideRepo, cli = cliInvocationName()) {
832
894
  if (insideRepo) {
833
895
  return [
834
896
  " You can install it later with:",
835
- " abg dev # registers marketplace and installs plugin"
897
+ ` ${cli} dev # registers marketplace and installs plugin`
836
898
  ];
837
899
  }
838
900
  return [
@@ -840,46 +902,68 @@ function pluginInstallFallbackGuidance(insideRepo) {
840
902
  ...MARKETPLACE_STEPS.map((step) => ` ${step}`)
841
903
  ];
842
904
  }
905
+ function formatDepChecks(checks, cli) {
906
+ const lines = [];
907
+ for (const check of checks) {
908
+ lines.push(` ${check.status.toUpperCase().padEnd(4)} ${check.name}: ${check.detail}`);
909
+ if ((check.status === "warn" || check.status === "fail") && check.hint) {
910
+ lines.push(` \u21B3 ${check.hint}`);
911
+ }
912
+ }
913
+ lines.push(` \u9A8C\u8BC1\u5B89\u88C5: ${cli} doctor`);
914
+ return lines;
915
+ }
843
916
  function checkBun() {
844
917
  try {
845
918
  const version = execSync("bun --version", { encoding: "utf-8" }).trim();
846
- console.log(` bun: ${version}`);
919
+ return { name: "bun", status: "ok", detail: version };
847
920
  } catch {
848
- console.error(" ERROR: bun not found in PATH.");
849
- console.error(" Install Bun: https://bun.sh");
850
- process.exit(1);
921
+ return {
922
+ name: "bun",
923
+ status: "fail",
924
+ detail: "not found in PATH",
925
+ hint: "Install Bun: https://bun.sh"
926
+ };
851
927
  }
852
928
  }
853
929
  function checkClaude() {
930
+ let versionOutput;
854
931
  try {
855
- const versionOutput = execSync("claude --version", { encoding: "utf-8" }).trim();
856
- const match = versionOutput.match(/(\d+\.\d+\.\d+)/);
857
- if (match) {
858
- const version = match[1];
859
- console.log(` claude: ${version}`);
860
- if (compareVersions(version, MIN_CLAUDE_VERSION) < 0) {
861
- console.error(` ERROR: Claude Code version ${version} is too old.`);
862
- console.error(` Channels require >= ${MIN_CLAUDE_VERSION}.`);
863
- console.error(" Update: npm update -g @anthropic-ai/claude-code");
864
- process.exit(1);
865
- }
866
- } else {
867
- console.log(` claude: ${versionOutput} (version check skipped)`);
868
- }
932
+ versionOutput = execSync("claude --version", { encoding: "utf-8" }).trim();
869
933
  } catch {
870
- console.error(" ERROR: claude not found in PATH.");
871
- console.error(" Install Claude Code: npm install -g @anthropic-ai/claude-code");
872
- process.exit(1);
934
+ return {
935
+ name: "claude",
936
+ status: "fail",
937
+ detail: "not found in PATH",
938
+ hint: "Install Claude Code: npm install -g @anthropic-ai/claude-code"
939
+ };
940
+ }
941
+ const match = versionOutput.match(/(\d+\.\d+\.\d+)/);
942
+ if (!match) {
943
+ return { name: "claude", status: "ok", detail: `${versionOutput} (version check skipped)` };
944
+ }
945
+ const version = match[1];
946
+ if (compareVersions(version, MIN_CLAUDE_VERSION) < 0) {
947
+ return {
948
+ name: "claude",
949
+ status: "fail",
950
+ detail: `${version} is too old (channels require >= ${MIN_CLAUDE_VERSION})`,
951
+ hint: "Update: npm update -g @anthropic-ai/claude-code"
952
+ };
873
953
  }
954
+ return { name: "claude", status: "ok", detail: version };
874
955
  }
875
956
  function checkCodex() {
876
957
  try {
877
958
  const version = execSync("codex --version", { encoding: "utf-8" }).trim();
878
- console.log(` codex: ${version}`);
959
+ return { name: "codex", status: "ok", detail: version };
879
960
  } catch {
880
- console.error(" ERROR: codex not found in PATH.");
881
- console.error(" Install Codex: https://github.com/openai/codex");
882
- process.exit(1);
961
+ return {
962
+ name: "codex",
963
+ status: "warn",
964
+ detail: "not found in PATH (the Codex side will be unavailable until installed)",
965
+ hint: "Install Codex when you want to pair: https://github.com/openai/codex"
966
+ };
883
967
  }
884
968
  }
885
969
  function writeCollaborationSections(projectRoot) {
@@ -905,7 +989,7 @@ function writeCollaborationSections(projectRoot) {
905
989
  results.push(`${name}: unchanged (section already up to date)`);
906
990
  continue;
907
991
  }
908
- writeFileSync3(path, updated, "utf-8");
992
+ writeFileSync2(path, updated, "utf-8");
909
993
  if (existing === "") {
910
994
  results.push(`${name}: created with collaboration section`);
911
995
  } else if (existing.includes(`<!-- ${MARKER_ID}:start -->`)) {
@@ -918,6 +1002,7 @@ function writeCollaborationSections(projectRoot) {
918
1002
  }
919
1003
  var MIN_CLAUDE_VERSION = "2.1.80";
920
1004
  var init_init = __esm(() => {
1005
+ init_cli_invocation();
921
1006
  init_config_service();
922
1007
  init_cli();
923
1008
  init_pkg_root();
@@ -1041,7 +1126,7 @@ var init_dev = __esm(() => {
1041
1126
  });
1042
1127
 
1043
1128
  // src/control-protocol.ts
1044
- var CLOSE_CODE_REPLACED = 4001, CLOSE_CODE_EVICTED_STALE = 4002, CLOSE_CODE_PROBE_IN_PROGRESS = 4003, CLOSE_CODE_PAIR_MISMATCH = 4004;
1129
+ var CLOSE_CODE_REPLACED = 4001, CLOSE_CODE_EVICTED_STALE = 4002, CLOSE_CODE_PROBE_IN_PROGRESS = 4003, CLOSE_CODE_PAIR_MISMATCH = 4004, CLOSE_CODE_TOKEN_MISMATCH = 4005, CLOSE_CODE_CONTRACT_MISMATCH = 4006;
1045
1130
 
1046
1131
  // src/interrupt-timing.ts
1047
1132
  var CLIENT_REPLY_TIMEOUT_MS = 15000, INTERRUPT_CLIENT_MARGIN_MS = 2000, MAX_INTERRUPT_TIMEOUT_MS;
@@ -1049,6 +1134,76 @@ var init_interrupt_timing = __esm(() => {
1049
1134
  MAX_INTERRUPT_TIMEOUT_MS = CLIENT_REPLY_TIMEOUT_MS - INTERRUPT_CLIENT_MARGIN_MS;
1050
1135
  });
1051
1136
 
1137
+ // src/pending-request-registry.ts
1138
+ class PendingRequestRegistry {
1139
+ entries = new Map;
1140
+ setTimer;
1141
+ clearTimer;
1142
+ constructor(deps = {}) {
1143
+ this.setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
1144
+ this.clearTimer = deps.clearTimer ?? ((handle) => clearTimeout(handle));
1145
+ }
1146
+ get size() {
1147
+ return this.entries.size;
1148
+ }
1149
+ has(id) {
1150
+ return this.entries.has(id);
1151
+ }
1152
+ register(id, options) {
1153
+ const existing = this.entries.get(id);
1154
+ if (existing) {
1155
+ this.clearTimer(existing.timer);
1156
+ this.entries.delete(id);
1157
+ }
1158
+ return new Promise((resolve3, reject) => {
1159
+ const timer = this.setTimer(() => {
1160
+ if (!this.entries.has(id))
1161
+ return;
1162
+ this.entries.delete(id);
1163
+ options.onTimeout({ resolve: resolve3, reject });
1164
+ }, options.timeoutMs);
1165
+ if (options.unref) {
1166
+ timer.unref?.();
1167
+ }
1168
+ this.entries.set(id, { resolve: resolve3, reject, timer });
1169
+ });
1170
+ }
1171
+ settle(id, value) {
1172
+ const entry = this.entries.get(id);
1173
+ if (!entry)
1174
+ return false;
1175
+ this.clearTimer(entry.timer);
1176
+ this.entries.delete(id);
1177
+ entry.resolve(value);
1178
+ return true;
1179
+ }
1180
+ reject(id, error) {
1181
+ const entry = this.entries.get(id);
1182
+ if (!entry)
1183
+ return false;
1184
+ this.clearTimer(entry.timer);
1185
+ this.entries.delete(id);
1186
+ entry.reject(error);
1187
+ return true;
1188
+ }
1189
+ settleAll(value) {
1190
+ const make = typeof value === "function" ? value : () => value;
1191
+ for (const [id, entry] of this.entries) {
1192
+ this.clearTimer(entry.timer);
1193
+ this.entries.delete(id);
1194
+ entry.resolve(make(id));
1195
+ }
1196
+ }
1197
+ rejectAll(error) {
1198
+ const make = typeof error === "function" ? error : () => error;
1199
+ for (const [id, entry] of this.entries) {
1200
+ this.clearTimer(entry.timer);
1201
+ this.entries.delete(id);
1202
+ entry.reject(make(id));
1203
+ }
1204
+ }
1205
+ }
1206
+
1052
1207
  // src/daemon-client.ts
1053
1208
  import { EventEmitter } from "events";
1054
1209
  var nextSocketId = 0, DaemonClient;
@@ -1060,7 +1215,8 @@ var init_daemon_client = __esm(() => {
1060
1215
  ws = null;
1061
1216
  wsId = 0;
1062
1217
  nextRequestId = 1;
1063
- pendingReplies = new Map;
1218
+ pendingReplies = new PendingRequestRegistry;
1219
+ pendingEventWaiters = new PendingRequestRegistry;
1064
1220
  constructor(url, options = {}) {
1065
1221
  super();
1066
1222
  this.url = url;
@@ -1106,82 +1262,73 @@ var init_daemon_client = __esm(() => {
1106
1262
  });
1107
1263
  }
1108
1264
  attachClaude() {
1265
+ const identity = this.resolveIdentity();
1109
1266
  this.send({
1110
1267
  type: "claude_connect",
1111
- ...this.options.identity ? { identity: this.options.identity } : {}
1268
+ ...identity ? { identity } : {}
1112
1269
  });
1113
1270
  }
1271
+ resolveIdentity() {
1272
+ const opt = this.options.identity;
1273
+ return typeof opt === "function" ? opt() : opt;
1274
+ }
1114
1275
  async attachClaudeAndWaitForStatus(timeoutMs = 1000) {
1115
1276
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
1116
1277
  return null;
1117
1278
  }
1118
- return await new Promise((resolve3) => {
1119
- let settled = false;
1120
- let timer = null;
1121
- const cleanup = () => {
1122
- if (settled)
1123
- return;
1124
- settled = true;
1125
- if (timer) {
1126
- clearTimeout(timer);
1127
- timer = null;
1128
- }
1129
- this.off("status", onStatus);
1130
- this.off("rejected", onRejected);
1131
- this.off("disconnect", onDisconnect);
1132
- };
1133
- const finish = (value) => {
1134
- cleanup();
1135
- resolve3(value);
1136
- };
1137
- const onStatus = (status) => finish(status);
1138
- const onRejected = () => finish(null);
1139
- const onDisconnect = () => finish(null);
1140
- this.on("status", onStatus);
1141
- this.on("rejected", onRejected);
1142
- this.on("disconnect", onDisconnect);
1143
- timer = setTimeout(() => {
1144
- finish(null);
1145
- }, timeoutMs);
1146
- try {
1147
- this.attachClaude();
1148
- } catch {
1149
- finish(null);
1150
- }
1279
+ return this.awaitTypedResponse({
1280
+ key: "status",
1281
+ successEvent: "status",
1282
+ successValue: (status) => status,
1283
+ failValue: null,
1284
+ timeoutMs,
1285
+ send: () => this.attachClaude()
1151
1286
  });
1152
1287
  }
1153
1288
  async probeIncumbent(timeoutMs = 3000) {
1154
1289
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
1155
1290
  return { connected: false, alive: false };
1156
1291
  }
1157
- return await new Promise((resolve3) => {
1158
- let settled = false;
1159
- let timer = null;
1160
- const finish = (value) => {
1161
- if (settled)
1162
- return;
1163
- settled = true;
1164
- if (timer)
1165
- clearTimeout(timer);
1166
- this.off("incumbentStatus", onStatus);
1167
- this.off("disconnect", onDisconnect);
1168
- this.off("rejected", onRejected);
1169
- resolve3(value);
1170
- };
1171
- const onStatus = (s) => finish(s);
1172
- const onDisconnect = () => finish({ connected: false, alive: false });
1173
- const onRejected = () => finish({ connected: false, alive: false });
1174
- this.on("incumbentStatus", onStatus);
1175
- this.on("disconnect", onDisconnect);
1176
- this.on("rejected", onRejected);
1177
- timer = setTimeout(() => finish({ connected: false, alive: false }), timeoutMs);
1178
- try {
1179
- this.send({ type: "probe_incumbent" });
1180
- } catch {
1181
- finish({ connected: false, alive: false });
1182
- }
1292
+ return this.awaitTypedResponse({
1293
+ key: "incumbent_status",
1294
+ successEvent: "incumbentStatus",
1295
+ successValue: (s) => s,
1296
+ failValue: { connected: false, alive: false },
1297
+ timeoutMs,
1298
+ send: () => this.send({ type: "probe_incumbent" })
1183
1299
  });
1184
1300
  }
1301
+ awaitTypedResponse(opts) {
1302
+ const { key, successEvent, successValue, failValue, timeoutMs, send } = opts;
1303
+ const onSuccess = (payload) => {
1304
+ this.pendingEventWaiters.settle(key, successValue(payload));
1305
+ };
1306
+ const onRejected = () => {
1307
+ this.pendingEventWaiters.settle(key, failValue);
1308
+ };
1309
+ const onDisconnect = () => {
1310
+ this.pendingEventWaiters.settle(key, failValue);
1311
+ };
1312
+ const pending = this.pendingEventWaiters.register(key, {
1313
+ timeoutMs,
1314
+ onTimeout: ({ resolve: resolve3 }) => resolve3(failValue)
1315
+ });
1316
+ const cleanup = () => {
1317
+ this.off(successEvent, onSuccess);
1318
+ this.off("rejected", onRejected);
1319
+ this.off("disconnect", onDisconnect);
1320
+ };
1321
+ pending.finally(cleanup);
1322
+ this.on(successEvent, onSuccess);
1323
+ this.on("rejected", onRejected);
1324
+ this.on("disconnect", onDisconnect);
1325
+ try {
1326
+ send();
1327
+ } catch {
1328
+ this.pendingEventWaiters.settle(key, failValue);
1329
+ }
1330
+ return pending;
1331
+ }
1185
1332
  async disconnect() {
1186
1333
  if (!this.ws)
1187
1334
  return;
@@ -1199,21 +1346,19 @@ var init_daemon_client = __esm(() => {
1199
1346
  return { success: false, error: "AgentBridge daemon is not connected." };
1200
1347
  }
1201
1348
  const requestId = `reply_${Date.now()}_${this.nextRequestId++}`;
1202
- return new Promise((resolve3) => {
1203
- const timer = setTimeout(() => {
1204
- this.pendingReplies.delete(requestId);
1205
- resolve3({ success: false, error: "Timed out waiting for AgentBridge daemon reply." });
1206
- }, CLIENT_REPLY_TIMEOUT_MS);
1207
- this.pendingReplies.set(requestId, { resolve: resolve3, timer });
1208
- this.send({
1209
- type: "claude_to_codex",
1210
- requestId,
1211
- message,
1212
- ...requireReply ? { requireReply: true } : {},
1213
- ...onBusy && onBusy !== "reject" ? { onBusy } : {},
1214
- ...idempotencyKey ? { idempotencyKey } : {}
1215
- });
1349
+ const pending = this.pendingReplies.register(requestId, {
1350
+ timeoutMs: CLIENT_REPLY_TIMEOUT_MS,
1351
+ onTimeout: ({ resolve: resolve3 }) => resolve3({ success: false, error: "Timed out waiting for AgentBridge daemon reply." })
1216
1352
  });
1353
+ this.send({
1354
+ type: "claude_to_codex",
1355
+ requestId,
1356
+ message,
1357
+ ...requireReply ? { requireReply: true } : {},
1358
+ ...onBusy && onBusy !== "reject" ? { onBusy } : {},
1359
+ ...idempotencyKey ? { idempotencyKey } : {}
1360
+ });
1361
+ return pending;
1217
1362
  }
1218
1363
  attachSocketHandlers(ws, socketId) {
1219
1364
  ws.onmessage = (event) => {
@@ -1229,12 +1374,7 @@ var init_daemon_client = __esm(() => {
1229
1374
  this.emit("codexMessage", message.message);
1230
1375
  return;
1231
1376
  case "claude_to_codex_result": {
1232
- const pending = this.pendingReplies.get(message.requestId);
1233
- if (!pending)
1234
- return;
1235
- clearTimeout(pending.timer);
1236
- this.pendingReplies.delete(message.requestId);
1237
- pending.resolve({
1377
+ this.pendingReplies.settle(message.requestId, {
1238
1378
  success: message.success,
1239
1379
  error: message.error,
1240
1380
  ...message.code !== undefined ? { code: message.code } : {},
@@ -1265,7 +1405,7 @@ var init_daemon_client = __esm(() => {
1265
1405
  if (isCurrent) {
1266
1406
  this.ws = null;
1267
1407
  this.rejectPendingReplies("AgentBridge daemon disconnected.");
1268
- 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) {
1408
+ 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) {
1269
1409
  this.emit("rejected", event.code);
1270
1410
  } else {
1271
1411
  this.emit("disconnect");
@@ -1275,11 +1415,7 @@ var init_daemon_client = __esm(() => {
1275
1415
  ws.onerror = () => {};
1276
1416
  }
1277
1417
  rejectPendingReplies(error) {
1278
- for (const [requestId, pending] of this.pendingReplies.entries()) {
1279
- clearTimeout(pending.timer);
1280
- pending.resolve({ success: false, error });
1281
- this.pendingReplies.delete(requestId);
1282
- }
1418
+ this.pendingReplies.settleAll(() => ({ success: false, error }));
1283
1419
  }
1284
1420
  send(message) {
1285
1421
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
@@ -1298,6 +1434,10 @@ var init_daemon_client = __esm(() => {
1298
1434
  var CONTRACT_VERSION = 1;
1299
1435
 
1300
1436
  // src/build-info.ts
1437
+ function hasValidCodeHash(build) {
1438
+ const hash = build?.codeHash;
1439
+ return typeof hash === "string" && hash.length > 0 && hash !== CODE_HASH_SENTINEL;
1440
+ }
1301
1441
  function defineString(value, fallback) {
1302
1442
  return typeof value === "string" && value.length > 0 ? value : fallback;
1303
1443
  }
@@ -1312,7 +1452,14 @@ function defineNumber(value, fallback) {
1312
1452
  function sameRuntimeContract(a, b) {
1313
1453
  if (!a || !b)
1314
1454
  return false;
1315
- return a.version === b.version && a.commit === b.commit && a.contractVersion === b.contractVersion;
1455
+ if (a.version !== b.version || a.contractVersion !== b.contractVersion)
1456
+ return false;
1457
+ if (hasValidCodeHash(a) && hasValidCodeHash(b))
1458
+ return a.codeHash === b.codeHash;
1459
+ return a.commit === b.commit;
1460
+ }
1461
+ function runtimeContractComparisonBasis(a, b) {
1462
+ return hasValidCodeHash(a) && hasValidCodeHash(b) ? "codeHash" : "commit";
1316
1463
  }
1317
1464
  function compatibleContractVersion(a, b) {
1318
1465
  if (!a || !b)
@@ -1322,21 +1469,23 @@ function compatibleContractVersion(a, b) {
1322
1469
  function formatBuildInfo(build) {
1323
1470
  if (!build)
1324
1471
  return "<unknown>";
1325
- return `${build.version}/${build.commit}/${build.bundle}/contract-v${build.contractVersion}`;
1472
+ const codeHash = hasValidCodeHash(build) ? `/code-${build.codeHash}` : "";
1473
+ return `${build.version}/${build.commit}/${build.bundle}/contract-v${build.contractVersion}${codeHash}`;
1326
1474
  }
1327
- var BUILD_INFO;
1475
+ var CODE_HASH_SENTINEL = "source", BUILD_INFO;
1328
1476
  var init_build_info = __esm(() => {
1329
1477
  BUILD_INFO = Object.freeze({
1330
- version: defineString("0.1.12", "0.0.0-source"),
1331
- commit: defineString("eec6018", "source"),
1478
+ version: defineString("0.1.13", "0.0.0-source"),
1479
+ commit: defineString("7a71869", "source"),
1332
1480
  bundle: defineBundle("dist"),
1333
- contractVersion: defineNumber(1, CONTRACT_VERSION)
1481
+ contractVersion: defineNumber(1, CONTRACT_VERSION),
1482
+ codeHash: defineString("e1fd67d07c62", "source")
1334
1483
  });
1335
1484
  });
1336
1485
 
1337
1486
  // src/process-lifecycle.ts
1338
1487
  import { execFileSync as execFileSync4 } from "child_process";
1339
- import { basename } from "path";
1488
+ import { basename as basename2 } from "path";
1340
1489
  function parsePsProcessList(output) {
1341
1490
  const entries = [];
1342
1491
  for (const line of output.split(/\r?\n/)) {
@@ -1352,11 +1501,11 @@ function parsePsProcessList(output) {
1352
1501
  }
1353
1502
  function invokesCodexBinary(command) {
1354
1503
  const tokens = command.trim().split(/\s+/);
1355
- const exe = tokens[0] ? basename(tokens[0]) : "";
1504
+ const exe = tokens[0] ? basename2(tokens[0]) : "";
1356
1505
  if (exe === "codex")
1357
1506
  return true;
1358
1507
  if ((exe === "node" || exe === "bun") && tokens[1]) {
1359
- return basename(tokens[1]) === "codex";
1508
+ return basename2(tokens[1]) === "codex";
1360
1509
  }
1361
1510
  return false;
1362
1511
  }
@@ -1492,9 +1641,142 @@ var init_process_lifecycle = __esm(() => {
1492
1641
  isProcessAlive = pidLooksAlive;
1493
1642
  });
1494
1643
 
1644
+ // src/daemon-record.ts
1645
+ import { readFileSync as readFileSync4 } from "fs";
1646
+ function writeDaemonRecord(path, record) {
1647
+ atomicWriteJson(path, record);
1648
+ }
1649
+ function sanitizePorts(value) {
1650
+ if (typeof value !== "object" || value === null)
1651
+ return;
1652
+ const raw = value;
1653
+ const ports = {};
1654
+ if (typeof raw.appPort === "number")
1655
+ ports.appPort = raw.appPort;
1656
+ if (typeof raw.proxyPort === "number")
1657
+ ports.proxyPort = raw.proxyPort;
1658
+ if (typeof raw.controlPort === "number")
1659
+ ports.controlPort = raw.controlPort;
1660
+ return Object.keys(ports).length > 0 ? ports : undefined;
1661
+ }
1662
+ function readDaemonRecord(path, read = defaultRead) {
1663
+ let parsed;
1664
+ try {
1665
+ parsed = JSON.parse(read(path));
1666
+ } catch {
1667
+ return null;
1668
+ }
1669
+ if (typeof parsed !== "object" || parsed === null)
1670
+ return null;
1671
+ const obj = parsed;
1672
+ if (typeof obj.pid !== "number" || !Number.isFinite(obj.pid))
1673
+ return null;
1674
+ const phase = obj.phase === "ready" ? "ready" : "booting";
1675
+ const record = { pid: obj.pid, phase };
1676
+ if (typeof obj.startedAt === "number")
1677
+ record.startedAt = obj.startedAt;
1678
+ if (typeof obj.nonce === "string")
1679
+ record.nonce = obj.nonce;
1680
+ if (obj.pairId === null || typeof obj.pairId === "string")
1681
+ record.pairId = obj.pairId;
1682
+ if (obj.cwd === null || typeof obj.cwd === "string")
1683
+ record.cwd = obj.cwd;
1684
+ if (obj.stateDir === null || typeof obj.stateDir === "string")
1685
+ record.stateDir = obj.stateDir;
1686
+ if (typeof obj.proxyUrl === "string")
1687
+ record.proxyUrl = obj.proxyUrl;
1688
+ if (typeof obj.appServerUrl === "string")
1689
+ record.appServerUrl = obj.appServerUrl;
1690
+ const ports = sanitizePorts(obj.ports);
1691
+ if (ports !== undefined)
1692
+ record.ports = ports;
1693
+ if (typeof obj.build === "object" && obj.build !== null) {
1694
+ record.build = obj.build;
1695
+ }
1696
+ if (typeof obj.turnPhase === "string")
1697
+ record.turnPhase = obj.turnPhase;
1698
+ if (typeof obj.turnInProgress === "boolean")
1699
+ record.turnInProgress = obj.turnInProgress;
1700
+ if (typeof obj.attentionWindowActive === "boolean") {
1701
+ record.attentionWindowActive = obj.attentionWindowActive;
1702
+ }
1703
+ return record;
1704
+ }
1705
+ function synthesizeLegacyRecord(pidFilePath, statusFilePath, read = defaultRead) {
1706
+ let pidFromPidFile = null;
1707
+ try {
1708
+ const raw = read(pidFilePath).trim();
1709
+ const n = Number.parseInt(raw, 10);
1710
+ if (Number.isFinite(n))
1711
+ pidFromPidFile = n;
1712
+ } catch {}
1713
+ let status = null;
1714
+ try {
1715
+ const parsed = JSON.parse(read(statusFilePath));
1716
+ if (typeof parsed === "object" && parsed !== null)
1717
+ status = parsed;
1718
+ } catch {}
1719
+ const pidFromStatus = status && typeof status.pid === "number" && Number.isFinite(status.pid) ? status.pid : null;
1720
+ const pid = pidFromPidFile ?? pidFromStatus;
1721
+ if (pid === null)
1722
+ return null;
1723
+ const record = {
1724
+ pid,
1725
+ phase: status ? "ready" : "booting"
1726
+ };
1727
+ if (status) {
1728
+ if (typeof status.proxyUrl === "string")
1729
+ record.proxyUrl = status.proxyUrl;
1730
+ if (typeof status.appServerUrl === "string")
1731
+ record.appServerUrl = status.appServerUrl;
1732
+ const controlPort = typeof status.controlPort === "number" ? status.controlPort : undefined;
1733
+ const proxyPort = portFromUrl(status.proxyUrl);
1734
+ const appPort = portFromUrl(status.appServerUrl);
1735
+ if (controlPort !== undefined || proxyPort !== undefined || appPort !== undefined) {
1736
+ record.ports = {};
1737
+ if (appPort !== undefined)
1738
+ record.ports.appPort = appPort;
1739
+ if (proxyPort !== undefined)
1740
+ record.ports.proxyPort = proxyPort;
1741
+ if (controlPort !== undefined)
1742
+ record.ports.controlPort = controlPort;
1743
+ }
1744
+ if (status.pairId === null || typeof status.pairId === "string")
1745
+ record.pairId = status.pairId;
1746
+ if (status.cwd === null || typeof status.cwd === "string")
1747
+ record.cwd = status.cwd;
1748
+ if (status.stateDir === null || typeof status.stateDir === "string")
1749
+ record.stateDir = status.stateDir;
1750
+ if (typeof status.build === "object" && status.build !== null) {
1751
+ record.build = status.build;
1752
+ }
1753
+ if (typeof status.turnPhase === "string")
1754
+ record.turnPhase = status.turnPhase;
1755
+ if (typeof status.turnInProgress === "boolean")
1756
+ record.turnInProgress = status.turnInProgress;
1757
+ if (typeof status.attentionWindowActive === "boolean") {
1758
+ record.attentionWindowActive = status.attentionWindowActive;
1759
+ }
1760
+ }
1761
+ return record;
1762
+ }
1763
+ function readUnifiedDaemonRecord(paths, read = defaultRead) {
1764
+ return readDaemonRecord(paths.daemonRecordFile, read) ?? synthesizeLegacyRecord(paths.pidFile, paths.statusFile, read);
1765
+ }
1766
+ function portFromUrl(url) {
1767
+ if (typeof url !== "string")
1768
+ return;
1769
+ const match = url.match(/:(\d+)(?:[/?]|$)/);
1770
+ return match ? Number.parseInt(match[1], 10) : undefined;
1771
+ }
1772
+ var defaultRead = (path) => readFileSync4(path, "utf-8");
1773
+ var init_daemon_record = __esm(() => {
1774
+ init_atomic_json();
1775
+ });
1776
+
1495
1777
  // src/daemon-lifecycle.ts
1496
1778
  import { spawn } from "child_process";
1497
- import { existsSync as existsSync6, readFileSync as readFileSync4, statSync, unlinkSync, writeFileSync as writeFileSync4, openSync, closeSync, constants } from "fs";
1779
+ import { existsSync as existsSync6, readFileSync as readFileSync5, statSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3, openSync as openSync2, closeSync as closeSync2, constants } from "fs";
1498
1780
  import { fileURLToPath } from "url";
1499
1781
  function isReuseVerdict(verdict) {
1500
1782
  return verdict === "reuse" || verdict === "reuse-despite-drift";
@@ -1531,22 +1813,33 @@ function classifyDaemon(expectedPairId, status, buildInfo) {
1531
1813
  reason: "runtime build drift has a compatible contract and a live Codex TUI is attached"
1532
1814
  };
1533
1815
  }
1816
+ const basis = runtimeContractComparisonBasis(status.build, buildInfo) === "codeHash" ? "compared by codeHash" : "compared by commit stamp; legacy build without codeHash";
1534
1817
  return {
1535
1818
  verdict: "replace-drifted",
1536
- reason: `runtime build ${formatBuildInfo(status.build)} does not match launcher ${formatBuildInfo(buildInfo)}`
1819
+ reason: `runtime build ${formatBuildInfo(status.build)} does not match launcher ` + `${formatBuildInfo(buildInfo)} (${basis})`
1537
1820
  };
1538
1821
  }
1539
1822
  return { verdict: "reuse", reason: "daemon pair and runtime contract match" };
1540
1823
  }
1824
+ function resolveTiming(timing) {
1825
+ return {
1826
+ reuseReadyRetries: timing?.reuseReadyRetries ?? REUSE_READY_RETRIES,
1827
+ reuseReadyDelayMs: timing?.reuseReadyDelayMs ?? REUSE_READY_DELAY_MS,
1828
+ waitReadyRetries: timing?.waitReadyRetries ?? WAIT_READY_RETRIES,
1829
+ waitReadyDelayMs: timing?.waitReadyDelayMs ?? WAIT_READY_DELAY_MS
1830
+ };
1831
+ }
1541
1832
 
1542
1833
  class DaemonLifecycle {
1543
1834
  stateDir;
1544
1835
  controlPort;
1545
1836
  log;
1837
+ timing;
1546
1838
  constructor(opts) {
1547
1839
  this.stateDir = opts.stateDir;
1548
1840
  this.controlPort = opts.controlPort;
1549
1841
  this.log = opts.log;
1842
+ this.timing = resolveTiming(opts.timing);
1550
1843
  }
1551
1844
  get healthUrl() {
1552
1845
  return `http://127.0.0.1:${this.controlPort}/healthz`;
@@ -1603,7 +1896,7 @@ class DaemonLifecycle {
1603
1896
  break;
1604
1897
  }
1605
1898
  try {
1606
- await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
1899
+ await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
1607
1900
  return;
1608
1901
  } catch {
1609
1902
  this.log(`Daemon on control port ${this.controlPort} is healthy but not ready within reuse window \u2014 replacing`);
@@ -1616,7 +1909,7 @@ class DaemonLifecycle {
1616
1909
  if (isProcessAlive(existingPid)) {
1617
1910
  if (isAgentBridgeDaemon(existingPid)) {
1618
1911
  try {
1619
- await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
1912
+ await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
1620
1913
  return;
1621
1914
  } catch {
1622
1915
  this.log(`Existing daemon process ${existingPid} never became ready \u2014 replacing`);
@@ -1644,7 +1937,7 @@ class DaemonLifecycle {
1644
1937
  await this.kill(3000, status?.pid);
1645
1938
  } else {
1646
1939
  try {
1647
- await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
1940
+ await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
1648
1941
  return;
1649
1942
  } catch {
1650
1943
  this.log(`Daemon on control port ${this.controlPort} is healthy but not ready under startup lock \u2014 replacing`);
@@ -1653,7 +1946,7 @@ class DaemonLifecycle {
1653
1946
  }
1654
1947
  }
1655
1948
  this.launch();
1656
- await this.waitForReady();
1949
+ await this.waitForReady(this.timing.waitReadyRetries, this.timing.waitReadyDelayMs);
1657
1950
  });
1658
1951
  }
1659
1952
  async isHealthy() {
@@ -1680,7 +1973,7 @@ class DaemonLifecycle {
1680
1973
  return false;
1681
1974
  }
1682
1975
  }
1683
- async waitForReady(maxRetries = 40, delayMs = 250) {
1976
+ async waitForReady(maxRetries = WAIT_READY_RETRIES, delayMs = WAIT_READY_DELAY_MS) {
1684
1977
  for (let attempt = 0;attempt < maxRetries; attempt++) {
1685
1978
  if (await this.isReady())
1686
1979
  return;
@@ -1688,7 +1981,7 @@ class DaemonLifecycle {
1688
1981
  }
1689
1982
  throw new Error(`Timed out waiting for AgentBridge daemon readiness on ${this.readyUrl}`);
1690
1983
  }
1691
- async waitForReadyAndOurs(maxRetries = 40, delayMs = 250) {
1984
+ async waitForReadyAndOurs(maxRetries = WAIT_READY_RETRIES, delayMs = WAIT_READY_DELAY_MS) {
1692
1985
  for (let attempt = 0;attempt < maxRetries; attempt++) {
1693
1986
  if (await this.isReady()) {
1694
1987
  const status = await this.fetchStatus();
@@ -1704,22 +1997,35 @@ class DaemonLifecycle {
1704
1997
  }
1705
1998
  throw new Error(`Timed out waiting for AgentBridge daemon readiness+identity on ${this.readyUrl} (control port ${this.controlPort})`);
1706
1999
  }
2000
+ readDaemonRecord() {
2001
+ return readUnifiedDaemonRecord({
2002
+ daemonRecordFile: this.stateDir.daemonRecordFile,
2003
+ pidFile: this.stateDir.pidFile,
2004
+ statusFile: this.stateDir.statusFile
2005
+ });
2006
+ }
2007
+ writeDaemonRecord(record) {
2008
+ writeDaemonRecord(this.stateDir.daemonRecordFile, record);
2009
+ }
2010
+ removeDaemonRecord() {
2011
+ try {
2012
+ unlinkSync2(this.stateDir.daemonRecordFile);
2013
+ } catch {}
2014
+ }
1707
2015
  readStatus() {
1708
2016
  try {
1709
- const raw = readFileSync4(this.stateDir.statusFile, "utf-8");
2017
+ const raw = readFileSync5(this.stateDir.statusFile, "utf-8");
1710
2018
  return JSON.parse(raw);
1711
2019
  } catch {
1712
2020
  return null;
1713
2021
  }
1714
2022
  }
1715
2023
  writeStatus(status) {
1716
- this.stateDir.ensure();
1717
- writeFileSync4(this.stateDir.statusFile, JSON.stringify(status, null, 2) + `
1718
- `, "utf-8");
2024
+ atomicWriteJson(this.stateDir.statusFile, status);
1719
2025
  }
1720
2026
  readPid() {
1721
2027
  try {
1722
- const raw = readFileSync4(this.stateDir.pidFile, "utf-8").trim();
2028
+ const raw = readFileSync5(this.stateDir.pidFile, "utf-8").trim();
1723
2029
  if (!raw)
1724
2030
  return null;
1725
2031
  const pid = Number.parseInt(raw, 10);
@@ -1729,28 +2035,27 @@ class DaemonLifecycle {
1729
2035
  }
1730
2036
  }
1731
2037
  writePid(pid) {
1732
- this.stateDir.ensure();
1733
- writeFileSync4(this.stateDir.pidFile, `${pid ?? process.pid}
1734
- `, "utf-8");
2038
+ atomicWriteText(this.stateDir.pidFile, `${pid ?? process.pid}
2039
+ `);
1735
2040
  }
1736
2041
  removePidFile() {
1737
2042
  try {
1738
- unlinkSync(this.stateDir.pidFile);
2043
+ unlinkSync2(this.stateDir.pidFile);
1739
2044
  } catch {}
1740
2045
  }
1741
2046
  removeStatusFile() {
1742
2047
  try {
1743
- unlinkSync(this.stateDir.statusFile);
2048
+ unlinkSync2(this.stateDir.statusFile);
1744
2049
  } catch {}
1745
2050
  }
1746
2051
  markKilled() {
1747
2052
  this.stateDir.ensure();
1748
- writeFileSync4(this.stateDir.killedFile, `${Date.now()}
2053
+ writeFileSync3(this.stateDir.killedFile, `${Date.now()}
1749
2054
  `, "utf-8");
1750
2055
  }
1751
2056
  clearKilled() {
1752
2057
  try {
1753
- unlinkSync(this.stateDir.killedFile);
2058
+ unlinkSync2(this.stateDir.killedFile);
1754
2059
  } catch {}
1755
2060
  }
1756
2061
  wasKilled() {
@@ -1772,8 +2077,10 @@ class DaemonLifecycle {
1772
2077
  daemonProc.unref();
1773
2078
  }
1774
2079
  removeStalePidFile() {
1775
- this.log("Removing stale pid file");
2080
+ this.log("Removing stale daemon identity files");
1776
2081
  this.removePidFile();
2082
+ this.removeStatusFile();
2083
+ this.removeDaemonRecord();
1777
2084
  }
1778
2085
  async replaceUnhealthyDaemon(statusPid) {
1779
2086
  await this.withStartupLockStrict(async (locked) => {
@@ -1789,7 +2096,7 @@ class DaemonLifecycle {
1789
2096
  }
1790
2097
  if (isReuseVerdict(classification.verdict)) {
1791
2098
  try {
1792
- await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
2099
+ await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
1793
2100
  return;
1794
2101
  } catch {}
1795
2102
  }
@@ -1797,12 +2104,12 @@ class DaemonLifecycle {
1797
2104
  this.log(`Killing unhealthy daemon on control port ${this.controlPort} and relaunching`);
1798
2105
  await this.kill(3000, statusPid);
1799
2106
  this.launch();
1800
- await this.waitForReady();
2107
+ await this.waitForReady(this.timing.waitReadyRetries, this.timing.waitReadyDelayMs);
1801
2108
  });
1802
2109
  }
1803
2110
  async waitForContendedStartupLock() {
1804
2111
  this.log("Another process holds the startup lock, waiting for readiness+identity...");
1805
- await this.waitForReadyAndOurs();
2112
+ await this.waitForReadyAndOurs(this.timing.waitReadyRetries, this.timing.waitReadyDelayMs);
1806
2113
  }
1807
2114
  async withStartupLockStrict(fn) {
1808
2115
  const locked = this.acquireLockStrict();
@@ -1817,15 +2124,15 @@ class DaemonLifecycle {
1817
2124
  this.stateDir.ensure();
1818
2125
  let fd = null;
1819
2126
  try {
1820
- fd = openSync(this.stateDir.lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
1821
- writeFileSync4(fd, `${process.pid}
2127
+ fd = openSync2(this.stateDir.lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
2128
+ writeFileSync3(fd, `${process.pid}
1822
2129
  `);
1823
- closeSync(fd);
2130
+ closeSync2(fd);
1824
2131
  return true;
1825
2132
  } catch (err) {
1826
2133
  if (fd !== null && err.code !== "EEXIST") {
1827
2134
  try {
1828
- closeSync(fd);
2135
+ closeSync2(fd);
1829
2136
  } catch {}
1830
2137
  this.releaseLock();
1831
2138
  }
@@ -1833,7 +2140,7 @@ class DaemonLifecycle {
1833
2140
  if (reclaimed)
1834
2141
  return false;
1835
2142
  try {
1836
- const holderPid = Number.parseInt(readFileSync4(this.stateDir.lockFile, "utf-8").trim(), 10);
2143
+ const holderPid = Number.parseInt(readFileSync5(this.stateDir.lockFile, "utf-8").trim(), 10);
1837
2144
  if (Number.isFinite(holderPid) && !isProcessAlive(holderPid)) {
1838
2145
  this.log(`Stale startup lock from dead process ${holderPid}, reclaiming`);
1839
2146
  this.releaseLock();
@@ -1862,7 +2169,7 @@ class DaemonLifecycle {
1862
2169
  }
1863
2170
  releaseLock() {
1864
2171
  try {
1865
- unlinkSync(this.stateDir.lockFile);
2172
+ unlinkSync2(this.stateDir.lockFile);
1866
2173
  } catch {}
1867
2174
  }
1868
2175
  async kill(gracefulTimeoutMs = 3000, pidOverride) {
@@ -1908,6 +2215,7 @@ class DaemonLifecycle {
1908
2215
  cleanup() {
1909
2216
  this.removePidFile();
1910
2217
  this.removeStatusFile();
2218
+ this.removeDaemonRecord();
1911
2219
  }
1912
2220
  }
1913
2221
  async function fetchWithTimeout(url, timeoutMs = HEALTH_FETCH_TIMEOUT_MS) {
@@ -1919,10 +2227,12 @@ async function fetchWithTimeout(url, timeoutMs = HEALTH_FETCH_TIMEOUT_MS) {
1919
2227
  clearTimeout(timer);
1920
2228
  }
1921
2229
  }
1922
- var DEFAULT_DAEMON_ENTRY, DAEMON_ENTRY, DAEMON_PATH, REUSE_READY_RETRIES, REUSE_READY_DELAY_MS = 250, HEALTH_FETCH_TIMEOUT_MS = 500, LOCK_IDENTITY_GRACE_MS;
2230
+ var DEFAULT_DAEMON_ENTRY, DAEMON_ENTRY, DAEMON_PATH, REUSE_READY_RETRIES, REUSE_READY_DELAY_MS = 250, WAIT_READY_RETRIES = 40, WAIT_READY_DELAY_MS = 250, HEALTH_FETCH_TIMEOUT_MS = 500, LOCK_IDENTITY_GRACE_MS;
1923
2231
  var init_daemon_lifecycle = __esm(() => {
2232
+ init_atomic_json();
1924
2233
  init_build_info();
1925
2234
  init_process_lifecycle();
2235
+ init_daemon_record();
1926
2236
  DEFAULT_DAEMON_ENTRY = import.meta.url.endsWith(".ts") ? "./daemon.ts" : "./daemon.js";
1927
2237
  DAEMON_ENTRY = process.env.AGENTBRIDGE_DAEMON_ENTRY || DEFAULT_DAEMON_ENTRY;
1928
2238
  DAEMON_PATH = fileURLToPath(new URL(DAEMON_ENTRY, import.meta.url));
@@ -1933,26 +2243,22 @@ var init_daemon_lifecycle = __esm(() => {
1933
2243
  // src/pair-registry.ts
1934
2244
  import { execFileSync as execFileSync5 } from "child_process";
1935
2245
  import {
1936
- closeSync as closeSync2,
1937
2246
  existsSync as existsSync7,
1938
- fsyncSync,
1939
2247
  linkSync,
1940
2248
  lstatSync,
1941
- mkdirSync as mkdirSync3,
1942
- openSync as openSync2,
2249
+ mkdirSync as mkdirSync4,
1943
2250
  readdirSync,
1944
- readFileSync as readFileSync5,
2251
+ readFileSync as readFileSync6,
1945
2252
  realpathSync,
1946
- renameSync,
1947
2253
  rmSync as rmSync2,
1948
2254
  statSync as statSync2,
1949
- unlinkSync as unlinkSync2,
1950
- writeFileSync as writeFileSync5
2255
+ unlinkSync as unlinkSync3,
2256
+ writeFileSync as writeFileSync4
1951
2257
  } from "fs";
1952
2258
  import { createServer } from "net";
1953
- import { createHash, randomUUID } from "crypto";
2259
+ import { createHash, randomUUID as randomUUID2 } from "crypto";
1954
2260
  import { hostname, userInfo } from "os";
1955
- import { basename as basename2, join as join6, resolve as resolve3, sep } from "path";
2261
+ import { basename as basename3, join as join6, resolve as resolve3, sep } from "path";
1956
2262
  function portsForSlot(slot) {
1957
2263
  if (!Number.isInteger(slot) || slot < 0) {
1958
2264
  throw new PairError("PAIR_ID_INVALID", `Invalid slot: ${slot}`);
@@ -2001,7 +2307,7 @@ function readRegistry(base) {
2001
2307
  return { version: 1, pairs: [] };
2002
2308
  let parsed;
2003
2309
  try {
2004
- parsed = JSON.parse(readFileSync5(path, "utf-8"));
2310
+ parsed = JSON.parse(readFileSync6(path, "utf-8"));
2005
2311
  } catch (err) {
2006
2312
  throw new PairError("PAIR_REGISTRY_CORRUPT", `Registry JSON is not parseable at ${path}: ${err.message}`, {
2007
2313
  path
@@ -2032,26 +2338,14 @@ function readRegistry(base) {
2032
2338
  return parsed;
2033
2339
  }
2034
2340
  function writeRegistry(base, reg) {
2035
- mkdirSync3(pairsDir(base), { recursive: true });
2036
- const target = registryPath(base);
2037
- const tmp = `${target}.tmp.${process.pid}`;
2038
- const data = JSON.stringify(reg, null, 2) + `
2039
- `;
2040
- const fd = openSync2(tmp, "w");
2041
- try {
2042
- writeFileSync5(fd, data);
2043
- fsyncSync(fd);
2044
- } finally {
2045
- closeSync2(fd);
2046
- }
2047
- renameSync(tmp, target);
2341
+ atomicWriteJson(registryPath(base), reg, { fsync: true });
2048
2342
  }
2049
2343
  function lockFilePath(base) {
2050
2344
  return join6(pairsDir(base), LOCK_FILE_NAME);
2051
2345
  }
2052
2346
  function readLockOwner(lockFile) {
2053
2347
  try {
2054
- const parsed = JSON.parse(readFileSync5(lockFile, "utf-8"));
2348
+ const parsed = JSON.parse(readFileSync6(lockFile, "utf-8"));
2055
2349
  if (typeof parsed.pid === "number" && typeof parsed.nonce === "string")
2056
2350
  return parsed;
2057
2351
  return null;
@@ -2091,7 +2385,7 @@ function lockIsStale(lockFile) {
2091
2385
  }
2092
2386
  function attemptReclaim(lockFile) {
2093
2387
  const reclaimLock = `${lockFile}.reclaim`;
2094
- const myNonce = randomUUID();
2388
+ const myNonce = randomUUID2();
2095
2389
  const ownerJson = JSON.stringify({
2096
2390
  pid: process.pid,
2097
2391
  createdAt: Date.now(),
@@ -2099,10 +2393,10 @@ function attemptReclaim(lockFile) {
2099
2393
  hostname: safeHostname(),
2100
2394
  uid: safeUid()
2101
2395
  });
2102
- const tmp = `${reclaimLock}.acq.${process.pid}.${randomUUID()}`;
2396
+ const tmp = `${reclaimLock}.acq.${process.pid}.${randomUUID2()}`;
2103
2397
  let held = false;
2104
2398
  try {
2105
- writeFileSync5(tmp, ownerJson);
2399
+ writeFileSync4(tmp, ownerJson);
2106
2400
  try {
2107
2401
  linkSync(tmp, reclaimLock);
2108
2402
  held = true;
@@ -2110,7 +2404,7 @@ function attemptReclaim(lockFile) {
2110
2404
  if (err?.code === "EEXIST") {
2111
2405
  if (lockIsStale(reclaimLock)) {
2112
2406
  try {
2113
- unlinkSync2(reclaimLock);
2407
+ unlinkSync3(reclaimLock);
2114
2408
  } catch {}
2115
2409
  }
2116
2410
  return;
@@ -2119,7 +2413,7 @@ function attemptReclaim(lockFile) {
2119
2413
  }
2120
2414
  } finally {
2121
2415
  try {
2122
- unlinkSync2(tmp);
2416
+ unlinkSync3(tmp);
2123
2417
  } catch {}
2124
2418
  }
2125
2419
  if (!held)
@@ -2129,22 +2423,22 @@ function attemptReclaim(lockFile) {
2129
2423
  return;
2130
2424
  if (lockIsStale(lockFile)) {
2131
2425
  try {
2132
- unlinkSync2(lockFile);
2426
+ unlinkSync3(lockFile);
2133
2427
  } catch {}
2134
2428
  }
2135
2429
  } finally {
2136
2430
  if (readLockOwner(reclaimLock)?.nonce === myNonce) {
2137
2431
  try {
2138
- unlinkSync2(reclaimLock);
2432
+ unlinkSync3(reclaimLock);
2139
2433
  } catch {}
2140
2434
  }
2141
2435
  }
2142
2436
  }
2143
2437
  async function withRegistryLock(base, fn) {
2144
- mkdirSync3(pairsDir(base), { recursive: true });
2438
+ mkdirSync4(pairsDir(base), { recursive: true });
2145
2439
  const lockFile = lockFilePath(base);
2146
2440
  const deadline = Date.now() + LOCK_DEADLINE_MS;
2147
- const myNonce = randomUUID();
2441
+ const myNonce = randomUUID2();
2148
2442
  const ownerJson = JSON.stringify({
2149
2443
  pid: process.pid,
2150
2444
  createdAt: Date.now(),
@@ -2153,10 +2447,10 @@ async function withRegistryLock(base, fn) {
2153
2447
  uid: safeUid()
2154
2448
  });
2155
2449
  for (;; ) {
2156
- const tmp = `${lockFile}.acq.${process.pid}.${randomUUID()}`;
2450
+ const tmp = `${lockFile}.acq.${process.pid}.${randomUUID2()}`;
2157
2451
  let acquired = false;
2158
2452
  try {
2159
- writeFileSync5(tmp, ownerJson);
2453
+ writeFileSync4(tmp, ownerJson);
2160
2454
  try {
2161
2455
  linkSync(tmp, lockFile);
2162
2456
  acquired = true;
@@ -2166,7 +2460,7 @@ async function withRegistryLock(base, fn) {
2166
2460
  }
2167
2461
  } finally {
2168
2462
  try {
2169
- unlinkSync2(tmp);
2463
+ unlinkSync3(tmp);
2170
2464
  } catch {}
2171
2465
  }
2172
2466
  if (acquired) {
@@ -2176,7 +2470,7 @@ async function withRegistryLock(base, fn) {
2176
2470
  const current = readLockOwner(lockFile);
2177
2471
  if (!current || current.nonce === myNonce) {
2178
2472
  try {
2179
- unlinkSync2(lockFile);
2473
+ unlinkSync3(lockFile);
2180
2474
  } catch {}
2181
2475
  }
2182
2476
  }
@@ -2198,7 +2492,7 @@ function detectLegacyRootDaemon(base) {
2198
2492
  return null;
2199
2493
  let pid;
2200
2494
  try {
2201
- const raw = readFileSync5(rootPidFile, "utf-8").trim();
2495
+ const raw = readFileSync6(rootPidFile, "utf-8").trim();
2202
2496
  pid = Number.parseInt(raw, 10);
2203
2497
  } catch {
2204
2498
  return null;
@@ -2306,6 +2600,9 @@ async function removeAllocatedPairIfUnchanged(base, pairId, slot) {
2306
2600
  writeRegistry(base, { version: 1, pairs: nextPairs });
2307
2601
  });
2308
2602
  }
2603
+ function pairsRootDir(base) {
2604
+ return pairsDir(base);
2605
+ }
2309
2606
  function pairDirPath(base, pairId) {
2310
2607
  const id = validatePairId(pairId);
2311
2608
  return join6(pairsDir(base), id);
@@ -2348,12 +2645,17 @@ function pairDirDaemonAlive(base, pairId) {
2348
2645
  const dir = join6(pairsDir(base), pairId);
2349
2646
  const pids = [];
2350
2647
  try {
2351
- const pid = Number.parseInt(readFileSync5(join6(dir, "daemon.pid"), "utf-8").trim(), 10);
2648
+ const record = JSON.parse(readFileSync6(join6(dir, "daemon.json"), "utf-8"));
2649
+ if (typeof record?.pid === "number" && Number.isFinite(record.pid))
2650
+ pids.push(record.pid);
2651
+ } catch {}
2652
+ try {
2653
+ const pid = Number.parseInt(readFileSync6(join6(dir, "daemon.pid"), "utf-8").trim(), 10);
2352
2654
  if (Number.isFinite(pid))
2353
2655
  pids.push(pid);
2354
2656
  } catch {}
2355
2657
  try {
2356
- const status = JSON.parse(readFileSync5(join6(dir, "status.json"), "utf-8"));
2658
+ const status = JSON.parse(readFileSync6(join6(dir, "status.json"), "utf-8"));
2357
2659
  if (typeof status?.pid === "number")
2358
2660
  pids.push(status.pid);
2359
2661
  } catch {}
@@ -2387,10 +2689,54 @@ async function removeUnregisteredPairDir(base, pairId) {
2387
2689
  return { removed: removePairDir(base, pairId) };
2388
2690
  });
2389
2691
  }
2390
- var PAIR_BASE_PORT = 4500, PAIR_SLOT_STRIDE = 10, PAIR_ID_REGEX, DEFAULT_PAIR_NAME = "main", LOCK_FILE_NAME = ".registry.lock", REGISTRY_FILE_NAME = "registry.json", LOCK_DEADLINE_MS = 1e4, ORPHAN_GRACE_MS = 3000, LEGACY_ROOT_CONTROL_PORT = 4502, WINDOWS_RESERVED_RE, PairError, MAX_PAIR_SLOT;
2692
+ async function removeOrphanPairDirIgnoringRegistry(base, pairId) {
2693
+ return withRegistryLock(base, () => {
2694
+ if (pairDirDaemonAlive(base, pairId)) {
2695
+ return { removed: false, reason: "live" };
2696
+ }
2697
+ return { removed: removePairDir(base, pairId) };
2698
+ });
2699
+ }
2700
+ function isEntryReclaimable(signals) {
2701
+ return signals.cwdGone && signals.dead && signals.old;
2702
+ }
2703
+ function cwdMissing(cwd) {
2704
+ try {
2705
+ statSync2(cwd);
2706
+ return false;
2707
+ } catch (err) {
2708
+ return err?.code === "ENOENT";
2709
+ }
2710
+ }
2711
+ function parseCreatedAtMs(createdAt) {
2712
+ if (typeof createdAt !== "string")
2713
+ return null;
2714
+ const ms = Date.parse(createdAt);
2715
+ return Number.isFinite(ms) ? ms : null;
2716
+ }
2717
+ function classifyReclaimableEntries(base, now = Date.now()) {
2718
+ const reg = readRegistry(base);
2719
+ const out = [];
2720
+ for (const entry of reg.pairs) {
2721
+ const createdMs = parseCreatedAtMs(entry.createdAt);
2722
+ const ageMs = createdMs === null ? null : Math.max(0, now - createdMs);
2723
+ const signals = {
2724
+ cwdGone: cwdMissing(entry.cwd),
2725
+ dead: !pairDirDaemonAlive(base, entry.pairId),
2726
+ old: ageMs !== null && ageMs >= RECLAIMABLE_MIN_AGE_MS,
2727
+ ageMs
2728
+ };
2729
+ if (isEntryReclaimable(signals))
2730
+ out.push({ entry, signals });
2731
+ }
2732
+ return out;
2733
+ }
2734
+ var PAIR_BASE_PORT = 4500, PAIR_SLOT_STRIDE = 10, PAIR_ID_REGEX, DEFAULT_PAIR_NAME = "main", RECLAIMABLE_MIN_AGE_MS, LOCK_FILE_NAME = ".registry.lock", REGISTRY_FILE_NAME = "registry.json", LOCK_DEADLINE_MS = 1e4, ORPHAN_GRACE_MS = 3000, LEGACY_ROOT_CONTROL_PORT = 4502, WINDOWS_RESERVED_RE, PairError, MAX_PAIR_SLOT;
2391
2735
  var init_pair_registry = __esm(() => {
2736
+ init_atomic_json();
2392
2737
  init_process_lifecycle();
2393
2738
  PAIR_ID_REGEX = /^[A-Za-z0-9._-]{1,64}$/;
2739
+ RECLAIMABLE_MIN_AGE_MS = 24 * 60 * 60 * 1000;
2394
2740
  WINDOWS_RESERVED_RE = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
2395
2741
  PairError = class PairError extends Error {
2396
2742
  code;
@@ -2673,7 +3019,7 @@ var init_pair_resolver = __esm(() => {
2673
3019
  });
2674
3020
 
2675
3021
  // src/trace-log.ts
2676
- import { appendFileSync, existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync2, statSync as statSync3, unlinkSync as unlinkSync3 } from "fs";
3022
+ import { appendFileSync, existsSync as existsSync8, mkdirSync as mkdirSync5, readdirSync as readdirSync2, statSync as statSync3, unlinkSync as unlinkSync4 } from "fs";
2677
3023
  import { join as join8 } from "path";
2678
3024
  function pickRelevantEnv(env) {
2679
3025
  const picked = {};
@@ -2725,7 +3071,7 @@ function appendTraceEvent(input) {
2725
3071
  };
2726
3072
  const logsDir = join8(input.cwd, ".agentbridge", "logs");
2727
3073
  const isNewDayFile = !existsSync8(path);
2728
- mkdirSync4(logsDir, { recursive: true });
3074
+ mkdirSync5(logsDir, { recursive: true });
2729
3075
  if (isNewDayFile) {
2730
3076
  pruneOldTraceLogs(logsDir, path, Date.parse(timestamp));
2731
3077
  }
@@ -2751,7 +3097,7 @@ function pruneOldTraceLogs(logsDir, keepPath, nowMs) {
2751
3097
  continue;
2752
3098
  try {
2753
3099
  if (statSync3(filePath).mtimeMs < cutoff) {
2754
- unlinkSync3(filePath);
3100
+ unlinkSync4(filePath);
2755
3101
  }
2756
3102
  } catch {}
2757
3103
  }
@@ -2834,10 +3180,12 @@ var exports_claude = {};
2834
3180
  __export(exports_claude, {
2835
3181
  warnIfPluginCacheMissing: () => warnIfPluginCacheMissing,
2836
3182
  runClaude: () => runClaude,
3183
+ mapChildExitCode: () => mapChildExitCode,
2837
3184
  checkOwnedFlagConflicts: () => checkOwnedFlagConflicts
2838
3185
  });
2839
3186
  import { spawn as spawn2 } from "child_process";
2840
3187
  import { existsSync as existsSync9 } from "fs";
3188
+ import { constants as osConstants } from "os";
2841
3189
  async function runClaude(args) {
2842
3190
  const originalEnv = { ...process.env };
2843
3191
  const envGuardResult = guardAgentBridgeEnv({
@@ -2890,8 +3238,8 @@ async function runClaude(args) {
2890
3238
  stdio: "inherit",
2891
3239
  env: process.env
2892
3240
  });
2893
- child.on("exit", (code) => {
2894
- process.exit(code ?? 0);
3241
+ child.on("exit", (code, signal) => {
3242
+ process.exit(mapChildExitCode(code, signal));
2895
3243
  });
2896
3244
  child.on("error", (err) => {
2897
3245
  if (err.code === "ENOENT") {
@@ -2903,6 +3251,12 @@ async function runClaude(args) {
2903
3251
  process.exit(1);
2904
3252
  });
2905
3253
  }
3254
+ function mapChildExitCode(code, signal) {
3255
+ if (signal) {
3256
+ return 128 + (osConstants.signals[signal] ?? 0);
3257
+ }
3258
+ return code ?? 0;
3259
+ }
2906
3260
  function warnIfPluginCacheMissing(cacheRoot = pluginCacheRoot(), log = (msg) => console.error(msg)) {
2907
3261
  let cacheExists;
2908
3262
  try {
@@ -3003,7 +3357,7 @@ var init_claude = __esm(() => {
3003
3357
  });
3004
3358
 
3005
3359
  // src/agents-contract.ts
3006
- import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
3360
+ import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
3007
3361
  import { join as join9 } from "path";
3008
3362
  function checkAgentsMdContract(cwd) {
3009
3363
  const path = join9(cwd, "AGENTS.md");
@@ -3011,7 +3365,7 @@ function checkAgentsMdContract(cwd) {
3011
3365
  let content = "";
3012
3366
  if (exists) {
3013
3367
  try {
3014
- content = readFileSync6(path, "utf-8");
3368
+ content = readFileSync7(path, "utf-8");
3015
3369
  } catch {
3016
3370
  return {
3017
3371
  fresh: false,
@@ -3038,7 +3392,7 @@ function isFreshAgentsMdContract(content) {
3038
3392
  var init_agents_contract = () => {};
3039
3393
 
3040
3394
  // src/wrapper-exit-observability.ts
3041
- import { readFileSync as readFileSync7, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
3395
+ import { readFileSync as readFileSync8, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
3042
3396
  import { join as join10 } from "path";
3043
3397
  function discoverNativeChildPid(launcherPid, run) {
3044
3398
  try {
@@ -3049,17 +3403,15 @@ function discoverNativeChildPid(launcherPid, run) {
3049
3403
  return null;
3050
3404
  }
3051
3405
  }
3052
- function readTurnInProgress(statusFilePath, read = (p) => readFileSync7(p, "utf-8"), isPidAlive = defaultIsPidAlive) {
3053
- try {
3054
- const status = JSON.parse(read(statusFilePath));
3055
- if (typeof status.turnInProgress !== "boolean")
3056
- return null;
3057
- if (typeof status.pid === "number" && !isPidAlive(status.pid))
3058
- return null;
3059
- return status.turnInProgress;
3060
- } catch {
3406
+ function readUnifiedTurnInProgress(paths, read = (p) => readFileSync8(p, "utf-8"), isPidAlive = defaultIsPidAlive) {
3407
+ const record = readUnifiedDaemonRecord(paths, read);
3408
+ if (!record)
3061
3409
  return null;
3062
- }
3410
+ if (typeof record.turnInProgress !== "boolean")
3411
+ return null;
3412
+ if (typeof record.pid === "number" && !isPidAlive(record.pid))
3413
+ return null;
3414
+ return record.turnInProgress;
3063
3415
  }
3064
3416
  function refineCleanExitClassification(turnInProgress) {
3065
3417
  if (turnInProgress === true)
@@ -3068,14 +3420,14 @@ function refineCleanExitClassification(turnInProgress) {
3068
3420
  return "exit_0_idle";
3069
3421
  return "exit_0_turn_unknown";
3070
3422
  }
3071
- function findCodexSqliteLog(codexHome, fs = { readdir: readdirSync3, stat: statSync4 }) {
3423
+ function findCodexSqliteLog(codexHome, fs2 = { readdir: readdirSync3, stat: statSync4 }) {
3072
3424
  try {
3073
- const entries = fs.readdir(codexHome).filter((name) => /^logs.*\.sqlite$/.test(String(name)));
3425
+ const entries = fs2.readdir(codexHome).filter((name) => /^logs.*\.sqlite$/.test(String(name)));
3074
3426
  let best = null;
3075
3427
  for (const name of entries) {
3076
3428
  const path = join10(codexHome, String(name));
3077
3429
  try {
3078
- const mtime = fs.stat(path).mtimeMs;
3430
+ const mtime = fs2.stat(path).mtimeMs;
3079
3431
  if (!best || mtime > best.mtime)
3080
3432
  best = { path, mtime };
3081
3433
  } catch {}
@@ -3110,14 +3462,15 @@ function captureTuiLogTail(options) {
3110
3462
  var defaultIsPidAlive;
3111
3463
  var init_wrapper_exit_observability = __esm(() => {
3112
3464
  init_process_lifecycle();
3465
+ init_daemon_record();
3113
3466
  defaultIsPidAlive = pidLooksAlive;
3114
3467
  });
3115
3468
 
3116
3469
  // src/pair-command.ts
3117
- function pairScopedCommand(cmd) {
3470
+ function pairScopedCommand(cmd, name = cliInvocationName()) {
3118
3471
  const pairId = process.env.AGENTBRIDGE_PAIR_ID;
3119
3472
  if (!pairId)
3120
- return `agentbridge ${cmd}`;
3473
+ return `${name} ${cmd}`;
3121
3474
  let selector = process.env.AGENTBRIDGE_PAIR_NAME;
3122
3475
  if (!selector) {
3123
3476
  try {
@@ -3126,19 +3479,20 @@ function pairScopedCommand(cmd) {
3126
3479
  selector = pairId;
3127
3480
  }
3128
3481
  }
3129
- return `agentbridge --pair ${selector} ${cmd}`;
3482
+ return `${name} --pair ${selector} ${cmd}`;
3130
3483
  }
3131
3484
  var init_pair_command = __esm(() => {
3485
+ init_cli_invocation();
3132
3486
  init_pair_resolver();
3133
3487
  });
3134
3488
 
3135
3489
  // src/rotating-log.ts
3136
- import { appendFileSync as appendFileSync2, existsSync as existsSync11, renameSync as renameSync2, statSync as statSync5, unlinkSync as unlinkSync4 } from "fs";
3137
- import { dirname as dirname2 } from "path";
3490
+ import { appendFileSync as appendFileSync2, existsSync as existsSync11, renameSync as renameSync2, statSync as statSync5, unlinkSync as unlinkSync5 } from "fs";
3491
+ import { dirname as dirname3 } from "path";
3138
3492
  function appendRotatingLog(path, content, options = {}, fsOps = REAL_FS_OPS) {
3139
3493
  const maxBytes = options.maxBytes ?? positiveIntFromEnv("AGENTBRIDGE_LOG_MAX_BYTES", DEFAULT_MAX_BYTES);
3140
3494
  const keep = options.keep ?? positiveIntFromEnv("AGENTBRIDGE_LOG_ROTATE_KEEP", DEFAULT_KEEP);
3141
- if (!fsOps.existsSync(dirname2(path)))
3495
+ if (!fsOps.existsSync(dirname3(path)))
3142
3496
  return;
3143
3497
  rotateIfNeeded(path, Buffer.byteLength(content), maxBytes, keep, fsOps);
3144
3498
  fsOps.appendFileSync(path, content, "utf-8");
@@ -3196,7 +3550,7 @@ function rotateIfNeeded(path, incomingBytes, maxBytes, keep, fsOps) {
3196
3550
  var DEFAULT_MAX_BYTES, DEFAULT_KEEP = 3, REAL_FS_OPS;
3197
3551
  var init_rotating_log = __esm(() => {
3198
3552
  DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
3199
- REAL_FS_OPS = { statSync: statSync5, renameSync: renameSync2, unlinkSync: unlinkSync4, appendFileSync: appendFileSync2, existsSync: existsSync11 };
3553
+ REAL_FS_OPS = { statSync: statSync5, renameSync: renameSync2, unlinkSync: unlinkSync5, appendFileSync: appendFileSync2, existsSync: existsSync11 };
3200
3554
  });
3201
3555
 
3202
3556
  // src/stderr-ring-buffer.ts
@@ -3250,30 +3604,20 @@ var init_stderr_ring_buffer = __esm(() => {
3250
3604
  // src/thread-state.ts
3251
3605
  import {
3252
3606
  existsSync as existsSync12,
3253
- mkdirSync as mkdirSync5,
3254
3607
  readdirSync as readdirSync4,
3255
- readFileSync as readFileSync8,
3256
- renameSync as renameSync3,
3257
- writeFileSync as writeFileSync6
3608
+ readFileSync as readFileSync9
3258
3609
  } from "fs";
3259
3610
  import { homedir as homedir3 } from "os";
3260
- import { basename as basename3, dirname as dirname3, join as join11 } from "path";
3611
+ import { basename as basename4, join as join11 } from "path";
3261
3612
  function nowIso() {
3262
3613
  return new Date().toISOString();
3263
3614
  }
3264
3615
  function codexHome(env = process.env) {
3265
3616
  return env.CODEX_HOME && env.CODEX_HOME.length > 0 ? env.CODEX_HOME : join11(homedir3(), ".codex");
3266
3617
  }
3267
- function atomicWriteJson(path, value) {
3268
- mkdirSync5(dirname3(path), { recursive: true });
3269
- const tmp = `${path}.tmp.${process.pid}.${Date.now()}`;
3270
- writeFileSync6(tmp, JSON.stringify(value, null, 2) + `
3271
- `, "utf-8");
3272
- renameSync3(tmp, path);
3273
- }
3274
3618
  function readRawCurrentThread(stateDir) {
3275
3619
  try {
3276
- const parsed = JSON.parse(readFileSync8(stateDir.currentThreadFile, "utf-8"));
3620
+ const parsed = JSON.parse(readFileSync9(stateDir.currentThreadFile, "utf-8"));
3277
3621
  if (parsed?.version === 1 && typeof parsed.threadId === "string" && parsed.threadId.length > 0 && (parsed.status === "pending" || parsed.status === "current") && typeof parsed.cwd === "string") {
3278
3622
  return parsed;
3279
3623
  }
@@ -3304,7 +3648,7 @@ function findCodexRolloutFile(threadId, env = process.env, maxEntries = 20000) {
3304
3648
  }
3305
3649
  if (!entry.isFile())
3306
3650
  continue;
3307
- const name = basename3(entry.name);
3651
+ const name = basename4(entry.name);
3308
3652
  if (name === exactName || name.startsWith("rollout-") && name.endsWith(".jsonl") && name.includes(threadId)) {
3309
3653
  return path;
3310
3654
  }
@@ -3336,7 +3680,9 @@ function readUsableCurrentThread(identity, env = process.env) {
3336
3680
  atomicWriteJson(identity.stateDir.currentThreadFile, repaired);
3337
3681
  return repaired;
3338
3682
  }
3339
- var init_thread_state = () => {};
3683
+ var init_thread_state = __esm(() => {
3684
+ init_atomic_json();
3685
+ });
3340
3686
 
3341
3687
  // src/cli/codex.ts
3342
3688
  var exports_codex = {};
@@ -3351,9 +3697,9 @@ import {
3351
3697
  openSync as openSync3,
3352
3698
  writeSync,
3353
3699
  closeSync as closeSync3,
3354
- writeFileSync as writeFileSync7,
3355
- readFileSync as readFileSync9,
3356
- unlinkSync as unlinkSync5,
3700
+ writeFileSync as writeFileSync5,
3701
+ readFileSync as readFileSync10,
3702
+ unlinkSync as unlinkSync6,
3357
3703
  existsSync as existsSync13,
3358
3704
  mkdirSync as mkdirSync6
3359
3705
  } from "fs";
@@ -3525,9 +3871,9 @@ async function runCodex(args) {
3525
3871
  process.exit(1);
3526
3872
  }
3527
3873
  let proxyUrl;
3528
- const status = lifecycle.readStatus();
3529
- if (status?.proxyUrl) {
3530
- proxyUrl = status.proxyUrl;
3874
+ const record = lifecycle.readDaemonRecord();
3875
+ if (typeof record?.proxyUrl === "string" && record.proxyUrl.length > 0) {
3876
+ proxyUrl = record.proxyUrl;
3531
3877
  } else {
3532
3878
  const fallbackProxyPort = process.env.CODEX_PROXY_PORT ?? String(new ConfigService().loadOrDefault((msg) => console.error(`[agentbridge] ${msg}`)).codex.proxyPort);
3533
3879
  proxyUrl = `ws://127.0.0.1:${fallbackProxyPort}`;
@@ -3609,7 +3955,7 @@ async function runCodex(args) {
3609
3955
  env: buildChildEnv()
3610
3956
  });
3611
3957
  if (typeof child.pid === "number") {
3612
- writeFileSync7(stateDir.tuiPidFile, `${child.pid}
3958
+ writeFileSync5(stateDir.tuiPidFile, `${child.pid}
3613
3959
  `, "utf-8");
3614
3960
  appendWrapperLog(wrapperLogPath, `child pid=${child.pid}`);
3615
3961
  }
@@ -3649,7 +3995,7 @@ async function runCodex(args) {
3649
3995
  return;
3650
3996
  cleanedTuiPid = true;
3651
3997
  try {
3652
- unlinkSync5(stateDir.tuiPidFile);
3998
+ unlinkSync6(stateDir.tuiPidFile);
3653
3999
  } catch {}
3654
4000
  }
3655
4001
  function requestChildTermination(reason) {
@@ -3720,7 +4066,11 @@ async function runCodex(args) {
3720
4066
  else if (typeof code === "number" && code !== 0)
3721
4067
  classification = `nonzero_exit:${code}`;
3722
4068
  else if (code === 0 && tail.trim().length === 0) {
3723
- classification = refineCleanExitClassification(readTurnInProgress(stateDir.statusFile));
4069
+ classification = refineCleanExitClassification(readUnifiedTurnInProgress({
4070
+ daemonRecordFile: stateDir.daemonRecordFile,
4071
+ pidFile: stateDir.pidFile,
4072
+ statusFile: stateDir.statusFile
4073
+ }));
3724
4074
  }
3725
4075
  const tuiLogTail = captureTuiLogTail({
3726
4076
  codexHome: join12(homedir4(), ".codex"),
@@ -3779,12 +4129,12 @@ function guardNoLiveManagedTui(stateDir, proxyUrl) {
3779
4129
  if (pid) {
3780
4130
  if (!isProcessAlive(pid)) {
3781
4131
  try {
3782
- unlinkSync5(stateDir.tuiPidFile);
4132
+ unlinkSync6(stateDir.tuiPidFile);
3783
4133
  } catch {}
3784
4134
  } else if (!isManagedCodexTuiProcess(pid, proxyUrl)) {
3785
4135
  appendWrapperLog(stateDir.codexWrapperLogFile, `stale tui pid file pointed at unmanaged live pid=${pid}; removing`);
3786
4136
  try {
3787
- unlinkSync5(stateDir.tuiPidFile);
4137
+ unlinkSync6(stateDir.tuiPidFile);
3788
4138
  } catch {}
3789
4139
  } else {
3790
4140
  console.error(`[agentbridge] This pair already has a managed Codex TUI running (pid ${pid}).`);
@@ -3801,7 +4151,7 @@ function guardNoLiveManagedTui(stateDir, proxyUrl) {
3801
4151
  }
3802
4152
  function readTuiPid(stateDir) {
3803
4153
  try {
3804
- const raw = readFileSync9(stateDir.tuiPidFile, "utf-8").trim();
4154
+ const raw = readFileSync10(stateDir.tuiPidFile, "utf-8").trim();
3805
4155
  if (!raw)
3806
4156
  return null;
3807
4157
  const pid = Number.parseInt(raw, 10);
@@ -3815,7 +4165,12 @@ function isManagedCodexTuiProcess(pid, proxyUrl) {
3815
4165
  return cmd !== null && commandMatchesManagedCodexTui(cmd, proxyUrl);
3816
4166
  }
3817
4167
  function proxyHealthUrl(proxyUrl) {
3818
- const url = new URL(proxyUrl);
4168
+ let url;
4169
+ try {
4170
+ url = new URL(proxyUrl);
4171
+ } catch {
4172
+ throw new Error(`Malformed Codex proxy URL: ${JSON.stringify(proxyUrl)}`);
4173
+ }
3819
4174
  url.protocol = url.protocol === "wss:" ? "https:" : "http:";
3820
4175
  url.pathname = "/healthz";
3821
4176
  url.search = "";
@@ -4010,7 +4365,7 @@ __export(exports_kill, {
4010
4365
  runKill: () => runKill,
4011
4366
  formatKillReport: () => formatKillReport
4012
4367
  });
4013
- import { readFileSync as readFileSync10, unlinkSync as unlinkSync6 } from "fs";
4368
+ import { readFileSync as readFileSync11, unlinkSync as unlinkSync7 } from "fs";
4014
4369
  import { join as join14 } from "path";
4015
4370
  async function runKill(args = []) {
4016
4371
  const argError = validateKillArgs(args);
@@ -4031,8 +4386,9 @@ async function runKill(args = []) {
4031
4386
  const base = computeBaseDir();
4032
4387
  console.log(`AgentBridge Kill \u2014 stopping AgentBridge pair processes
4033
4388
  `);
4389
+ const cli = cliInvocationName();
4034
4390
  const results = [];
4035
- let restartCommand = "agentbridge claude";
4391
+ let restartCommand = `${cli} claude`;
4036
4392
  if (parsed.pairFlag !== undefined) {
4037
4393
  let pair;
4038
4394
  try {
@@ -4046,7 +4402,7 @@ async function runKill(args = []) {
4046
4402
  printKnownPairs(base);
4047
4403
  return;
4048
4404
  }
4049
- restartCommand = `agentbridge --pair ${pair.name ?? parsed.pairFlag} claude`;
4405
+ restartCommand = `${cli} --pair ${pair.name ?? parsed.pairFlag} claude`;
4050
4406
  results.push(await stopPairEntry(base, pair));
4051
4407
  } else if (parsed.all) {
4052
4408
  let registered = [];
@@ -4080,7 +4436,7 @@ async function runKill(args = []) {
4080
4436
  cwdPairs = listPairsForCwd(base, process.cwd());
4081
4437
  } catch (error) {
4082
4438
  const message = error instanceof Error ? error.message : String(error);
4083
- console.log(`\u26A0\uFE0F pair registry \u4E0D\u53EF\u8BFB\uFF08${message}\uFF09\u2014\u2014\u65E0\u6CD5\u6309\u76EE\u5F55\u5B9A\u4F4D pair\u3002` + "\u8FD0\u884C `abg kill --all` \u53EF\u964D\u7EA7\u4E3A\u5168\u76D8\u72B6\u6001\u76EE\u5F55\u626B\u63CF\uFF0C\u505C\u6B62\u6240\u6709\u80FD\u627E\u5230\u7684 pair\u3002");
4439
+ console.log(`\u26A0\uFE0F pair registry \u4E0D\u53EF\u8BFB\uFF08${message}\uFF09\u2014\u2014\u65E0\u6CD5\u6309\u76EE\u5F55\u5B9A\u4F4D pair\u3002` + `\u8FD0\u884C \`${cli} kill --all\` \u53EF\u964D\u7EA7\u4E3A\u5168\u76D8\u72B6\u6001\u76EE\u5F55\u626B\u63CF\uFF0C\u505C\u6B62\u6240\u6709\u80FD\u627E\u5230\u7684 pair\u3002`);
4084
4440
  process.exitCode = 2;
4085
4441
  }
4086
4442
  for (const pair of cwdPairs) {
@@ -4096,7 +4452,7 @@ async function runKill(args = []) {
4096
4452
  }
4097
4453
  if (results.length === 0) {
4098
4454
  console.log(`No AgentBridge pairs registered for current directory: ${process.cwd()}`);
4099
- console.log("Use `abg kill all` or `abg kill --all` to stop pairs from every directory.");
4455
+ console.log(`Use \`${cli} kill all\` or \`${cli} kill --all\` to stop pairs from every directory.`);
4100
4456
  return;
4101
4457
  }
4102
4458
  }
@@ -4159,22 +4515,18 @@ function listPairDirsSafe(base) {
4159
4515
  }
4160
4516
  }
4161
4517
  function portsFromStateDir(stateDir) {
4162
- try {
4163
- const raw = JSON.parse(readFileSync10(stateDir.statusFile, "utf-8"));
4164
- return {
4165
- appPort: portFromUrl(raw?.appServerUrl) ?? 0,
4166
- proxyPort: portFromUrl(raw?.proxyUrl) ?? 0,
4167
- controlPort: typeof raw?.controlPort === "number" ? raw.controlPort : 0
4168
- };
4169
- } catch {
4518
+ const record = readUnifiedDaemonRecord({
4519
+ daemonRecordFile: stateDir.daemonRecordFile,
4520
+ pidFile: stateDir.pidFile,
4521
+ statusFile: stateDir.statusFile
4522
+ });
4523
+ if (!record)
4170
4524
  return { appPort: 0, proxyPort: 0, controlPort: 0 };
4171
- }
4172
- }
4173
- function portFromUrl(url) {
4174
- if (typeof url !== "string")
4175
- return null;
4176
- const match = url.match(/:(\d+)(?:[/?]|$)/);
4177
- return match ? Number.parseInt(match[1], 10) : null;
4525
+ return {
4526
+ appPort: record.ports?.appPort ?? portFromUrl(record.appServerUrl) ?? 0,
4527
+ proxyPort: record.ports?.proxyPort ?? portFromUrl(record.proxyUrl) ?? 0,
4528
+ controlPort: record.ports?.controlPort ?? 0
4529
+ };
4178
4530
  }
4179
4531
  async function stopStateDir(label, stateDir, ports) {
4180
4532
  const portsLabel = `${ports.appPort}/${ports.proxyPort}/${ports.controlPort}`;
@@ -4187,8 +4539,8 @@ async function stopStateDir(label, stateDir, ports) {
4187
4539
  log
4188
4540
  });
4189
4541
  lifecycle.markKilled();
4190
- const status = lifecycle.readStatus();
4191
- const proxyUrl = typeof status?.proxyUrl === "string" && status.proxyUrl.length > 0 ? status.proxyUrl : `ws://127.0.0.1:${ports.proxyPort}`;
4542
+ const record = lifecycle.readDaemonRecord();
4543
+ const proxyUrl = typeof record?.proxyUrl === "string" && record.proxyUrl.length > 0 ? record.proxyUrl : `ws://127.0.0.1:${ports.proxyPort}`;
4192
4544
  const tuiKilled = await killManagedCodexTui(stateDir, proxyUrl, log);
4193
4545
  const daemonKilled = await lifecycle.kill();
4194
4546
  return { label, portsLabel, daemonKilled, tuiKilled, details };
@@ -4253,9 +4605,10 @@ function formatKillReport(results, frontends, restartCommand) {
4253
4605
  }
4254
4606
  lines.push("");
4255
4607
  if (stopped.length > 0) {
4608
+ const cliName = restartCommand.split(" ")[0] ?? "abg";
4256
4609
  lines.push("AgentBridge stopped.");
4257
4610
  lines.push(`Please restart Claude Code (\`${restartCommand}\`), switch to a new conversation, or run \`/resume\` to fully disconnect.`);
4258
- lines.push("\u2139\uFE0F \u5DF2\u5199\u5165 killed \u54E8\u5175\uFF1A\u88AB\u505C\u6B62\u7684 pair \u4E0D\u4F1A\u88AB\u81EA\u52A8\u590D\u6D3B\uFF1B" + `\u4E0B\u6B21 \`${restartCommand}\` / \`agentbridge codex\` \u4F1A\u6E05\u9664\u54E8\u5175\u5E76\u7528\u5F53\u524D\u5B89\u88C5\u7248\u672C\u542F\u52A8\u5168\u65B0 daemon\u3002`);
4611
+ lines.push("\u2139\uFE0F \u5DF2\u5199\u5165 killed \u54E8\u5175\uFF1A\u88AB\u505C\u6B62\u7684 pair \u4E0D\u4F1A\u88AB\u81EA\u52A8\u590D\u6D3B\uFF1B" + `\u4E0B\u6B21 \`${restartCommand}\` / \`${cliName} codex\` \u4F1A\u6E05\u9664\u54E8\u5175\u5E76\u7528\u5F53\u524D\u5B89\u88C5\u7248\u672C\u542F\u52A8\u5168\u65B0 daemon\u3002`);
4259
4612
  } else {
4260
4613
  lines.push("No running AgentBridge daemon or managed Codex TUI found.");
4261
4614
  lines.push("\u2139\uFE0F \u76EE\u6807 pair \u90FD\u6CA1\u6709\u5728\u8FD0\u884C\u7684\u8FDB\u7A0B\u2014\u2014\u5982\u679C\u4F60\u4ECD\u770B\u5230 AgentBridge \u6D3B\u52A8\uFF0C\u89C1\u4E0B\u65B9\u524D\u7AEF\u63D0\u793A\u3002");
@@ -4306,7 +4659,7 @@ async function killManagedCodexTui(stateDir, proxyUrl, log, gracefulTimeoutMs =
4306
4659
  }
4307
4660
  function readTuiPid2(stateDir) {
4308
4661
  try {
4309
- const raw = readFileSync10(stateDir.tuiPidFile, "utf-8").trim();
4662
+ const raw = readFileSync11(stateDir.tuiPidFile, "utf-8").trim();
4310
4663
  if (!raw)
4311
4664
  return null;
4312
4665
  const pid = Number.parseInt(raw, 10);
@@ -4317,7 +4670,7 @@ function readTuiPid2(stateDir) {
4317
4670
  }
4318
4671
  function removeTuiPidFile(stateDir) {
4319
4672
  try {
4320
- unlinkSync6(stateDir.tuiPidFile);
4673
+ unlinkSync7(stateDir.tuiPidFile);
4321
4674
  } catch {}
4322
4675
  }
4323
4676
  function isManagedCodexTuiProcess2(pid, proxyUrl) {
@@ -4325,7 +4678,9 @@ function isManagedCodexTuiProcess2(pid, proxyUrl) {
4325
4678
  return cmd !== null && commandMatchesManagedCodexTui(cmd, proxyUrl);
4326
4679
  }
4327
4680
  var init_kill = __esm(() => {
4681
+ init_cli_invocation();
4328
4682
  init_daemon_lifecycle();
4683
+ init_daemon_record();
4329
4684
  init_pair_registry();
4330
4685
  init_pair_resolver();
4331
4686
  init_process_lifecycle();
@@ -4338,6 +4693,13 @@ __export(exports_pairs, {
4338
4693
  runPairs: () => runPairs
4339
4694
  });
4340
4695
  import { join as join15 } from "path";
4696
+ function isRegistryCorruptError(error) {
4697
+ return error instanceof PairError && error.code === "PAIR_REGISTRY_CORRUPT";
4698
+ }
4699
+ function registryPathForNotice(base, error) {
4700
+ const fromDetails = error.details?.path;
4701
+ return typeof fromDetails === "string" && fromDetails.length > 0 ? fromDetails : join15(pairsRootDir(base), "registry.json");
4702
+ }
4341
4703
  async function runPairs(args = []) {
4342
4704
  const [command, ...rest] = args;
4343
4705
  if (command === "rm") {
@@ -4350,7 +4712,7 @@ async function runPairs(args = []) {
4350
4712
  }
4351
4713
  if (command && command !== "list" && command !== "--json" && command !== "--threads") {
4352
4714
  console.error(`Unknown pairs command: ${command}`);
4353
- console.error("Usage: abg pairs [--json] [--threads] | abg pairs rm <name|id> | abg pairs prune [--dry-run]");
4715
+ console.error("Usage: abg pairs [--json] [--threads] | abg pairs rm <name|id> | abg pairs prune [--apply]");
4354
4716
  process.exit(1);
4355
4717
  }
4356
4718
  const json = command === "--json" || rest.includes("--json");
@@ -4405,16 +4767,40 @@ async function runRemove(args) {
4405
4767
  }
4406
4768
  }
4407
4769
  async function runPrune(args) {
4408
- const dryRun = args.includes("--dry-run");
4770
+ const apply = args.includes("--apply");
4409
4771
  for (const arg of args) {
4410
- if (arg !== "--dry-run") {
4772
+ if (arg !== "--apply" && arg !== "--dry-run") {
4411
4773
  console.error(`Unknown prune argument: ${arg}`);
4412
- console.error("Usage: abg pairs prune [--dry-run]");
4774
+ console.error("Usage: abg pairs prune [--apply]");
4413
4775
  process.exit(1);
4414
4776
  }
4415
4777
  }
4778
+ if (apply && args.includes("--dry-run")) {
4779
+ console.error("Error: --apply and --dry-run are mutually exclusive.");
4780
+ console.error("Usage: abg pairs prune [--apply]");
4781
+ process.exit(1);
4782
+ }
4416
4783
  const base = computeBaseDir();
4417
- const registered = new Set(listPairs(base).map((pair) => pair.pairId.toLowerCase()));
4784
+ let reclaimable;
4785
+ let registryReadable = true;
4786
+ try {
4787
+ reclaimable = classifyReclaimableEntries(base);
4788
+ } catch (error) {
4789
+ if (!isRegistryCorruptError(error))
4790
+ throw error;
4791
+ registryReadable = false;
4792
+ reclaimable = [];
4793
+ console.error(`\u26A0\uFE0F pair registry \u4E0D\u53EF\u8BFB\uFF08${error.message}\uFF09\u2014\u2014` + `\u4F4D\u4E8E ${registryPathForNotice(base, error)}\u3002` + `\u8DF3\u8FC7 registry \u6761\u76EE\u56DE\u6536\uFF0C\u964D\u7EA7\u4E3A\u78C1\u76D8\u626B\u63CF\u6E05\u7406\u5B64\u513F\u76EE\u5F55\uFF08\u65E0\u9700\u53EF\u89E3\u6790\u7684 registry\uFF09\u3002` + `\u4FEE\u590D\u6216\u5220\u9664\u8BE5\u6587\u4EF6\u540E\u53EF\u6062\u590D\u5B8C\u6574 prune\u3002`);
4794
+ process.exitCode = 2;
4795
+ }
4796
+ const reclaimableIds = new Set(reclaimable.map((c) => c.entry.pairId.toLowerCase()));
4797
+ const dirResult = pruneOrphanDirs(base, apply, reclaimableIds, registryReadable);
4798
+ const entryResult = await pruneReclaimableEntries(reclaimable, base, apply);
4799
+ const resolvedDirResult = await dirResult;
4800
+ printPruneSummary(resolvedDirResult, entryResult, apply);
4801
+ }
4802
+ async function pruneOrphanDirs(base, apply, reclaimableIds, registryReadable) {
4803
+ const registered = registryReadable ? new Set(listPairs(base).map((pair) => pair.pairId.toLowerCase())) : new Set;
4418
4804
  const removed = [];
4419
4805
  const kept = [];
4420
4806
  for (const name of listPairDirs(base)) {
@@ -4429,6 +4815,9 @@ async function runPrune(args) {
4429
4815
  kept.push({ name, reason: "directory name is not a canonical pair id" });
4430
4816
  continue;
4431
4817
  }
4818
+ if (reclaimableIds.has(id.toLowerCase())) {
4819
+ continue;
4820
+ }
4432
4821
  if (registered.has(id.toLowerCase())) {
4433
4822
  kept.push({ name, reason: "registered \u2014 use `abg pairs rm`" });
4434
4823
  continue;
@@ -4437,12 +4826,12 @@ async function runPrune(args) {
4437
4826
  kept.push({ name, reason: "daemon still alive" });
4438
4827
  continue;
4439
4828
  }
4440
- if (dryRun) {
4829
+ if (!apply) {
4441
4830
  removed.push(name);
4442
4831
  continue;
4443
4832
  }
4444
4833
  try {
4445
- const outcome = await removeUnregisteredPairDir(base, id);
4834
+ const outcome = registryReadable ? await removeUnregisteredPairDir(base, id) : await removeOrphanPairDirIgnoringRegistry(base, id);
4446
4835
  if (outcome.removed) {
4447
4836
  removed.push(name);
4448
4837
  } else if (outcome.reason === "registered") {
@@ -4456,33 +4845,90 @@ async function runPrune(args) {
4456
4845
  kept.push({ name, reason: `error: ${err instanceof Error ? err.message : String(err)}` });
4457
4846
  }
4458
4847
  }
4459
- printPruneSummary(removed, kept, dryRun);
4848
+ return { removed, kept };
4849
+ }
4850
+ async function pruneReclaimableEntries(candidates, base, apply) {
4851
+ const reclaimed = [];
4852
+ const kept = [];
4853
+ for (const candidate of candidates) {
4854
+ const reason = describeReclaimReason(candidate);
4855
+ if (!apply) {
4856
+ reclaimed.push({ pairId: candidate.entry.pairId, slot: candidate.entry.slot, reason });
4857
+ continue;
4858
+ }
4859
+ try {
4860
+ const res = await removePairEntryAndDir(base, candidate.entry.pairId);
4861
+ if (res.keptLive) {
4862
+ kept.push({ pairId: candidate.entry.pairId, reason: "became live during prune" });
4863
+ } else {
4864
+ reclaimed.push({ pairId: candidate.entry.pairId, slot: candidate.entry.slot, reason });
4865
+ }
4866
+ } catch (err) {
4867
+ kept.push({
4868
+ pairId: candidate.entry.pairId,
4869
+ reason: `error: ${err instanceof Error ? err.message : String(err)}`
4870
+ });
4871
+ }
4872
+ }
4873
+ return { reclaimed, kept };
4460
4874
  }
4461
- function printPruneSummary(removed, kept, dryRun) {
4462
- if (removed.length === 0 && kept.length === 0) {
4463
- console.log("No pair directories found.");
4875
+ function describeReclaimReason(candidate) {
4876
+ const { signals } = candidate;
4877
+ const age = signals.ageMs === null ? "age?" : `age ${formatAgeDays(signals.ageMs)}`;
4878
+ return `cwd-gone, dead, ${age}`;
4879
+ }
4880
+ function formatAgeDays(ageMs) {
4881
+ const days = ageMs / (24 * 60 * 60 * 1000);
4882
+ return days >= 10 ? `${Math.round(days)}d` : `${days.toFixed(1)}d`;
4883
+ }
4884
+ function printPruneSummary(dirResult, entryResult, apply) {
4885
+ const { removed: dirsRemoved, kept: dirsKept } = dirResult;
4886
+ const { reclaimed: entriesReclaimed, kept: entriesKept } = entryResult;
4887
+ const nothingFound = dirsRemoved.length === 0 && dirsKept.length === 0 && entriesReclaimed.length === 0 && entriesKept.length === 0;
4888
+ if (nothingFound) {
4889
+ console.log("Nothing to prune: no orphan pair directories or reclaimable entries found.");
4464
4890
  return;
4465
4891
  }
4466
- if (removed.length > 0) {
4467
- console.log(dryRun ? "Would remove orphan pair directories:" : "Removed orphan pair directories:");
4468
- for (const name of removed)
4892
+ if (dirsRemoved.length > 0) {
4893
+ console.log(apply ? "Removed orphan pair directories:" : "Would remove orphan pair directories:");
4894
+ for (const name of dirsRemoved)
4469
4895
  console.log(` ${name}`);
4470
- } else {
4471
- console.log(dryRun ? "No orphan pair directories to remove." : "No orphan pair directories removed.");
4472
4896
  }
4473
- if (kept.length > 0) {
4897
+ if (entriesReclaimed.length > 0) {
4898
+ console.log(apply ? "Reclaimed registry entries:" : "Would reclaim registry entries:");
4899
+ for (const { pairId, slot, reason } of entriesReclaimed) {
4900
+ console.log(` ${pairId} (slot ${slot}) \u2014 ${reason}`);
4901
+ }
4902
+ }
4903
+ if (dirsRemoved.length === 0 && entriesReclaimed.length === 0) {
4904
+ console.log(apply ? "Nothing was reclaimed." : "Nothing to reclaim.");
4905
+ }
4906
+ const keptLines = [
4907
+ ...dirsKept.map(({ name, reason }) => ` ${name} (${reason})`),
4908
+ ...entriesKept.map(({ pairId, reason }) => ` ${pairId} (${reason})`)
4909
+ ];
4910
+ if (keptLines.length > 0) {
4474
4911
  console.log("Kept:");
4475
- for (const { name, reason } of kept)
4476
- console.log(` ${name} (${reason})`);
4912
+ for (const line of keptLines)
4913
+ console.log(line);
4477
4914
  }
4478
- if (dryRun) {
4915
+ if (!apply) {
4479
4916
  console.log(`
4480
- (dry run \u2014 nothing was deleted. Re-run without --dry-run to apply.)`);
4917
+ (dry run \u2014 nothing was deleted. Re-run with --apply to reclaim.)`);
4481
4918
  }
4482
4919
  }
4483
4920
  async function collectRows() {
4484
4921
  const base = computeBaseDir();
4485
- const rows = await Promise.all(listPairs(base).map((pair) => rowForPair(base, pair)));
4922
+ let rows;
4923
+ try {
4924
+ rows = await Promise.all(listPairs(base).map((pair) => rowForPair(base, pair)));
4925
+ } catch (error) {
4926
+ if (!isRegistryCorruptError(error))
4927
+ throw error;
4928
+ console.error(`\u26A0\uFE0F pair registry \u4E0D\u53EF\u8BFB\uFF08${error.message}\uFF09\u2014\u2014` + `\u4F4D\u4E8E ${registryPathForNotice(base, error)}\u3002` + `\u964D\u7EA7\u4E3A\u78C1\u76D8\u626B\u63CF\u5217\u51FA ${pairsRootDir(base)} \u4E0B\u7684 pair \u76EE\u5F55\uFF08slot/name/cwd \u7B49\u9700 registry \u7684\u5B57\u6BB5\u663E\u793A\u4E3A -\uFF09\u3002` + `\u4FEE\u590D\u6216\u5220\u9664\u8BE5\u6587\u4EF6\u540E\u53EF\u6062\u590D\u5B8C\u6574\u5217\u8868\uFF1B\u7528 \`abg pairs prune\` \u6E05\u7406\u5B64\u513F\u76EE\u5F55\u3002`);
4929
+ process.exitCode = 2;
4930
+ rows = await collectDiskScanRows(base);
4931
+ }
4486
4932
  const legacy = detectLegacyRootDaemon(base);
4487
4933
  if (legacy) {
4488
4934
  rows.push({
@@ -4509,9 +4955,9 @@ async function rowForPair(base, pair) {
4509
4955
  controlPort: ports.controlPort,
4510
4956
  log: () => {}
4511
4957
  });
4512
- const [running, status] = await Promise.all([
4958
+ const [running, record] = await Promise.all([
4513
4959
  lifecycle.isHealthy(),
4514
- Promise.resolve(lifecycle.readStatus())
4960
+ Promise.resolve(lifecycle.readDaemonRecord())
4515
4961
  ]);
4516
4962
  const thread = readRawCurrentThread(stateDir);
4517
4963
  return {
@@ -4522,7 +4968,42 @@ async function rowForPair(base, pair) {
4522
4968
  source: pair.source,
4523
4969
  cwd: pair.cwd,
4524
4970
  running,
4525
- pid: typeof status?.pid === "number" ? status.pid : null,
4971
+ pid: typeof record?.pid === "number" ? record.pid : null,
4972
+ threadId: thread?.threadId ?? null,
4973
+ threadStatus: thread?.status ?? null,
4974
+ threadUpdatedAt: thread?.updatedAt ?? null
4975
+ };
4976
+ }
4977
+ async function collectDiskScanRows(base) {
4978
+ const names = listPairDirsSafe2(base);
4979
+ return Promise.all(names.map((name) => rowForDiskScanDir(base, name)));
4980
+ }
4981
+ function listPairDirsSafe2(base) {
4982
+ try {
4983
+ return listPairDirs(base);
4984
+ } catch {
4985
+ return [];
4986
+ }
4987
+ }
4988
+ async function rowForDiskScanDir(base, dirName) {
4989
+ const stateDir = new StateDirResolver(join15(base, "pairs", dirName));
4990
+ const record = new DaemonLifecycle({ stateDir, controlPort: 0, log: () => {} }).readDaemonRecord();
4991
+ const ports = {
4992
+ appPort: record?.ports?.appPort ?? 0,
4993
+ proxyPort: record?.ports?.proxyPort ?? 0,
4994
+ controlPort: record?.ports?.controlPort ?? 0
4995
+ };
4996
+ const running = ports.controlPort > 0 ? await new DaemonLifecycle({ stateDir, controlPort: ports.controlPort, log: () => {} }).isHealthy() : false;
4997
+ const thread = readRawCurrentThread(stateDir);
4998
+ return {
4999
+ pairId: dirName,
5000
+ name: "-",
5001
+ slot: null,
5002
+ ports,
5003
+ source: "cwd",
5004
+ cwd: "-",
5005
+ running,
5006
+ pid: typeof record?.pid === "number" ? record.pid : null,
4526
5007
  threadId: thread?.threadId ?? null,
4527
5008
  threadStatus: thread?.status ?? null,
4528
5009
  threadUpdatedAt: thread?.updatedAt ?? null
@@ -4612,7 +5093,7 @@ import {
4612
5093
  copyFileSync,
4613
5094
  existsSync as existsSync14,
4614
5095
  mkdirSync as mkdirSync7,
4615
- readFileSync as readFileSync11
5096
+ readFileSync as readFileSync12
4616
5097
  } from "fs";
4617
5098
  import { dirname as dirname5, join as join16 } from "path";
4618
5099
  function isKickoffText(text) {
@@ -4623,7 +5104,7 @@ function isKickoffText(text) {
4623
5104
  function extractFirstRealUserMessage(rolloutPath) {
4624
5105
  if (!existsSync14(rolloutPath))
4625
5106
  return null;
4626
- const raw = readFileSync11(rolloutPath, "utf-8");
5107
+ const raw = readFileSync12(rolloutPath, "utf-8");
4627
5108
  for (const line of raw.split(`
4628
5109
  `)) {
4629
5110
  if (!line.trim())
@@ -4790,9 +5271,11 @@ var init_resume_pollution = __esm(() => {
4790
5271
  var exports_doctor = {};
4791
5272
  __export(exports_doctor, {
4792
5273
  runDoctor: () => runDoctor,
4793
- formatDoctorReport: () => formatDoctorReport
5274
+ formatDoctorReport: () => formatDoctorReport,
5275
+ evaluateArtifactAlignment: () => evaluateArtifactAlignment,
5276
+ describeBuildDrift: () => describeBuildDrift
4794
5277
  });
4795
- import { existsSync as existsSync15, readFileSync as readFileSync12, readdirSync as readdirSync6, realpathSync as realpathSync3, statSync as statSync7 } from "fs";
5278
+ import { existsSync as existsSync15, readFileSync as readFileSync13, readdirSync as readdirSync6, realpathSync as realpathSync3, statSync as statSync7 } from "fs";
4796
5279
  import { join as join17 } from "path";
4797
5280
  async function runDoctor(args = []) {
4798
5281
  if (args[0] === "resume-pollution") {
@@ -4869,6 +5352,7 @@ function runResumePollution(args) {
4869
5352
  }
4870
5353
  async function buildDoctorReport(pair, registered) {
4871
5354
  const cwd = process.cwd();
5355
+ const cli = cliInvocationName();
4872
5356
  const env = inspectAgentBridgeEnv({ cwd, env: process.env });
4873
5357
  const [health, ready] = registered ? await Promise.all([
4874
5358
  fetchDaemonStatus(pair.ports.controlPort, "/healthz"),
@@ -4888,20 +5372,20 @@ async function buildDoctorReport(pair, registered) {
4888
5372
  name: "pair registration",
4889
5373
  status: registered ? "ok" : "warn",
4890
5374
  detail: registered ? pair.manual ? "manual mode (explicit env)" : `registered as ${pair.pairId}` : `not registered yet \u2014 would be ${pair.pairId} (created on first launch)`,
4891
- hint: registered ? undefined : "\u8BE5\u76EE\u5F55\u8FD8\u6CA1\u6709\u6CE8\u518C\u8FC7 pair\uFF1A\u8FD0\u884C `agentbridge claude` \u5373\u4F1A\u521B\u5EFA\u3002\u4EE5\u4E0B\u68C0\u67E5\u6309\u672A\u542F\u52A8\u72B6\u6001\u89E3\u8BFB\u3002"
5375
+ hint: registered ? undefined : `\u8BE5\u76EE\u5F55\u8FD8\u6CA1\u6709\u6CE8\u518C\u8FC7 pair\uFF1A\u8FD0\u884C \`${cli} claude\` \u5373\u4F1A\u521B\u5EFA\u3002\u4EE5\u4E0B\u68C0\u67E5\u6309\u672A\u542F\u52A8\u72B6\u6001\u89E3\u8BFB\u3002`
4892
5376
  });
4893
5377
  checks.push({
4894
5378
  name: "env",
4895
5379
  status: env.ok ? "ok" : "fail",
4896
5380
  detail: env.ok ? "AgentBridge env matches cwd" : env.reasons.join("; "),
4897
- hint: env.ok ? undefined : "\u73AF\u5883\u53D8\u91CF\u4E0E\u5F53\u524D\u76EE\u5F55\u4E0D\u5339\u914D\uFF1A\u8BF7\u5728\u6B63\u786E\u7684\u9879\u76EE\u76EE\u5F55\u91CC\u91CD\u65B0\u8FD0\u884C `agentbridge claude`\uFF0C\u4E0D\u8981\u590D\u7528\u5176\u4ED6\u76EE\u5F55\u7684\u4F1A\u8BDD\u73AF\u5883\u3002"
5381
+ hint: env.ok ? undefined : `\u73AF\u5883\u53D8\u91CF\u4E0E\u5F53\u524D\u76EE\u5F55\u4E0D\u5339\u914D\uFF1A\u8BF7\u5728\u6B63\u786E\u7684\u9879\u76EE\u76EE\u5F55\u91CC\u91CD\u65B0\u8FD0\u884C \`${cli} claude\`\uFF0C\u4E0D\u8981\u590D\u7528\u5176\u4ED6\u76EE\u5F55\u7684\u4F1A\u8BDD\u73AF\u5883\u3002`
4898
5382
  });
4899
- checks.push(configParseabilityCheck(cwd));
5383
+ checks.push(configParseabilityCheck(cwd, cli));
4900
5384
  checks.push({
4901
5385
  name: "daemon health",
4902
5386
  status: health ? "ok" : "warn",
4903
5387
  detail: health ? `healthz reachable pid=${health.pid}` : registered ? `no daemon reachable on :${pair.ports.controlPort}` : "n/a \u2014 pair not registered",
4904
- hint: health ? undefined : "daemon \u672A\u8FD0\u884C\u3002\u8FD0\u884C `agentbridge claude`\uFF08\u6216 `agentbridge codex`\uFF09\u4F1A\u81EA\u52A8\u542F\u52A8\u5B83\u3002"
5388
+ hint: health ? undefined : `daemon \u672A\u8FD0\u884C\u3002\u8FD0\u884C \`${cli} claude\`\uFF08\u6216 \`${cli} codex\`\uFF09\u4F1A\u81EA\u52A8\u542F\u52A8\u5B83\u3002`
4905
5389
  });
4906
5390
  checks.push({
4907
5391
  name: "daemon readiness",
@@ -4909,18 +5393,26 @@ async function buildDoctorReport(pair, registered) {
4909
5393
  detail: ready ? `ready thread=${ready.threadId ?? "none"}` : health ? "readyz is not OK" : "n/a \u2014 daemon not running",
4910
5394
  hint: !ready && health ? "daemon \u5728\u8FD0\u884C\u4F46 codex app-server \u5C1A\u672A\u5C31\u7EEA\uFF1B\u7A0D\u5019\u7247\u523B\u91CD\u8BD5\uFF0C\u6301\u7EED\u4E0D\u5C31\u7EEA\u8BF7\u67E5\u770B\u4E0B\u65B9 daemon log\u3002" : undefined
4911
5395
  });
5396
+ const appServerInfo = health?.appServerInfo ?? null;
5397
+ checks.push({
5398
+ name: "codex app-server",
5399
+ status: health ? "ok" : "skip",
5400
+ detail: !health ? "n/a \u2014 daemon not running" : appServerInfo ? `version=${appServerInfo.version ?? "unknown"}` + (appServerInfo.platformOs ? ` platform=${appServerInfo.platformOs}` : "") : "not captured yet \u2014 connect Codex (initialize handshake) to populate",
5401
+ hint: health && appServerInfo && appServerInfo.version === null ? "app-server \u672A\u8FD4\u56DE\u53EF\u89E3\u6790\u7684\u7248\u672C\u53F7\uFF08userAgent \u5F02\u5E38\uFF09\u3002\u82E5\u521A\u5347\u7EA7\u8FC7 Codex\uFF0C\u8BF7\u6838\u5BF9 codex-adapter \u7684 version-coupling checklist\u3002" : undefined
5402
+ });
5403
+ const drift = buildDrift === true ? describeBuildDrift(health?.build, BUILD_INFO, cli) : null;
4912
5404
  checks.push({
4913
5405
  name: "build drift",
4914
5406
  status: buildDrift === false ? "ok" : buildDrift === true ? "fail" : "skip",
4915
- detail: buildDrift === false ? `runtime matches launcher ${formatBuildInfo(BUILD_INFO)}` : buildDrift === true ? `runtime ${formatBuildInfo(health?.build)} differs from launcher ${formatBuildInfo(BUILD_INFO)}` : launcherStamped ? "n/a \u2014 daemon not running" : "n/a \u2014 launcher running from source (unstamped)",
4916
- hint: buildDrift === true ? "daemon \u8FD0\u884C\u7684\u662F\u65E7\u6784\u5EFA\uFF08\u901A\u5E38\u7531\u65E7\u7248 CLI \u6216\u672A\u91CD\u5F00\u7684 Claude Code \u7A97\u53E3\u542F\u52A8\uFF09\u3002" + "\u6CA1\u6709\u8FDB\u884C\u4E2D\u7684 Codex \u4F1A\u8BDD\u65F6\uFF0C\u8FD0\u884C `abg kill` \u540E\u91CD\u65B0 `agentbridge claude` \u5373\u53EF\u5BF9\u9F50\uFF1B" + "\u6709\u6D3B\u8DC3\u4F1A\u8BDD\u5219\u7B49\u6536\u5C3E\u540E\u518D\u91CD\u542F\u2014\u2014\u7248\u672C\u5DEE\u5F02\u4E0D\u4F1A\u5F3A\u6740\u6D3B\u8DC3\u4F1A\u8BDD\uFF0C\u53EF\u4EE5\u7EE7\u7EED\u7528\u3002" : undefined
5407
+ detail: buildDrift === false ? `runtime matches launcher ${formatBuildInfo(BUILD_INFO)}` : drift ? drift.detail : launcherStamped ? "n/a \u2014 daemon not running" : "n/a \u2014 launcher running from source (unstamped)",
5408
+ hint: drift?.hint
4917
5409
  });
4918
5410
  checks.push(artifactAlignmentCheck());
4919
5411
  checks.push({
4920
5412
  name: "current thread",
4921
5413
  status: usableThread ? "ok" : rawThread ? "warn" : registered ? "warn" : "skip",
4922
5414
  detail: usableThread ? `current=${usableThread.threadId}` : rawThread ? rawThread.status === "current" ? `stored ${rawThread.threadId} has no rollout file yet` : `stored ${rawThread.threadId} is still ${rawThread.status} (no first response yet)` : registered ? "no current-thread.json for this pair" : "n/a \u2014 pair not registered",
4923
- hint: usableThread ? undefined : rawThread ? "\u901A\u5E38\u65E0\u5BB3\uFF1A\u7EBF\u7A0B\u8FD8\u6CA1\u6709\u4EA7\u751F\u9996\u6761\u56DE\u5E94\u3001\u6216 rollout \u6587\u4EF6\u5C1A\u672A\u843D\u76D8\u3002" + "\u4EC5\u5F53 `abg codex`\uFF08resume\uFF09\u5931\u8D25\u65F6\u624D\u9700\u8981\u5904\u7406\uFF1A\u7528 `abg codex --new` \u5F00\u65B0\u7EBF\u7A0B\u3002" : registered ? "\u5C1A\u65E0\u7EBF\u7A0B\u8BB0\u5F55\uFF1A\u8FDE\u63A5 Codex \u540E\u5EFA\u7ACB\u9996\u4E2A\u7EBF\u7A0B\u65F6\u4F1A\u81EA\u52A8\u5199\u5165\uFF0C\u65E0\u9700\u5904\u7406\u3002" : undefined
5415
+ hint: usableThread ? undefined : rawThread ? "\u901A\u5E38\u65E0\u5BB3\uFF1A\u7EBF\u7A0B\u8FD8\u6CA1\u6709\u4EA7\u751F\u9996\u6761\u56DE\u5E94\u3001\u6216 rollout \u6587\u4EF6\u5C1A\u672A\u843D\u76D8\u3002" + `\u4EC5\u5F53 \`${cli} codex\`\uFF08resume\uFF09\u5931\u8D25\u65F6\u624D\u9700\u8981\u5904\u7406\uFF1A\u7528 \`${cli} codex --new\` \u5F00\u65B0\u7EBF\u7A0B\u3002` : registered ? "\u5C1A\u65E0\u7EBF\u7A0B\u8BB0\u5F55\uFF1A\u8FDE\u63A5 Codex \u540E\u5EFA\u7ACB\u9996\u4E2A\u7EBF\u7A0B\u65F6\u4F1A\u81EA\u52A8\u5199\u5165\uFF0C\u65E0\u9700\u5904\u7406\u3002" : undefined
4924
5416
  });
4925
5417
  const pairProxyUrl = `ws://127.0.0.1:${pair.ports.proxyPort}`;
4926
5418
  const managedTuis = listManagedCodexTuiProcesses();
@@ -4937,19 +5429,19 @@ async function buildDoctorReport(pair, registered) {
4937
5429
  name: "codex tui (this pair)",
4938
5430
  status: attachedHere.length > 0 ? "ok" : "warn",
4939
5431
  detail: attachedHere.length > 0 ? `${attachedHere.length} attached to ${pairProxyUrl} (pid ${attachedHere.map((t) => t.pid).join(", ")})` : `no managed Codex TUI attached to this pair's proxy ${pairProxyUrl}`,
4940
- hint: attachedHere.length > 0 ? undefined : "\u53E6\u5F00\u4E00\u4E2A\u7EC8\u7AEF\u3001\u5728\u540C\u4E00\u76EE\u5F55\u8FD0\u884C `agentbridge codex` \u8FDE\u63A5\u672C pair\u3002"
5432
+ hint: attachedHere.length > 0 ? undefined : `\u53E6\u5F00\u4E00\u4E2A\u7EC8\u7AEF\u3001\u5728\u540C\u4E00\u76EE\u5F55\u8FD0\u884C \`${cli} codex\` \u8FDE\u63A5\u672C pair\u3002`
4941
5433
  });
4942
5434
  checks.push({
4943
5435
  name: "codex tui (other pairs)",
4944
5436
  status: attachedElsewhere.length > 0 ? "warn" : "ok",
4945
5437
  detail: attachedElsewhere.length > 0 ? `${attachedElsewhere.length} managed Codex TUI(s) attached to a DIFFERENT pair/proxy \u2014 likely started from another cwd, will not bridge here: ` + attachedElsewhere.map((t) => `pid ${t.pid}\u2192${t.remoteUrl ?? "?"}`).join(", ") : "no managed Codex TUI attached to another pair",
4946
- hint: attachedElsewhere.length > 0 ? "\u8FD9\u4E9B TUI \u5C5E\u4E8E\u5176\u4ED6\u76EE\u5F55\u7684 pair\uFF0C\u4E0D\u5F71\u54CD\u672C pair\uFF1B\u5B83\u4EEC\u4E0D\u4F1A\u6865\u63A5\u5230\u8FD9\u91CC\u3002\u5982\u4E0D\u518D\u9700\u8981\uFF0C\u53BB\u5BF9\u5E94\u76EE\u5F55\u8FD0\u884C `abg kill`\u3002" : undefined
5438
+ hint: attachedElsewhere.length > 0 ? `\u8FD9\u4E9B TUI \u5C5E\u4E8E\u5176\u4ED6\u76EE\u5F55\u7684 pair\uFF0C\u4E0D\u5F71\u54CD\u672C pair\uFF1B\u5B83\u4EEC\u4E0D\u4F1A\u6865\u63A5\u5230\u8FD9\u91CC\u3002\u5982\u4E0D\u518D\u9700\u8981\uFF0C\u53BB\u5BF9\u5E94\u76EE\u5F55\u8FD0\u884C \`${cli} kill\`\u3002` : undefined
4947
5439
  });
4948
5440
  for (const [name, path] of [
4949
5441
  ["daemon log", pair.stateDir.logFile],
4950
5442
  ["codex wrapper log", pair.stateDir.codexWrapperLogFile]
4951
5443
  ]) {
4952
- checks.push(logCheck(name, path));
5444
+ checks.push(logCheck(name, path, cli));
4953
5445
  }
4954
5446
  return {
4955
5447
  cwd,
@@ -4970,61 +5462,102 @@ async function buildDoctorReport(pair, registered) {
4970
5462
  checks
4971
5463
  };
4972
5464
  }
5465
+ function describeBuildDrift(runtime, launcher, cli = "abg") {
5466
+ const basis = runtimeContractComparisonBasis(runtime, launcher);
5467
+ const baseDetail = `runtime ${formatBuildInfo(runtime)} differs from launcher ${formatBuildInfo(launcher)}`;
5468
+ const baseHint = "daemon \u8FD0\u884C\u7684\u662F\u65E7\u6784\u5EFA\uFF08\u901A\u5E38\u7531\u65E7\u7248 CLI \u6216\u672A\u91CD\u5F00\u7684 Claude Code \u7A97\u53E3\u542F\u52A8\uFF09\u3002" + `\u6CA1\u6709\u8FDB\u884C\u4E2D\u7684 Codex \u4F1A\u8BDD\u65F6\uFF0C\u8FD0\u884C \`${cli} kill\` \u540E\u91CD\u65B0 \`${cli} claude\` \u5373\u53EF\u5BF9\u9F50\uFF1B` + "\u6709\u6D3B\u8DC3\u4F1A\u8BDD\u5219\u7B49\u6536\u5C3E\u540E\u518D\u91CD\u542F\u2014\u2014\u7248\u672C\u5DEE\u5F02\u4E0D\u4F1A\u5F3A\u6740\u6D3B\u8DC3\u4F1A\u8BDD\uFF0C\u53EF\u4EE5\u7EE7\u7EED\u7528\u3002";
5469
+ if (basis === "codeHash") {
5470
+ return { detail: `${baseDetail} [compared by codeHash \u2014 real code difference]`, hint: baseHint };
5471
+ }
5472
+ return {
5473
+ detail: `${baseDetail} [compared by commit stamp \u2014 legacy build without codeHash]`,
5474
+ hint: baseHint + "\uFF08\u6CE8\u610F\uFF1A\u672C\u5224\u5B9A\u57FA\u4E8E commit stamp \u53E3\u5F84\u2014\u2014\u6709\u4E00\u4FA7\u662F\u7F3A codeHash \u7684\u65E7\u6784\u5EFA\uFF1Bsquash \u5408\u5E76\u4F1A\u8BA9 stamp \u6EDE\u540E\u4E00\u683C\uFF0C" + "\u6E90\u7801\u4E00\u81F4\u65F6\u4E5F\u53EF\u80FD\u8BEF\u62A5\u3002\u5347\u7EA7\u4E24\u7AEF\u5230\u5E26 codeHash \u7684\u6784\u5EFA\u540E\u5C06\u6309\u4EE3\u7801\u5185\u5BB9\u5224\u5B9A\u3002\uFF09"
5475
+ };
5476
+ }
5477
+ function isUsableCodeHash(hash) {
5478
+ return typeof hash === "string" && hash.length > 0 && hash !== "source";
5479
+ }
5480
+ function evaluateArtifactAlignment(stamps) {
5481
+ if (stamps.length < 2) {
5482
+ return {
5483
+ name: "artifact alignment",
5484
+ status: "skip",
5485
+ detail: "n/a \u2014 fewer than two stamped artifacts found"
5486
+ };
5487
+ }
5488
+ if (stamps.every((stamp) => isUsableCodeHash(stamp.codeHash))) {
5489
+ const rendered2 = stamps.map((stamp) => `${stamp.label}=${stamp.codeHash}`).join(", ");
5490
+ if (new Set(stamps.map((stamp) => stamp.codeHash)).size === 1) {
5491
+ return { name: "artifact alignment", status: "ok", detail: `codeHash basis: ${rendered2}` };
5492
+ }
5493
+ return {
5494
+ name: "artifact alignment",
5495
+ status: "fail",
5496
+ detail: `deployed artifacts contain DIFFERENT code (codeHash basis): ${rendered2}`,
5497
+ hint: "\u90E8\u7F72\u7269\u4EE3\u7801\u5206\u88C2\u4F1A\u5BFC\u81F4\u4E92\u76F8\u66FF\u6362 daemon\uFF08\u6740\u6389\u6D3B\u4F1A\u8BDD\uFF09\u3002\u5728\u4ED3\u5E93\u76EE\u5F55\u8FD0\u884C `bun run install:global` " + "\u4E00\u6B21\u6027\u5BF9\u9F50\u5168\u5C40 CLI \u4E0E\u63D2\u4EF6\u7F13\u5B58\uFF0C\u7136\u540E\u5173\u95ED\u5E76\u91CD\u5F00\u4ECD\u5728\u4F7F\u7528\u65E7\u63D2\u4EF6\u7684 Claude Code \u7A97\u53E3\u3002"
5498
+ };
5499
+ }
5500
+ const rendered = stamps.map((stamp) => `${stamp.label}=${stamp.commit}`).join(", ");
5501
+ if (new Set(stamps.map((stamp) => stamp.commit)).size === 1) {
5502
+ return {
5503
+ name: "artifact alignment",
5504
+ status: "ok",
5505
+ detail: `legacy commit-stamp basis: ${rendered}`
5506
+ };
5507
+ }
5508
+ return {
5509
+ name: "artifact alignment",
5510
+ status: "fail",
5511
+ detail: `deployed artifacts are at DIFFERENT builds (legacy commit-stamp basis): ${rendered}`,
5512
+ hint: "\uFF08stamp \u53E3\u5F84\uFF1A\u5B58\u5728\u7F3A codeHash \u7684\u65E7\u90E8\u7F72\u7269\uFF0C\u4E14 squash \u5408\u5E76\u4F1A\u8BA9 stamp \u6EDE\u540E\u4E00\u683C\uFF0C\u6E90\u7801\u4E00\u81F4\u65F6\u4E5F\u53EF\u80FD\u8BEF\u62A5\u3002\uFF09" + "\u90E8\u7F72\u7269\u7248\u672C\u5206\u88C2\u4F1A\u5BFC\u81F4\u4E92\u76F8\u66FF\u6362 daemon\uFF08\u6740\u6389\u6D3B\u4F1A\u8BDD\uFF09\u3002\u5728\u4ED3\u5E93\u76EE\u5F55\u8FD0\u884C `bun run install:global` " + "\u4E00\u6B21\u6027\u5BF9\u9F50\u5168\u5C40 CLI \u4E0E\u63D2\u4EF6\u7F13\u5B58\u5E76\u5347\u7EA7\u5230\u5E26 codeHash \u7684\u6784\u5EFA\uFF0C\u7136\u540E\u5173\u95ED\u5E76\u91CD\u5F00\u4ECD\u5728\u4F7F\u7528\u65E7\u63D2\u4EF6\u7684 Claude Code \u7A97\u53E3\uFF1B" + "\u5BF9\u9F50\u540E\u6B64\u68C0\u67E5\u5C06\u6309\u4EE3\u7801\u5185\u5BB9\uFF08codeHash\uFF09\u5224\u5B9A\uFF0Cstamp \u6EDE\u540E\u4E0D\u518D\u8BEF\u62A5\u3002"
5513
+ };
5514
+ }
4973
5515
  function artifactAlignmentCheck() {
4974
5516
  const stamps = [];
4975
5517
  if (BUILD_INFO.commit !== "source") {
4976
- stamps.push({ label: `launcher(${BUILD_INFO.bundle})`, commit: BUILD_INFO.commit });
5518
+ stamps.push({
5519
+ label: `launcher(${BUILD_INFO.bundle})`,
5520
+ commit: BUILD_INFO.commit,
5521
+ codeHash: hasValidCodeHash(BUILD_INFO) ? BUILD_INFO.codeHash ?? null : null
5522
+ });
4977
5523
  }
4978
5524
  const bin = Bun.which("agentbridge") ?? Bun.which("abg");
4979
5525
  if (bin) {
4980
5526
  try {
4981
- const commit = extractBundleCommit(realpathSync3(bin));
4982
- if (commit)
4983
- stamps.push({ label: "global-cli", commit });
5527
+ const stamp = extractBundleStamp(realpathSync3(bin));
5528
+ if (stamp)
5529
+ stamps.push({ label: "global-cli", ...stamp });
4984
5530
  } catch {}
4985
5531
  }
4986
5532
  const cacheRoot = pluginCacheRoot();
4987
5533
  try {
4988
5534
  for (const version of readdirSync6(cacheRoot)) {
4989
- const commit = extractBundleCommit(join17(cacheRoot, version, "server", "daemon.js"));
4990
- if (commit)
4991
- stamps.push({ label: `plugin-cache@${version}`, commit });
5535
+ const stamp = extractBundleStamp(join17(cacheRoot, version, "server", "daemon.js"));
5536
+ if (stamp)
5537
+ stamps.push({ label: `plugin-cache@${version}`, ...stamp });
4992
5538
  }
4993
5539
  } catch {}
4994
5540
  const repoBundle = join17(process.cwd(), "plugins", "agentbridge", "server", "daemon.js");
4995
5541
  if (existsSync15(repoBundle)) {
4996
- const commit = extractBundleCommit(repoBundle);
4997
- if (commit)
4998
- stamps.push({ label: "repo-bundle", commit });
4999
- }
5000
- if (stamps.length < 2) {
5001
- return {
5002
- name: "artifact alignment",
5003
- status: "skip",
5004
- detail: "n/a \u2014 fewer than two stamped artifacts found"
5005
- };
5542
+ const stamp = extractBundleStamp(repoBundle);
5543
+ if (stamp)
5544
+ stamps.push({ label: "repo-bundle", ...stamp });
5006
5545
  }
5007
- const commits = new Set(stamps.map((s) => s.commit));
5008
- const rendered = stamps.map((s) => `${s.label}=${s.commit}`).join(", ");
5009
- if (commits.size === 1) {
5010
- return { name: "artifact alignment", status: "ok", detail: rendered };
5011
- }
5012
- return {
5013
- name: "artifact alignment",
5014
- status: "fail",
5015
- detail: `deployed artifacts are at DIFFERENT builds: ${rendered}`,
5016
- hint: "\u90E8\u7F72\u7269\u7248\u672C\u5206\u88C2\u4F1A\u5BFC\u81F4\u4E92\u76F8\u66FF\u6362 daemon\uFF08\u6740\u6389\u6D3B\u4F1A\u8BDD\uFF09\u3002\u5728\u4ED3\u5E93\u76EE\u5F55\u8FD0\u884C `bun run install:global` " + "\u4E00\u6B21\u6027\u5BF9\u9F50\u5168\u5C40 CLI \u4E0E\u63D2\u4EF6\u7F13\u5B58\uFF0C\u7136\u540E\u5173\u95ED\u5E76\u91CD\u5F00\u4ECD\u5728\u4F7F\u7528\u65E7\u63D2\u4EF6\u7684 Claude Code \u7A97\u53E3\u3002"
5017
- };
5546
+ return evaluateArtifactAlignment(stamps);
5018
5547
  }
5019
- function extractBundleCommit(path) {
5548
+ function extractBundleStamp(path) {
5020
5549
  try {
5021
- const match = readFileSync12(path, "utf-8").match(/commit:\s*defineString\("([^"]+)",\s*"source"\)/);
5022
- return match ? match[1] : null;
5550
+ const text = readFileSync13(path, "utf-8");
5551
+ const commit = text.match(/commit:\s*defineString\("([^"]+)",\s*"source"\)/)?.[1] ?? null;
5552
+ if (!commit)
5553
+ return null;
5554
+ const codeHash = text.match(/codeHash:\s*defineString\("([^"]+)",\s*"source"\)/)?.[1] ?? null;
5555
+ return { commit, codeHash };
5023
5556
  } catch {
5024
5557
  return null;
5025
5558
  }
5026
5559
  }
5027
- function configParseabilityCheck(cwd) {
5560
+ function configParseabilityCheck(cwd, cli) {
5028
5561
  const desc = new ConfigService(cwd).describeConfig();
5029
5562
  if (desc.state === "absent") {
5030
5563
  return {
@@ -5038,7 +5571,7 @@ function configParseabilityCheck(cwd) {
5038
5571
  name: "config.json",
5039
5572
  status: "warn",
5040
5573
  detail: `unparseable at ${desc.path} (${desc.reason}) \u2014 custom thresholds NOT in effect, using defaults`,
5041
- hint: "config.json \u635F\u574F\u6216\u5B57\u6BB5\u7C7B\u578B\u9519\u8BEF\uFF1Abridge \u5DF2\u56DE\u9000\u5230\u9ED8\u8BA4\u9608\u503C\uFF0C\u4F60\u7684\u81EA\u5B9A\u4E49 budget/idle \u8BBE\u7F6E\u672A\u751F\u6548\u3002" + "\u4FEE\u6B63\u8BE5\u6587\u4EF6\u7684 JSON \u8BED\u6CD5/\u5B57\u6BB5\u7C7B\u578B\u540E\u91CD\u542F `agentbridge claude` \u5373\u53EF\u91CD\u65B0\u751F\u6548\u3002"
5574
+ hint: "config.json \u635F\u574F\u6216\u5B57\u6BB5\u7C7B\u578B\u9519\u8BEF\uFF1Abridge \u5DF2\u56DE\u9000\u5230\u9ED8\u8BA4\u9608\u503C\uFF0C\u4F60\u7684\u81EA\u5B9A\u4E49 budget/idle \u8BBE\u7F6E\u672A\u751F\u6548\u3002" + `\u4FEE\u6B63\u8BE5\u6587\u4EF6\u7684 JSON \u8BED\u6CD5/\u5B57\u6BB5\u7C7B\u578B\u540E\u91CD\u542F \`${cli} claude\` \u5373\u53EF\u91CD\u65B0\u751F\u6548\u3002`
5042
5575
  };
5043
5576
  }
5044
5577
  return {
@@ -5047,7 +5580,7 @@ function configParseabilityCheck(cwd) {
5047
5580
  detail: desc.customValues ? `parsed at ${desc.path} \u2014 custom values in effect` : `parsed at ${desc.path} \u2014 all values match defaults`
5048
5581
  };
5049
5582
  }
5050
- function logCheck(name, path) {
5583
+ function logCheck(name, path, cli) {
5051
5584
  if (!existsSync15(path)) {
5052
5585
  return {
5053
5586
  name,
@@ -5062,7 +5595,7 @@ function logCheck(name, path) {
5062
5595
  name,
5063
5596
  status: "warn",
5064
5597
  detail: `${path} (${stat.size} bytes, oversized; stop the pair, rebuild/reinstall, then rotate or remove this log)`,
5065
- hint: "\u65E5\u5FD7\u8FC7\u5927\uFF1A`abg kill` \u505C\u6B62 pair \u540E\u5220\u9664\u8BE5\u6587\u4EF6\u518D\u91CD\u542F\u5373\u53EF\u3002"
5598
+ hint: `\u65E5\u5FD7\u8FC7\u5927\uFF1A\`${cli} kill\` \u505C\u6B62 pair \u540E\u5220\u9664\u8BE5\u6587\u4EF6\u518D\u91CD\u542F\u5373\u53EF\u3002`
5066
5599
  };
5067
5600
  }
5068
5601
  return { name, status: "ok", detail: `${path} (${stat.size} bytes)` };
@@ -5100,6 +5633,7 @@ var LARGE_LOG_WARN_BYTES;
5100
5633
  var init_doctor = __esm(() => {
5101
5634
  init_plugin_cache();
5102
5635
  init_build_info();
5636
+ init_cli_invocation();
5103
5637
  init_config_service();
5104
5638
  init_env_guard();
5105
5639
  init_pair_resolver();
@@ -5201,6 +5735,7 @@ __export(exports_budget, {
5201
5735
  });
5202
5736
  async function runBudget(args) {
5203
5737
  const json = args.includes("--json");
5738
+ const cli = cliInvocationName();
5204
5739
  const { pairFlag } = parsePairFlag(args.filter((arg) => arg !== "--json"));
5205
5740
  let resolution;
5206
5741
  try {
@@ -5220,7 +5755,7 @@ async function runBudget(args) {
5220
5755
  if (json) {
5221
5756
  console.log(JSON.stringify({ ok: false, error: "pair_not_registered" }));
5222
5757
  } else {
5223
- console.error("\u8BE5\u76EE\u5F55\u5C1A\u65E0 pair\uFF0C\u5148\u8FD0\u884C abg claude");
5758
+ console.error(`\u8BE5\u76EE\u5F55\u5C1A\u65E0 pair\uFF0C\u5148\u8FD0\u884C ${cli} claude`);
5224
5759
  }
5225
5760
  process.exit(1);
5226
5761
  return;
@@ -5230,7 +5765,7 @@ async function runBudget(args) {
5230
5765
  if (json) {
5231
5766
  console.log(JSON.stringify({ ok: false, pairId: pair.pairId, error: "daemon_unreachable" }));
5232
5767
  } else {
5233
- console.error(`AgentBridge daemon \u672A\u8FD0\u884C\uFF08pair ${pair.pairId}\uFF0C\u63A7\u5236\u7AEF\u53E3 ${pair.ports.controlPort}\uFF09\u3002` + "\u5148\u8FD0\u884C `abg claude` \u542F\u52A8\u4F1A\u8BDD\u3002");
5768
+ console.error(`AgentBridge daemon \u672A\u8FD0\u884C\uFF08pair ${pair.pairId}\uFF0C\u63A7\u5236\u7AEF\u53E3 ${pair.ports.controlPort}\uFF09\u3002` + `\u5148\u8FD0\u884C \`${cli} claude\` \u542F\u52A8\u4F1A\u8BDD\u3002`);
5234
5769
  }
5235
5770
  process.exit(1);
5236
5771
  }
@@ -5242,6 +5777,7 @@ async function runBudget(args) {
5242
5777
  console.log(status.budget ? renderBudgetSnapshot(status.budget) : BUDGET_UNAVAILABLE_TEXT);
5243
5778
  }
5244
5779
  var init_budget = __esm(() => {
5780
+ init_cli_invocation();
5245
5781
  init_pair_resolver();
5246
5782
  init_render();
5247
5783
  });
@@ -5254,7 +5790,7 @@ __export(exports_logs, {
5254
5790
  parseLogsArgs: () => parseLogsArgs,
5255
5791
  followLog: () => followLog
5256
5792
  });
5257
- import { existsSync as existsSync16, readFileSync as readFileSync13 } from "fs";
5793
+ import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
5258
5794
  import { spawn as spawn4 } from "child_process";
5259
5795
  function parseLogsArgs(args) {
5260
5796
  let codex = false;
@@ -5347,7 +5883,7 @@ async function runLogs(args) {
5347
5883
  function printTail(logPath, count, label, pairName) {
5348
5884
  let text;
5349
5885
  try {
5350
- text = readFileSync13(logPath, "utf8");
5886
+ text = readFileSync14(logPath, "utf8");
5351
5887
  } catch (err) {
5352
5888
  console.error(`[agentbridge] failed to read ${label} for pair ${pairName}: ` + `${err instanceof Error ? err.message : String(err)} (${logPath})`);
5353
5889
  process.exit(1);
@@ -5492,6 +6028,7 @@ async function main(command, restArgs) {
5492
6028
  }
5493
6029
  }
5494
6030
  function printHelp() {
6031
+ const cli = cliInvocationName();
5495
6032
  console.log(`
5496
6033
  AgentBridge \u2014 Multi-agent collaboration bridge
5497
6034
 
@@ -5509,8 +6046,9 @@ Commands:
5509
6046
  No target: print resume commands for this directory's last
5510
6047
  Claude session + this pair's current Codex thread.
5511
6048
  With target: resume that side directly.
5512
- pairs [rm <name|id> | prune [--dry-run]]
5513
- List pairs; remove one (rm), or delete orphan state dirs (prune)
6049
+ pairs [rm <name|id> | prune [--apply]]
6050
+ List pairs; remove one (rm), or reclaim orphan dirs + stranded
6051
+ entries (prune previews by default; --apply to delete)
5514
6052
  doctor [--json] Diagnose env, daemon, build drift, logs, and current thread
5515
6053
  doctor resume-pollution [--apply] Find/fix old AgentBridge kickoff metadata
5516
6054
  budget [--json] Show both agents' subscription quota snapshot (5h/weekly, drift, pause state)
@@ -5542,30 +6080,30 @@ Multi-pair:
5542
6080
  contesting it \u2014 pick another --pair name (or kill the live one first).
5543
6081
 
5544
6082
  Examples:
5545
- abg init # First-time setup
5546
- abg claude # Start the "main" pair for this directory
5547
- abg codex # Connect Codex to this directory's "main" pair
5548
- abg resume # Print resume commands for both sides
5549
- abg resume claude # Resume the last Claude Code session here
5550
- abg resume codex # Resume this pair's current Codex thread
5551
- abg claude --safe # One launch without the max-permission default
5552
- abg --pair work claude # Start a named pair "work" (this directory)
5553
- abg --pair work codex # Connect Codex to the "work" pair
5554
- abg --pair review claude # A second, parallel pair
5555
- abg pairs # List all pairs and their ports/status
5556
- abg pairs --threads # Include current thread mapping
5557
- abg doctor --json # Emit a structured diagnostics report
5558
- abg logs # Tail the last 100 lines of this pair's daemon log
5559
- abg logs -f -n 200 # Follow the log, starting from the last 200 lines
5560
- abg logs --codex # Tail the codex wrapper log instead
5561
- abg --pair work logs # Tail the "work" pair's daemon log
5562
- abg pairs rm work # Stop this directory's "work" pair and free its slot
5563
- abg pairs rm work-1a2b3c4d # ...or by its full id (from that pair's directory)
5564
- abg pairs prune --dry-run # Preview orphan pair dirs (no registry entry, not live)
5565
- abg pairs prune # ...delete those orphan state directories
5566
- abg --pair work kill # Stop only this directory's "work" pair
5567
- abg kill # Stop this directory's pairs (+ any legacy-root daemon)
5568
- abg kill all # Stop every pair in every directory (+ legacy-root)
6083
+ ${cli} init # First-time setup
6084
+ ${cli} claude # Start the "main" pair for this directory
6085
+ ${cli} codex # Connect Codex to this directory's "main" pair
6086
+ ${cli} resume # Print resume commands for both sides
6087
+ ${cli} resume claude # Resume the last Claude Code session here
6088
+ ${cli} resume codex # Resume this pair's current Codex thread
6089
+ ${cli} claude --safe # One launch without the max-permission default
6090
+ ${cli} --pair work claude # Start a named pair "work" (this directory)
6091
+ ${cli} --pair work codex # Connect Codex to the "work" pair
6092
+ ${cli} --pair review claude # A second, parallel pair
6093
+ ${cli} pairs # List all pairs and their ports/status
6094
+ ${cli} pairs --threads # Include current thread mapping
6095
+ ${cli} doctor --json # Emit a structured diagnostics report
6096
+ ${cli} logs # Tail the last 100 lines of this pair's daemon log
6097
+ ${cli} logs -f -n 200 # Follow the log, starting from the last 200 lines
6098
+ ${cli} logs --codex # Tail the codex wrapper log instead
6099
+ ${cli} --pair work logs # Tail the "work" pair's daemon log
6100
+ ${cli} pairs rm work # Stop this directory's "work" pair and free its slot
6101
+ ${cli} pairs rm work-1a2b3c4d # ...or by its full id (from that pair's directory)
6102
+ ${cli} pairs prune # Preview reclaimable: orphan dirs + stranded entries (cwd-gone, dead, >1d)
6103
+ ${cli} pairs prune --apply # ...actually delete the previewed dirs + entries
6104
+ ${cli} --pair work kill # Stop only this directory's "work" pair
6105
+ ${cli} kill # Stop this directory's pairs (+ any legacy-root daemon)
6106
+ ${cli} kill all # Stop every pair in every directory (+ legacy-root)
5569
6107
  `.trim());
5570
6108
  }
5571
6109
  function printVersion() {
@@ -5578,6 +6116,7 @@ function printVersion() {
5578
6116
  }
5579
6117
  var MARKETPLACE_NAME = "agentbridge", PLUGIN_NAME = "agentbridge", REFRESH_COMMANDS, NOTIFY_COMMANDS, PAIR_AWARE_COMMANDS;
5580
6118
  var init_cli = __esm(() => {
6119
+ init_cli_invocation();
5581
6120
  REFRESH_COMMANDS = new Set(["claude", "codex", "resume"]);
5582
6121
  NOTIFY_COMMANDS = new Set(["claude", "codex", "init", "dev", "resume"]);
5583
6122
  PAIR_AWARE_COMMANDS = new Set(["claude", "codex", "kill", "doctor", "budget", "resume", "logs"]);