@raysonmeng/agentbridge 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.14",
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);
@@ -381,6 +438,9 @@ function normalizeBoundedInteger(value, fallback, min, max) {
381
438
  return fallback;
382
439
  return parsed;
383
440
  }
441
+ function normalizeStrategy(value, fallback) {
442
+ return value === "conserve" || value === "maximize" ? value : fallback;
443
+ }
384
444
  function normalizeBoolean(value, fallback) {
385
445
  if (typeof value === "boolean")
386
446
  return value;
@@ -429,8 +489,26 @@ function normalizeBudgetConfig(raw, fallback = DEFAULT_BUDGET_CONFIG) {
429
489
  timeWindowSec: normalizeBoundedInteger(parallel.timeWindowSec, fallback.parallel.timeWindowSec, 60, 604800)
430
490
  },
431
491
  codexTierControl: normalizeBoolean(budget.codexTierControl, fallback.codexTierControl) && codexTiers.full !== null,
432
- codexTiers
492
+ codexTiers,
493
+ strategy: normalizeStrategy(budget.strategy, fallback.strategy)
494
+ };
495
+ }
496
+ function applyBudgetEnvOverrides(budget, env = process.env) {
497
+ const overlay = {
498
+ enabled: env.AGENTBRIDGE_BUDGET_ENABLED ?? budget.enabled,
499
+ pollSeconds: env.AGENTBRIDGE_BUDGET_POLL_SECONDS ?? budget.pollSeconds,
500
+ pauseAt: env.AGENTBRIDGE_BUDGET_PAUSE_AT ?? budget.pauseAt,
501
+ resumeBelow: env.AGENTBRIDGE_BUDGET_RESUME_BELOW ?? budget.resumeBelow,
502
+ syncDriftPct: env.AGENTBRIDGE_BUDGET_SYNC_DRIFT_PCT ?? budget.syncDriftPct,
503
+ parallel: {
504
+ minRemainingPct: env.AGENTBRIDGE_BUDGET_PARALLEL_MIN_REMAINING_PCT ?? budget.parallel.minRemainingPct,
505
+ timeWindowSec: env.AGENTBRIDGE_BUDGET_PARALLEL_TIME_WINDOW_SEC ?? budget.parallel.timeWindowSec
506
+ },
507
+ codexTierControl: env.AGENTBRIDGE_BUDGET_CODEX_TIER_CONTROL ?? budget.codexTierControl,
508
+ codexTiers: budget.codexTiers,
509
+ strategy: env.AGENTBRIDGE_BUDGET_STRATEGY ?? budget.strategy
433
510
  };
511
+ return normalizeBudgetConfig(overlay, budget);
434
512
  }
435
513
  function normalizeConfig(raw) {
436
514
  if (!isRecord(raw))
@@ -442,13 +520,13 @@ function normalizeConfig(raw) {
442
520
  return {
443
521
  version: typeof config.version === "string" ? config.version : DEFAULT_CONFIG.version,
444
522
  codex: {
445
- appPort: normalizeInteger(codex.appPort ?? daemon.port, DEFAULT_CONFIG.codex.appPort),
446
- proxyPort: normalizeInteger(codex.proxyPort ?? daemon.proxyPort, DEFAULT_CONFIG.codex.proxyPort)
523
+ appPort: normalizeBoundedInteger(codex.appPort ?? daemon.port, DEFAULT_CONFIG.codex.appPort, 1, 65535),
524
+ proxyPort: normalizeBoundedInteger(codex.proxyPort ?? daemon.proxyPort, DEFAULT_CONFIG.codex.proxyPort, 1, 65535)
447
525
  },
448
526
  turnCoordination: {
449
- attentionWindowSeconds: normalizeInteger(turnCoordination.attentionWindowSeconds, DEFAULT_CONFIG.turnCoordination.attentionWindowSeconds)
527
+ attentionWindowSeconds: normalizeBoundedInteger(turnCoordination.attentionWindowSeconds, DEFAULT_CONFIG.turnCoordination.attentionWindowSeconds, 0, Number.MAX_SAFE_INTEGER)
450
528
  },
451
- idleShutdownSeconds: normalizeInteger(config.idleShutdownSeconds, DEFAULT_CONFIG.idleShutdownSeconds),
529
+ idleShutdownSeconds: normalizeBoundedInteger(config.idleShutdownSeconds, DEFAULT_CONFIG.idleShutdownSeconds, 1, Number.MAX_SAFE_INTEGER),
452
530
  budget: normalizeBudgetConfig(config.budget)
453
531
  };
454
532
  }
@@ -520,9 +598,7 @@ class ConfigService {
520
598
  };
521
599
  }
522
600
  save(config) {
523
- this.ensureConfigDir();
524
- writeFileSync2(this.configPath, JSON.stringify(config, null, 2) + `
525
- `, "utf-8");
601
+ atomicWriteJson(this.configPath, config);
526
602
  }
527
603
  initDefaults() {
528
604
  this.ensureConfigDir();
@@ -538,12 +614,13 @@ class ConfigService {
538
614
  }
539
615
  ensureConfigDir() {
540
616
  if (!existsSync2(this.configDir)) {
541
- mkdirSync2(this.configDir, { recursive: true });
617
+ mkdirSync3(this.configDir, { recursive: true });
542
618
  }
543
619
  }
544
620
  }
545
621
  var DEFAULT_BUDGET_CONFIG, DEFAULT_CONFIG, CONFIG_DIR = ".agentbridge", CONFIG_FILE = "config.json", NOOP_LOGGER = () => {};
546
622
  var init_config_service = __esm(() => {
623
+ init_atomic_json();
547
624
  DEFAULT_BUDGET_CONFIG = {
548
625
  enabled: true,
549
626
  pollSeconds: 300,
@@ -559,7 +636,8 @@ var init_config_service = __esm(() => {
559
636
  full: null,
560
637
  balanced: { effort: "medium" },
561
638
  eco: { effort: "low" }
562
- }
639
+ },
640
+ strategy: "conserve"
563
641
  };
564
642
  DEFAULT_CONFIG = {
565
643
  version: "1.0",
@@ -576,7 +654,7 @@ var init_config_service = __esm(() => {
576
654
  });
577
655
 
578
656
  // src/cli/pkg-root.ts
579
- import { dirname, join as join3 } from "path";
657
+ import { dirname as dirname2, join as join3 } from "path";
580
658
  import { existsSync as existsSync3 } from "fs";
581
659
  import { execFileSync } from "child_process";
582
660
  function findPackageRoot() {
@@ -585,7 +663,7 @@ function findPackageRoot() {
585
663
  if (existsSync3(join3(dir, "package.json"))) {
586
664
  return dir;
587
665
  }
588
- const parent = dirname(dir);
666
+ const parent = dirname2(dir);
589
667
  if (parent === dir) {
590
668
  throw new Error("Could not find package.json in any parent directory");
591
669
  }
@@ -760,18 +838,24 @@ __export(exports_init, {
760
838
  writeCollaborationSections: () => writeCollaborationSections,
761
839
  runInit: () => runInit,
762
840
  pluginInstallFallbackGuidance: () => pluginInstallFallbackGuidance,
841
+ formatDepChecks: () => formatDepChecks,
763
842
  compareVersions: () => compareVersions
764
843
  });
765
844
  import { execSync, execFileSync as execFileSync2 } from "child_process";
766
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
845
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
767
846
  import { join as join5 } from "path";
768
847
  async function runInit() {
769
848
  console.log(`AgentBridge Init
770
849
  `);
850
+ const cli = cliInvocationName();
771
851
  console.log("Checking dependencies...");
772
- checkBun();
773
- checkClaude();
774
- checkCodex();
852
+ const depChecks = [checkBun(), checkClaude(), checkCodex()];
853
+ for (const line of formatDepChecks(depChecks, cli)) {
854
+ console.log(line);
855
+ }
856
+ if (depChecks.some((check) => check.status === "fail")) {
857
+ process.exit(1);
858
+ }
775
859
  console.log("");
776
860
  console.log("Generating project config...");
777
861
  const configService = new ConfigService;
@@ -803,7 +887,7 @@ async function runInit() {
803
887
  pluginInstalled = true;
804
888
  } catch {
805
889
  console.log(" Plugin install skipped (marketplace registration or install failed).");
806
- for (const line of pluginInstallFallbackGuidance(detectRepoCheckout())) {
890
+ for (const line of pluginInstallFallbackGuidance(detectRepoCheckout(), cli)) {
807
891
  console.log(line);
808
892
  }
809
893
  }
@@ -818,8 +902,8 @@ async function runInit() {
818
902
  }
819
903
  console.log("Next steps:");
820
904
  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");
905
+ console.log(` 2. Start Claude Code: ${cli} claude`);
906
+ console.log(` 3. Start Codex TUI: ${cli} codex`);
823
907
  }
824
908
  function detectRepoCheckout() {
825
909
  try {
@@ -828,11 +912,11 @@ function detectRepoCheckout() {
828
912
  return false;
829
913
  }
830
914
  }
831
- function pluginInstallFallbackGuidance(insideRepo) {
915
+ function pluginInstallFallbackGuidance(insideRepo, cli = cliInvocationName()) {
832
916
  if (insideRepo) {
833
917
  return [
834
918
  " You can install it later with:",
835
- " abg dev # registers marketplace and installs plugin"
919
+ ` ${cli} dev # registers marketplace and installs plugin`
836
920
  ];
837
921
  }
838
922
  return [
@@ -840,46 +924,68 @@ function pluginInstallFallbackGuidance(insideRepo) {
840
924
  ...MARKETPLACE_STEPS.map((step) => ` ${step}`)
841
925
  ];
842
926
  }
927
+ function formatDepChecks(checks, cli) {
928
+ const lines = [];
929
+ for (const check of checks) {
930
+ lines.push(` ${check.status.toUpperCase().padEnd(4)} ${check.name}: ${check.detail}`);
931
+ if ((check.status === "warn" || check.status === "fail") && check.hint) {
932
+ lines.push(` \u21B3 ${check.hint}`);
933
+ }
934
+ }
935
+ lines.push(` \u9A8C\u8BC1\u5B89\u88C5: ${cli} doctor`);
936
+ return lines;
937
+ }
843
938
  function checkBun() {
844
939
  try {
845
940
  const version = execSync("bun --version", { encoding: "utf-8" }).trim();
846
- console.log(` bun: ${version}`);
941
+ return { name: "bun", status: "ok", detail: version };
847
942
  } catch {
848
- console.error(" ERROR: bun not found in PATH.");
849
- console.error(" Install Bun: https://bun.sh");
850
- process.exit(1);
943
+ return {
944
+ name: "bun",
945
+ status: "fail",
946
+ detail: "not found in PATH",
947
+ hint: "Install Bun: https://bun.sh"
948
+ };
851
949
  }
852
950
  }
853
951
  function checkClaude() {
952
+ let versionOutput;
854
953
  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
- }
954
+ versionOutput = execSync("claude --version", { encoding: "utf-8" }).trim();
869
955
  } 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);
956
+ return {
957
+ name: "claude",
958
+ status: "fail",
959
+ detail: "not found in PATH",
960
+ hint: "Install Claude Code: npm install -g @anthropic-ai/claude-code"
961
+ };
962
+ }
963
+ const match = versionOutput.match(/(\d+\.\d+\.\d+)/);
964
+ if (!match) {
965
+ return { name: "claude", status: "ok", detail: `${versionOutput} (version check skipped)` };
966
+ }
967
+ const version = match[1];
968
+ if (compareVersions(version, MIN_CLAUDE_VERSION) < 0) {
969
+ return {
970
+ name: "claude",
971
+ status: "fail",
972
+ detail: `${version} is too old (channels require >= ${MIN_CLAUDE_VERSION})`,
973
+ hint: "Update: npm update -g @anthropic-ai/claude-code"
974
+ };
873
975
  }
976
+ return { name: "claude", status: "ok", detail: version };
874
977
  }
875
978
  function checkCodex() {
876
979
  try {
877
980
  const version = execSync("codex --version", { encoding: "utf-8" }).trim();
878
- console.log(` codex: ${version}`);
981
+ return { name: "codex", status: "ok", detail: version };
879
982
  } catch {
880
- console.error(" ERROR: codex not found in PATH.");
881
- console.error(" Install Codex: https://github.com/openai/codex");
882
- process.exit(1);
983
+ return {
984
+ name: "codex",
985
+ status: "warn",
986
+ detail: "not found in PATH (the Codex side will be unavailable until installed)",
987
+ hint: "Install Codex when you want to pair: https://github.com/openai/codex"
988
+ };
883
989
  }
884
990
  }
885
991
  function writeCollaborationSections(projectRoot) {
@@ -905,7 +1011,7 @@ function writeCollaborationSections(projectRoot) {
905
1011
  results.push(`${name}: unchanged (section already up to date)`);
906
1012
  continue;
907
1013
  }
908
- writeFileSync3(path, updated, "utf-8");
1014
+ writeFileSync2(path, updated, "utf-8");
909
1015
  if (existing === "") {
910
1016
  results.push(`${name}: created with collaboration section`);
911
1017
  } else if (existing.includes(`<!-- ${MARKER_ID}:start -->`)) {
@@ -918,6 +1024,7 @@ function writeCollaborationSections(projectRoot) {
918
1024
  }
919
1025
  var MIN_CLAUDE_VERSION = "2.1.80";
920
1026
  var init_init = __esm(() => {
1027
+ init_cli_invocation();
921
1028
  init_config_service();
922
1029
  init_cli();
923
1030
  init_pkg_root();
@@ -1041,7 +1148,7 @@ var init_dev = __esm(() => {
1041
1148
  });
1042
1149
 
1043
1150
  // 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;
1151
+ 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
1152
 
1046
1153
  // src/interrupt-timing.ts
1047
1154
  var CLIENT_REPLY_TIMEOUT_MS = 15000, INTERRUPT_CLIENT_MARGIN_MS = 2000, MAX_INTERRUPT_TIMEOUT_MS;
@@ -1049,6 +1156,76 @@ var init_interrupt_timing = __esm(() => {
1049
1156
  MAX_INTERRUPT_TIMEOUT_MS = CLIENT_REPLY_TIMEOUT_MS - INTERRUPT_CLIENT_MARGIN_MS;
1050
1157
  });
1051
1158
 
1159
+ // src/pending-request-registry.ts
1160
+ class PendingRequestRegistry {
1161
+ entries = new Map;
1162
+ setTimer;
1163
+ clearTimer;
1164
+ constructor(deps = {}) {
1165
+ this.setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
1166
+ this.clearTimer = deps.clearTimer ?? ((handle) => clearTimeout(handle));
1167
+ }
1168
+ get size() {
1169
+ return this.entries.size;
1170
+ }
1171
+ has(id) {
1172
+ return this.entries.has(id);
1173
+ }
1174
+ register(id, options) {
1175
+ const existing = this.entries.get(id);
1176
+ if (existing) {
1177
+ this.clearTimer(existing.timer);
1178
+ this.entries.delete(id);
1179
+ }
1180
+ return new Promise((resolve3, reject) => {
1181
+ const timer = this.setTimer(() => {
1182
+ if (!this.entries.has(id))
1183
+ return;
1184
+ this.entries.delete(id);
1185
+ options.onTimeout({ resolve: resolve3, reject });
1186
+ }, options.timeoutMs);
1187
+ if (options.unref) {
1188
+ timer.unref?.();
1189
+ }
1190
+ this.entries.set(id, { resolve: resolve3, reject, timer });
1191
+ });
1192
+ }
1193
+ settle(id, value) {
1194
+ const entry = this.entries.get(id);
1195
+ if (!entry)
1196
+ return false;
1197
+ this.clearTimer(entry.timer);
1198
+ this.entries.delete(id);
1199
+ entry.resolve(value);
1200
+ return true;
1201
+ }
1202
+ reject(id, error) {
1203
+ const entry = this.entries.get(id);
1204
+ if (!entry)
1205
+ return false;
1206
+ this.clearTimer(entry.timer);
1207
+ this.entries.delete(id);
1208
+ entry.reject(error);
1209
+ return true;
1210
+ }
1211
+ settleAll(value) {
1212
+ const make = typeof value === "function" ? value : () => value;
1213
+ for (const [id, entry] of this.entries) {
1214
+ this.clearTimer(entry.timer);
1215
+ this.entries.delete(id);
1216
+ entry.resolve(make(id));
1217
+ }
1218
+ }
1219
+ rejectAll(error) {
1220
+ const make = typeof error === "function" ? error : () => error;
1221
+ for (const [id, entry] of this.entries) {
1222
+ this.clearTimer(entry.timer);
1223
+ this.entries.delete(id);
1224
+ entry.reject(make(id));
1225
+ }
1226
+ }
1227
+ }
1228
+
1052
1229
  // src/daemon-client.ts
1053
1230
  import { EventEmitter } from "events";
1054
1231
  var nextSocketId = 0, DaemonClient;
@@ -1060,7 +1237,8 @@ var init_daemon_client = __esm(() => {
1060
1237
  ws = null;
1061
1238
  wsId = 0;
1062
1239
  nextRequestId = 1;
1063
- pendingReplies = new Map;
1240
+ pendingReplies = new PendingRequestRegistry;
1241
+ pendingEventWaiters = new PendingRequestRegistry;
1064
1242
  constructor(url, options = {}) {
1065
1243
  super();
1066
1244
  this.url = url;
@@ -1106,82 +1284,73 @@ var init_daemon_client = __esm(() => {
1106
1284
  });
1107
1285
  }
1108
1286
  attachClaude() {
1287
+ const identity = this.resolveIdentity();
1109
1288
  this.send({
1110
1289
  type: "claude_connect",
1111
- ...this.options.identity ? { identity: this.options.identity } : {}
1290
+ ...identity ? { identity } : {}
1112
1291
  });
1113
1292
  }
1293
+ resolveIdentity() {
1294
+ const opt = this.options.identity;
1295
+ return typeof opt === "function" ? opt() : opt;
1296
+ }
1114
1297
  async attachClaudeAndWaitForStatus(timeoutMs = 1000) {
1115
1298
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
1116
1299
  return null;
1117
1300
  }
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
- }
1301
+ return this.awaitTypedResponse({
1302
+ key: "status",
1303
+ successEvent: "status",
1304
+ successValue: (status) => status,
1305
+ failValue: null,
1306
+ timeoutMs,
1307
+ send: () => this.attachClaude()
1151
1308
  });
1152
1309
  }
1153
1310
  async probeIncumbent(timeoutMs = 3000) {
1154
1311
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
1155
1312
  return { connected: false, alive: false };
1156
1313
  }
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
- }
1314
+ return this.awaitTypedResponse({
1315
+ key: "incumbent_status",
1316
+ successEvent: "incumbentStatus",
1317
+ successValue: (s) => s,
1318
+ failValue: { connected: false, alive: false },
1319
+ timeoutMs,
1320
+ send: () => this.send({ type: "probe_incumbent" })
1183
1321
  });
1184
1322
  }
1323
+ awaitTypedResponse(opts) {
1324
+ const { key, successEvent, successValue, failValue, timeoutMs, send } = opts;
1325
+ const onSuccess = (payload) => {
1326
+ this.pendingEventWaiters.settle(key, successValue(payload));
1327
+ };
1328
+ const onRejected = () => {
1329
+ this.pendingEventWaiters.settle(key, failValue);
1330
+ };
1331
+ const onDisconnect = () => {
1332
+ this.pendingEventWaiters.settle(key, failValue);
1333
+ };
1334
+ const pending = this.pendingEventWaiters.register(key, {
1335
+ timeoutMs,
1336
+ onTimeout: ({ resolve: resolve3 }) => resolve3(failValue)
1337
+ });
1338
+ const cleanup = () => {
1339
+ this.off(successEvent, onSuccess);
1340
+ this.off("rejected", onRejected);
1341
+ this.off("disconnect", onDisconnect);
1342
+ };
1343
+ pending.finally(cleanup);
1344
+ this.on(successEvent, onSuccess);
1345
+ this.on("rejected", onRejected);
1346
+ this.on("disconnect", onDisconnect);
1347
+ try {
1348
+ send();
1349
+ } catch {
1350
+ this.pendingEventWaiters.settle(key, failValue);
1351
+ }
1352
+ return pending;
1353
+ }
1185
1354
  async disconnect() {
1186
1355
  if (!this.ws)
1187
1356
  return;
@@ -1199,21 +1368,19 @@ var init_daemon_client = __esm(() => {
1199
1368
  return { success: false, error: "AgentBridge daemon is not connected." };
1200
1369
  }
1201
1370
  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
- });
1371
+ const pending = this.pendingReplies.register(requestId, {
1372
+ timeoutMs: CLIENT_REPLY_TIMEOUT_MS,
1373
+ onTimeout: ({ resolve: resolve3 }) => resolve3({ success: false, error: "Timed out waiting for AgentBridge daemon reply." })
1374
+ });
1375
+ this.send({
1376
+ type: "claude_to_codex",
1377
+ requestId,
1378
+ message,
1379
+ ...requireReply ? { requireReply: true } : {},
1380
+ ...onBusy && onBusy !== "reject" ? { onBusy } : {},
1381
+ ...idempotencyKey ? { idempotencyKey } : {}
1216
1382
  });
1383
+ return pending;
1217
1384
  }
1218
1385
  attachSocketHandlers(ws, socketId) {
1219
1386
  ws.onmessage = (event) => {
@@ -1229,12 +1396,7 @@ var init_daemon_client = __esm(() => {
1229
1396
  this.emit("codexMessage", message.message);
1230
1397
  return;
1231
1398
  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({
1399
+ this.pendingReplies.settle(message.requestId, {
1238
1400
  success: message.success,
1239
1401
  error: message.error,
1240
1402
  ...message.code !== undefined ? { code: message.code } : {},
@@ -1265,7 +1427,7 @@ var init_daemon_client = __esm(() => {
1265
1427
  if (isCurrent) {
1266
1428
  this.ws = null;
1267
1429
  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) {
1430
+ 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
1431
  this.emit("rejected", event.code);
1270
1432
  } else {
1271
1433
  this.emit("disconnect");
@@ -1275,11 +1437,7 @@ var init_daemon_client = __esm(() => {
1275
1437
  ws.onerror = () => {};
1276
1438
  }
1277
1439
  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
- }
1440
+ this.pendingReplies.settleAll(() => ({ success: false, error }));
1283
1441
  }
1284
1442
  send(message) {
1285
1443
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
@@ -1298,6 +1456,10 @@ var init_daemon_client = __esm(() => {
1298
1456
  var CONTRACT_VERSION = 1;
1299
1457
 
1300
1458
  // src/build-info.ts
1459
+ function hasValidCodeHash(build) {
1460
+ const hash = build?.codeHash;
1461
+ return typeof hash === "string" && hash.length > 0 && hash !== CODE_HASH_SENTINEL;
1462
+ }
1301
1463
  function defineString(value, fallback) {
1302
1464
  return typeof value === "string" && value.length > 0 ? value : fallback;
1303
1465
  }
@@ -1312,7 +1474,14 @@ function defineNumber(value, fallback) {
1312
1474
  function sameRuntimeContract(a, b) {
1313
1475
  if (!a || !b)
1314
1476
  return false;
1315
- return a.version === b.version && a.commit === b.commit && a.contractVersion === b.contractVersion;
1477
+ if (a.version !== b.version || a.contractVersion !== b.contractVersion)
1478
+ return false;
1479
+ if (hasValidCodeHash(a) && hasValidCodeHash(b))
1480
+ return a.codeHash === b.codeHash;
1481
+ return a.commit === b.commit;
1482
+ }
1483
+ function runtimeContractComparisonBasis(a, b) {
1484
+ return hasValidCodeHash(a) && hasValidCodeHash(b) ? "codeHash" : "commit";
1316
1485
  }
1317
1486
  function compatibleContractVersion(a, b) {
1318
1487
  if (!a || !b)
@@ -1322,21 +1491,23 @@ function compatibleContractVersion(a, b) {
1322
1491
  function formatBuildInfo(build) {
1323
1492
  if (!build)
1324
1493
  return "<unknown>";
1325
- return `${build.version}/${build.commit}/${build.bundle}/contract-v${build.contractVersion}`;
1494
+ const codeHash = hasValidCodeHash(build) ? `/code-${build.codeHash}` : "";
1495
+ return `${build.version}/${build.commit}/${build.bundle}/contract-v${build.contractVersion}${codeHash}`;
1326
1496
  }
1327
- var BUILD_INFO;
1497
+ var CODE_HASH_SENTINEL = "source", BUILD_INFO;
1328
1498
  var init_build_info = __esm(() => {
1329
1499
  BUILD_INFO = Object.freeze({
1330
- version: defineString("0.1.12", "0.0.0-source"),
1331
- commit: defineString("eec6018", "source"),
1500
+ version: defineString("0.1.14", "0.0.0-source"),
1501
+ commit: defineString("f5a9562", "source"),
1332
1502
  bundle: defineBundle("dist"),
1333
- contractVersion: defineNumber(1, CONTRACT_VERSION)
1503
+ contractVersion: defineNumber(1, CONTRACT_VERSION),
1504
+ codeHash: defineString("e05d18c3cc72", "source")
1334
1505
  });
1335
1506
  });
1336
1507
 
1337
1508
  // src/process-lifecycle.ts
1338
1509
  import { execFileSync as execFileSync4 } from "child_process";
1339
- import { basename } from "path";
1510
+ import { basename as basename2 } from "path";
1340
1511
  function parsePsProcessList(output) {
1341
1512
  const entries = [];
1342
1513
  for (const line of output.split(/\r?\n/)) {
@@ -1352,11 +1523,11 @@ function parsePsProcessList(output) {
1352
1523
  }
1353
1524
  function invokesCodexBinary(command) {
1354
1525
  const tokens = command.trim().split(/\s+/);
1355
- const exe = tokens[0] ? basename(tokens[0]) : "";
1526
+ const exe = tokens[0] ? basename2(tokens[0]) : "";
1356
1527
  if (exe === "codex")
1357
1528
  return true;
1358
1529
  if ((exe === "node" || exe === "bun") && tokens[1]) {
1359
- return basename(tokens[1]) === "codex";
1530
+ return basename2(tokens[1]) === "codex";
1360
1531
  }
1361
1532
  return false;
1362
1533
  }
@@ -1492,9 +1663,142 @@ var init_process_lifecycle = __esm(() => {
1492
1663
  isProcessAlive = pidLooksAlive;
1493
1664
  });
1494
1665
 
1666
+ // src/daemon-record.ts
1667
+ import { readFileSync as readFileSync4 } from "fs";
1668
+ function writeDaemonRecord(path, record) {
1669
+ atomicWriteJson(path, record);
1670
+ }
1671
+ function sanitizePorts(value) {
1672
+ if (typeof value !== "object" || value === null)
1673
+ return;
1674
+ const raw = value;
1675
+ const ports = {};
1676
+ if (typeof raw.appPort === "number")
1677
+ ports.appPort = raw.appPort;
1678
+ if (typeof raw.proxyPort === "number")
1679
+ ports.proxyPort = raw.proxyPort;
1680
+ if (typeof raw.controlPort === "number")
1681
+ ports.controlPort = raw.controlPort;
1682
+ return Object.keys(ports).length > 0 ? ports : undefined;
1683
+ }
1684
+ function readDaemonRecord(path, read = defaultRead) {
1685
+ let parsed;
1686
+ try {
1687
+ parsed = JSON.parse(read(path));
1688
+ } catch {
1689
+ return null;
1690
+ }
1691
+ if (typeof parsed !== "object" || parsed === null)
1692
+ return null;
1693
+ const obj = parsed;
1694
+ if (typeof obj.pid !== "number" || !Number.isFinite(obj.pid))
1695
+ return null;
1696
+ const phase = obj.phase === "ready" ? "ready" : "booting";
1697
+ const record = { pid: obj.pid, phase };
1698
+ if (typeof obj.startedAt === "number")
1699
+ record.startedAt = obj.startedAt;
1700
+ if (typeof obj.nonce === "string")
1701
+ record.nonce = obj.nonce;
1702
+ if (obj.pairId === null || typeof obj.pairId === "string")
1703
+ record.pairId = obj.pairId;
1704
+ if (obj.cwd === null || typeof obj.cwd === "string")
1705
+ record.cwd = obj.cwd;
1706
+ if (obj.stateDir === null || typeof obj.stateDir === "string")
1707
+ record.stateDir = obj.stateDir;
1708
+ if (typeof obj.proxyUrl === "string")
1709
+ record.proxyUrl = obj.proxyUrl;
1710
+ if (typeof obj.appServerUrl === "string")
1711
+ record.appServerUrl = obj.appServerUrl;
1712
+ const ports = sanitizePorts(obj.ports);
1713
+ if (ports !== undefined)
1714
+ record.ports = ports;
1715
+ if (typeof obj.build === "object" && obj.build !== null) {
1716
+ record.build = obj.build;
1717
+ }
1718
+ if (typeof obj.turnPhase === "string")
1719
+ record.turnPhase = obj.turnPhase;
1720
+ if (typeof obj.turnInProgress === "boolean")
1721
+ record.turnInProgress = obj.turnInProgress;
1722
+ if (typeof obj.attentionWindowActive === "boolean") {
1723
+ record.attentionWindowActive = obj.attentionWindowActive;
1724
+ }
1725
+ return record;
1726
+ }
1727
+ function synthesizeLegacyRecord(pidFilePath, statusFilePath, read = defaultRead) {
1728
+ let pidFromPidFile = null;
1729
+ try {
1730
+ const raw = read(pidFilePath).trim();
1731
+ const n = Number.parseInt(raw, 10);
1732
+ if (Number.isFinite(n))
1733
+ pidFromPidFile = n;
1734
+ } catch {}
1735
+ let status = null;
1736
+ try {
1737
+ const parsed = JSON.parse(read(statusFilePath));
1738
+ if (typeof parsed === "object" && parsed !== null)
1739
+ status = parsed;
1740
+ } catch {}
1741
+ const pidFromStatus = status && typeof status.pid === "number" && Number.isFinite(status.pid) ? status.pid : null;
1742
+ const pid = pidFromPidFile ?? pidFromStatus;
1743
+ if (pid === null)
1744
+ return null;
1745
+ const record = {
1746
+ pid,
1747
+ phase: status ? "ready" : "booting"
1748
+ };
1749
+ if (status) {
1750
+ if (typeof status.proxyUrl === "string")
1751
+ record.proxyUrl = status.proxyUrl;
1752
+ if (typeof status.appServerUrl === "string")
1753
+ record.appServerUrl = status.appServerUrl;
1754
+ const controlPort = typeof status.controlPort === "number" ? status.controlPort : undefined;
1755
+ const proxyPort = portFromUrl(status.proxyUrl);
1756
+ const appPort = portFromUrl(status.appServerUrl);
1757
+ if (controlPort !== undefined || proxyPort !== undefined || appPort !== undefined) {
1758
+ record.ports = {};
1759
+ if (appPort !== undefined)
1760
+ record.ports.appPort = appPort;
1761
+ if (proxyPort !== undefined)
1762
+ record.ports.proxyPort = proxyPort;
1763
+ if (controlPort !== undefined)
1764
+ record.ports.controlPort = controlPort;
1765
+ }
1766
+ if (status.pairId === null || typeof status.pairId === "string")
1767
+ record.pairId = status.pairId;
1768
+ if (status.cwd === null || typeof status.cwd === "string")
1769
+ record.cwd = status.cwd;
1770
+ if (status.stateDir === null || typeof status.stateDir === "string")
1771
+ record.stateDir = status.stateDir;
1772
+ if (typeof status.build === "object" && status.build !== null) {
1773
+ record.build = status.build;
1774
+ }
1775
+ if (typeof status.turnPhase === "string")
1776
+ record.turnPhase = status.turnPhase;
1777
+ if (typeof status.turnInProgress === "boolean")
1778
+ record.turnInProgress = status.turnInProgress;
1779
+ if (typeof status.attentionWindowActive === "boolean") {
1780
+ record.attentionWindowActive = status.attentionWindowActive;
1781
+ }
1782
+ }
1783
+ return record;
1784
+ }
1785
+ function readUnifiedDaemonRecord(paths, read = defaultRead) {
1786
+ return readDaemonRecord(paths.daemonRecordFile, read) ?? synthesizeLegacyRecord(paths.pidFile, paths.statusFile, read);
1787
+ }
1788
+ function portFromUrl(url) {
1789
+ if (typeof url !== "string")
1790
+ return;
1791
+ const match = url.match(/:(\d+)(?:[/?]|$)/);
1792
+ return match ? Number.parseInt(match[1], 10) : undefined;
1793
+ }
1794
+ var defaultRead = (path) => readFileSync4(path, "utf-8");
1795
+ var init_daemon_record = __esm(() => {
1796
+ init_atomic_json();
1797
+ });
1798
+
1495
1799
  // src/daemon-lifecycle.ts
1496
1800
  import { spawn } from "child_process";
1497
- import { existsSync as existsSync6, readFileSync as readFileSync4, statSync, unlinkSync, writeFileSync as writeFileSync4, openSync, closeSync, constants } from "fs";
1801
+ import { existsSync as existsSync6, readFileSync as readFileSync5, statSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3, openSync as openSync2, closeSync as closeSync2, constants } from "fs";
1498
1802
  import { fileURLToPath } from "url";
1499
1803
  function isReuseVerdict(verdict) {
1500
1804
  return verdict === "reuse" || verdict === "reuse-despite-drift";
@@ -1531,22 +1835,33 @@ function classifyDaemon(expectedPairId, status, buildInfo) {
1531
1835
  reason: "runtime build drift has a compatible contract and a live Codex TUI is attached"
1532
1836
  };
1533
1837
  }
1838
+ const basis = runtimeContractComparisonBasis(status.build, buildInfo) === "codeHash" ? "compared by codeHash" : "compared by commit stamp; legacy build without codeHash";
1534
1839
  return {
1535
1840
  verdict: "replace-drifted",
1536
- reason: `runtime build ${formatBuildInfo(status.build)} does not match launcher ${formatBuildInfo(buildInfo)}`
1841
+ reason: `runtime build ${formatBuildInfo(status.build)} does not match launcher ` + `${formatBuildInfo(buildInfo)} (${basis})`
1537
1842
  };
1538
1843
  }
1539
1844
  return { verdict: "reuse", reason: "daemon pair and runtime contract match" };
1540
1845
  }
1846
+ function resolveTiming(timing) {
1847
+ return {
1848
+ reuseReadyRetries: timing?.reuseReadyRetries ?? REUSE_READY_RETRIES,
1849
+ reuseReadyDelayMs: timing?.reuseReadyDelayMs ?? REUSE_READY_DELAY_MS,
1850
+ waitReadyRetries: timing?.waitReadyRetries ?? WAIT_READY_RETRIES,
1851
+ waitReadyDelayMs: timing?.waitReadyDelayMs ?? WAIT_READY_DELAY_MS
1852
+ };
1853
+ }
1541
1854
 
1542
1855
  class DaemonLifecycle {
1543
1856
  stateDir;
1544
1857
  controlPort;
1545
1858
  log;
1859
+ timing;
1546
1860
  constructor(opts) {
1547
1861
  this.stateDir = opts.stateDir;
1548
1862
  this.controlPort = opts.controlPort;
1549
1863
  this.log = opts.log;
1864
+ this.timing = resolveTiming(opts.timing);
1550
1865
  }
1551
1866
  get healthUrl() {
1552
1867
  return `http://127.0.0.1:${this.controlPort}/healthz`;
@@ -1603,7 +1918,7 @@ class DaemonLifecycle {
1603
1918
  break;
1604
1919
  }
1605
1920
  try {
1606
- await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
1921
+ await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
1607
1922
  return;
1608
1923
  } catch {
1609
1924
  this.log(`Daemon on control port ${this.controlPort} is healthy but not ready within reuse window \u2014 replacing`);
@@ -1616,7 +1931,7 @@ class DaemonLifecycle {
1616
1931
  if (isProcessAlive(existingPid)) {
1617
1932
  if (isAgentBridgeDaemon(existingPid)) {
1618
1933
  try {
1619
- await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
1934
+ await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
1620
1935
  return;
1621
1936
  } catch {
1622
1937
  this.log(`Existing daemon process ${existingPid} never became ready \u2014 replacing`);
@@ -1644,7 +1959,7 @@ class DaemonLifecycle {
1644
1959
  await this.kill(3000, status?.pid);
1645
1960
  } else {
1646
1961
  try {
1647
- await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
1962
+ await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
1648
1963
  return;
1649
1964
  } catch {
1650
1965
  this.log(`Daemon on control port ${this.controlPort} is healthy but not ready under startup lock \u2014 replacing`);
@@ -1653,7 +1968,7 @@ class DaemonLifecycle {
1653
1968
  }
1654
1969
  }
1655
1970
  this.launch();
1656
- await this.waitForReady();
1971
+ await this.waitForReady(this.timing.waitReadyRetries, this.timing.waitReadyDelayMs);
1657
1972
  });
1658
1973
  }
1659
1974
  async isHealthy() {
@@ -1680,7 +1995,7 @@ class DaemonLifecycle {
1680
1995
  return false;
1681
1996
  }
1682
1997
  }
1683
- async waitForReady(maxRetries = 40, delayMs = 250) {
1998
+ async waitForReady(maxRetries = WAIT_READY_RETRIES, delayMs = WAIT_READY_DELAY_MS) {
1684
1999
  for (let attempt = 0;attempt < maxRetries; attempt++) {
1685
2000
  if (await this.isReady())
1686
2001
  return;
@@ -1688,7 +2003,7 @@ class DaemonLifecycle {
1688
2003
  }
1689
2004
  throw new Error(`Timed out waiting for AgentBridge daemon readiness on ${this.readyUrl}`);
1690
2005
  }
1691
- async waitForReadyAndOurs(maxRetries = 40, delayMs = 250) {
2006
+ async waitForReadyAndOurs(maxRetries = WAIT_READY_RETRIES, delayMs = WAIT_READY_DELAY_MS) {
1692
2007
  for (let attempt = 0;attempt < maxRetries; attempt++) {
1693
2008
  if (await this.isReady()) {
1694
2009
  const status = await this.fetchStatus();
@@ -1704,22 +2019,35 @@ class DaemonLifecycle {
1704
2019
  }
1705
2020
  throw new Error(`Timed out waiting for AgentBridge daemon readiness+identity on ${this.readyUrl} (control port ${this.controlPort})`);
1706
2021
  }
2022
+ readDaemonRecord() {
2023
+ return readUnifiedDaemonRecord({
2024
+ daemonRecordFile: this.stateDir.daemonRecordFile,
2025
+ pidFile: this.stateDir.pidFile,
2026
+ statusFile: this.stateDir.statusFile
2027
+ });
2028
+ }
2029
+ writeDaemonRecord(record) {
2030
+ writeDaemonRecord(this.stateDir.daemonRecordFile, record);
2031
+ }
2032
+ removeDaemonRecord() {
2033
+ try {
2034
+ unlinkSync2(this.stateDir.daemonRecordFile);
2035
+ } catch {}
2036
+ }
1707
2037
  readStatus() {
1708
2038
  try {
1709
- const raw = readFileSync4(this.stateDir.statusFile, "utf-8");
2039
+ const raw = readFileSync5(this.stateDir.statusFile, "utf-8");
1710
2040
  return JSON.parse(raw);
1711
2041
  } catch {
1712
2042
  return null;
1713
2043
  }
1714
2044
  }
1715
2045
  writeStatus(status) {
1716
- this.stateDir.ensure();
1717
- writeFileSync4(this.stateDir.statusFile, JSON.stringify(status, null, 2) + `
1718
- `, "utf-8");
2046
+ atomicWriteJson(this.stateDir.statusFile, status);
1719
2047
  }
1720
2048
  readPid() {
1721
2049
  try {
1722
- const raw = readFileSync4(this.stateDir.pidFile, "utf-8").trim();
2050
+ const raw = readFileSync5(this.stateDir.pidFile, "utf-8").trim();
1723
2051
  if (!raw)
1724
2052
  return null;
1725
2053
  const pid = Number.parseInt(raw, 10);
@@ -1729,28 +2057,27 @@ class DaemonLifecycle {
1729
2057
  }
1730
2058
  }
1731
2059
  writePid(pid) {
1732
- this.stateDir.ensure();
1733
- writeFileSync4(this.stateDir.pidFile, `${pid ?? process.pid}
1734
- `, "utf-8");
2060
+ atomicWriteText(this.stateDir.pidFile, `${pid ?? process.pid}
2061
+ `);
1735
2062
  }
1736
2063
  removePidFile() {
1737
2064
  try {
1738
- unlinkSync(this.stateDir.pidFile);
2065
+ unlinkSync2(this.stateDir.pidFile);
1739
2066
  } catch {}
1740
2067
  }
1741
2068
  removeStatusFile() {
1742
2069
  try {
1743
- unlinkSync(this.stateDir.statusFile);
2070
+ unlinkSync2(this.stateDir.statusFile);
1744
2071
  } catch {}
1745
2072
  }
1746
2073
  markKilled() {
1747
2074
  this.stateDir.ensure();
1748
- writeFileSync4(this.stateDir.killedFile, `${Date.now()}
2075
+ writeFileSync3(this.stateDir.killedFile, `${Date.now()}
1749
2076
  `, "utf-8");
1750
2077
  }
1751
2078
  clearKilled() {
1752
2079
  try {
1753
- unlinkSync(this.stateDir.killedFile);
2080
+ unlinkSync2(this.stateDir.killedFile);
1754
2081
  } catch {}
1755
2082
  }
1756
2083
  wasKilled() {
@@ -1772,8 +2099,10 @@ class DaemonLifecycle {
1772
2099
  daemonProc.unref();
1773
2100
  }
1774
2101
  removeStalePidFile() {
1775
- this.log("Removing stale pid file");
2102
+ this.log("Removing stale daemon identity files");
1776
2103
  this.removePidFile();
2104
+ this.removeStatusFile();
2105
+ this.removeDaemonRecord();
1777
2106
  }
1778
2107
  async replaceUnhealthyDaemon(statusPid) {
1779
2108
  await this.withStartupLockStrict(async (locked) => {
@@ -1789,7 +2118,7 @@ class DaemonLifecycle {
1789
2118
  }
1790
2119
  if (isReuseVerdict(classification.verdict)) {
1791
2120
  try {
1792
- await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
2121
+ await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
1793
2122
  return;
1794
2123
  } catch {}
1795
2124
  }
@@ -1797,12 +2126,12 @@ class DaemonLifecycle {
1797
2126
  this.log(`Killing unhealthy daemon on control port ${this.controlPort} and relaunching`);
1798
2127
  await this.kill(3000, statusPid);
1799
2128
  this.launch();
1800
- await this.waitForReady();
2129
+ await this.waitForReady(this.timing.waitReadyRetries, this.timing.waitReadyDelayMs);
1801
2130
  });
1802
2131
  }
1803
2132
  async waitForContendedStartupLock() {
1804
2133
  this.log("Another process holds the startup lock, waiting for readiness+identity...");
1805
- await this.waitForReadyAndOurs();
2134
+ await this.waitForReadyAndOurs(this.timing.waitReadyRetries, this.timing.waitReadyDelayMs);
1806
2135
  }
1807
2136
  async withStartupLockStrict(fn) {
1808
2137
  const locked = this.acquireLockStrict();
@@ -1817,15 +2146,15 @@ class DaemonLifecycle {
1817
2146
  this.stateDir.ensure();
1818
2147
  let fd = null;
1819
2148
  try {
1820
- fd = openSync(this.stateDir.lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
1821
- writeFileSync4(fd, `${process.pid}
2149
+ fd = openSync2(this.stateDir.lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
2150
+ writeFileSync3(fd, `${process.pid}
1822
2151
  `);
1823
- closeSync(fd);
2152
+ closeSync2(fd);
1824
2153
  return true;
1825
2154
  } catch (err) {
1826
2155
  if (fd !== null && err.code !== "EEXIST") {
1827
2156
  try {
1828
- closeSync(fd);
2157
+ closeSync2(fd);
1829
2158
  } catch {}
1830
2159
  this.releaseLock();
1831
2160
  }
@@ -1833,7 +2162,7 @@ class DaemonLifecycle {
1833
2162
  if (reclaimed)
1834
2163
  return false;
1835
2164
  try {
1836
- const holderPid = Number.parseInt(readFileSync4(this.stateDir.lockFile, "utf-8").trim(), 10);
2165
+ const holderPid = Number.parseInt(readFileSync5(this.stateDir.lockFile, "utf-8").trim(), 10);
1837
2166
  if (Number.isFinite(holderPid) && !isProcessAlive(holderPid)) {
1838
2167
  this.log(`Stale startup lock from dead process ${holderPid}, reclaiming`);
1839
2168
  this.releaseLock();
@@ -1862,7 +2191,7 @@ class DaemonLifecycle {
1862
2191
  }
1863
2192
  releaseLock() {
1864
2193
  try {
1865
- unlinkSync(this.stateDir.lockFile);
2194
+ unlinkSync2(this.stateDir.lockFile);
1866
2195
  } catch {}
1867
2196
  }
1868
2197
  async kill(gracefulTimeoutMs = 3000, pidOverride) {
@@ -1908,6 +2237,7 @@ class DaemonLifecycle {
1908
2237
  cleanup() {
1909
2238
  this.removePidFile();
1910
2239
  this.removeStatusFile();
2240
+ this.removeDaemonRecord();
1911
2241
  }
1912
2242
  }
1913
2243
  async function fetchWithTimeout(url, timeoutMs = HEALTH_FETCH_TIMEOUT_MS) {
@@ -1919,10 +2249,12 @@ async function fetchWithTimeout(url, timeoutMs = HEALTH_FETCH_TIMEOUT_MS) {
1919
2249
  clearTimeout(timer);
1920
2250
  }
1921
2251
  }
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;
2252
+ 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
2253
  var init_daemon_lifecycle = __esm(() => {
2254
+ init_atomic_json();
1924
2255
  init_build_info();
1925
2256
  init_process_lifecycle();
2257
+ init_daemon_record();
1926
2258
  DEFAULT_DAEMON_ENTRY = import.meta.url.endsWith(".ts") ? "./daemon.ts" : "./daemon.js";
1927
2259
  DAEMON_ENTRY = process.env.AGENTBRIDGE_DAEMON_ENTRY || DEFAULT_DAEMON_ENTRY;
1928
2260
  DAEMON_PATH = fileURLToPath(new URL(DAEMON_ENTRY, import.meta.url));
@@ -1933,26 +2265,22 @@ var init_daemon_lifecycle = __esm(() => {
1933
2265
  // src/pair-registry.ts
1934
2266
  import { execFileSync as execFileSync5 } from "child_process";
1935
2267
  import {
1936
- closeSync as closeSync2,
1937
2268
  existsSync as existsSync7,
1938
- fsyncSync,
1939
2269
  linkSync,
1940
2270
  lstatSync,
1941
- mkdirSync as mkdirSync3,
1942
- openSync as openSync2,
2271
+ mkdirSync as mkdirSync4,
1943
2272
  readdirSync,
1944
- readFileSync as readFileSync5,
2273
+ readFileSync as readFileSync6,
1945
2274
  realpathSync,
1946
- renameSync,
1947
2275
  rmSync as rmSync2,
1948
2276
  statSync as statSync2,
1949
- unlinkSync as unlinkSync2,
1950
- writeFileSync as writeFileSync5
2277
+ unlinkSync as unlinkSync3,
2278
+ writeFileSync as writeFileSync4
1951
2279
  } from "fs";
1952
2280
  import { createServer } from "net";
1953
- import { createHash, randomUUID } from "crypto";
2281
+ import { createHash, randomUUID as randomUUID2 } from "crypto";
1954
2282
  import { hostname, userInfo } from "os";
1955
- import { basename as basename2, join as join6, resolve as resolve3, sep } from "path";
2283
+ import { basename as basename3, join as join6, resolve as resolve3, sep } from "path";
1956
2284
  function portsForSlot(slot) {
1957
2285
  if (!Number.isInteger(slot) || slot < 0) {
1958
2286
  throw new PairError("PAIR_ID_INVALID", `Invalid slot: ${slot}`);
@@ -2001,7 +2329,7 @@ function readRegistry(base) {
2001
2329
  return { version: 1, pairs: [] };
2002
2330
  let parsed;
2003
2331
  try {
2004
- parsed = JSON.parse(readFileSync5(path, "utf-8"));
2332
+ parsed = JSON.parse(readFileSync6(path, "utf-8"));
2005
2333
  } catch (err) {
2006
2334
  throw new PairError("PAIR_REGISTRY_CORRUPT", `Registry JSON is not parseable at ${path}: ${err.message}`, {
2007
2335
  path
@@ -2032,26 +2360,14 @@ function readRegistry(base) {
2032
2360
  return parsed;
2033
2361
  }
2034
2362
  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);
2363
+ atomicWriteJson(registryPath(base), reg, { fsync: true });
2048
2364
  }
2049
2365
  function lockFilePath(base) {
2050
2366
  return join6(pairsDir(base), LOCK_FILE_NAME);
2051
2367
  }
2052
2368
  function readLockOwner(lockFile) {
2053
2369
  try {
2054
- const parsed = JSON.parse(readFileSync5(lockFile, "utf-8"));
2370
+ const parsed = JSON.parse(readFileSync6(lockFile, "utf-8"));
2055
2371
  if (typeof parsed.pid === "number" && typeof parsed.nonce === "string")
2056
2372
  return parsed;
2057
2373
  return null;
@@ -2091,7 +2407,7 @@ function lockIsStale(lockFile) {
2091
2407
  }
2092
2408
  function attemptReclaim(lockFile) {
2093
2409
  const reclaimLock = `${lockFile}.reclaim`;
2094
- const myNonce = randomUUID();
2410
+ const myNonce = randomUUID2();
2095
2411
  const ownerJson = JSON.stringify({
2096
2412
  pid: process.pid,
2097
2413
  createdAt: Date.now(),
@@ -2099,10 +2415,10 @@ function attemptReclaim(lockFile) {
2099
2415
  hostname: safeHostname(),
2100
2416
  uid: safeUid()
2101
2417
  });
2102
- const tmp = `${reclaimLock}.acq.${process.pid}.${randomUUID()}`;
2418
+ const tmp = `${reclaimLock}.acq.${process.pid}.${randomUUID2()}`;
2103
2419
  let held = false;
2104
2420
  try {
2105
- writeFileSync5(tmp, ownerJson);
2421
+ writeFileSync4(tmp, ownerJson);
2106
2422
  try {
2107
2423
  linkSync(tmp, reclaimLock);
2108
2424
  held = true;
@@ -2110,7 +2426,7 @@ function attemptReclaim(lockFile) {
2110
2426
  if (err?.code === "EEXIST") {
2111
2427
  if (lockIsStale(reclaimLock)) {
2112
2428
  try {
2113
- unlinkSync2(reclaimLock);
2429
+ unlinkSync3(reclaimLock);
2114
2430
  } catch {}
2115
2431
  }
2116
2432
  return;
@@ -2119,7 +2435,7 @@ function attemptReclaim(lockFile) {
2119
2435
  }
2120
2436
  } finally {
2121
2437
  try {
2122
- unlinkSync2(tmp);
2438
+ unlinkSync3(tmp);
2123
2439
  } catch {}
2124
2440
  }
2125
2441
  if (!held)
@@ -2129,22 +2445,22 @@ function attemptReclaim(lockFile) {
2129
2445
  return;
2130
2446
  if (lockIsStale(lockFile)) {
2131
2447
  try {
2132
- unlinkSync2(lockFile);
2448
+ unlinkSync3(lockFile);
2133
2449
  } catch {}
2134
2450
  }
2135
2451
  } finally {
2136
2452
  if (readLockOwner(reclaimLock)?.nonce === myNonce) {
2137
2453
  try {
2138
- unlinkSync2(reclaimLock);
2454
+ unlinkSync3(reclaimLock);
2139
2455
  } catch {}
2140
2456
  }
2141
2457
  }
2142
2458
  }
2143
2459
  async function withRegistryLock(base, fn) {
2144
- mkdirSync3(pairsDir(base), { recursive: true });
2460
+ mkdirSync4(pairsDir(base), { recursive: true });
2145
2461
  const lockFile = lockFilePath(base);
2146
2462
  const deadline = Date.now() + LOCK_DEADLINE_MS;
2147
- const myNonce = randomUUID();
2463
+ const myNonce = randomUUID2();
2148
2464
  const ownerJson = JSON.stringify({
2149
2465
  pid: process.pid,
2150
2466
  createdAt: Date.now(),
@@ -2153,10 +2469,10 @@ async function withRegistryLock(base, fn) {
2153
2469
  uid: safeUid()
2154
2470
  });
2155
2471
  for (;; ) {
2156
- const tmp = `${lockFile}.acq.${process.pid}.${randomUUID()}`;
2472
+ const tmp = `${lockFile}.acq.${process.pid}.${randomUUID2()}`;
2157
2473
  let acquired = false;
2158
2474
  try {
2159
- writeFileSync5(tmp, ownerJson);
2475
+ writeFileSync4(tmp, ownerJson);
2160
2476
  try {
2161
2477
  linkSync(tmp, lockFile);
2162
2478
  acquired = true;
@@ -2166,7 +2482,7 @@ async function withRegistryLock(base, fn) {
2166
2482
  }
2167
2483
  } finally {
2168
2484
  try {
2169
- unlinkSync2(tmp);
2485
+ unlinkSync3(tmp);
2170
2486
  } catch {}
2171
2487
  }
2172
2488
  if (acquired) {
@@ -2176,7 +2492,7 @@ async function withRegistryLock(base, fn) {
2176
2492
  const current = readLockOwner(lockFile);
2177
2493
  if (!current || current.nonce === myNonce) {
2178
2494
  try {
2179
- unlinkSync2(lockFile);
2495
+ unlinkSync3(lockFile);
2180
2496
  } catch {}
2181
2497
  }
2182
2498
  }
@@ -2198,7 +2514,7 @@ function detectLegacyRootDaemon(base) {
2198
2514
  return null;
2199
2515
  let pid;
2200
2516
  try {
2201
- const raw = readFileSync5(rootPidFile, "utf-8").trim();
2517
+ const raw = readFileSync6(rootPidFile, "utf-8").trim();
2202
2518
  pid = Number.parseInt(raw, 10);
2203
2519
  } catch {
2204
2520
  return null;
@@ -2306,6 +2622,9 @@ async function removeAllocatedPairIfUnchanged(base, pairId, slot) {
2306
2622
  writeRegistry(base, { version: 1, pairs: nextPairs });
2307
2623
  });
2308
2624
  }
2625
+ function pairsRootDir(base) {
2626
+ return pairsDir(base);
2627
+ }
2309
2628
  function pairDirPath(base, pairId) {
2310
2629
  const id = validatePairId(pairId);
2311
2630
  return join6(pairsDir(base), id);
@@ -2348,12 +2667,17 @@ function pairDirDaemonAlive(base, pairId) {
2348
2667
  const dir = join6(pairsDir(base), pairId);
2349
2668
  const pids = [];
2350
2669
  try {
2351
- const pid = Number.parseInt(readFileSync5(join6(dir, "daemon.pid"), "utf-8").trim(), 10);
2670
+ const record = JSON.parse(readFileSync6(join6(dir, "daemon.json"), "utf-8"));
2671
+ if (typeof record?.pid === "number" && Number.isFinite(record.pid))
2672
+ pids.push(record.pid);
2673
+ } catch {}
2674
+ try {
2675
+ const pid = Number.parseInt(readFileSync6(join6(dir, "daemon.pid"), "utf-8").trim(), 10);
2352
2676
  if (Number.isFinite(pid))
2353
2677
  pids.push(pid);
2354
2678
  } catch {}
2355
2679
  try {
2356
- const status = JSON.parse(readFileSync5(join6(dir, "status.json"), "utf-8"));
2680
+ const status = JSON.parse(readFileSync6(join6(dir, "status.json"), "utf-8"));
2357
2681
  if (typeof status?.pid === "number")
2358
2682
  pids.push(status.pid);
2359
2683
  } catch {}
@@ -2387,10 +2711,54 @@ async function removeUnregisteredPairDir(base, pairId) {
2387
2711
  return { removed: removePairDir(base, pairId) };
2388
2712
  });
2389
2713
  }
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;
2714
+ async function removeOrphanPairDirIgnoringRegistry(base, pairId) {
2715
+ return withRegistryLock(base, () => {
2716
+ if (pairDirDaemonAlive(base, pairId)) {
2717
+ return { removed: false, reason: "live" };
2718
+ }
2719
+ return { removed: removePairDir(base, pairId) };
2720
+ });
2721
+ }
2722
+ function isEntryReclaimable(signals) {
2723
+ return signals.cwdGone && signals.dead && signals.old;
2724
+ }
2725
+ function cwdMissing(cwd) {
2726
+ try {
2727
+ statSync2(cwd);
2728
+ return false;
2729
+ } catch (err) {
2730
+ return err?.code === "ENOENT";
2731
+ }
2732
+ }
2733
+ function parseCreatedAtMs(createdAt) {
2734
+ if (typeof createdAt !== "string")
2735
+ return null;
2736
+ const ms = Date.parse(createdAt);
2737
+ return Number.isFinite(ms) ? ms : null;
2738
+ }
2739
+ function classifyReclaimableEntries(base, now = Date.now()) {
2740
+ const reg = readRegistry(base);
2741
+ const out = [];
2742
+ for (const entry of reg.pairs) {
2743
+ const createdMs = parseCreatedAtMs(entry.createdAt);
2744
+ const ageMs = createdMs === null ? null : Math.max(0, now - createdMs);
2745
+ const signals = {
2746
+ cwdGone: cwdMissing(entry.cwd),
2747
+ dead: !pairDirDaemonAlive(base, entry.pairId),
2748
+ old: ageMs !== null && ageMs >= RECLAIMABLE_MIN_AGE_MS,
2749
+ ageMs
2750
+ };
2751
+ if (isEntryReclaimable(signals))
2752
+ out.push({ entry, signals });
2753
+ }
2754
+ return out;
2755
+ }
2756
+ 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
2757
  var init_pair_registry = __esm(() => {
2758
+ init_atomic_json();
2392
2759
  init_process_lifecycle();
2393
2760
  PAIR_ID_REGEX = /^[A-Za-z0-9._-]{1,64}$/;
2761
+ RECLAIMABLE_MIN_AGE_MS = 24 * 60 * 60 * 1000;
2394
2762
  WINDOWS_RESERVED_RE = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
2395
2763
  PairError = class PairError extends Error {
2396
2764
  code;
@@ -2673,7 +3041,7 @@ var init_pair_resolver = __esm(() => {
2673
3041
  });
2674
3042
 
2675
3043
  // src/trace-log.ts
2676
- import { appendFileSync, existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync2, statSync as statSync3, unlinkSync as unlinkSync3 } from "fs";
3044
+ import { appendFileSync, existsSync as existsSync8, mkdirSync as mkdirSync5, readdirSync as readdirSync2, statSync as statSync3, unlinkSync as unlinkSync4 } from "fs";
2677
3045
  import { join as join8 } from "path";
2678
3046
  function pickRelevantEnv(env) {
2679
3047
  const picked = {};
@@ -2725,7 +3093,7 @@ function appendTraceEvent(input) {
2725
3093
  };
2726
3094
  const logsDir = join8(input.cwd, ".agentbridge", "logs");
2727
3095
  const isNewDayFile = !existsSync8(path);
2728
- mkdirSync4(logsDir, { recursive: true });
3096
+ mkdirSync5(logsDir, { recursive: true });
2729
3097
  if (isNewDayFile) {
2730
3098
  pruneOldTraceLogs(logsDir, path, Date.parse(timestamp));
2731
3099
  }
@@ -2751,7 +3119,7 @@ function pruneOldTraceLogs(logsDir, keepPath, nowMs) {
2751
3119
  continue;
2752
3120
  try {
2753
3121
  if (statSync3(filePath).mtimeMs < cutoff) {
2754
- unlinkSync3(filePath);
3122
+ unlinkSync4(filePath);
2755
3123
  }
2756
3124
  } catch {}
2757
3125
  }
@@ -2834,10 +3202,12 @@ var exports_claude = {};
2834
3202
  __export(exports_claude, {
2835
3203
  warnIfPluginCacheMissing: () => warnIfPluginCacheMissing,
2836
3204
  runClaude: () => runClaude,
3205
+ mapChildExitCode: () => mapChildExitCode,
2837
3206
  checkOwnedFlagConflicts: () => checkOwnedFlagConflicts
2838
3207
  });
2839
3208
  import { spawn as spawn2 } from "child_process";
2840
3209
  import { existsSync as existsSync9 } from "fs";
3210
+ import { constants as osConstants } from "os";
2841
3211
  async function runClaude(args) {
2842
3212
  const originalEnv = { ...process.env };
2843
3213
  const envGuardResult = guardAgentBridgeEnv({
@@ -2890,8 +3260,8 @@ async function runClaude(args) {
2890
3260
  stdio: "inherit",
2891
3261
  env: process.env
2892
3262
  });
2893
- child.on("exit", (code) => {
2894
- process.exit(code ?? 0);
3263
+ child.on("exit", (code, signal) => {
3264
+ process.exit(mapChildExitCode(code, signal));
2895
3265
  });
2896
3266
  child.on("error", (err) => {
2897
3267
  if (err.code === "ENOENT") {
@@ -2903,6 +3273,12 @@ async function runClaude(args) {
2903
3273
  process.exit(1);
2904
3274
  });
2905
3275
  }
3276
+ function mapChildExitCode(code, signal) {
3277
+ if (signal) {
3278
+ return 128 + (osConstants.signals[signal] ?? 0);
3279
+ }
3280
+ return code ?? 0;
3281
+ }
2906
3282
  function warnIfPluginCacheMissing(cacheRoot = pluginCacheRoot(), log = (msg) => console.error(msg)) {
2907
3283
  let cacheExists;
2908
3284
  try {
@@ -3003,7 +3379,7 @@ var init_claude = __esm(() => {
3003
3379
  });
3004
3380
 
3005
3381
  // src/agents-contract.ts
3006
- import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
3382
+ import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
3007
3383
  import { join as join9 } from "path";
3008
3384
  function checkAgentsMdContract(cwd) {
3009
3385
  const path = join9(cwd, "AGENTS.md");
@@ -3011,7 +3387,7 @@ function checkAgentsMdContract(cwd) {
3011
3387
  let content = "";
3012
3388
  if (exists) {
3013
3389
  try {
3014
- content = readFileSync6(path, "utf-8");
3390
+ content = readFileSync7(path, "utf-8");
3015
3391
  } catch {
3016
3392
  return {
3017
3393
  fresh: false,
@@ -3038,7 +3414,7 @@ function isFreshAgentsMdContract(content) {
3038
3414
  var init_agents_contract = () => {};
3039
3415
 
3040
3416
  // src/wrapper-exit-observability.ts
3041
- import { readFileSync as readFileSync7, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
3417
+ import { readFileSync as readFileSync8, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
3042
3418
  import { join as join10 } from "path";
3043
3419
  function discoverNativeChildPid(launcherPid, run) {
3044
3420
  try {
@@ -3049,17 +3425,15 @@ function discoverNativeChildPid(launcherPid, run) {
3049
3425
  return null;
3050
3426
  }
3051
3427
  }
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 {
3428
+ function readUnifiedTurnInProgress(paths, read = (p) => readFileSync8(p, "utf-8"), isPidAlive = defaultIsPidAlive) {
3429
+ const record = readUnifiedDaemonRecord(paths, read);
3430
+ if (!record)
3061
3431
  return null;
3062
- }
3432
+ if (typeof record.turnInProgress !== "boolean")
3433
+ return null;
3434
+ if (typeof record.pid === "number" && !isPidAlive(record.pid))
3435
+ return null;
3436
+ return record.turnInProgress;
3063
3437
  }
3064
3438
  function refineCleanExitClassification(turnInProgress) {
3065
3439
  if (turnInProgress === true)
@@ -3068,14 +3442,14 @@ function refineCleanExitClassification(turnInProgress) {
3068
3442
  return "exit_0_idle";
3069
3443
  return "exit_0_turn_unknown";
3070
3444
  }
3071
- function findCodexSqliteLog(codexHome, fs = { readdir: readdirSync3, stat: statSync4 }) {
3445
+ function findCodexSqliteLog(codexHome, fs2 = { readdir: readdirSync3, stat: statSync4 }) {
3072
3446
  try {
3073
- const entries = fs.readdir(codexHome).filter((name) => /^logs.*\.sqlite$/.test(String(name)));
3447
+ const entries = fs2.readdir(codexHome).filter((name) => /^logs.*\.sqlite$/.test(String(name)));
3074
3448
  let best = null;
3075
3449
  for (const name of entries) {
3076
3450
  const path = join10(codexHome, String(name));
3077
3451
  try {
3078
- const mtime = fs.stat(path).mtimeMs;
3452
+ const mtime = fs2.stat(path).mtimeMs;
3079
3453
  if (!best || mtime > best.mtime)
3080
3454
  best = { path, mtime };
3081
3455
  } catch {}
@@ -3110,14 +3484,15 @@ function captureTuiLogTail(options) {
3110
3484
  var defaultIsPidAlive;
3111
3485
  var init_wrapper_exit_observability = __esm(() => {
3112
3486
  init_process_lifecycle();
3487
+ init_daemon_record();
3113
3488
  defaultIsPidAlive = pidLooksAlive;
3114
3489
  });
3115
3490
 
3116
3491
  // src/pair-command.ts
3117
- function pairScopedCommand(cmd) {
3492
+ function pairScopedCommand(cmd, name = cliInvocationName()) {
3118
3493
  const pairId = process.env.AGENTBRIDGE_PAIR_ID;
3119
3494
  if (!pairId)
3120
- return `agentbridge ${cmd}`;
3495
+ return `${name} ${cmd}`;
3121
3496
  let selector = process.env.AGENTBRIDGE_PAIR_NAME;
3122
3497
  if (!selector) {
3123
3498
  try {
@@ -3126,19 +3501,20 @@ function pairScopedCommand(cmd) {
3126
3501
  selector = pairId;
3127
3502
  }
3128
3503
  }
3129
- return `agentbridge --pair ${selector} ${cmd}`;
3504
+ return `${name} --pair ${selector} ${cmd}`;
3130
3505
  }
3131
3506
  var init_pair_command = __esm(() => {
3507
+ init_cli_invocation();
3132
3508
  init_pair_resolver();
3133
3509
  });
3134
3510
 
3135
3511
  // 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";
3512
+ import { appendFileSync as appendFileSync2, existsSync as existsSync11, renameSync as renameSync2, statSync as statSync5, unlinkSync as unlinkSync5 } from "fs";
3513
+ import { dirname as dirname3 } from "path";
3138
3514
  function appendRotatingLog(path, content, options = {}, fsOps = REAL_FS_OPS) {
3139
3515
  const maxBytes = options.maxBytes ?? positiveIntFromEnv("AGENTBRIDGE_LOG_MAX_BYTES", DEFAULT_MAX_BYTES);
3140
3516
  const keep = options.keep ?? positiveIntFromEnv("AGENTBRIDGE_LOG_ROTATE_KEEP", DEFAULT_KEEP);
3141
- if (!fsOps.existsSync(dirname2(path)))
3517
+ if (!fsOps.existsSync(dirname3(path)))
3142
3518
  return;
3143
3519
  rotateIfNeeded(path, Buffer.byteLength(content), maxBytes, keep, fsOps);
3144
3520
  fsOps.appendFileSync(path, content, "utf-8");
@@ -3196,7 +3572,7 @@ function rotateIfNeeded(path, incomingBytes, maxBytes, keep, fsOps) {
3196
3572
  var DEFAULT_MAX_BYTES, DEFAULT_KEEP = 3, REAL_FS_OPS;
3197
3573
  var init_rotating_log = __esm(() => {
3198
3574
  DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
3199
- REAL_FS_OPS = { statSync: statSync5, renameSync: renameSync2, unlinkSync: unlinkSync4, appendFileSync: appendFileSync2, existsSync: existsSync11 };
3575
+ REAL_FS_OPS = { statSync: statSync5, renameSync: renameSync2, unlinkSync: unlinkSync5, appendFileSync: appendFileSync2, existsSync: existsSync11 };
3200
3576
  });
3201
3577
 
3202
3578
  // src/stderr-ring-buffer.ts
@@ -3250,30 +3626,20 @@ var init_stderr_ring_buffer = __esm(() => {
3250
3626
  // src/thread-state.ts
3251
3627
  import {
3252
3628
  existsSync as existsSync12,
3253
- mkdirSync as mkdirSync5,
3254
3629
  readdirSync as readdirSync4,
3255
- readFileSync as readFileSync8,
3256
- renameSync as renameSync3,
3257
- writeFileSync as writeFileSync6
3630
+ readFileSync as readFileSync9
3258
3631
  } from "fs";
3259
3632
  import { homedir as homedir3 } from "os";
3260
- import { basename as basename3, dirname as dirname3, join as join11 } from "path";
3633
+ import { basename as basename4, join as join11 } from "path";
3261
3634
  function nowIso() {
3262
3635
  return new Date().toISOString();
3263
3636
  }
3264
3637
  function codexHome(env = process.env) {
3265
3638
  return env.CODEX_HOME && env.CODEX_HOME.length > 0 ? env.CODEX_HOME : join11(homedir3(), ".codex");
3266
3639
  }
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
3640
  function readRawCurrentThread(stateDir) {
3275
3641
  try {
3276
- const parsed = JSON.parse(readFileSync8(stateDir.currentThreadFile, "utf-8"));
3642
+ const parsed = JSON.parse(readFileSync9(stateDir.currentThreadFile, "utf-8"));
3277
3643
  if (parsed?.version === 1 && typeof parsed.threadId === "string" && parsed.threadId.length > 0 && (parsed.status === "pending" || parsed.status === "current") && typeof parsed.cwd === "string") {
3278
3644
  return parsed;
3279
3645
  }
@@ -3304,7 +3670,7 @@ function findCodexRolloutFile(threadId, env = process.env, maxEntries = 20000) {
3304
3670
  }
3305
3671
  if (!entry.isFile())
3306
3672
  continue;
3307
- const name = basename3(entry.name);
3673
+ const name = basename4(entry.name);
3308
3674
  if (name === exactName || name.startsWith("rollout-") && name.endsWith(".jsonl") && name.includes(threadId)) {
3309
3675
  return path;
3310
3676
  }
@@ -3316,12 +3682,26 @@ function readUsableCurrentThread(identity, env = process.env) {
3316
3682
  const state = readRawCurrentThread(identity.stateDir);
3317
3683
  if (!state)
3318
3684
  return null;
3319
- if (state.status !== "current")
3320
- return null;
3321
3685
  if (state.pairId !== identity.pairId)
3322
3686
  return null;
3323
3687
  if (state.cwd !== identity.cwd)
3324
3688
  return null;
3689
+ if (state.status === "pending") {
3690
+ const rolloutPath2 = findCodexRolloutFile(state.threadId, env);
3691
+ if (!rolloutPath2)
3692
+ return null;
3693
+ const promoted = {
3694
+ ...state,
3695
+ status: "current",
3696
+ rolloutPath: rolloutPath2,
3697
+ rolloutVerifiedAt: nowIso(),
3698
+ updatedAt: nowIso()
3699
+ };
3700
+ try {
3701
+ atomicWriteJson(identity.stateDir.currentThreadFile, promoted);
3702
+ } catch {}
3703
+ return promoted;
3704
+ }
3325
3705
  if (state.rolloutPath && existsSync12(state.rolloutPath))
3326
3706
  return state;
3327
3707
  const rolloutPath = findCodexRolloutFile(state.threadId, env);
@@ -3336,7 +3716,9 @@ function readUsableCurrentThread(identity, env = process.env) {
3336
3716
  atomicWriteJson(identity.stateDir.currentThreadFile, repaired);
3337
3717
  return repaired;
3338
3718
  }
3339
- var init_thread_state = () => {};
3719
+ var init_thread_state = __esm(() => {
3720
+ init_atomic_json();
3721
+ });
3340
3722
 
3341
3723
  // src/cli/codex.ts
3342
3724
  var exports_codex = {};
@@ -3351,9 +3733,9 @@ import {
3351
3733
  openSync as openSync3,
3352
3734
  writeSync,
3353
3735
  closeSync as closeSync3,
3354
- writeFileSync as writeFileSync7,
3355
- readFileSync as readFileSync9,
3356
- unlinkSync as unlinkSync5,
3736
+ writeFileSync as writeFileSync5,
3737
+ readFileSync as readFileSync10,
3738
+ unlinkSync as unlinkSync6,
3357
3739
  existsSync as existsSync13,
3358
3740
  mkdirSync as mkdirSync6
3359
3741
  } from "fs";
@@ -3413,10 +3795,12 @@ function resolveCodexResumeArgs(parsed, pair, env = process.env) {
3413
3795
  const current = readUsableCurrentThread(identity, env);
3414
3796
  if (parsed.resumeCurrent) {
3415
3797
  if (!current) {
3798
+ const raw = readRawCurrentThread(identity.stateDir);
3799
+ const pending = raw && raw.status === "pending" && raw.pairId === identity.pairId && raw.cwd === identity.cwd ? raw : null;
3416
3800
  return {
3417
3801
  rest: parsed.rest,
3418
3802
  mode: "resume-current",
3419
- error: "No verified current Codex thread for this pair. Start a new one with `abg codex --new`, or resume a specific thread with `abg codex resume <threadId>`."
3803
+ error: pending ? `No verified current Codex thread for this pair. Found a pending (unverified) thread ${pending.threadId} \u2014 its Codex rollout file was not found. Try \`abg codex resume ${pending.threadId}\`, or start fresh with \`abg codex --new\`.` : "No verified current Codex thread for this pair. Start a new one with `abg codex --new`, or resume a specific thread with `abg codex resume <threadId>`."
3420
3804
  };
3421
3805
  }
3422
3806
  return {
@@ -3525,9 +3909,9 @@ async function runCodex(args) {
3525
3909
  process.exit(1);
3526
3910
  }
3527
3911
  let proxyUrl;
3528
- const status = lifecycle.readStatus();
3529
- if (status?.proxyUrl) {
3530
- proxyUrl = status.proxyUrl;
3912
+ const record = lifecycle.readDaemonRecord();
3913
+ if (typeof record?.proxyUrl === "string" && record.proxyUrl.length > 0) {
3914
+ proxyUrl = record.proxyUrl;
3531
3915
  } else {
3532
3916
  const fallbackProxyPort = process.env.CODEX_PROXY_PORT ?? String(new ConfigService().loadOrDefault((msg) => console.error(`[agentbridge] ${msg}`)).codex.proxyPort);
3533
3917
  proxyUrl = `ws://127.0.0.1:${fallbackProxyPort}`;
@@ -3609,7 +3993,7 @@ async function runCodex(args) {
3609
3993
  env: buildChildEnv()
3610
3994
  });
3611
3995
  if (typeof child.pid === "number") {
3612
- writeFileSync7(stateDir.tuiPidFile, `${child.pid}
3996
+ writeFileSync5(stateDir.tuiPidFile, `${child.pid}
3613
3997
  `, "utf-8");
3614
3998
  appendWrapperLog(wrapperLogPath, `child pid=${child.pid}`);
3615
3999
  }
@@ -3649,7 +4033,7 @@ async function runCodex(args) {
3649
4033
  return;
3650
4034
  cleanedTuiPid = true;
3651
4035
  try {
3652
- unlinkSync5(stateDir.tuiPidFile);
4036
+ unlinkSync6(stateDir.tuiPidFile);
3653
4037
  } catch {}
3654
4038
  }
3655
4039
  function requestChildTermination(reason) {
@@ -3720,7 +4104,11 @@ async function runCodex(args) {
3720
4104
  else if (typeof code === "number" && code !== 0)
3721
4105
  classification = `nonzero_exit:${code}`;
3722
4106
  else if (code === 0 && tail.trim().length === 0) {
3723
- classification = refineCleanExitClassification(readTurnInProgress(stateDir.statusFile));
4107
+ classification = refineCleanExitClassification(readUnifiedTurnInProgress({
4108
+ daemonRecordFile: stateDir.daemonRecordFile,
4109
+ pidFile: stateDir.pidFile,
4110
+ statusFile: stateDir.statusFile
4111
+ }));
3724
4112
  }
3725
4113
  const tuiLogTail = captureTuiLogTail({
3726
4114
  codexHome: join12(homedir4(), ".codex"),
@@ -3779,12 +4167,12 @@ function guardNoLiveManagedTui(stateDir, proxyUrl) {
3779
4167
  if (pid) {
3780
4168
  if (!isProcessAlive(pid)) {
3781
4169
  try {
3782
- unlinkSync5(stateDir.tuiPidFile);
4170
+ unlinkSync6(stateDir.tuiPidFile);
3783
4171
  } catch {}
3784
4172
  } else if (!isManagedCodexTuiProcess(pid, proxyUrl)) {
3785
4173
  appendWrapperLog(stateDir.codexWrapperLogFile, `stale tui pid file pointed at unmanaged live pid=${pid}; removing`);
3786
4174
  try {
3787
- unlinkSync5(stateDir.tuiPidFile);
4175
+ unlinkSync6(stateDir.tuiPidFile);
3788
4176
  } catch {}
3789
4177
  } else {
3790
4178
  console.error(`[agentbridge] This pair already has a managed Codex TUI running (pid ${pid}).`);
@@ -3801,7 +4189,7 @@ function guardNoLiveManagedTui(stateDir, proxyUrl) {
3801
4189
  }
3802
4190
  function readTuiPid(stateDir) {
3803
4191
  try {
3804
- const raw = readFileSync9(stateDir.tuiPidFile, "utf-8").trim();
4192
+ const raw = readFileSync10(stateDir.tuiPidFile, "utf-8").trim();
3805
4193
  if (!raw)
3806
4194
  return null;
3807
4195
  const pid = Number.parseInt(raw, 10);
@@ -3815,7 +4203,12 @@ function isManagedCodexTuiProcess(pid, proxyUrl) {
3815
4203
  return cmd !== null && commandMatchesManagedCodexTui(cmd, proxyUrl);
3816
4204
  }
3817
4205
  function proxyHealthUrl(proxyUrl) {
3818
- const url = new URL(proxyUrl);
4206
+ let url;
4207
+ try {
4208
+ url = new URL(proxyUrl);
4209
+ } catch {
4210
+ throw new Error(`Malformed Codex proxy URL: ${JSON.stringify(proxyUrl)}`);
4211
+ }
3819
4212
  url.protocol = url.protocol === "wss:" ? "https:" : "http:";
3820
4213
  url.pathname = "/healthz";
3821
4214
  url.search = "";
@@ -4010,7 +4403,7 @@ __export(exports_kill, {
4010
4403
  runKill: () => runKill,
4011
4404
  formatKillReport: () => formatKillReport
4012
4405
  });
4013
- import { readFileSync as readFileSync10, unlinkSync as unlinkSync6 } from "fs";
4406
+ import { readFileSync as readFileSync11, unlinkSync as unlinkSync7 } from "fs";
4014
4407
  import { join as join14 } from "path";
4015
4408
  async function runKill(args = []) {
4016
4409
  const argError = validateKillArgs(args);
@@ -4031,8 +4424,9 @@ async function runKill(args = []) {
4031
4424
  const base = computeBaseDir();
4032
4425
  console.log(`AgentBridge Kill \u2014 stopping AgentBridge pair processes
4033
4426
  `);
4427
+ const cli = cliInvocationName();
4034
4428
  const results = [];
4035
- let restartCommand = "agentbridge claude";
4429
+ let restartCommand = `${cli} claude`;
4036
4430
  if (parsed.pairFlag !== undefined) {
4037
4431
  let pair;
4038
4432
  try {
@@ -4046,7 +4440,7 @@ async function runKill(args = []) {
4046
4440
  printKnownPairs(base);
4047
4441
  return;
4048
4442
  }
4049
- restartCommand = `agentbridge --pair ${pair.name ?? parsed.pairFlag} claude`;
4443
+ restartCommand = `${cli} --pair ${pair.name ?? parsed.pairFlag} claude`;
4050
4444
  results.push(await stopPairEntry(base, pair));
4051
4445
  } else if (parsed.all) {
4052
4446
  let registered = [];
@@ -4080,7 +4474,7 @@ async function runKill(args = []) {
4080
4474
  cwdPairs = listPairsForCwd(base, process.cwd());
4081
4475
  } catch (error) {
4082
4476
  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");
4477
+ 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
4478
  process.exitCode = 2;
4085
4479
  }
4086
4480
  for (const pair of cwdPairs) {
@@ -4096,7 +4490,7 @@ async function runKill(args = []) {
4096
4490
  }
4097
4491
  if (results.length === 0) {
4098
4492
  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.");
4493
+ console.log(`Use \`${cli} kill all\` or \`${cli} kill --all\` to stop pairs from every directory.`);
4100
4494
  return;
4101
4495
  }
4102
4496
  }
@@ -4159,22 +4553,18 @@ function listPairDirsSafe(base) {
4159
4553
  }
4160
4554
  }
4161
4555
  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 {
4556
+ const record = readUnifiedDaemonRecord({
4557
+ daemonRecordFile: stateDir.daemonRecordFile,
4558
+ pidFile: stateDir.pidFile,
4559
+ statusFile: stateDir.statusFile
4560
+ });
4561
+ if (!record)
4170
4562
  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;
4563
+ return {
4564
+ appPort: record.ports?.appPort ?? portFromUrl(record.appServerUrl) ?? 0,
4565
+ proxyPort: record.ports?.proxyPort ?? portFromUrl(record.proxyUrl) ?? 0,
4566
+ controlPort: record.ports?.controlPort ?? 0
4567
+ };
4178
4568
  }
4179
4569
  async function stopStateDir(label, stateDir, ports) {
4180
4570
  const portsLabel = `${ports.appPort}/${ports.proxyPort}/${ports.controlPort}`;
@@ -4187,8 +4577,8 @@ async function stopStateDir(label, stateDir, ports) {
4187
4577
  log
4188
4578
  });
4189
4579
  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}`;
4580
+ const record = lifecycle.readDaemonRecord();
4581
+ const proxyUrl = typeof record?.proxyUrl === "string" && record.proxyUrl.length > 0 ? record.proxyUrl : `ws://127.0.0.1:${ports.proxyPort}`;
4192
4582
  const tuiKilled = await killManagedCodexTui(stateDir, proxyUrl, log);
4193
4583
  const daemonKilled = await lifecycle.kill();
4194
4584
  return { label, portsLabel, daemonKilled, tuiKilled, details };
@@ -4253,9 +4643,10 @@ function formatKillReport(results, frontends, restartCommand) {
4253
4643
  }
4254
4644
  lines.push("");
4255
4645
  if (stopped.length > 0) {
4646
+ const cliName = restartCommand.split(" ")[0] ?? "abg";
4256
4647
  lines.push("AgentBridge stopped.");
4257
4648
  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`);
4649
+ 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
4650
  } else {
4260
4651
  lines.push("No running AgentBridge daemon or managed Codex TUI found.");
4261
4652
  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 +4697,7 @@ async function killManagedCodexTui(stateDir, proxyUrl, log, gracefulTimeoutMs =
4306
4697
  }
4307
4698
  function readTuiPid2(stateDir) {
4308
4699
  try {
4309
- const raw = readFileSync10(stateDir.tuiPidFile, "utf-8").trim();
4700
+ const raw = readFileSync11(stateDir.tuiPidFile, "utf-8").trim();
4310
4701
  if (!raw)
4311
4702
  return null;
4312
4703
  const pid = Number.parseInt(raw, 10);
@@ -4317,7 +4708,7 @@ function readTuiPid2(stateDir) {
4317
4708
  }
4318
4709
  function removeTuiPidFile(stateDir) {
4319
4710
  try {
4320
- unlinkSync6(stateDir.tuiPidFile);
4711
+ unlinkSync7(stateDir.tuiPidFile);
4321
4712
  } catch {}
4322
4713
  }
4323
4714
  function isManagedCodexTuiProcess2(pid, proxyUrl) {
@@ -4325,7 +4716,9 @@ function isManagedCodexTuiProcess2(pid, proxyUrl) {
4325
4716
  return cmd !== null && commandMatchesManagedCodexTui(cmd, proxyUrl);
4326
4717
  }
4327
4718
  var init_kill = __esm(() => {
4719
+ init_cli_invocation();
4328
4720
  init_daemon_lifecycle();
4721
+ init_daemon_record();
4329
4722
  init_pair_registry();
4330
4723
  init_pair_resolver();
4331
4724
  init_process_lifecycle();
@@ -4338,6 +4731,13 @@ __export(exports_pairs, {
4338
4731
  runPairs: () => runPairs
4339
4732
  });
4340
4733
  import { join as join15 } from "path";
4734
+ function isRegistryCorruptError(error) {
4735
+ return error instanceof PairError && error.code === "PAIR_REGISTRY_CORRUPT";
4736
+ }
4737
+ function registryPathForNotice(base, error) {
4738
+ const fromDetails = error.details?.path;
4739
+ return typeof fromDetails === "string" && fromDetails.length > 0 ? fromDetails : join15(pairsRootDir(base), "registry.json");
4740
+ }
4341
4741
  async function runPairs(args = []) {
4342
4742
  const [command, ...rest] = args;
4343
4743
  if (command === "rm") {
@@ -4350,7 +4750,7 @@ async function runPairs(args = []) {
4350
4750
  }
4351
4751
  if (command && command !== "list" && command !== "--json" && command !== "--threads") {
4352
4752
  console.error(`Unknown pairs command: ${command}`);
4353
- console.error("Usage: abg pairs [--json] [--threads] | abg pairs rm <name|id> | abg pairs prune [--dry-run]");
4753
+ console.error("Usage: abg pairs [--json] [--threads] | abg pairs rm <name|id> | abg pairs prune [--apply]");
4354
4754
  process.exit(1);
4355
4755
  }
4356
4756
  const json = command === "--json" || rest.includes("--json");
@@ -4405,16 +4805,40 @@ async function runRemove(args) {
4405
4805
  }
4406
4806
  }
4407
4807
  async function runPrune(args) {
4408
- const dryRun = args.includes("--dry-run");
4808
+ const apply = args.includes("--apply");
4409
4809
  for (const arg of args) {
4410
- if (arg !== "--dry-run") {
4810
+ if (arg !== "--apply" && arg !== "--dry-run") {
4411
4811
  console.error(`Unknown prune argument: ${arg}`);
4412
- console.error("Usage: abg pairs prune [--dry-run]");
4812
+ console.error("Usage: abg pairs prune [--apply]");
4413
4813
  process.exit(1);
4414
4814
  }
4415
4815
  }
4816
+ if (apply && args.includes("--dry-run")) {
4817
+ console.error("Error: --apply and --dry-run are mutually exclusive.");
4818
+ console.error("Usage: abg pairs prune [--apply]");
4819
+ process.exit(1);
4820
+ }
4416
4821
  const base = computeBaseDir();
4417
- const registered = new Set(listPairs(base).map((pair) => pair.pairId.toLowerCase()));
4822
+ let reclaimable;
4823
+ let registryReadable = true;
4824
+ try {
4825
+ reclaimable = classifyReclaimableEntries(base);
4826
+ } catch (error) {
4827
+ if (!isRegistryCorruptError(error))
4828
+ throw error;
4829
+ registryReadable = false;
4830
+ reclaimable = [];
4831
+ 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`);
4832
+ process.exitCode = 2;
4833
+ }
4834
+ const reclaimableIds = new Set(reclaimable.map((c) => c.entry.pairId.toLowerCase()));
4835
+ const dirResult = pruneOrphanDirs(base, apply, reclaimableIds, registryReadable);
4836
+ const entryResult = await pruneReclaimableEntries(reclaimable, base, apply);
4837
+ const resolvedDirResult = await dirResult;
4838
+ printPruneSummary(resolvedDirResult, entryResult, apply);
4839
+ }
4840
+ async function pruneOrphanDirs(base, apply, reclaimableIds, registryReadable) {
4841
+ const registered = registryReadable ? new Set(listPairs(base).map((pair) => pair.pairId.toLowerCase())) : new Set;
4418
4842
  const removed = [];
4419
4843
  const kept = [];
4420
4844
  for (const name of listPairDirs(base)) {
@@ -4429,6 +4853,9 @@ async function runPrune(args) {
4429
4853
  kept.push({ name, reason: "directory name is not a canonical pair id" });
4430
4854
  continue;
4431
4855
  }
4856
+ if (reclaimableIds.has(id.toLowerCase())) {
4857
+ continue;
4858
+ }
4432
4859
  if (registered.has(id.toLowerCase())) {
4433
4860
  kept.push({ name, reason: "registered \u2014 use `abg pairs rm`" });
4434
4861
  continue;
@@ -4437,12 +4864,12 @@ async function runPrune(args) {
4437
4864
  kept.push({ name, reason: "daemon still alive" });
4438
4865
  continue;
4439
4866
  }
4440
- if (dryRun) {
4867
+ if (!apply) {
4441
4868
  removed.push(name);
4442
4869
  continue;
4443
4870
  }
4444
4871
  try {
4445
- const outcome = await removeUnregisteredPairDir(base, id);
4872
+ const outcome = registryReadable ? await removeUnregisteredPairDir(base, id) : await removeOrphanPairDirIgnoringRegistry(base, id);
4446
4873
  if (outcome.removed) {
4447
4874
  removed.push(name);
4448
4875
  } else if (outcome.reason === "registered") {
@@ -4456,33 +4883,90 @@ async function runPrune(args) {
4456
4883
  kept.push({ name, reason: `error: ${err instanceof Error ? err.message : String(err)}` });
4457
4884
  }
4458
4885
  }
4459
- printPruneSummary(removed, kept, dryRun);
4886
+ return { removed, kept };
4460
4887
  }
4461
- function printPruneSummary(removed, kept, dryRun) {
4462
- if (removed.length === 0 && kept.length === 0) {
4463
- console.log("No pair directories found.");
4888
+ async function pruneReclaimableEntries(candidates, base, apply) {
4889
+ const reclaimed = [];
4890
+ const kept = [];
4891
+ for (const candidate of candidates) {
4892
+ const reason = describeReclaimReason(candidate);
4893
+ if (!apply) {
4894
+ reclaimed.push({ pairId: candidate.entry.pairId, slot: candidate.entry.slot, reason });
4895
+ continue;
4896
+ }
4897
+ try {
4898
+ const res = await removePairEntryAndDir(base, candidate.entry.pairId);
4899
+ if (res.keptLive) {
4900
+ kept.push({ pairId: candidate.entry.pairId, reason: "became live during prune" });
4901
+ } else {
4902
+ reclaimed.push({ pairId: candidate.entry.pairId, slot: candidate.entry.slot, reason });
4903
+ }
4904
+ } catch (err) {
4905
+ kept.push({
4906
+ pairId: candidate.entry.pairId,
4907
+ reason: `error: ${err instanceof Error ? err.message : String(err)}`
4908
+ });
4909
+ }
4910
+ }
4911
+ return { reclaimed, kept };
4912
+ }
4913
+ function describeReclaimReason(candidate) {
4914
+ const { signals } = candidate;
4915
+ const age = signals.ageMs === null ? "age?" : `age ${formatAgeDays(signals.ageMs)}`;
4916
+ return `cwd-gone, dead, ${age}`;
4917
+ }
4918
+ function formatAgeDays(ageMs) {
4919
+ const days = ageMs / (24 * 60 * 60 * 1000);
4920
+ return days >= 10 ? `${Math.round(days)}d` : `${days.toFixed(1)}d`;
4921
+ }
4922
+ function printPruneSummary(dirResult, entryResult, apply) {
4923
+ const { removed: dirsRemoved, kept: dirsKept } = dirResult;
4924
+ const { reclaimed: entriesReclaimed, kept: entriesKept } = entryResult;
4925
+ const nothingFound = dirsRemoved.length === 0 && dirsKept.length === 0 && entriesReclaimed.length === 0 && entriesKept.length === 0;
4926
+ if (nothingFound) {
4927
+ console.log("Nothing to prune: no orphan pair directories or reclaimable entries found.");
4464
4928
  return;
4465
4929
  }
4466
- if (removed.length > 0) {
4467
- console.log(dryRun ? "Would remove orphan pair directories:" : "Removed orphan pair directories:");
4468
- for (const name of removed)
4930
+ if (dirsRemoved.length > 0) {
4931
+ console.log(apply ? "Removed orphan pair directories:" : "Would remove orphan pair directories:");
4932
+ for (const name of dirsRemoved)
4469
4933
  console.log(` ${name}`);
4470
- } else {
4471
- console.log(dryRun ? "No orphan pair directories to remove." : "No orphan pair directories removed.");
4472
4934
  }
4473
- if (kept.length > 0) {
4935
+ if (entriesReclaimed.length > 0) {
4936
+ console.log(apply ? "Reclaimed registry entries:" : "Would reclaim registry entries:");
4937
+ for (const { pairId, slot, reason } of entriesReclaimed) {
4938
+ console.log(` ${pairId} (slot ${slot}) \u2014 ${reason}`);
4939
+ }
4940
+ }
4941
+ if (dirsRemoved.length === 0 && entriesReclaimed.length === 0) {
4942
+ console.log(apply ? "Nothing was reclaimed." : "Nothing to reclaim.");
4943
+ }
4944
+ const keptLines = [
4945
+ ...dirsKept.map(({ name, reason }) => ` ${name} (${reason})`),
4946
+ ...entriesKept.map(({ pairId, reason }) => ` ${pairId} (${reason})`)
4947
+ ];
4948
+ if (keptLines.length > 0) {
4474
4949
  console.log("Kept:");
4475
- for (const { name, reason } of kept)
4476
- console.log(` ${name} (${reason})`);
4950
+ for (const line of keptLines)
4951
+ console.log(line);
4477
4952
  }
4478
- if (dryRun) {
4953
+ if (!apply) {
4479
4954
  console.log(`
4480
- (dry run \u2014 nothing was deleted. Re-run without --dry-run to apply.)`);
4955
+ (dry run \u2014 nothing was deleted. Re-run with --apply to reclaim.)`);
4481
4956
  }
4482
4957
  }
4483
4958
  async function collectRows() {
4484
4959
  const base = computeBaseDir();
4485
- const rows = await Promise.all(listPairs(base).map((pair) => rowForPair(base, pair)));
4960
+ let rows;
4961
+ try {
4962
+ rows = await Promise.all(listPairs(base).map((pair) => rowForPair(base, pair)));
4963
+ } catch (error) {
4964
+ if (!isRegistryCorruptError(error))
4965
+ throw error;
4966
+ 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`);
4967
+ process.exitCode = 2;
4968
+ rows = await collectDiskScanRows(base);
4969
+ }
4486
4970
  const legacy = detectLegacyRootDaemon(base);
4487
4971
  if (legacy) {
4488
4972
  rows.push({
@@ -4509,9 +4993,9 @@ async function rowForPair(base, pair) {
4509
4993
  controlPort: ports.controlPort,
4510
4994
  log: () => {}
4511
4995
  });
4512
- const [running, status] = await Promise.all([
4996
+ const [running, record] = await Promise.all([
4513
4997
  lifecycle.isHealthy(),
4514
- Promise.resolve(lifecycle.readStatus())
4998
+ Promise.resolve(lifecycle.readDaemonRecord())
4515
4999
  ]);
4516
5000
  const thread = readRawCurrentThread(stateDir);
4517
5001
  return {
@@ -4522,7 +5006,42 @@ async function rowForPair(base, pair) {
4522
5006
  source: pair.source,
4523
5007
  cwd: pair.cwd,
4524
5008
  running,
4525
- pid: typeof status?.pid === "number" ? status.pid : null,
5009
+ pid: typeof record?.pid === "number" ? record.pid : null,
5010
+ threadId: thread?.threadId ?? null,
5011
+ threadStatus: thread?.status ?? null,
5012
+ threadUpdatedAt: thread?.updatedAt ?? null
5013
+ };
5014
+ }
5015
+ async function collectDiskScanRows(base) {
5016
+ const names = listPairDirsSafe2(base);
5017
+ return Promise.all(names.map((name) => rowForDiskScanDir(base, name)));
5018
+ }
5019
+ function listPairDirsSafe2(base) {
5020
+ try {
5021
+ return listPairDirs(base);
5022
+ } catch {
5023
+ return [];
5024
+ }
5025
+ }
5026
+ async function rowForDiskScanDir(base, dirName) {
5027
+ const stateDir = new StateDirResolver(join15(base, "pairs", dirName));
5028
+ const record = new DaemonLifecycle({ stateDir, controlPort: 0, log: () => {} }).readDaemonRecord();
5029
+ const ports = {
5030
+ appPort: record?.ports?.appPort ?? 0,
5031
+ proxyPort: record?.ports?.proxyPort ?? 0,
5032
+ controlPort: record?.ports?.controlPort ?? 0
5033
+ };
5034
+ const running = ports.controlPort > 0 ? await new DaemonLifecycle({ stateDir, controlPort: ports.controlPort, log: () => {} }).isHealthy() : false;
5035
+ const thread = readRawCurrentThread(stateDir);
5036
+ return {
5037
+ pairId: dirName,
5038
+ name: "-",
5039
+ slot: null,
5040
+ ports,
5041
+ source: "cwd",
5042
+ cwd: "-",
5043
+ running,
5044
+ pid: typeof record?.pid === "number" ? record.pid : null,
4526
5045
  threadId: thread?.threadId ?? null,
4527
5046
  threadStatus: thread?.status ?? null,
4528
5047
  threadUpdatedAt: thread?.updatedAt ?? null
@@ -4588,6 +5107,215 @@ var init_pairs = __esm(() => {
4588
5107
  init_thread_state();
4589
5108
  init_kill();
4590
5109
  });
5110
+ // src/budget/types.ts
5111
+ var STALE_MAX_AGE_SEC = 600;
5112
+
5113
+ // src/budget/budget-state.ts
5114
+ function isDecisionGrade(usage, now) {
5115
+ if (!usage)
5116
+ return false;
5117
+ const freshWindow = usage.fiveHour !== null && usage.fiveHour.resetEpoch > now || usage.weekly !== null && usage.weekly.resetEpoch > now;
5118
+ if (!freshWindow)
5119
+ return false;
5120
+ if (usage.fetchedAt > 0 && now - usage.fetchedAt > STALE_MAX_AGE_SEC)
5121
+ return false;
5122
+ return true;
5123
+ }
5124
+ var init_budget_state = () => {};
5125
+
5126
+ // src/budget/burn-view.ts
5127
+ function agentWeeklyFiveHourWindowsLeft(usage, now) {
5128
+ if (!usage || usage.stale || !usage.ok)
5129
+ return null;
5130
+ if (!isDecisionGrade(usage, now))
5131
+ return null;
5132
+ const weekly = usage.weekly;
5133
+ if (!weekly || weekly.resetEpoch <= now)
5134
+ return null;
5135
+ if (weekly.burnConfident !== true)
5136
+ return null;
5137
+ if (weekly.runwaySeconds === undefined)
5138
+ return null;
5139
+ return weekly.fiveHourWindowsLeft ?? null;
5140
+ }
5141
+ var init_burn_view = __esm(() => {
5142
+ init_budget_state();
5143
+ });
5144
+
5145
+ // src/budget/render.ts
5146
+ function resolveGuardHardHint(env = process.env) {
5147
+ const raw = env.AGENTBRIDGE_GUARD_HARD_HINT;
5148
+ if (raw === undefined || raw.trim() === "")
5149
+ return DEFAULT_GUARD_HARD_PCT;
5150
+ const parsed = Number(raw);
5151
+ if (!Number.isFinite(parsed) || parsed < 1 || parsed > 100)
5152
+ return DEFAULT_GUARD_HARD_PCT;
5153
+ return parsed;
5154
+ }
5155
+ function formatEpoch(epochSeconds) {
5156
+ if (!epochSeconds || epochSeconds <= 0)
5157
+ return "\u672A\u77E5";
5158
+ return new Date(epochSeconds * 1000).toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z");
5159
+ }
5160
+ function formatWindow(window, label) {
5161
+ if (!window)
5162
+ return `${label} \u672A\u77E5`;
5163
+ return `${label} ${window.util}%\uFF08\u91CD\u7F6E ${formatEpoch(window.resetEpoch)}\uFF09`;
5164
+ }
5165
+ function formatAgent(name, usage, snapshotAt) {
5166
+ if (!usage)
5167
+ return `${name}\uFF1A\u672A\u77E5\uFF08\u63A2\u6D4B\u4E0D\u53EF\u7528\uFF09`;
5168
+ const parts = [
5169
+ formatWindow(usage.fiveHour, "5h"),
5170
+ formatWindow(usage.weekly, "\u5468"),
5171
+ `\u95E8\u63A7 ${usage.gateUtil}%`,
5172
+ `\u9884\u8B66 ${usage.warnUtil}%`
5173
+ ];
5174
+ if (usage.rateLimitedUntil > 0) {
5175
+ parts.push(`\u9650\u6D41\u81F3 ${formatEpoch(usage.rateLimitedUntil)}`);
5176
+ }
5177
+ if (usage.parsedVia === "positional") {
5178
+ parts.push("\u26A0\uFE0F \u7A97\u53E3\u8BC6\u522B\u4F7F\u7528\u4F4D\u7F6E\u515C\u5E95");
5179
+ }
5180
+ const ageSec = usage.fetchedAt > 0 ? snapshotAt - usage.fetchedAt : 0;
5181
+ if (ageSec > 300) {
5182
+ parts.push(`\u26A0\uFE0F \u6570\u636E\u91C7\u96C6\u4E8E ${Math.round(ageSec / 60)} \u5206\u949F\u524D`);
5183
+ } else if (usage.stale) {
5184
+ parts.push("\uFF08\u7F13\u5B58\u6570\u636E\uFF09");
5185
+ }
5186
+ return `${name}\uFF1A${parts.join(" \xB7 ")}`;
5187
+ }
5188
+ function formatDuration(seconds) {
5189
+ const totalMinutes = Math.max(0, Math.round(seconds / 60));
5190
+ const hours = Math.floor(totalMinutes / 60);
5191
+ const minutes = totalMinutes % 60;
5192
+ if (hours === 0)
5193
+ return `${minutes}\u5206\u949F`;
5194
+ return `${hours}\u5C0F\u65F6${minutes}\u5206\u949F`;
5195
+ }
5196
+ function formatClockTime(epochSeconds) {
5197
+ const date = new Date(epochSeconds * 1000);
5198
+ const hh = String(date.getHours()).padStart(2, "0");
5199
+ const mm = String(date.getMinutes()).padStart(2, "0");
5200
+ return `${hh}:${mm}`;
5201
+ }
5202
+ function formatWindowRate(label, rate) {
5203
+ if (!rate)
5204
+ return null;
5205
+ if (!rate.confident)
5206
+ return `${label} \u91C7\u6837\u4E2D`;
5207
+ return `${label} \u2248${rate.pctPerHour.toFixed(2)}%/h`;
5208
+ }
5209
+ function formatRunwaySegment(runway, basisWindow, snapshotAt) {
5210
+ const truncatedByReset = basisWindow !== null && basisWindow.resetEpoch > 0 && snapshotAt + runway.seconds >= basisWindow.resetEpoch - RESET_TRUNCATION_EPSILON_SEC;
5211
+ const clock = runway.depletedAtEpoch ? formatClockTime(runway.depletedAtEpoch) : null;
5212
+ let clockNote;
5213
+ if (clock) {
5214
+ clockNote = truncatedByReset ? `\u81F3 ${clock} \u7A97\u53E3\u5237\u65B0\u5373\u622A\u65AD\uFF0C` : `\u81F3 ${clock}\uFF0C`;
5215
+ } else {
5216
+ clockNote = truncatedByReset ? "\u7A97\u53E3\u5237\u65B0\u5373\u622A\u65AD\uFF0C" : "";
5217
+ }
5218
+ return `\u7EA6\u53EF\u518D\u5DE5\u4F5C ${formatDuration(runway.seconds)}\uFF08${clockNote}${WINDOW_LABELS[runway.basis]}\u4E3A\u7EA6\u675F\uFF09`;
5219
+ }
5220
+ function formatBurnRateLine(name, usage, rates, runway, snapshotAt, guardHardPct) {
5221
+ const parts = [
5222
+ formatWindowRate("5h", rates.fiveHour),
5223
+ formatWindowRate("\u5468", rates.weekly)
5224
+ ].filter((part) => part !== null);
5225
+ if (parts.length === 0 && !runway)
5226
+ return null;
5227
+ if (runway) {
5228
+ const basisWindow = usage ? usage[runway.basis] : null;
5229
+ parts.push(formatRunwaySegment(runway, basisWindow, snapshotAt));
5230
+ }
5231
+ if (guardHardPct !== null) {
5232
+ parts.push(`\u5916\u5C42 guard \u786C\u7EBF ${guardHardPct}%\uFF08v3 \u4E0D\u53EF\u8D8A\u8FC7\uFF1Brunway \u4E3A\u4E2D\u6027\u53E3\u5F84\uFF0CClaude \u4F1A\u5148\u5728\u786C\u7EBF\u88AB\u5916\u5C42\u505C\u4F4F\uFF09`);
5233
+ }
5234
+ return `${name} \u71C3\u5C3D\u7387\uFF1A${parts.join(" \xB7 ")}`;
5235
+ }
5236
+ function formatFiveHourWindowsLeftLine(snapshot) {
5237
+ const values = [];
5238
+ const claude = agentWeeklyFiveHourWindowsLeft(snapshot.claude, snapshot.updatedAt);
5239
+ const codex = agentWeeklyFiveHourWindowsLeft(snapshot.codex, snapshot.updatedAt);
5240
+ if (claude !== null)
5241
+ values.push(["Claude", claude]);
5242
+ if (codex !== null)
5243
+ values.push(["Codex", codex]);
5244
+ if (values.length === 0)
5245
+ return null;
5246
+ const unique = [...new Set(values.map(([, value]) => value.toFixed(1)))];
5247
+ if (unique.length === 1)
5248
+ return `\u6309\u5F53\u524D\u8282\u594F\uFF0C\u5468\u989D\u5EA6\u8FD8\u591F ~${unique[0]} \u4E2A 5h \u7A97\u53E3`;
5249
+ const byAgent = values.map(([name, value]) => `${name} ~${value.toFixed(1)}`).join(" / ");
5250
+ return `\u6309\u5F53\u524D\u8282\u594F\uFF0C\u5468\u989D\u5EA6\u8FD8\u591F ${byAgent} \u4E2A 5h \u7A97\u53E3`;
5251
+ }
5252
+ function renderBudgetSnapshot(snapshot, options = {}) {
5253
+ const guardHardPct = options.guardHardPct ?? resolveGuardHardHint();
5254
+ const lines = [];
5255
+ lines.push(`\u3010\u9884\u7B97\u5FEB\u7167 \xB7 \u8D26\u53F7\u7EA7\u3011\u9636\u6BB5\uFF1A${PHASE_LABELS[snapshot.phase]} \xB7 \u66F4\u65B0\u4E8E ${formatEpoch(snapshot.updatedAt)}`);
5256
+ lines.push(formatAgent("Claude", snapshot.claude, snapshot.updatedAt));
5257
+ lines.push(formatAgent("Codex", snapshot.codex, snapshot.updatedAt));
5258
+ if (snapshot.burnRate) {
5259
+ const claudeLine = formatBurnRateLine("Claude", snapshot.claude, snapshot.burnRate.claude, snapshot.runway?.claude ?? null, snapshot.updatedAt, guardHardPct);
5260
+ if (claudeLine)
5261
+ lines.push(claudeLine);
5262
+ const codexLine = formatBurnRateLine("Codex", snapshot.codex, snapshot.burnRate.codex, snapshot.runway?.codex ?? null, snapshot.updatedAt, null);
5263
+ if (codexLine)
5264
+ lines.push(codexLine);
5265
+ }
5266
+ const fiveHourWindowsLeftLine = formatFiveHourWindowsLeftLine(snapshot);
5267
+ if (fiveHourWindowsLeftLine)
5268
+ lines.push(fiveHourWindowsLeftLine);
5269
+ if (snapshot.claude && snapshot.codex) {
5270
+ const abs = Math.abs(snapshot.driftPct);
5271
+ if (abs > 0) {
5272
+ const heavier = snapshot.driftPct > 0 ? "Claude" : "Codex";
5273
+ const lighter = snapshot.driftPct > 0 ? "Codex" : "Claude";
5274
+ lines.push(`\u6F02\u79FB\uFF1A${heavier} \u6BD4 ${lighter} \u9AD8 ${abs} \u4E2A\u767E\u5206\u70B9`);
5275
+ } else {
5276
+ lines.push("\u6F02\u79FB\uFF1A\u53CC\u65B9\u6301\u5E73");
5277
+ }
5278
+ }
5279
+ if (snapshot.paused) {
5280
+ const resume = snapshot.resumeAfterEpoch ? `\uFF1B\u9884\u8BA1\u6062\u590D ${formatEpoch(snapshot.resumeAfterEpoch)}\uFF08\u4EE5\u5B9E\u6D4B\u4E3A\u51C6\uFF1B\u63D0\u524D\u5237\u65B0\u4F1A\u66F4\u65E9\u89E3\u9664\uFF09` : "";
5281
+ const reason = snapshot.pauseReason ?? "\u989D\u5EA6\u63A5\u8FD1\u8017\u5C3D";
5282
+ if (snapshot.pauseSide === "claude" && !snapshot.gateClosed) {
5283
+ lines.push(`\u63A5\u529B\u4E2D\uFF1AClaude \u4FA7\u989D\u5EA6\u8017\u5C3D\uFF0C\u5DF2\u4EA4\u63A5 Codex \u7EE7\u7EED\u63A8\u8FDB\uFF08\u95F8\u95E8\u5F00\u653E\uFF09 \u2014 ${reason}${resume}`);
5284
+ } else if (snapshot.pauseSide === "codex") {
5285
+ lines.push(`\u6682\u505C\uFF1ACodex \u4FA7\u989D\u5EA6\u8017\u5C3D\uFF08\u95F8\u95E8\u5173\u95ED\uFF0CClaude \u53EF solo \u63A8\u8FDB\u72EC\u7ACB\u90E8\u5206\uFF09 \u2014 ${reason}${resume}`);
5286
+ } else {
5287
+ lines.push(`\u6682\u505C\uFF1A\u53CC\u4FA7\u8054\u5408\u6682\u505C\uFF08\u95F8\u95E8\u5173\u95ED\uFF09 \u2014 ${reason}${resume}`);
5288
+ }
5289
+ } else {
5290
+ lines.push("\u6682\u505C\uFF1A\u5426");
5291
+ }
5292
+ if (snapshot.parallelRecommended) {
5293
+ lines.push("\u5E76\u884C\u5EFA\u8BAE\uFF1A\u989D\u5EA6\u5BCC\u4F59\u4E14\u4E34\u8FD1\u7ED3\u7B97\uFF0C\u5EFA\u8BAE\u62C6\u5206\u66F4\u591A\u5E76\u884C\u5B50\u4EFB\u52A1");
5294
+ }
5295
+ if (snapshot.codexTier !== "full") {
5296
+ lines.push(`Codex \u6863\u4F4D\uFF1A${snapshot.codexTier}`);
5297
+ }
5298
+ if (snapshot.claudeAdvice) {
5299
+ lines.push(`Claude \u5EFA\u8BAE\uFF1A${snapshot.claudeAdvice}`);
5300
+ }
5301
+ lines.push("\u6CE8\uFF1A\u767E\u5206\u6BD4\u4E3A\u8BA2\u9605\u8D26\u53F7\u7EA7\u7528\u91CF\uFF08\u540C\u673A\u5176\u4ED6\u4F1A\u8BDD\u5171\u4EAB\u540C\u4E00\u989D\u5EA6\u6C60\uFF09\u3002");
5302
+ return lines.join(`
5303
+ `);
5304
+ }
5305
+ var DEFAULT_GUARD_HARD_PCT = 92, WINDOW_LABELS, RESET_TRUNCATION_EPSILON_SEC = 60, PHASE_LABELS, BUDGET_UNAVAILABLE_TEXT = "\u9884\u7B97\u611F\u77E5\u4E0D\u53EF\u7528\uFF1A\u672A\u68C0\u6D4B\u5230 agent-quota-guard \u63A2\u9488\uFF08~/.budget-guard/bin/budget-probe\uFF09\u6216 budget \u529F\u80FD\u5DF2\u7981\u7528\u3002\u534F\u4F5C\u4E0D\u53D7\u5F71\u54CD\u3002";
5306
+ var init_render = __esm(() => {
5307
+ init_burn_view();
5308
+ WINDOW_LABELS = {
5309
+ fiveHour: "5h \u7A97\u53E3",
5310
+ weekly: "\u5468\u7A97\u53E3"
5311
+ };
5312
+ PHASE_LABELS = {
5313
+ normal: "normal\uFF08\u6B63\u5E38\uFF09",
5314
+ balance: "balance\uFF08\u9700\u5747\u8861\uFF09",
5315
+ parallel: "parallel\uFF08\u5EFA\u8BAE\u5E76\u884C\u63D0\u901F\uFF09",
5316
+ paused: "paused\uFF08\u9884\u7B97\u5E72\u9884\u4E2D\uFF09"
5317
+ };
5318
+ });
4591
5319
 
4592
5320
  // src/daemon-status.ts
4593
5321
  async function fetchDaemonStatus(port, path = "/healthz", timeoutMs = DAEMON_STATUS_FETCH_TIMEOUT_MS) {
@@ -4612,7 +5340,7 @@ import {
4612
5340
  copyFileSync,
4613
5341
  existsSync as existsSync14,
4614
5342
  mkdirSync as mkdirSync7,
4615
- readFileSync as readFileSync11
5343
+ readFileSync as readFileSync12
4616
5344
  } from "fs";
4617
5345
  import { dirname as dirname5, join as join16 } from "path";
4618
5346
  function isKickoffText(text) {
@@ -4623,7 +5351,7 @@ function isKickoffText(text) {
4623
5351
  function extractFirstRealUserMessage(rolloutPath) {
4624
5352
  if (!existsSync14(rolloutPath))
4625
5353
  return null;
4626
- const raw = readFileSync11(rolloutPath, "utf-8");
5354
+ const raw = readFileSync12(rolloutPath, "utf-8");
4627
5355
  for (const line of raw.split(`
4628
5356
  `)) {
4629
5357
  if (!line.trim())
@@ -4790,9 +5518,13 @@ var init_resume_pollution = __esm(() => {
4790
5518
  var exports_doctor = {};
4791
5519
  __export(exports_doctor, {
4792
5520
  runDoctor: () => runDoctor,
4793
- formatDoctorReport: () => formatDoctorReport
5521
+ formatDoctorReport: () => formatDoctorReport,
5522
+ evaluateBudgetStrategyGuard: () => evaluateBudgetStrategyGuard,
5523
+ evaluateArtifactAlignment: () => evaluateArtifactAlignment,
5524
+ describeBuildDrift: () => describeBuildDrift,
5525
+ V3_DEFAULT_TARGET_UTIL: () => V3_DEFAULT_TARGET_UTIL
4794
5526
  });
4795
- import { existsSync as existsSync15, readFileSync as readFileSync12, readdirSync as readdirSync6, realpathSync as realpathSync3, statSync as statSync7 } from "fs";
5527
+ import { existsSync as existsSync15, readFileSync as readFileSync13, readdirSync as readdirSync6, realpathSync as realpathSync3, statSync as statSync7 } from "fs";
4796
5528
  import { join as join17 } from "path";
4797
5529
  async function runDoctor(args = []) {
4798
5530
  if (args[0] === "resume-pollution") {
@@ -4869,6 +5601,7 @@ function runResumePollution(args) {
4869
5601
  }
4870
5602
  async function buildDoctorReport(pair, registered) {
4871
5603
  const cwd = process.cwd();
5604
+ const cli = cliInvocationName();
4872
5605
  const env = inspectAgentBridgeEnv({ cwd, env: process.env });
4873
5606
  const [health, ready] = registered ? await Promise.all([
4874
5607
  fetchDaemonStatus(pair.ports.controlPort, "/healthz"),
@@ -4888,20 +5621,21 @@ async function buildDoctorReport(pair, registered) {
4888
5621
  name: "pair registration",
4889
5622
  status: registered ? "ok" : "warn",
4890
5623
  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"
5624
+ 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
5625
  });
4893
5626
  checks.push({
4894
5627
  name: "env",
4895
5628
  status: env.ok ? "ok" : "fail",
4896
5629
  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"
5630
+ 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
5631
  });
4899
- checks.push(configParseabilityCheck(cwd));
5632
+ checks.push(configParseabilityCheck(cwd, cli));
5633
+ checks.push(budgetStrategyGuardCheck(cwd));
4900
5634
  checks.push({
4901
5635
  name: "daemon health",
4902
5636
  status: health ? "ok" : "warn",
4903
5637
  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"
5638
+ 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
5639
  });
4906
5640
  checks.push({
4907
5641
  name: "daemon readiness",
@@ -4909,18 +5643,26 @@ async function buildDoctorReport(pair, registered) {
4909
5643
  detail: ready ? `ready thread=${ready.threadId ?? "none"}` : health ? "readyz is not OK" : "n/a \u2014 daemon not running",
4910
5644
  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
5645
  });
5646
+ const appServerInfo = health?.appServerInfo ?? null;
5647
+ checks.push({
5648
+ name: "codex app-server",
5649
+ status: health ? "ok" : "skip",
5650
+ 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",
5651
+ 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
5652
+ });
5653
+ const drift = buildDrift === true ? describeBuildDrift(health?.build, BUILD_INFO, cli) : null;
4912
5654
  checks.push({
4913
5655
  name: "build drift",
4914
5656
  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
5657
+ 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)",
5658
+ hint: drift?.hint
4917
5659
  });
4918
5660
  checks.push(artifactAlignmentCheck());
4919
5661
  checks.push({
4920
5662
  name: "current thread",
4921
5663
  status: usableThread ? "ok" : rawThread ? "warn" : registered ? "warn" : "skip",
4922
5664
  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
5665
+ 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
5666
  });
4925
5667
  const pairProxyUrl = `ws://127.0.0.1:${pair.ports.proxyPort}`;
4926
5668
  const managedTuis = listManagedCodexTuiProcesses();
@@ -4937,19 +5679,19 @@ async function buildDoctorReport(pair, registered) {
4937
5679
  name: "codex tui (this pair)",
4938
5680
  status: attachedHere.length > 0 ? "ok" : "warn",
4939
5681
  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"
5682
+ 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
5683
  });
4942
5684
  checks.push({
4943
5685
  name: "codex tui (other pairs)",
4944
5686
  status: attachedElsewhere.length > 0 ? "warn" : "ok",
4945
5687
  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
5688
+ 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
5689
  });
4948
5690
  for (const [name, path] of [
4949
5691
  ["daemon log", pair.stateDir.logFile],
4950
5692
  ["codex wrapper log", pair.stateDir.codexWrapperLogFile]
4951
5693
  ]) {
4952
- checks.push(logCheck(name, path));
5694
+ checks.push(logCheck(name, path, cli));
4953
5695
  }
4954
5696
  return {
4955
5697
  cwd,
@@ -4970,61 +5712,102 @@ async function buildDoctorReport(pair, registered) {
4970
5712
  checks
4971
5713
  };
4972
5714
  }
5715
+ function describeBuildDrift(runtime, launcher, cli = "abg") {
5716
+ const basis = runtimeContractComparisonBasis(runtime, launcher);
5717
+ const baseDetail = `runtime ${formatBuildInfo(runtime)} differs from launcher ${formatBuildInfo(launcher)}`;
5718
+ 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";
5719
+ if (basis === "codeHash") {
5720
+ return { detail: `${baseDetail} [compared by codeHash \u2014 real code difference]`, hint: baseHint };
5721
+ }
5722
+ return {
5723
+ detail: `${baseDetail} [compared by commit stamp \u2014 legacy build without codeHash]`,
5724
+ 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"
5725
+ };
5726
+ }
5727
+ function isUsableCodeHash(hash) {
5728
+ return typeof hash === "string" && hash.length > 0 && hash !== "source";
5729
+ }
5730
+ function evaluateArtifactAlignment(stamps) {
5731
+ if (stamps.length < 2) {
5732
+ return {
5733
+ name: "artifact alignment",
5734
+ status: "skip",
5735
+ detail: "n/a \u2014 fewer than two stamped artifacts found"
5736
+ };
5737
+ }
5738
+ if (stamps.every((stamp) => isUsableCodeHash(stamp.codeHash))) {
5739
+ const rendered2 = stamps.map((stamp) => `${stamp.label}=${stamp.codeHash}`).join(", ");
5740
+ if (new Set(stamps.map((stamp) => stamp.codeHash)).size === 1) {
5741
+ return { name: "artifact alignment", status: "ok", detail: `codeHash basis: ${rendered2}` };
5742
+ }
5743
+ return {
5744
+ name: "artifact alignment",
5745
+ status: "fail",
5746
+ detail: `deployed artifacts contain DIFFERENT code (codeHash basis): ${rendered2}`,
5747
+ 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"
5748
+ };
5749
+ }
5750
+ const rendered = stamps.map((stamp) => `${stamp.label}=${stamp.commit}`).join(", ");
5751
+ if (new Set(stamps.map((stamp) => stamp.commit)).size === 1) {
5752
+ return {
5753
+ name: "artifact alignment",
5754
+ status: "ok",
5755
+ detail: `legacy commit-stamp basis: ${rendered}`
5756
+ };
5757
+ }
5758
+ return {
5759
+ name: "artifact alignment",
5760
+ status: "fail",
5761
+ detail: `deployed artifacts are at DIFFERENT builds (legacy commit-stamp basis): ${rendered}`,
5762
+ 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"
5763
+ };
5764
+ }
4973
5765
  function artifactAlignmentCheck() {
4974
5766
  const stamps = [];
4975
5767
  if (BUILD_INFO.commit !== "source") {
4976
- stamps.push({ label: `launcher(${BUILD_INFO.bundle})`, commit: BUILD_INFO.commit });
5768
+ stamps.push({
5769
+ label: `launcher(${BUILD_INFO.bundle})`,
5770
+ commit: BUILD_INFO.commit,
5771
+ codeHash: hasValidCodeHash(BUILD_INFO) ? BUILD_INFO.codeHash ?? null : null
5772
+ });
4977
5773
  }
4978
5774
  const bin = Bun.which("agentbridge") ?? Bun.which("abg");
4979
5775
  if (bin) {
4980
5776
  try {
4981
- const commit = extractBundleCommit(realpathSync3(bin));
4982
- if (commit)
4983
- stamps.push({ label: "global-cli", commit });
5777
+ const stamp = extractBundleStamp(realpathSync3(bin));
5778
+ if (stamp)
5779
+ stamps.push({ label: "global-cli", ...stamp });
4984
5780
  } catch {}
4985
5781
  }
4986
5782
  const cacheRoot = pluginCacheRoot();
4987
5783
  try {
4988
5784
  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 });
5785
+ const stamp = extractBundleStamp(join17(cacheRoot, version, "server", "daemon.js"));
5786
+ if (stamp)
5787
+ stamps.push({ label: `plugin-cache@${version}`, ...stamp });
4992
5788
  }
4993
5789
  } catch {}
4994
5790
  const repoBundle = join17(process.cwd(), "plugins", "agentbridge", "server", "daemon.js");
4995
5791
  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
- };
5006
- }
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 };
5792
+ const stamp = extractBundleStamp(repoBundle);
5793
+ if (stamp)
5794
+ stamps.push({ label: "repo-bundle", ...stamp });
5011
5795
  }
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
- };
5796
+ return evaluateArtifactAlignment(stamps);
5018
5797
  }
5019
- function extractBundleCommit(path) {
5798
+ function extractBundleStamp(path) {
5020
5799
  try {
5021
- const match = readFileSync12(path, "utf-8").match(/commit:\s*defineString\("([^"]+)",\s*"source"\)/);
5022
- return match ? match[1] : null;
5800
+ const text = readFileSync13(path, "utf-8");
5801
+ const commit = text.match(/commit:\s*defineString\("([^"]+)",\s*"source"\)/)?.[1] ?? null;
5802
+ if (!commit)
5803
+ return null;
5804
+ const codeHash = text.match(/codeHash:\s*defineString\("([^"]+)",\s*"source"\)/)?.[1] ?? null;
5805
+ return { commit, codeHash };
5023
5806
  } catch {
5024
5807
  return null;
5025
5808
  }
5026
5809
  }
5027
- function configParseabilityCheck(cwd) {
5810
+ function configParseabilityCheck(cwd, cli) {
5028
5811
  const desc = new ConfigService(cwd).describeConfig();
5029
5812
  if (desc.state === "absent") {
5030
5813
  return {
@@ -5038,7 +5821,7 @@ function configParseabilityCheck(cwd) {
5038
5821
  name: "config.json",
5039
5822
  status: "warn",
5040
5823
  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"
5824
+ 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
5825
  };
5043
5826
  }
5044
5827
  return {
@@ -5047,7 +5830,34 @@ function configParseabilityCheck(cwd) {
5047
5830
  detail: desc.customValues ? `parsed at ${desc.path} \u2014 custom values in effect` : `parsed at ${desc.path} \u2014 all values match defaults`
5048
5831
  };
5049
5832
  }
5050
- function logCheck(name, path) {
5833
+ function evaluateBudgetStrategyGuard(strategy, guardHardPct, targetUtilPct = V3_DEFAULT_TARGET_UTIL) {
5834
+ if (strategy !== "maximize") {
5835
+ return {
5836
+ name: "budget strategy",
5837
+ status: "ok",
5838
+ detail: "strategy=conserve \u2014 v2-equivalent budget behavior (v3 maximize is opt-in)"
5839
+ };
5840
+ }
5841
+ if (guardHardPct >= targetUtilPct) {
5842
+ return {
5843
+ name: "budget strategy",
5844
+ status: "ok",
5845
+ detail: `strategy=maximize \u2014 outer guard hard line ${guardHardPct}% covers targetUtil ${targetUtilPct}%`
5846
+ };
5847
+ }
5848
+ return {
5849
+ name: "budget strategy",
5850
+ status: "warn",
5851
+ detail: `strategy=maximize but the outer quota-guard hard line (${guardHardPct}%) is below ` + `targetUtil (${targetUtilPct}%) \u2014 the ${guardHardPct}\u2192${targetUtilPct} band is unreachable for Claude`,
5852
+ hint: "v3 \u4E0D\u53EF\u8D8A\u8FC7\u5916\u5C42 quota-guard \u786C\u7EBF\uFF1AClaude \u4FA7\u8FBE\u5230 guard \u786C\u7EBF\u65F6\u8FDB\u7A0B\u4F1A\u88AB\u5916\u5C42\u5F3A\u505C\uFF0C" + `maximize \u7684 ${guardHardPct}%\u2192${targetUtilPct}% \u533A\u95F4\u5B9E\u9645\u70E7\u4E0D\u5230\u3002\u60F3\u771F\u6B63\u7528\u5230 targetUtil\uFF0C` + "\u9700\u81EA\u884C\u8C03\u9AD8 quota-guard \u7684 BUDGET_HARD\uFF08\u672C\u4ED3\u5E93\u4E0D\u4EE3\u6539\u5916\u5C42\u914D\u7F6E\uFF09\uFF1B\u5C55\u793A\u4FA7\u5DF2\u6309 guard \u7EBF\u6536\u53E3\u3002"
5853
+ };
5854
+ }
5855
+ function budgetStrategyGuardCheck(cwd) {
5856
+ const config = new ConfigService(cwd).loadOrDefault();
5857
+ const budget = applyBudgetEnvOverrides(config.budget);
5858
+ return evaluateBudgetStrategyGuard(budget.strategy, resolveGuardHardHint());
5859
+ }
5860
+ function logCheck(name, path, cli) {
5051
5861
  if (!existsSync15(path)) {
5052
5862
  return {
5053
5863
  name,
@@ -5062,7 +5872,7 @@ function logCheck(name, path) {
5062
5872
  name,
5063
5873
  status: "warn",
5064
5874
  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"
5875
+ hint: `\u65E5\u5FD7\u8FC7\u5927\uFF1A\`${cli} kill\` \u505C\u6B62 pair \u540E\u5220\u9664\u8BE5\u6587\u4EF6\u518D\u91CD\u542F\u5373\u53EF\u3002`
5066
5876
  };
5067
5877
  }
5068
5878
  return { name, status: "ok", detail: `${path} (${stat.size} bytes)` };
@@ -5096,11 +5906,13 @@ function printDoctorReport(report) {
5096
5906
  console.log(line);
5097
5907
  }
5098
5908
  }
5099
- var LARGE_LOG_WARN_BYTES;
5909
+ var LARGE_LOG_WARN_BYTES, V3_DEFAULT_TARGET_UTIL = 97;
5100
5910
  var init_doctor = __esm(() => {
5101
5911
  init_plugin_cache();
5102
5912
  init_build_info();
5913
+ init_cli_invocation();
5103
5914
  init_config_service();
5915
+ init_render();
5104
5916
  init_env_guard();
5105
5917
  init_pair_resolver();
5106
5918
  init_thread_state();
@@ -5109,91 +5921,6 @@ var init_doctor = __esm(() => {
5109
5921
  LARGE_LOG_WARN_BYTES = 100 * 1024 * 1024;
5110
5922
  });
5111
5923
 
5112
- // src/budget/render.ts
5113
- function formatEpoch(epochSeconds) {
5114
- if (!epochSeconds || epochSeconds <= 0)
5115
- return "\u672A\u77E5";
5116
- return new Date(epochSeconds * 1000).toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z");
5117
- }
5118
- function formatWindow(window, label) {
5119
- if (!window)
5120
- return `${label} \u672A\u77E5`;
5121
- return `${label} ${window.util}%\uFF08\u91CD\u7F6E ${formatEpoch(window.resetEpoch)}\uFF09`;
5122
- }
5123
- function formatAgent(name, usage, snapshotAt) {
5124
- if (!usage)
5125
- return `${name}\uFF1A\u672A\u77E5\uFF08\u63A2\u6D4B\u4E0D\u53EF\u7528\uFF09`;
5126
- const parts = [
5127
- formatWindow(usage.fiveHour, "5h"),
5128
- formatWindow(usage.weekly, "\u5468"),
5129
- `\u95E8\u63A7 ${usage.gateUtil}%`,
5130
- `\u9884\u8B66 ${usage.warnUtil}%`
5131
- ];
5132
- if (usage.rateLimitedUntil > 0) {
5133
- parts.push(`\u9650\u6D41\u81F3 ${formatEpoch(usage.rateLimitedUntil)}`);
5134
- }
5135
- if (usage.parsedVia === "positional") {
5136
- parts.push("\u26A0\uFE0F \u7A97\u53E3\u8BC6\u522B\u4F7F\u7528\u4F4D\u7F6E\u515C\u5E95");
5137
- }
5138
- const ageSec = usage.fetchedAt > 0 ? snapshotAt - usage.fetchedAt : 0;
5139
- if (ageSec > 300) {
5140
- parts.push(`\u26A0\uFE0F \u6570\u636E\u91C7\u96C6\u4E8E ${Math.round(ageSec / 60)} \u5206\u949F\u524D`);
5141
- } else if (usage.stale) {
5142
- parts.push("\uFF08\u7F13\u5B58\u6570\u636E\uFF09");
5143
- }
5144
- return `${name}\uFF1A${parts.join(" \xB7 ")}`;
5145
- }
5146
- function renderBudgetSnapshot(snapshot) {
5147
- const lines = [];
5148
- lines.push(`\u3010\u9884\u7B97\u5FEB\u7167 \xB7 \u8D26\u53F7\u7EA7\u3011\u9636\u6BB5\uFF1A${PHASE_LABELS[snapshot.phase]} \xB7 \u66F4\u65B0\u4E8E ${formatEpoch(snapshot.updatedAt)}`);
5149
- lines.push(formatAgent("Claude", snapshot.claude, snapshot.updatedAt));
5150
- lines.push(formatAgent("Codex", snapshot.codex, snapshot.updatedAt));
5151
- if (snapshot.claude && snapshot.codex) {
5152
- const abs = Math.abs(snapshot.driftPct);
5153
- if (abs > 0) {
5154
- const heavier = snapshot.driftPct > 0 ? "Claude" : "Codex";
5155
- const lighter = snapshot.driftPct > 0 ? "Codex" : "Claude";
5156
- lines.push(`\u6F02\u79FB\uFF1A${heavier} \u6BD4 ${lighter} \u9AD8 ${abs} \u4E2A\u767E\u5206\u70B9`);
5157
- } else {
5158
- lines.push("\u6F02\u79FB\uFF1A\u53CC\u65B9\u6301\u5E73");
5159
- }
5160
- }
5161
- if (snapshot.paused) {
5162
- const resume = snapshot.resumeAfterEpoch ? `\uFF1B\u9884\u8BA1\u6062\u590D ${formatEpoch(snapshot.resumeAfterEpoch)}\uFF08\u4EE5\u5B9E\u6D4B\u4E3A\u51C6\uFF1B\u63D0\u524D\u5237\u65B0\u4F1A\u66F4\u65E9\u89E3\u9664\uFF09` : "";
5163
- const reason = snapshot.pauseReason ?? "\u989D\u5EA6\u63A5\u8FD1\u8017\u5C3D";
5164
- if (snapshot.pauseSide === "claude" && !snapshot.gateClosed) {
5165
- lines.push(`\u63A5\u529B\u4E2D\uFF1AClaude \u4FA7\u989D\u5EA6\u8017\u5C3D\uFF0C\u5DF2\u4EA4\u63A5 Codex \u7EE7\u7EED\u63A8\u8FDB\uFF08\u95F8\u95E8\u5F00\u653E\uFF09 \u2014 ${reason}${resume}`);
5166
- } else if (snapshot.pauseSide === "codex") {
5167
- lines.push(`\u6682\u505C\uFF1ACodex \u4FA7\u989D\u5EA6\u8017\u5C3D\uFF08\u95F8\u95E8\u5173\u95ED\uFF0CClaude \u53EF solo \u63A8\u8FDB\u72EC\u7ACB\u90E8\u5206\uFF09 \u2014 ${reason}${resume}`);
5168
- } else {
5169
- lines.push(`\u6682\u505C\uFF1A\u53CC\u4FA7\u8054\u5408\u6682\u505C\uFF08\u95F8\u95E8\u5173\u95ED\uFF09 \u2014 ${reason}${resume}`);
5170
- }
5171
- } else {
5172
- lines.push("\u6682\u505C\uFF1A\u5426");
5173
- }
5174
- if (snapshot.parallelRecommended) {
5175
- lines.push("\u5E76\u884C\u5EFA\u8BAE\uFF1A\u989D\u5EA6\u5BCC\u4F59\u4E14\u4E34\u8FD1\u7ED3\u7B97\uFF0C\u5EFA\u8BAE\u62C6\u5206\u66F4\u591A\u5E76\u884C\u5B50\u4EFB\u52A1");
5176
- }
5177
- if (snapshot.codexTier !== "full") {
5178
- lines.push(`Codex \u6863\u4F4D\uFF1A${snapshot.codexTier}`);
5179
- }
5180
- if (snapshot.claudeAdvice) {
5181
- lines.push(`Claude \u5EFA\u8BAE\uFF1A${snapshot.claudeAdvice}`);
5182
- }
5183
- lines.push("\u6CE8\uFF1A\u767E\u5206\u6BD4\u4E3A\u8BA2\u9605\u8D26\u53F7\u7EA7\u7528\u91CF\uFF08\u540C\u673A\u5176\u4ED6\u4F1A\u8BDD\u5171\u4EAB\u540C\u4E00\u989D\u5EA6\u6C60\uFF09\u3002");
5184
- return lines.join(`
5185
- `);
5186
- }
5187
- var PHASE_LABELS, BUDGET_UNAVAILABLE_TEXT = "\u9884\u7B97\u611F\u77E5\u4E0D\u53EF\u7528\uFF1A\u672A\u68C0\u6D4B\u5230 agent-quota-guard \u63A2\u9488\uFF08~/.budget-guard/bin/budget-probe\uFF09\u6216 budget \u529F\u80FD\u5DF2\u7981\u7528\u3002\u534F\u4F5C\u4E0D\u53D7\u5F71\u54CD\u3002";
5188
- var init_render = __esm(() => {
5189
- PHASE_LABELS = {
5190
- normal: "normal\uFF08\u6B63\u5E38\uFF09",
5191
- balance: "balance\uFF08\u9700\u5747\u8861\uFF09",
5192
- parallel: "parallel\uFF08\u5EFA\u8BAE\u5E76\u884C\u63D0\u901F\uFF09",
5193
- paused: "paused\uFF08\u9884\u7B97\u5E72\u9884\u4E2D\uFF09"
5194
- };
5195
- });
5196
-
5197
5924
  // src/cli/budget.ts
5198
5925
  var exports_budget = {};
5199
5926
  __export(exports_budget, {
@@ -5201,6 +5928,7 @@ __export(exports_budget, {
5201
5928
  });
5202
5929
  async function runBudget(args) {
5203
5930
  const json = args.includes("--json");
5931
+ const cli = cliInvocationName();
5204
5932
  const { pairFlag } = parsePairFlag(args.filter((arg) => arg !== "--json"));
5205
5933
  let resolution;
5206
5934
  try {
@@ -5220,7 +5948,7 @@ async function runBudget(args) {
5220
5948
  if (json) {
5221
5949
  console.log(JSON.stringify({ ok: false, error: "pair_not_registered" }));
5222
5950
  } else {
5223
- console.error("\u8BE5\u76EE\u5F55\u5C1A\u65E0 pair\uFF0C\u5148\u8FD0\u884C abg claude");
5951
+ console.error(`\u8BE5\u76EE\u5F55\u5C1A\u65E0 pair\uFF0C\u5148\u8FD0\u884C ${cli} claude`);
5224
5952
  }
5225
5953
  process.exit(1);
5226
5954
  return;
@@ -5230,7 +5958,7 @@ async function runBudget(args) {
5230
5958
  if (json) {
5231
5959
  console.log(JSON.stringify({ ok: false, pairId: pair.pairId, error: "daemon_unreachable" }));
5232
5960
  } 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");
5961
+ 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
5962
  }
5235
5963
  process.exit(1);
5236
5964
  }
@@ -5242,6 +5970,7 @@ async function runBudget(args) {
5242
5970
  console.log(status.budget ? renderBudgetSnapshot(status.budget) : BUDGET_UNAVAILABLE_TEXT);
5243
5971
  }
5244
5972
  var init_budget = __esm(() => {
5973
+ init_cli_invocation();
5245
5974
  init_pair_resolver();
5246
5975
  init_render();
5247
5976
  });
@@ -5254,7 +5983,7 @@ __export(exports_logs, {
5254
5983
  parseLogsArgs: () => parseLogsArgs,
5255
5984
  followLog: () => followLog
5256
5985
  });
5257
- import { existsSync as existsSync16, readFileSync as readFileSync13 } from "fs";
5986
+ import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
5258
5987
  import { spawn as spawn4 } from "child_process";
5259
5988
  function parseLogsArgs(args) {
5260
5989
  let codex = false;
@@ -5347,7 +6076,7 @@ async function runLogs(args) {
5347
6076
  function printTail(logPath, count, label, pairName) {
5348
6077
  let text;
5349
6078
  try {
5350
- text = readFileSync13(logPath, "utf8");
6079
+ text = readFileSync14(logPath, "utf8");
5351
6080
  } catch (err) {
5352
6081
  console.error(`[agentbridge] failed to read ${label} for pair ${pairName}: ` + `${err instanceof Error ? err.message : String(err)} (${logPath})`);
5353
6082
  process.exit(1);
@@ -5492,6 +6221,7 @@ async function main(command, restArgs) {
5492
6221
  }
5493
6222
  }
5494
6223
  function printHelp() {
6224
+ const cli = cliInvocationName();
5495
6225
  console.log(`
5496
6226
  AgentBridge \u2014 Multi-agent collaboration bridge
5497
6227
 
@@ -5509,8 +6239,9 @@ Commands:
5509
6239
  No target: print resume commands for this directory's last
5510
6240
  Claude session + this pair's current Codex thread.
5511
6241
  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)
6242
+ pairs [rm <name|id> | prune [--apply]]
6243
+ List pairs; remove one (rm), or reclaim orphan dirs + stranded
6244
+ entries (prune previews by default; --apply to delete)
5514
6245
  doctor [--json] Diagnose env, daemon, build drift, logs, and current thread
5515
6246
  doctor resume-pollution [--apply] Find/fix old AgentBridge kickoff metadata
5516
6247
  budget [--json] Show both agents' subscription quota snapshot (5h/weekly, drift, pause state)
@@ -5542,30 +6273,30 @@ Multi-pair:
5542
6273
  contesting it \u2014 pick another --pair name (or kill the live one first).
5543
6274
 
5544
6275
  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)
6276
+ ${cli} init # First-time setup
6277
+ ${cli} claude # Start the "main" pair for this directory
6278
+ ${cli} codex # Connect Codex to this directory's "main" pair
6279
+ ${cli} resume # Print resume commands for both sides
6280
+ ${cli} resume claude # Resume the last Claude Code session here
6281
+ ${cli} resume codex # Resume this pair's current Codex thread
6282
+ ${cli} claude --safe # One launch without the max-permission default
6283
+ ${cli} --pair work claude # Start a named pair "work" (this directory)
6284
+ ${cli} --pair work codex # Connect Codex to the "work" pair
6285
+ ${cli} --pair review claude # A second, parallel pair
6286
+ ${cli} pairs # List all pairs and their ports/status
6287
+ ${cli} pairs --threads # Include current thread mapping
6288
+ ${cli} doctor --json # Emit a structured diagnostics report
6289
+ ${cli} logs # Tail the last 100 lines of this pair's daemon log
6290
+ ${cli} logs -f -n 200 # Follow the log, starting from the last 200 lines
6291
+ ${cli} logs --codex # Tail the codex wrapper log instead
6292
+ ${cli} --pair work logs # Tail the "work" pair's daemon log
6293
+ ${cli} pairs rm work # Stop this directory's "work" pair and free its slot
6294
+ ${cli} pairs rm work-1a2b3c4d # ...or by its full id (from that pair's directory)
6295
+ ${cli} pairs prune # Preview reclaimable: orphan dirs + stranded entries (cwd-gone, dead, >1d)
6296
+ ${cli} pairs prune --apply # ...actually delete the previewed dirs + entries
6297
+ ${cli} --pair work kill # Stop only this directory's "work" pair
6298
+ ${cli} kill # Stop this directory's pairs (+ any legacy-root daemon)
6299
+ ${cli} kill all # Stop every pair in every directory (+ legacy-root)
5569
6300
  `.trim());
5570
6301
  }
5571
6302
  function printVersion() {
@@ -5578,6 +6309,7 @@ function printVersion() {
5578
6309
  }
5579
6310
  var MARKETPLACE_NAME = "agentbridge", PLUGIN_NAME = "agentbridge", REFRESH_COMMANDS, NOTIFY_COMMANDS, PAIR_AWARE_COMMANDS;
5580
6311
  var init_cli = __esm(() => {
6312
+ init_cli_invocation();
5581
6313
  REFRESH_COMMANDS = new Set(["claude", "codex", "resume"]);
5582
6314
  NOTIFY_COMMANDS = new Set(["claude", "codex", "init", "dev", "resume"]);
5583
6315
  PAIR_AWARE_COMMANDS = new Set(["claude", "codex", "kill", "doctor", "budget", "resume", "logs"]);