@raysonmeng/agentbridge 0.1.10 → 0.1.12

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
@@ -120,7 +120,7 @@ function parsePositiveIntEnv(name, fallback, log = () => {}, env = process.env)
120
120
  var require_package = __commonJS((exports, module) => {
121
121
  module.exports = {
122
122
  name: "@raysonmeng/agentbridge",
123
- version: "0.1.10",
123
+ version: "0.1.12",
124
124
  description: "Bridge between Claude Code and Codex \u2014 bidirectional agent communication via MCP Channel + JSON-RPC",
125
125
  type: "module",
126
126
  packageManager: "bun@1.3.11",
@@ -323,6 +323,48 @@ import { join as join2 } from "path";
323
323
  function isRecord(value) {
324
324
  return typeof value === "object" && value !== null && !Array.isArray(value);
325
325
  }
326
+ function isCoercibleNumber(value) {
327
+ if (typeof value === "number")
328
+ return Number.isFinite(value);
329
+ if (typeof value === "string")
330
+ return Number.isFinite(Number(value));
331
+ return false;
332
+ }
333
+ function findShapeViolation(raw) {
334
+ if ("idleShutdownSeconds" in raw && !isCoercibleNumber(raw.idleShutdownSeconds)) {
335
+ return "idleShutdownSeconds is present but not a number";
336
+ }
337
+ if ("budget" in raw) {
338
+ const budget = raw.budget;
339
+ if (!isRecord(budget)) {
340
+ return "budget is present but not an object";
341
+ }
342
+ const numericKeys = ["pauseAt", "resumeBelow", "pollSeconds", "syncDriftPct"];
343
+ for (const key of numericKeys) {
344
+ if (key in budget && !isCoercibleNumber(budget[key])) {
345
+ return `budget.${key} is present but not a number`;
346
+ }
347
+ }
348
+ if ("parallel" in budget) {
349
+ const parallel = budget.parallel;
350
+ if (!isRecord(parallel)) {
351
+ return "budget.parallel is present but not an object";
352
+ }
353
+ for (const key of ["minRemainingPct", "timeWindowSec"]) {
354
+ if (key in parallel && !isCoercibleNumber(parallel[key])) {
355
+ return `budget.parallel.${key} is present but not a number`;
356
+ }
357
+ }
358
+ }
359
+ }
360
+ return null;
361
+ }
362
+ function hasCustomDecisionValues(config) {
363
+ const d = DEFAULT_CONFIG;
364
+ const b = config.budget;
365
+ const db = d.budget;
366
+ return config.idleShutdownSeconds !== d.idleShutdownSeconds || config.turnCoordination.attentionWindowSeconds !== d.turnCoordination.attentionWindowSeconds || config.codex.appPort !== d.codex.appPort || config.codex.proxyPort !== d.codex.proxyPort || b.enabled !== db.enabled || b.pollSeconds !== db.pollSeconds || b.pauseAt !== db.pauseAt || b.resumeBelow !== db.resumeBelow || b.syncDriftPct !== db.syncDriftPct || b.parallel.minRemainingPct !== db.parallel.minRemainingPct || b.parallel.timeWindowSec !== db.parallel.timeWindowSec || b.codexTierControl !== db.codexTierControl;
367
+ }
326
368
  function normalizeInteger(value, fallback) {
327
369
  if (typeof value === "number" && Number.isFinite(value))
328
370
  return value;
@@ -358,35 +400,35 @@ function normalizeCodexOverride(raw) {
358
400
  override.effort = raw.effort.trim();
359
401
  return Object.keys(override).length > 0 ? override : null;
360
402
  }
361
- function normalizeCodexTiers(raw) {
403
+ function normalizeCodexTiers(raw, fallback = DEFAULT_BUDGET_CONFIG.codexTiers) {
362
404
  const tiers = isRecord(raw) ? raw : {};
363
405
  return {
364
406
  full: normalizeCodexOverride(tiers.full),
365
- balanced: normalizeCodexOverride(tiers.balanced) ?? DEFAULT_BUDGET_CONFIG.codexTiers.balanced,
366
- eco: normalizeCodexOverride(tiers.eco) ?? DEFAULT_BUDGET_CONFIG.codexTiers.eco
407
+ balanced: normalizeCodexOverride(tiers.balanced) ?? fallback.balanced,
408
+ eco: normalizeCodexOverride(tiers.eco) ?? fallback.eco
367
409
  };
368
410
  }
369
- function normalizeBudgetConfig(raw) {
411
+ function normalizeBudgetConfig(raw, fallback = DEFAULT_BUDGET_CONFIG) {
370
412
  const budget = isRecord(raw) ? raw : {};
371
413
  const parallel = isRecord(budget.parallel) ? budget.parallel : {};
372
- const codexTiers = normalizeCodexTiers(budget.codexTiers);
373
- let pauseAt = normalizeBoundedInteger(budget.pauseAt, DEFAULT_BUDGET_CONFIG.pauseAt, 1, 100);
374
- let resumeBelow = normalizeBoundedInteger(budget.resumeBelow, DEFAULT_BUDGET_CONFIG.resumeBelow, 0, 99);
414
+ const codexTiers = normalizeCodexTiers(budget.codexTiers, fallback.codexTiers);
415
+ let pauseAt = normalizeBoundedInteger(budget.pauseAt, fallback.pauseAt, 1, 100);
416
+ let resumeBelow = normalizeBoundedInteger(budget.resumeBelow, fallback.resumeBelow, 0, 99);
375
417
  if (pauseAt <= resumeBelow) {
376
418
  pauseAt = DEFAULT_BUDGET_CONFIG.pauseAt;
377
419
  resumeBelow = DEFAULT_BUDGET_CONFIG.resumeBelow;
378
420
  }
379
421
  return {
380
- enabled: normalizeBoolean(budget.enabled, DEFAULT_BUDGET_CONFIG.enabled),
381
- pollSeconds: normalizeBoundedInteger(budget.pollSeconds, DEFAULT_BUDGET_CONFIG.pollSeconds, 5, 3600),
422
+ enabled: normalizeBoolean(budget.enabled, fallback.enabled),
423
+ pollSeconds: normalizeBoundedInteger(budget.pollSeconds, fallback.pollSeconds, 5, 3600),
382
424
  pauseAt,
383
425
  resumeBelow,
384
- syncDriftPct: normalizeBoundedInteger(budget.syncDriftPct, DEFAULT_BUDGET_CONFIG.syncDriftPct, 1, 100),
426
+ syncDriftPct: normalizeBoundedInteger(budget.syncDriftPct, fallback.syncDriftPct, 1, 100),
385
427
  parallel: {
386
- minRemainingPct: normalizeBoundedInteger(parallel.minRemainingPct, DEFAULT_BUDGET_CONFIG.parallel.minRemainingPct, 1, 100),
387
- timeWindowSec: normalizeBoundedInteger(parallel.timeWindowSec, DEFAULT_BUDGET_CONFIG.parallel.timeWindowSec, 60, 604800)
428
+ minRemainingPct: normalizeBoundedInteger(parallel.minRemainingPct, fallback.parallel.minRemainingPct, 1, 100),
429
+ timeWindowSec: normalizeBoundedInteger(parallel.timeWindowSec, fallback.parallel.timeWindowSec, 60, 604800)
388
430
  },
389
- codexTierControl: normalizeBoolean(budget.codexTierControl, DEFAULT_BUDGET_CONFIG.codexTierControl) && codexTiers.full !== null,
431
+ codexTierControl: normalizeBoolean(budget.codexTierControl, fallback.codexTierControl) && codexTiers.full !== null,
390
432
  codexTiers
391
433
  };
392
434
  }
@@ -423,15 +465,59 @@ class ConfigService {
423
465
  return existsSync2(this.configPath);
424
466
  }
425
467
  load() {
468
+ let raw;
426
469
  try {
427
- const raw = readFileSync2(this.configPath, "utf-8");
428
- return normalizeConfig(JSON.parse(raw));
429
- } catch {
430
- return null;
470
+ raw = readFileSync2(this.configPath, "utf-8");
471
+ } catch (err) {
472
+ if (err?.code === "ENOENT") {
473
+ return { state: "absent" };
474
+ }
475
+ return { state: "corrupt", reason: `config.json is unreadable: ${err.message}` };
431
476
  }
477
+ let parsed;
478
+ try {
479
+ parsed = JSON.parse(raw);
480
+ } catch (err) {
481
+ return {
482
+ state: "corrupt",
483
+ reason: `config.json is not valid JSON: ${err.message}`
484
+ };
485
+ }
486
+ if (!isRecord(parsed)) {
487
+ return { state: "corrupt", reason: "config.json is not a JSON object" };
488
+ }
489
+ const violation = findShapeViolation(parsed);
490
+ if (violation) {
491
+ return { state: "corrupt", reason: `config.json is shape-invalid: ${violation}` };
492
+ }
493
+ const config = normalizeConfig(parsed);
494
+ if (!config) {
495
+ return { state: "corrupt", reason: "config.json could not be normalized" };
496
+ }
497
+ return { state: "parsed", config };
498
+ }
499
+ loadOrDefault(log = NOOP_LOGGER) {
500
+ const result = this.load();
501
+ if (result.state === "parsed")
502
+ return result.config;
503
+ if (result.state === "corrupt") {
504
+ log(`config.json at ${this.configPath} is unusable (${result.reason}); ` + "falling back to defaults \u2014 your custom budget thresholds / idle-shutdown settings are NOT in effect. " + "Fix the file and restart to re-apply them.");
505
+ }
506
+ return structuredClone(DEFAULT_CONFIG);
432
507
  }
433
- loadOrDefault() {
434
- return this.load() ?? structuredClone(DEFAULT_CONFIG);
508
+ describeConfig() {
509
+ const result = this.load();
510
+ if (result.state === "absent") {
511
+ return { state: "absent", path: this.configPath, customValues: false };
512
+ }
513
+ if (result.state === "corrupt") {
514
+ return { state: "corrupt", path: this.configPath, reason: result.reason, customValues: false };
515
+ }
516
+ return {
517
+ state: "parsed",
518
+ path: this.configPath,
519
+ customValues: hasCustomDecisionValues(result.config)
520
+ };
435
521
  }
436
522
  save(config) {
437
523
  this.ensureConfigDir();
@@ -456,11 +542,11 @@ class ConfigService {
456
542
  }
457
543
  }
458
544
  }
459
- var DEFAULT_BUDGET_CONFIG, DEFAULT_CONFIG, CONFIG_DIR = ".agentbridge", CONFIG_FILE = "config.json";
545
+ var DEFAULT_BUDGET_CONFIG, DEFAULT_CONFIG, CONFIG_DIR = ".agentbridge", CONFIG_FILE = "config.json", NOOP_LOGGER = () => {};
460
546
  var init_config_service = __esm(() => {
461
547
  DEFAULT_BUDGET_CONFIG = {
462
548
  enabled: true,
463
- pollSeconds: 60,
549
+ pollSeconds: 300,
464
550
  pauseAt: 90,
465
551
  resumeBelow: 30,
466
552
  syncDriftPct: 10,
@@ -513,6 +599,30 @@ function registerMarketplace(marketplaceRoot) {
513
599
  }
514
600
  var init_pkg_root = () => {};
515
601
 
602
+ // src/cli/plugin-cache.ts
603
+ import { existsSync as existsSync4 } from "fs";
604
+ import { homedir as homedir2 } from "os";
605
+ import { join as join4, resolve } from "path";
606
+ function pluginCacheRoot(home = homedir2()) {
607
+ return join4(home, ".claude", "plugins", "cache", MARKETPLACE_NAME, PLUGIN_NAME);
608
+ }
609
+ function isInsideRepoCheckout(projectRoot) {
610
+ const buildScript = resolve(projectRoot, "scripts", "build-bundles.mjs");
611
+ return existsSync4(buildScript);
612
+ }
613
+ function shouldWarnMissingPluginCache(cacheExists) {
614
+ return !cacheExists;
615
+ }
616
+ var MARKETPLACE_STEPS;
617
+ var init_plugin_cache = __esm(() => {
618
+ init_cli();
619
+ MARKETPLACE_STEPS = [
620
+ `/plugin marketplace add raysonmeng/agent-bridge`,
621
+ `/plugin install ${PLUGIN_NAME}@${MARKETPLACE_NAME}`,
622
+ `/reload-plugins`
623
+ ];
624
+ });
625
+
516
626
  // src/marker-section.ts
517
627
  function upsertMarkedSection(content, sectionId, section) {
518
628
  const startMarker = MARKER_START(sectionId);
@@ -649,11 +759,12 @@ var exports_init = {};
649
759
  __export(exports_init, {
650
760
  writeCollaborationSections: () => writeCollaborationSections,
651
761
  runInit: () => runInit,
762
+ pluginInstallFallbackGuidance: () => pluginInstallFallbackGuidance,
652
763
  compareVersions: () => compareVersions
653
764
  });
654
765
  import { execSync, execFileSync as execFileSync2 } from "child_process";
655
766
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
656
- import { join as join4 } from "path";
767
+ import { join as join5 } from "path";
657
768
  async function runInit() {
658
769
  console.log(`AgentBridge Init
659
770
  `);
@@ -681,25 +792,54 @@ async function runInit() {
681
792
  }
682
793
  console.log("");
683
794
  console.log("Installing AgentBridge plugin...");
795
+ let pluginInstalled = false;
684
796
  try {
685
- registerMarketplace(findPackageRoot());
797
+ const packageRoot = findPackageRoot();
798
+ registerMarketplace(packageRoot);
686
799
  execFileSync2("claude", ["plugin", "install", `${PLUGIN_NAME}@${MARKETPLACE_NAME}`], {
687
800
  stdio: "inherit"
688
801
  });
689
802
  console.log(" Plugin installed successfully.");
803
+ pluginInstalled = true;
690
804
  } catch {
691
805
  console.log(" Plugin install skipped (marketplace registration or install failed).");
692
- console.log(" You can install it later with:");
693
- console.log(` abg dev # registers marketplace and installs plugin`);
806
+ for (const line of pluginInstallFallbackGuidance(detectRepoCheckout())) {
807
+ console.log(line);
808
+ }
694
809
  }
695
810
  console.log("");
696
- console.log(`Setup complete!
811
+ if (pluginInstalled) {
812
+ console.log(`Setup complete!
813
+ `);
814
+ } else {
815
+ console.log(`Setup incomplete \u2014 plugin not installed.
697
816
  `);
817
+ process.exitCode = 1;
818
+ }
698
819
  console.log("Next steps:");
699
820
  console.log(" 1. If Claude Code is already running, execute /reload-plugins in your session");
700
821
  console.log(" 2. Start Claude Code: agentbridge claude");
701
822
  console.log(" 3. Start Codex TUI: agentbridge codex");
702
823
  }
824
+ function detectRepoCheckout() {
825
+ try {
826
+ return isInsideRepoCheckout(findPackageRoot());
827
+ } catch {
828
+ return false;
829
+ }
830
+ }
831
+ function pluginInstallFallbackGuidance(insideRepo) {
832
+ if (insideRepo) {
833
+ return [
834
+ " You can install it later with:",
835
+ " abg dev # registers marketplace and installs plugin"
836
+ ];
837
+ }
838
+ return [
839
+ " Install the plugin from Claude Code with these steps:",
840
+ ...MARKETPLACE_STEPS.map((step) => ` ${step}`)
841
+ ];
842
+ }
703
843
  function checkBun() {
704
844
  try {
705
845
  const version = execSync("bun --version", { encoding: "utf-8" }).trim();
@@ -745,8 +885,8 @@ function checkCodex() {
745
885
  function writeCollaborationSections(projectRoot) {
746
886
  const results = [];
747
887
  const files = [
748
- { name: "CLAUDE.md", path: join4(projectRoot, "CLAUDE.md"), section: CLAUDE_MD_SECTION },
749
- { name: "AGENTS.md", path: join4(projectRoot, "AGENTS.md"), section: AGENTS_MD_SECTION }
888
+ { name: "CLAUDE.md", path: join5(projectRoot, "CLAUDE.md"), section: CLAUDE_MD_SECTION },
889
+ { name: "AGENTS.md", path: join5(projectRoot, "AGENTS.md"), section: AGENTS_MD_SECTION }
750
890
  ];
751
891
  for (const { name, path, section } of files) {
752
892
  let existing = "";
@@ -781,6 +921,7 @@ var init_init = __esm(() => {
781
921
  init_config_service();
782
922
  init_cli();
783
923
  init_pkg_root();
924
+ init_plugin_cache();
784
925
  init_version_utils();
785
926
  });
786
927
 
@@ -790,19 +931,17 @@ __export(exports_dev, {
790
931
  runDev: () => runDev
791
932
  });
792
933
  import { execFileSync as execFileSync3, spawnSync } from "child_process";
793
- import { resolve } from "path";
794
- import { existsSync as existsSync4, cpSync, rmSync } from "fs";
795
- import { homedir as homedir2 } from "os";
934
+ import { resolve as resolve2 } from "path";
935
+ import { existsSync as existsSync5, cpSync, rmSync } from "fs";
796
936
  async function runDev(args = []) {
797
937
  console.log(`AgentBridge Dev Setup
798
938
  `);
799
939
  const skipBuild = args.includes("--skip-build");
800
940
  const projectRoot = findPackageRoot();
801
- const marketplacePath = resolve(projectRoot, ".claude-plugin", "marketplace.json");
802
- const pluginDir = resolve(projectRoot, "plugins", "agentbridge");
803
- const pluginManifest = resolve(pluginDir, ".claude-plugin", "plugin.json");
804
- const buildScript = resolve(projectRoot, "scripts", "build-bundles.mjs");
805
- if (!existsSync4(buildScript)) {
941
+ const marketplacePath = resolve2(projectRoot, ".claude-plugin", "marketplace.json");
942
+ const pluginDir = resolve2(projectRoot, "plugins", "agentbridge");
943
+ const pluginManifest = resolve2(pluginDir, ".claude-plugin", "plugin.json");
944
+ if (!isInsideRepoCheckout(projectRoot)) {
806
945
  console.error(" ERROR: 'agentbridge dev' must run inside an AgentBridge repository checkout \u2014");
807
946
  console.error(" the published package does not ship the build scripts.");
808
947
  console.error("");
@@ -839,12 +978,12 @@ async function runDev(args = []) {
839
978
  console.log(` \u2713 Plugin built successfully
840
979
  `);
841
980
  }
842
- if (!existsSync4(pluginManifest)) {
981
+ if (!existsSync5(pluginManifest)) {
843
982
  console.error(` ERROR: Plugin manifest not found at ${pluginManifest}`);
844
983
  console.error(" Run 'bun run build:plugin' first, or check your working tree.");
845
984
  process.exit(1);
846
985
  }
847
- if (!existsSync4(marketplacePath)) {
986
+ if (!existsSync5(marketplacePath)) {
848
987
  console.error(` ERROR: Marketplace manifest not found at ${marketplacePath}`);
849
988
  process.exit(1);
850
989
  }
@@ -873,12 +1012,12 @@ Installing plugin...`);
873
1012
  }
874
1013
  console.log(`
875
1014
  Syncing local plugin to cache...`);
876
- const cacheDir = resolve(homedir2(), ".claude", "plugins", "cache", MARKETPLACE_NAME, PLUGIN_NAME);
877
- if (existsSync4(cacheDir)) {
1015
+ const cacheDir = pluginCacheRoot();
1016
+ if (existsSync5(cacheDir)) {
878
1017
  const versionDirs = Bun.spawnSync(["ls", cacheDir]).stdout.toString().trim().split(`
879
1018
  `).filter(Boolean);
880
1019
  for (const ver of versionDirs) {
881
- const targetDir = resolve(cacheDir, ver);
1020
+ const targetDir = resolve2(cacheDir, ver);
882
1021
  rmSync(targetDir, { recursive: true, force: true });
883
1022
  cpSync(pluginDir, targetDir, { recursive: true });
884
1023
  console.log(` Synced to ${targetDir}`);
@@ -898,15 +1037,23 @@ Syncing local plugin to cache...`);
898
1037
  var init_dev = __esm(() => {
899
1038
  init_cli();
900
1039
  init_pkg_root();
1040
+ init_plugin_cache();
901
1041
  });
902
1042
 
903
1043
  // src/control-protocol.ts
904
1044
  var CLOSE_CODE_REPLACED = 4001, CLOSE_CODE_EVICTED_STALE = 4002, CLOSE_CODE_PROBE_IN_PROGRESS = 4003, CLOSE_CODE_PAIR_MISMATCH = 4004;
905
1045
 
1046
+ // src/interrupt-timing.ts
1047
+ var CLIENT_REPLY_TIMEOUT_MS = 15000, INTERRUPT_CLIENT_MARGIN_MS = 2000, MAX_INTERRUPT_TIMEOUT_MS;
1048
+ var init_interrupt_timing = __esm(() => {
1049
+ MAX_INTERRUPT_TIMEOUT_MS = CLIENT_REPLY_TIMEOUT_MS - INTERRUPT_CLIENT_MARGIN_MS;
1050
+ });
1051
+
906
1052
  // src/daemon-client.ts
907
1053
  import { EventEmitter } from "events";
908
1054
  var nextSocketId = 0, DaemonClient;
909
1055
  var init_daemon_client = __esm(() => {
1056
+ init_interrupt_timing();
910
1057
  DaemonClient = class DaemonClient extends EventEmitter {
911
1058
  url;
912
1059
  options;
@@ -933,7 +1080,7 @@ var init_daemon_client = __esm(() => {
933
1080
  this.ws = null;
934
1081
  }
935
1082
  const socketId = ++nextSocketId;
936
- await new Promise((resolve2, reject) => {
1083
+ await new Promise((resolve3, reject) => {
937
1084
  const ws = new WebSocket(this.url);
938
1085
  let settled = false;
939
1086
  ws.onopen = () => {
@@ -942,7 +1089,7 @@ var init_daemon_client = __esm(() => {
942
1089
  this.wsId = socketId;
943
1090
  this.attachSocketHandlers(ws, socketId);
944
1091
  this.log(`ws#${socketId} opened and attached`);
945
- resolve2();
1092
+ resolve3();
946
1093
  };
947
1094
  ws.onerror = () => {
948
1095
  if (settled)
@@ -968,7 +1115,7 @@ var init_daemon_client = __esm(() => {
968
1115
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
969
1116
  return null;
970
1117
  }
971
- return await new Promise((resolve2) => {
1118
+ return await new Promise((resolve3) => {
972
1119
  let settled = false;
973
1120
  let timer = null;
974
1121
  const cleanup = () => {
@@ -985,7 +1132,7 @@ var init_daemon_client = __esm(() => {
985
1132
  };
986
1133
  const finish = (value) => {
987
1134
  cleanup();
988
- resolve2(value);
1135
+ resolve3(value);
989
1136
  };
990
1137
  const onStatus = (status) => finish(status);
991
1138
  const onRejected = () => finish(null);
@@ -1007,7 +1154,7 @@ var init_daemon_client = __esm(() => {
1007
1154
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
1008
1155
  return { connected: false, alive: false };
1009
1156
  }
1010
- return await new Promise((resolve2) => {
1157
+ return await new Promise((resolve3) => {
1011
1158
  let settled = false;
1012
1159
  let timer = null;
1013
1160
  const finish = (value) => {
@@ -1019,7 +1166,7 @@ var init_daemon_client = __esm(() => {
1019
1166
  this.off("incumbentStatus", onStatus);
1020
1167
  this.off("disconnect", onDisconnect);
1021
1168
  this.off("rejected", onRejected);
1022
- resolve2(value);
1169
+ resolve3(value);
1023
1170
  };
1024
1171
  const onStatus = (s) => finish(s);
1025
1172
  const onDisconnect = () => finish({ connected: false, alive: false });
@@ -1047,23 +1194,24 @@ var init_daemon_client = __esm(() => {
1047
1194
  this.ws = null;
1048
1195
  this.rejectPendingReplies("Daemon connection closed");
1049
1196
  }
1050
- async sendReply(message, requireReply, onBusy) {
1197
+ async sendReply(message, requireReply, onBusy, idempotencyKey) {
1051
1198
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
1052
1199
  return { success: false, error: "AgentBridge daemon is not connected." };
1053
1200
  }
1054
1201
  const requestId = `reply_${Date.now()}_${this.nextRequestId++}`;
1055
- return new Promise((resolve2) => {
1202
+ return new Promise((resolve3) => {
1056
1203
  const timer = setTimeout(() => {
1057
1204
  this.pendingReplies.delete(requestId);
1058
- resolve2({ success: false, error: "Timed out waiting for AgentBridge daemon reply." });
1059
- }, 15000);
1060
- this.pendingReplies.set(requestId, { resolve: resolve2, timer });
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 });
1061
1208
  this.send({
1062
1209
  type: "claude_to_codex",
1063
1210
  requestId,
1064
1211
  message,
1065
1212
  ...requireReply ? { requireReply: true } : {},
1066
- ...onBusy && onBusy !== "reject" ? { onBusy } : {}
1213
+ ...onBusy && onBusy !== "reject" ? { onBusy } : {},
1214
+ ...idempotencyKey ? { idempotencyKey } : {}
1067
1215
  });
1068
1216
  });
1069
1217
  }
@@ -1086,9 +1234,23 @@ var init_daemon_client = __esm(() => {
1086
1234
  return;
1087
1235
  clearTimeout(pending.timer);
1088
1236
  this.pendingReplies.delete(message.requestId);
1089
- pending.resolve({ success: message.success, error: message.error });
1237
+ pending.resolve({
1238
+ success: message.success,
1239
+ error: message.error,
1240
+ ...message.code !== undefined ? { code: message.code } : {},
1241
+ ...message.phase !== undefined ? { phase: message.phase } : {},
1242
+ ...message.retryAfterMs !== undefined ? { retryAfterMs: message.retryAfterMs } : {}
1243
+ });
1090
1244
  return;
1091
1245
  }
1246
+ case "turn_started":
1247
+ this.emit("turnStarted", {
1248
+ requestId: message.requestId,
1249
+ ...message.idempotencyKey !== undefined ? { idempotencyKey: message.idempotencyKey } : {},
1250
+ threadId: message.threadId,
1251
+ turnId: message.turnId
1252
+ });
1253
+ return;
1092
1254
  case "status":
1093
1255
  this.emit("status", message.status);
1094
1256
  return;
@@ -1165,17 +1327,217 @@ function formatBuildInfo(build) {
1165
1327
  var BUILD_INFO;
1166
1328
  var init_build_info = __esm(() => {
1167
1329
  BUILD_INFO = Object.freeze({
1168
- version: defineString("0.1.10", "0.0.0-source"),
1169
- commit: defineString("51a44cb", "source"),
1330
+ version: defineString("0.1.12", "0.0.0-source"),
1331
+ commit: defineString("eec6018", "source"),
1170
1332
  bundle: defineBundle("dist"),
1171
1333
  contractVersion: defineNumber(1, CONTRACT_VERSION)
1172
1334
  });
1173
1335
  });
1174
1336
 
1337
+ // src/process-lifecycle.ts
1338
+ import { execFileSync as execFileSync4 } from "child_process";
1339
+ import { basename } from "path";
1340
+ function parsePsProcessList(output) {
1341
+ const entries = [];
1342
+ for (const line of output.split(/\r?\n/)) {
1343
+ const match = line.match(/^\s*(\d+)\s+(.+?)\s*$/);
1344
+ if (!match)
1345
+ continue;
1346
+ const pid = Number.parseInt(match[1], 10);
1347
+ if (!Number.isFinite(pid))
1348
+ continue;
1349
+ entries.push({ pid, command: match[2] });
1350
+ }
1351
+ return entries;
1352
+ }
1353
+ function invokesCodexBinary(command) {
1354
+ const tokens = command.trim().split(/\s+/);
1355
+ const exe = tokens[0] ? basename(tokens[0]) : "";
1356
+ if (exe === "codex")
1357
+ return true;
1358
+ if ((exe === "node" || exe === "bun") && tokens[1]) {
1359
+ return basename(tokens[1]) === "codex";
1360
+ }
1361
+ return false;
1362
+ }
1363
+ function commandMatchesManagedCodexTui(command, proxyUrl) {
1364
+ if (!invokesCodexBinary(command))
1365
+ return false;
1366
+ if (!command.includes("tui_app_server"))
1367
+ return false;
1368
+ const remoteUrl = extractRemoteUrl(command);
1369
+ if (!remoteUrl)
1370
+ return false;
1371
+ if (!proxyUrl)
1372
+ return true;
1373
+ return remoteTargetsProxy(remoteUrl, proxyUrl);
1374
+ }
1375
+ function findManagedCodexTuiProcessesFromList(processes, proxyUrl) {
1376
+ return processes.filter((entry) => commandMatchesManagedCodexTui(entry.command, proxyUrl));
1377
+ }
1378
+ function findManagedCodexTuiProcesses(proxyUrl) {
1379
+ try {
1380
+ const output = execFileSync4("ps", ["-axo", "pid=,command="], { encoding: "utf-8" });
1381
+ return findManagedCodexTuiProcessesFromList(parsePsProcessList(output), proxyUrl).filter((entry) => entry.pid !== process.pid);
1382
+ } catch {
1383
+ return [];
1384
+ }
1385
+ }
1386
+ function listManagedCodexTuiProcessesFromList(processes) {
1387
+ return processes.filter((entry) => commandMatchesManagedCodexTui(entry.command)).map((entry) => ({ ...entry, remoteUrl: extractRemoteUrl(entry.command) }));
1388
+ }
1389
+ function listManagedCodexTuiProcesses() {
1390
+ try {
1391
+ const output = execFileSync4("ps", ["-axo", "pid=,command="], { encoding: "utf-8" });
1392
+ return listManagedCodexTuiProcessesFromList(parsePsProcessList(output)).filter((entry) => entry.pid !== process.pid);
1393
+ } catch {
1394
+ return [];
1395
+ }
1396
+ }
1397
+ function listBridgeFrontendProcessesFromList(processes) {
1398
+ return processes.filter((entry) => /(?:^|[\s/\\])bridge-server\.js(?:\s|$)/.test(entry.command) && (entry.command.includes("agentbridge") || entry.command.includes("agent_bridge")));
1399
+ }
1400
+ function listBridgeFrontendProcesses() {
1401
+ try {
1402
+ const output = execFileSync4("ps", ["-axo", "pid=,command="], { encoding: "utf-8" });
1403
+ return listBridgeFrontendProcessesFromList(parsePsProcessList(output)).filter((entry) => entry.pid !== process.pid);
1404
+ } catch {
1405
+ return [];
1406
+ }
1407
+ }
1408
+ function commandForPid(pid) {
1409
+ try {
1410
+ return execFileSync4("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf-8" }).trim();
1411
+ } catch {
1412
+ return null;
1413
+ }
1414
+ }
1415
+ function pidLooksAlive(pid) {
1416
+ if (!Number.isInteger(pid) || pid <= 0)
1417
+ return false;
1418
+ try {
1419
+ process.kill(pid, 0);
1420
+ return true;
1421
+ } catch (err) {
1422
+ return err?.code === "EPERM";
1423
+ }
1424
+ }
1425
+ function isAgentBridgeDaemon(pid, lookup = commandForPid) {
1426
+ const cmd = lookup(pid);
1427
+ if (cmd === null)
1428
+ return false;
1429
+ const hasDaemonEntry = /(?:^|[\s/\\])[\w.-]*-?daemon\.(?:ts|js)(?:\s|$)/.test(cmd);
1430
+ const hasAgentbridge = cmd.includes("agentbridge") || cmd.includes("agent_bridge");
1431
+ return hasDaemonEntry && hasAgentbridge;
1432
+ }
1433
+ function isAgentBridgeProcess(pid, lookup = commandForPid) {
1434
+ const cmd = lookup(pid);
1435
+ if (cmd === null)
1436
+ return false;
1437
+ return cmd.includes("agentbridge") || cmd.includes("agent_bridge");
1438
+ }
1439
+ function terminateProcessSync(pid, options = {}) {
1440
+ const gracefulTimeoutMs = options.gracefulTimeoutMs ?? 2000;
1441
+ const target = options.processGroup && process.platform !== "win32" ? -pid : pid;
1442
+ const label = options.processGroup && process.platform !== "win32" ? `process group ${pid}` : `pid ${pid}`;
1443
+ try {
1444
+ process.kill(target, "SIGTERM");
1445
+ options.log?.(`Sent SIGTERM to ${label}`);
1446
+ } catch {
1447
+ return !isProcessAlive(pid);
1448
+ }
1449
+ if (waitForExitSync(pid, gracefulTimeoutMs))
1450
+ return true;
1451
+ try {
1452
+ process.kill(target, "SIGKILL");
1453
+ options.log?.(`Sent SIGKILL to ${label}`);
1454
+ } catch {}
1455
+ return waitForExitSync(pid, 500);
1456
+ }
1457
+ function waitForExitSync(pid, timeoutMs) {
1458
+ const deadline = Date.now() + timeoutMs;
1459
+ while (Date.now() < deadline) {
1460
+ if (!isProcessAlive(pid))
1461
+ return true;
1462
+ sleepSync(50);
1463
+ }
1464
+ return !isProcessAlive(pid);
1465
+ }
1466
+ function sleepSync(ms) {
1467
+ const buffer = new SharedArrayBuffer(4);
1468
+ const view = new Int32Array(buffer);
1469
+ Atomics.wait(view, 0, 0, ms);
1470
+ }
1471
+ function extractRemoteUrl(command) {
1472
+ const equals = command.match(/(?:^|\s)--remote=([^\s]+)/);
1473
+ if (equals)
1474
+ return equals[1];
1475
+ const separate = command.match(/(?:^|\s)--remote\s+([^\s]+)/);
1476
+ return separate?.[1] ?? null;
1477
+ }
1478
+ function remoteTargetsProxy(remoteUrl, proxyUrl) {
1479
+ try {
1480
+ const remote = new URL(remoteUrl);
1481
+ const proxy = new URL(proxyUrl);
1482
+ return remote.protocol === proxy.protocol && remote.hostname === proxy.hostname && remote.port === proxy.port && normalizePath(remote.pathname) === normalizePath(proxy.pathname);
1483
+ } catch {
1484
+ return remoteUrl === proxyUrl;
1485
+ }
1486
+ }
1487
+ function normalizePath(pathname) {
1488
+ return pathname === "" ? "/" : pathname;
1489
+ }
1490
+ var isProcessAlive;
1491
+ var init_process_lifecycle = __esm(() => {
1492
+ isProcessAlive = pidLooksAlive;
1493
+ });
1494
+
1175
1495
  // src/daemon-lifecycle.ts
1176
- import { spawn, execFileSync as execFileSync4 } from "child_process";
1177
- import { existsSync as existsSync5, readFileSync as readFileSync4, statSync, unlinkSync, writeFileSync as writeFileSync4, openSync, closeSync, constants } from "fs";
1496
+ import { spawn } from "child_process";
1497
+ import { existsSync as existsSync6, readFileSync as readFileSync4, statSync, unlinkSync, writeFileSync as writeFileSync4, openSync, closeSync, constants } from "fs";
1178
1498
  import { fileURLToPath } from "url";
1499
+ function isReuseVerdict(verdict) {
1500
+ return verdict === "reuse" || verdict === "reuse-despite-drift";
1501
+ }
1502
+ function classifyDaemon(expectedPairId, status, buildInfo) {
1503
+ if (!status) {
1504
+ return { verdict: "unreachable", reason: "daemon status is unavailable or unparseable" };
1505
+ }
1506
+ const reportedPairId = status.pairId;
1507
+ if (!expectedPairId && reportedPairId != null) {
1508
+ return {
1509
+ verdict: "manual-conflict",
1510
+ reason: `manual mode must not adopt registered pair ${reportedPairId}`
1511
+ };
1512
+ }
1513
+ if (expectedPairId) {
1514
+ if (reportedPairId == null) {
1515
+ return {
1516
+ verdict: "replace-foreign",
1517
+ reason: `pair ${expectedPairId} found daemon without pair identity`
1518
+ };
1519
+ }
1520
+ if (reportedPairId !== expectedPairId) {
1521
+ return {
1522
+ verdict: "replace-foreign",
1523
+ reason: `pair ${expectedPairId} found daemon for pair ${reportedPairId}`
1524
+ };
1525
+ }
1526
+ }
1527
+ if (!sameRuntimeContract(status.build, buildInfo)) {
1528
+ if (compatibleContractVersion(status.build, buildInfo) && status.tuiConnected === true) {
1529
+ return {
1530
+ verdict: "reuse-despite-drift",
1531
+ reason: "runtime build drift has a compatible contract and a live Codex TUI is attached"
1532
+ };
1533
+ }
1534
+ return {
1535
+ verdict: "replace-drifted",
1536
+ reason: `runtime build ${formatBuildInfo(status.build)} does not match launcher ${formatBuildInfo(buildInfo)}`
1537
+ };
1538
+ }
1539
+ return { verdict: "reuse", reason: "daemon pair and runtime contract match" };
1540
+ }
1179
1541
 
1180
1542
  class DaemonLifecycle {
1181
1543
  stateDir;
@@ -1208,52 +1570,37 @@ class DaemonLifecycle {
1208
1570
  return null;
1209
1571
  }
1210
1572
  }
1211
- isForeignDaemon(status) {
1212
- const expected = this.expectedPairId;
1213
- if (!expected)
1214
- return false;
1215
- if (!status)
1216
- return false;
1217
- const reported = status.pairId;
1218
- if (reported == null)
1219
- return true;
1220
- return reported !== expected;
1221
- }
1222
- isRegisteredPairDaemonInManualMode(status) {
1223
- return !this.expectedPairId && status?.pairId != null;
1224
- }
1225
- isBuildDrifted(status) {
1226
- if (process.env.AGENTBRIDGE_ALLOW_BUILD_DRIFT === "1")
1227
- return false;
1228
- const runtime = status?.build;
1229
- if (!runtime)
1230
- return true;
1231
- return !sameRuntimeContract(runtime, BUILD_INFO);
1573
+ classifyDaemon(status) {
1574
+ const classification = classifyDaemon(this.expectedPairId, status, BUILD_INFO);
1575
+ if (process.env.AGENTBRIDGE_ALLOW_BUILD_DRIFT === "1" && (classification.verdict === "replace-drifted" || classification.verdict === "unreachable")) {
1576
+ return { verdict: "reuse", reason: "build drift replacement disabled by AGENTBRIDGE_ALLOW_BUILD_DRIFT" };
1577
+ }
1578
+ return classification;
1232
1579
  }
1233
- canReuseDespiteDrift(status) {
1234
- if (!compatibleContractVersion(status?.build, BUILD_INFO))
1235
- return false;
1236
- return status?.tuiConnected === true;
1580
+ manualConflictError(status) {
1581
+ return new Error(`Control port ${this.controlPort} is owned by registered pair ${status?.pairId}. ` + `This session has no pair identity (manual mode) and will not reuse or replace it \u2014 ` + `start with \`agentbridge claude\` from that pair's directory, or set AGENTBRIDGE_CONTROL_PORT to a free port.`);
1237
1582
  }
1238
1583
  async ensureRunning() {
1239
1584
  if (await this.isHealthy()) {
1240
1585
  const status = await this.fetchStatus();
1241
- if (this.isRegisteredPairDaemonInManualMode(status)) {
1242
- throw new Error(`Control port ${this.controlPort} is owned by registered pair ${status?.pairId}. ` + `This session has no pair identity (manual mode) and will not reuse or replace it \u2014 ` + `start with \`agentbridge claude\` from that pair's directory, or set AGENTBRIDGE_CONTROL_PORT to a free port.`);
1243
- }
1244
- if (this.isForeignDaemon(status)) {
1245
- this.log(`Control port ${this.controlPort} held by a daemon for pair ${status?.pairId ?? "<none>"}, ` + `but this pair is ${this.expectedPairId} \u2014 replacing foreign daemon`);
1246
- await this.replaceUnhealthyDaemon(status?.pid);
1247
- return;
1248
- }
1249
- if (this.isBuildDrifted(status)) {
1250
- if (this.canReuseDespiteDrift(status)) {
1251
- this.log(`Daemon on control port ${this.controlPort} is running build ${formatBuildInfo(status?.build)} ` + `(launcher ${formatBuildInfo(BUILD_INFO)}) but a live Codex TUI is attached \u2014 reusing instead of ` + `replacing; the new build is picked up at the next restart (abg kill, then relaunch)`);
1252
- } else {
1586
+ const classification = this.classifyDaemon(status);
1587
+ switch (classification.verdict) {
1588
+ case "manual-conflict":
1589
+ throw this.manualConflictError(status);
1590
+ case "replace-foreign":
1591
+ this.log(`Control port ${this.controlPort} held by a daemon for pair ${status?.pairId ?? "<none>"}, ` + `but this pair is ${this.expectedPairId} \u2014 replacing foreign daemon`);
1592
+ await this.replaceUnhealthyDaemon(status?.pid);
1593
+ return;
1594
+ case "replace-drifted":
1595
+ case "unreachable":
1253
1596
  this.log(`Daemon on control port ${this.controlPort} is running build ${formatBuildInfo(status?.build)} ` + `but launcher is ${formatBuildInfo(BUILD_INFO)} \u2014 replacing drifted daemon`);
1254
1597
  await this.replaceUnhealthyDaemon(status?.pid);
1255
1598
  return;
1256
- }
1599
+ case "reuse-despite-drift":
1600
+ this.log(`Daemon on control port ${this.controlPort} is running build ${formatBuildInfo(status?.build)} ` + `(launcher ${formatBuildInfo(BUILD_INFO)}) but a live Codex TUI is attached \u2014 reusing instead of ` + `replacing; the new build is picked up at the next restart (abg kill, then relaunch)`);
1601
+ break;
1602
+ case "reuse":
1603
+ break;
1257
1604
  }
1258
1605
  try {
1259
1606
  await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
@@ -1267,7 +1614,7 @@ class DaemonLifecycle {
1267
1614
  const existingPid = this.readPid();
1268
1615
  if (existingPid) {
1269
1616
  if (isProcessAlive(existingPid)) {
1270
- if (this.isDaemonProcess(existingPid)) {
1617
+ if (isAgentBridgeDaemon(existingPid)) {
1271
1618
  try {
1272
1619
  await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
1273
1620
  return;
@@ -1283,14 +1630,17 @@ class DaemonLifecycle {
1283
1630
  }
1284
1631
  await this.withStartupLockStrict(async (locked) => {
1285
1632
  if (!locked) {
1286
- this.log("Another process holds the startup lock, waiting for readiness+identity...");
1287
- await this.waitForReadyAndOurs();
1633
+ await this.waitForContendedStartupLock();
1288
1634
  return;
1289
1635
  }
1290
1636
  if (await this.isHealthy()) {
1291
1637
  const status = await this.fetchStatus();
1292
- if (this.isForeignDaemon(status) || this.isBuildDrifted(status) && !this.canReuseDespiteDrift(status)) {
1293
- this.log(`Daemon on control port ${this.controlPort} is not reusable under startup lock ` + `(pair=${status?.pairId ?? "<none>"}, build=${formatBuildInfo(status?.build)}) \u2014 replacing`);
1638
+ const classification = this.classifyDaemon(status);
1639
+ if (classification.verdict === "manual-conflict") {
1640
+ throw this.manualConflictError(status);
1641
+ }
1642
+ if (!isReuseVerdict(classification.verdict)) {
1643
+ this.log(`Daemon on control port ${this.controlPort} is not reusable under startup lock ` + `(pair=${status?.pairId ?? "<none>"}, build=${formatBuildInfo(status?.build)}, ` + `reason=${classification.reason}) \u2014 replacing`);
1294
1644
  await this.kill(3000, status?.pid);
1295
1645
  } else {
1296
1646
  try {
@@ -1318,7 +1668,7 @@ class DaemonLifecycle {
1318
1668
  for (let attempt = 0;attempt < maxRetries; attempt++) {
1319
1669
  if (await this.isHealthy())
1320
1670
  return;
1321
- await new Promise((resolve2) => setTimeout(resolve2, delayMs));
1671
+ await new Promise((resolve3) => setTimeout(resolve3, delayMs));
1322
1672
  }
1323
1673
  throw new Error(`Timed out waiting for AgentBridge daemon health on ${this.healthUrl}`);
1324
1674
  }
@@ -1334,7 +1684,7 @@ class DaemonLifecycle {
1334
1684
  for (let attempt = 0;attempt < maxRetries; attempt++) {
1335
1685
  if (await this.isReady())
1336
1686
  return;
1337
- await new Promise((resolve2) => setTimeout(resolve2, delayMs));
1687
+ await new Promise((resolve3) => setTimeout(resolve3, delayMs));
1338
1688
  }
1339
1689
  throw new Error(`Timed out waiting for AgentBridge daemon readiness on ${this.readyUrl}`);
1340
1690
  }
@@ -1342,11 +1692,15 @@ class DaemonLifecycle {
1342
1692
  for (let attempt = 0;attempt < maxRetries; attempt++) {
1343
1693
  if (await this.isReady()) {
1344
1694
  const status = await this.fetchStatus();
1345
- if (!this.isForeignDaemon(status) && (!this.isBuildDrifted(status) || this.canReuseDespiteDrift(status))) {
1695
+ const classification = this.classifyDaemon(status);
1696
+ if (classification.verdict === "manual-conflict") {
1697
+ throw this.manualConflictError(status);
1698
+ }
1699
+ if (isReuseVerdict(classification.verdict)) {
1346
1700
  return;
1347
1701
  }
1348
1702
  }
1349
- await new Promise((resolve2) => setTimeout(resolve2, delayMs));
1703
+ await new Promise((resolve3) => setTimeout(resolve3, delayMs));
1350
1704
  }
1351
1705
  throw new Error(`Timed out waiting for AgentBridge daemon readiness+identity on ${this.readyUrl} (control port ${this.controlPort})`);
1352
1706
  }
@@ -1400,7 +1754,7 @@ class DaemonLifecycle {
1400
1754
  } catch {}
1401
1755
  }
1402
1756
  wasKilled() {
1403
- return existsSync5(this.stateDir.killedFile);
1757
+ return existsSync6(this.stateDir.killedFile);
1404
1758
  }
1405
1759
  launch() {
1406
1760
  this.stateDir.ensure();
@@ -1424,13 +1778,16 @@ class DaemonLifecycle {
1424
1778
  async replaceUnhealthyDaemon(statusPid) {
1425
1779
  await this.withStartupLockStrict(async (locked) => {
1426
1780
  if (!locked) {
1427
- this.log("Another process holds the startup lock, waiting for readiness+identity...");
1428
- await this.waitForReadyAndOurs();
1781
+ await this.waitForContendedStartupLock();
1429
1782
  return;
1430
1783
  }
1431
1784
  if (await this.isHealthy()) {
1432
1785
  const status = await this.fetchStatus();
1433
- if (!this.isForeignDaemon(status) && (!this.isBuildDrifted(status) || this.canReuseDespiteDrift(status))) {
1786
+ const classification = this.classifyDaemon(status);
1787
+ if (classification.verdict === "manual-conflict") {
1788
+ throw this.manualConflictError(status);
1789
+ }
1790
+ if (isReuseVerdict(classification.verdict)) {
1434
1791
  try {
1435
1792
  await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
1436
1793
  return;
@@ -1443,6 +1800,10 @@ class DaemonLifecycle {
1443
1800
  await this.waitForReady();
1444
1801
  });
1445
1802
  }
1803
+ async waitForContendedStartupLock() {
1804
+ this.log("Another process holds the startup lock, waiting for readiness+identity...");
1805
+ await this.waitForReadyAndOurs();
1806
+ }
1446
1807
  async withStartupLockStrict(fn) {
1447
1808
  const locked = this.acquireLockStrict();
1448
1809
  try {
@@ -1478,7 +1839,7 @@ class DaemonLifecycle {
1478
1839
  this.releaseLock();
1479
1840
  return this.acquireLockStrict(true);
1480
1841
  }
1481
- if (Number.isFinite(holderPid) && this.lockAgeMs() > LOCK_IDENTITY_GRACE_MS && !this.isAgentBridgeProcess(holderPid)) {
1842
+ if (Number.isFinite(holderPid) && this.lockAgeMs() > LOCK_IDENTITY_GRACE_MS && !isAgentBridgeProcess(holderPid)) {
1482
1843
  this.log(`Startup lock is ${Math.round(this.lockAgeMs() / 1000)}s old and holder pid ${holderPid} ` + `is an unrelated process (pid recycled), reclaiming`);
1483
1844
  this.releaseLock();
1484
1845
  return this.acquireLockStrict(true);
@@ -1499,14 +1860,6 @@ class DaemonLifecycle {
1499
1860
  return 0;
1500
1861
  }
1501
1862
  }
1502
- isAgentBridgeProcess(pid) {
1503
- try {
1504
- const cmd = execFileSync4("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf-8" }).trim();
1505
- return cmd.includes("agentbridge") || cmd.includes("agent_bridge");
1506
- } catch {
1507
- return false;
1508
- }
1509
- }
1510
1863
  releaseLock() {
1511
1864
  try {
1512
1865
  unlinkSync(this.stateDir.lockFile);
@@ -1524,7 +1877,7 @@ class DaemonLifecycle {
1524
1877
  this.cleanup();
1525
1878
  return false;
1526
1879
  }
1527
- if (!this.isDaemonProcess(pid)) {
1880
+ if (!isAgentBridgeDaemon(pid)) {
1528
1881
  this.log(`Pid ${pid} is alive but is NOT an AgentBridge daemon \u2014 refusing to kill. Cleaning up stale pid file.`);
1529
1882
  this.cleanup();
1530
1883
  return false;
@@ -1543,7 +1896,7 @@ class DaemonLifecycle {
1543
1896
  this.cleanup();
1544
1897
  return true;
1545
1898
  }
1546
- await new Promise((resolve2) => setTimeout(resolve2, 200));
1899
+ await new Promise((resolve3) => setTimeout(resolve3, 200));
1547
1900
  }
1548
1901
  this.log(`Daemon pid ${pid} did not stop gracefully, sending SIGKILL`);
1549
1902
  try {
@@ -1552,16 +1905,6 @@ class DaemonLifecycle {
1552
1905
  this.cleanup();
1553
1906
  return true;
1554
1907
  }
1555
- isDaemonProcess(pid) {
1556
- try {
1557
- const cmd = execFileSync4("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf-8" }).trim();
1558
- const hasDaemonEntry = /(?:^|[\s/\\])[\w.-]*-?daemon\.(?:ts|js)(?:\s|$)/.test(cmd);
1559
- const hasAgentbridge = cmd.includes("agentbridge") || cmd.includes("agent_bridge");
1560
- return hasDaemonEntry && hasAgentbridge;
1561
- } catch {
1562
- return false;
1563
- }
1564
- }
1565
1908
  cleanup() {
1566
1909
  this.removePidFile();
1567
1910
  this.removeStatusFile();
@@ -1576,17 +1919,10 @@ async function fetchWithTimeout(url, timeoutMs = HEALTH_FETCH_TIMEOUT_MS) {
1576
1919
  clearTimeout(timer);
1577
1920
  }
1578
1921
  }
1579
- function isProcessAlive(pid) {
1580
- try {
1581
- process.kill(pid, 0);
1582
- return true;
1583
- } catch {
1584
- return false;
1585
- }
1586
- }
1587
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;
1588
1923
  var init_daemon_lifecycle = __esm(() => {
1589
1924
  init_build_info();
1925
+ init_process_lifecycle();
1590
1926
  DEFAULT_DAEMON_ENTRY = import.meta.url.endsWith(".ts") ? "./daemon.ts" : "./daemon.js";
1591
1927
  DAEMON_ENTRY = process.env.AGENTBRIDGE_DAEMON_ENTRY || DEFAULT_DAEMON_ENTRY;
1592
1928
  DAEMON_PATH = fileURLToPath(new URL(DAEMON_ENTRY, import.meta.url));
@@ -1598,7 +1934,7 @@ var init_daemon_lifecycle = __esm(() => {
1598
1934
  import { execFileSync as execFileSync5 } from "child_process";
1599
1935
  import {
1600
1936
  closeSync as closeSync2,
1601
- existsSync as existsSync6,
1937
+ existsSync as existsSync7,
1602
1938
  fsyncSync,
1603
1939
  linkSync,
1604
1940
  lstatSync,
@@ -1616,7 +1952,7 @@ import {
1616
1952
  import { createServer } from "net";
1617
1953
  import { createHash, randomUUID } from "crypto";
1618
1954
  import { hostname, userInfo } from "os";
1619
- import { basename, join as join5, resolve as resolve2, sep } from "path";
1955
+ import { basename as basename2, join as join6, resolve as resolve3, sep } from "path";
1620
1956
  function portsForSlot(slot) {
1621
1957
  if (!Number.isInteger(slot) || slot < 0) {
1622
1958
  throw new PairError("PAIR_ID_INVALID", `Invalid slot: ${slot}`);
@@ -1654,14 +1990,14 @@ function pickLowestFreeSlot(entries) {
1654
1990
  return slot;
1655
1991
  }
1656
1992
  function pairsDir(base) {
1657
- return join5(base, "pairs");
1993
+ return join6(base, "pairs");
1658
1994
  }
1659
1995
  function registryPath(base) {
1660
- return join5(pairsDir(base), REGISTRY_FILE_NAME);
1996
+ return join6(pairsDir(base), REGISTRY_FILE_NAME);
1661
1997
  }
1662
1998
  function readRegistry(base) {
1663
1999
  const path = registryPath(base);
1664
- if (!existsSync6(path))
2000
+ if (!existsSync7(path))
1665
2001
  return { version: 1, pairs: [] };
1666
2002
  let parsed;
1667
2003
  try {
@@ -1711,7 +2047,7 @@ function writeRegistry(base, reg) {
1711
2047
  renameSync(tmp, target);
1712
2048
  }
1713
2049
  function lockFilePath(base) {
1714
- return join5(pairsDir(base), LOCK_FILE_NAME);
2050
+ return join6(pairsDir(base), LOCK_FILE_NAME);
1715
2051
  }
1716
2052
  function readLockOwner(lockFile) {
1717
2053
  try {
@@ -1723,16 +2059,6 @@ function readLockOwner(lockFile) {
1723
2059
  return null;
1724
2060
  }
1725
2061
  }
1726
- function pidLooksAlive(pid) {
1727
- if (!Number.isInteger(pid) || pid <= 0)
1728
- return false;
1729
- try {
1730
- process.kill(pid, 0);
1731
- return true;
1732
- } catch (err) {
1733
- return err?.code === "EPERM";
1734
- }
1735
- }
1736
2062
  function lockFileAgeMs(lockFile) {
1737
2063
  try {
1738
2064
  return Date.now() - statSync2(lockFile).mtimeMs;
@@ -1755,7 +2081,7 @@ function safeUid() {
1755
2081
  }
1756
2082
  }
1757
2083
  function sleep(ms) {
1758
- return new Promise((resolve3) => setTimeout(resolve3, ms));
2084
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
1759
2085
  }
1760
2086
  function lockIsStale(lockFile) {
1761
2087
  const owner = readLockOwner(lockFile);
@@ -1866,17 +2192,9 @@ async function withRegistryLock(base, fn) {
1866
2192
  await sleep(25 + Math.floor(Math.random() * 50));
1867
2193
  }
1868
2194
  }
1869
- function isDaemonProcess(pid) {
1870
- try {
1871
- const cmd = execFileSync5("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf-8" }).trim();
1872
- return cmd.includes("daemon") && (cmd.includes("agentbridge") || cmd.includes("agent_bridge"));
1873
- } catch {
1874
- return false;
1875
- }
1876
- }
1877
2195
  function detectLegacyRootDaemon(base) {
1878
- const rootPidFile = join5(base, "daemon.pid");
1879
- if (!existsSync6(rootPidFile))
2196
+ const rootPidFile = join6(base, "daemon.pid");
2197
+ if (!existsSync7(rootPidFile))
1880
2198
  return null;
1881
2199
  let pid;
1882
2200
  try {
@@ -1885,16 +2203,16 @@ function detectLegacyRootDaemon(base) {
1885
2203
  } catch {
1886
2204
  return null;
1887
2205
  }
1888
- if (!Number.isFinite(pid) || !pidLooksAlive(pid) || !isDaemonProcess(pid))
2206
+ if (!Number.isFinite(pid) || !pidLooksAlive(pid) || !isAgentBridgeDaemon(pid))
1889
2207
  return null;
1890
2208
  return { pid, controlPort: LEGACY_ROOT_CONTROL_PORT };
1891
2209
  }
1892
2210
  function probePortFree(port) {
1893
- return new Promise((resolve3) => {
2211
+ return new Promise((resolve4) => {
1894
2212
  const server = createServer();
1895
- server.once("error", () => resolve3(false));
2213
+ server.once("error", () => resolve4(false));
1896
2214
  server.once("listening", () => {
1897
- server.close(() => resolve3(true));
2215
+ server.close(() => resolve4(true));
1898
2216
  });
1899
2217
  server.listen(port, "127.0.0.1");
1900
2218
  });
@@ -1971,7 +2289,7 @@ async function resolvePair(base, opts) {
1971
2289
  pairId: entry.pairId,
1972
2290
  slot,
1973
2291
  ports,
1974
- stateDir: join5(pairsDir(base), entry.pairId),
2292
+ stateDir: join6(pairsDir(base), entry.pairId),
1975
2293
  name: entry.name ?? name,
1976
2294
  entry,
1977
2295
  warning
@@ -1979,7 +2297,7 @@ async function resolvePair(base, opts) {
1979
2297
  }
1980
2298
  async function removeAllocatedPairIfUnchanged(base, pairId, slot) {
1981
2299
  await withRegistryLock(base, () => {
1982
- if (existsSync6(pairDirPath(base, pairId)) || pairDirDaemonAlive(base, pairId))
2300
+ if (existsSync7(pairDirPath(base, pairId)) || pairDirDaemonAlive(base, pairId))
1983
2301
  return;
1984
2302
  const reg = readRegistry(base);
1985
2303
  const nextPairs = reg.pairs.filter((pair) => !(pair.pairId === pairId && pair.slot === slot));
@@ -1990,19 +2308,19 @@ async function removeAllocatedPairIfUnchanged(base, pairId, slot) {
1990
2308
  }
1991
2309
  function pairDirPath(base, pairId) {
1992
2310
  const id = validatePairId(pairId);
1993
- return join5(pairsDir(base), id);
2311
+ return join6(pairsDir(base), id);
1994
2312
  }
1995
2313
  function removePairDir(base, pairId) {
1996
2314
  const id = validatePairId(pairId);
1997
2315
  const root = pairsDir(base);
1998
- const dir = join5(root, id);
1999
- const canonicalRoot = resolve2(root);
2000
- const canonicalDir = resolve2(dir);
2316
+ const dir = join6(root, id);
2317
+ const canonicalRoot = resolve3(root);
2318
+ const canonicalDir = resolve3(dir);
2001
2319
  if (canonicalDir === canonicalRoot || !canonicalDir.startsWith(canonicalRoot + sep)) {
2002
2320
  throw new PairError("PAIR_ID_INVALID", `Refusing to remove a pair dir outside ${canonicalRoot}: ${canonicalDir}`, { pairId });
2003
2321
  }
2004
2322
  assertPairsRootNotSymlinked(root);
2005
- if (!existsSync6(canonicalDir))
2323
+ if (!existsSync7(canonicalDir))
2006
2324
  return false;
2007
2325
  rmSync2(canonicalDir, { recursive: true, force: true });
2008
2326
  return true;
@@ -2020,22 +2338,22 @@ function assertPairsRootNotSymlinked(root) {
2020
2338
  }
2021
2339
  function listPairDirs(base) {
2022
2340
  const root = pairsDir(base);
2023
- if (!existsSync6(root))
2341
+ if (!existsSync7(root))
2024
2342
  return [];
2025
2343
  if (lstatSync(root).isSymbolicLink())
2026
2344
  return [];
2027
2345
  return readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
2028
2346
  }
2029
2347
  function pairDirDaemonAlive(base, pairId) {
2030
- const dir = join5(pairsDir(base), pairId);
2348
+ const dir = join6(pairsDir(base), pairId);
2031
2349
  const pids = [];
2032
2350
  try {
2033
- const pid = Number.parseInt(readFileSync5(join5(dir, "daemon.pid"), "utf-8").trim(), 10);
2351
+ const pid = Number.parseInt(readFileSync5(join6(dir, "daemon.pid"), "utf-8").trim(), 10);
2034
2352
  if (Number.isFinite(pid))
2035
2353
  pids.push(pid);
2036
2354
  } catch {}
2037
2355
  try {
2038
- const status = JSON.parse(readFileSync5(join5(dir, "status.json"), "utf-8"));
2356
+ const status = JSON.parse(readFileSync5(join6(dir, "status.json"), "utf-8"));
2039
2357
  if (typeof status?.pid === "number")
2040
2358
  pids.push(status.pid);
2041
2359
  } catch {}
@@ -2071,6 +2389,7 @@ async function removeUnregisteredPairDir(base, pairId) {
2071
2389
  }
2072
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;
2073
2391
  var init_pair_registry = __esm(() => {
2392
+ init_process_lifecycle();
2074
2393
  PAIR_ID_REGEX = /^[A-Za-z0-9._-]{1,64}$/;
2075
2394
  WINDOWS_RESERVED_RE = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
2076
2395
  PairError = class PairError extends Error {
@@ -2167,7 +2486,7 @@ var init_env_guard = __esm(() => {
2167
2486
 
2168
2487
  // src/pair-resolver.ts
2169
2488
  import { realpathSync as realpathSync2 } from "fs";
2170
- import { join as join6, resolve as resolve3 } from "path";
2489
+ import { join as join7, resolve as resolve4 } from "path";
2171
2490
  function computeBaseDir() {
2172
2491
  return process.env.AGENTBRIDGE_BASE_DIR || process.env.AGENTBRIDGE_STATE_DIR || StateDirResolver.platformBaseDir();
2173
2492
  }
@@ -2294,7 +2613,7 @@ function resolvePairReadOnly(pairFlag) {
2294
2613
  pairId: entry.pairId,
2295
2614
  slot: entry.slot,
2296
2615
  ports: portsForEntry(entry),
2297
- stateDir: new StateDirResolver(join6(base, "pairs", entry.pairId)),
2616
+ stateDir: new StateDirResolver(join7(base, "pairs", entry.pairId)),
2298
2617
  name: entry.name ?? name,
2299
2618
  manual: false
2300
2619
  }
@@ -2307,7 +2626,7 @@ function resolvePairReadOnly(pairFlag) {
2307
2626
  pairId,
2308
2627
  slot: null,
2309
2628
  ports: { appPort: 0, proxyPort: 0, controlPort: 0 },
2310
- stateDir: new StateDirResolver(join6(base, "pairs", pairId)),
2629
+ stateDir: new StateDirResolver(join7(base, "pairs", pairId)),
2311
2630
  name,
2312
2631
  manual: false
2313
2632
  }
@@ -2337,7 +2656,7 @@ function portsForEntry(entry) {
2337
2656
  return portsForSlot(entry.slot);
2338
2657
  }
2339
2658
  function canonicalizeCwd(cwd) {
2340
- const absolute = resolve3(cwd);
2659
+ const absolute = resolve4(cwd);
2341
2660
  try {
2342
2661
  return realpathSync2.native(absolute);
2343
2662
  } catch {
@@ -2354,8 +2673,8 @@ var init_pair_resolver = __esm(() => {
2354
2673
  });
2355
2674
 
2356
2675
  // src/trace-log.ts
2357
- import { appendFileSync, mkdirSync as mkdirSync4 } from "fs";
2358
- import { join as join7 } from "path";
2676
+ import { appendFileSync, existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync2, statSync as statSync3, unlinkSync as unlinkSync3 } from "fs";
2677
+ import { join as join8 } from "path";
2359
2678
  function pickRelevantEnv(env) {
2360
2679
  const picked = {};
2361
2680
  for (const [key, value] of Object.entries(env)) {
@@ -2390,7 +2709,7 @@ function redactArgv(argv) {
2390
2709
  }
2391
2710
  function traceLogPath(cwd, timestamp) {
2392
2711
  const day = timestamp.slice(0, 10);
2393
- return join7(cwd, ".agentbridge", "logs", `trace-${day}.jsonl`);
2712
+ return join8(cwd, ".agentbridge", "logs", `trace-${day}.jsonl`);
2394
2713
  }
2395
2714
  function appendTraceEvent(input) {
2396
2715
  const timestamp = input.timestamp ?? new Date().toISOString();
@@ -2404,11 +2723,39 @@ function appendTraceEvent(input) {
2404
2723
  ...input.env ? { env: pickRelevantEnv(input.env) } : {},
2405
2724
  ...input.data ? { data: redactData(input.data) } : {}
2406
2725
  };
2407
- mkdirSync4(join7(input.cwd, ".agentbridge", "logs"), { recursive: true });
2726
+ const logsDir = join8(input.cwd, ".agentbridge", "logs");
2727
+ const isNewDayFile = !existsSync8(path);
2728
+ mkdirSync4(logsDir, { recursive: true });
2729
+ if (isNewDayFile) {
2730
+ pruneOldTraceLogs(logsDir, path, Date.parse(timestamp));
2731
+ }
2408
2732
  appendFileSync(path, JSON.stringify(event) + `
2409
2733
  `, "utf-8");
2410
2734
  return path;
2411
2735
  }
2736
+ function pruneOldTraceLogs(logsDir, keepPath, nowMs) {
2737
+ if (!Number.isFinite(nowMs))
2738
+ return;
2739
+ const cutoff = nowMs - TRACE_RETENTION_DAYS * 24 * 60 * 60 * 1000;
2740
+ let entries;
2741
+ try {
2742
+ entries = readdirSync2(logsDir);
2743
+ } catch {
2744
+ return;
2745
+ }
2746
+ for (const name of entries) {
2747
+ if (!TRACE_FILE_RE.test(name))
2748
+ continue;
2749
+ const filePath = join8(logsDir, name);
2750
+ if (filePath === keepPath)
2751
+ continue;
2752
+ try {
2753
+ if (statSync3(filePath).mtimeMs < cutoff) {
2754
+ unlinkSync3(filePath);
2755
+ }
2756
+ } catch {}
2757
+ }
2758
+ }
2412
2759
  function isEnvSnapshot(key, value) {
2413
2760
  return /env$/i.test(key) && !!value && typeof value === "object" && !Array.isArray(value);
2414
2761
  }
@@ -2434,8 +2781,9 @@ function redactData(value, key = "") {
2434
2781
  }
2435
2782
  return value;
2436
2783
  }
2437
- var SECRET_KEY_RE, SECRET_ARG_RE, RELEVANT_ENV_RE;
2784
+ var TRACE_RETENTION_DAYS = 7, TRACE_FILE_RE, SECRET_KEY_RE, SECRET_ARG_RE, RELEVANT_ENV_RE;
2438
2785
  var init_trace_log = __esm(() => {
2786
+ TRACE_FILE_RE = /^trace-\d{4}-\d{2}-\d{2}\.jsonl$/;
2439
2787
  SECRET_KEY_RE = /(token|secret|password|passwd|api[_-]?key|auth|cookie|session)/i;
2440
2788
  SECRET_ARG_RE = /^--?(?:token|secret|password|passwd|apikey|api-key|api_key|auth|cookie|session)(?:=.*)?$/i;
2441
2789
  RELEVANT_ENV_RE = /^(AGENTBRIDGE_|CODEX_)/;
@@ -2484,10 +2832,12 @@ var init_max_permissions = __esm(() => {
2484
2832
  // src/cli/claude.ts
2485
2833
  var exports_claude = {};
2486
2834
  __export(exports_claude, {
2835
+ warnIfPluginCacheMissing: () => warnIfPluginCacheMissing,
2487
2836
  runClaude: () => runClaude,
2488
2837
  checkOwnedFlagConflicts: () => checkOwnedFlagConflicts
2489
2838
  });
2490
2839
  import { spawn as spawn2 } from "child_process";
2840
+ import { existsSync as existsSync9 } from "fs";
2491
2841
  async function runClaude(args) {
2492
2842
  const originalEnv = { ...process.env };
2493
2843
  const envGuardResult = guardAgentBridgeEnv({
@@ -2525,6 +2875,7 @@ async function runClaude(args) {
2525
2875
  }
2526
2876
  await assertPairNotLive(lifecycle, pair);
2527
2877
  lifecycle.clearKilled();
2878
+ warnIfPluginCacheMissing();
2528
2879
  const channelEntry = `plugin:${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
2529
2880
  if (permissionPlan.inject) {
2530
2881
  console.error(`[agentbridge] running with ${CLAUDE_MAX_PERMISSION_FLAG} (default; opt out with --safe or AGENTBRIDGE_SAFE=1)`);
@@ -2552,6 +2903,18 @@ async function runClaude(args) {
2552
2903
  process.exit(1);
2553
2904
  });
2554
2905
  }
2906
+ function warnIfPluginCacheMissing(cacheRoot = pluginCacheRoot(), log = (msg) => console.error(msg)) {
2907
+ let cacheExists;
2908
+ try {
2909
+ cacheExists = existsSync9(cacheRoot);
2910
+ } catch {
2911
+ return false;
2912
+ }
2913
+ if (!shouldWarnMissingPluginCache(cacheExists))
2914
+ return false;
2915
+ log("[agentbridge] \u26A0\uFE0F Plugin not installed (no plugin cache found). Run `abg init`, " + `or in Claude Code: ${MARKETPLACE_STEPS.join(" \u2192 ")}. Launching anyway\u2026`);
2916
+ return true;
2917
+ }
2555
2918
  function traceCliStart(event, args, originalEnv, envGuardAction, pair) {
2556
2919
  try {
2557
2920
  appendTraceEvent({
@@ -2628,6 +2991,7 @@ function checkOwnedFlagConflicts(args, commandName, ownedFlags) {
2628
2991
  var OWNED_FLAGS;
2629
2992
  var init_claude = __esm(() => {
2630
2993
  init_cli();
2994
+ init_plugin_cache();
2631
2995
  init_daemon_client();
2632
2996
  init_daemon_lifecycle();
2633
2997
  init_build_info();
@@ -2639,11 +3003,11 @@ var init_claude = __esm(() => {
2639
3003
  });
2640
3004
 
2641
3005
  // src/agents-contract.ts
2642
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
2643
- import { join as join8 } from "path";
3006
+ import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
3007
+ import { join as join9 } from "path";
2644
3008
  function checkAgentsMdContract(cwd) {
2645
- const path = join8(cwd, "AGENTS.md");
2646
- const exists = existsSync7(path);
3009
+ const path = join9(cwd, "AGENTS.md");
3010
+ const exists = existsSync10(path);
2647
3011
  let content = "";
2648
3012
  if (exists) {
2649
3013
  try {
@@ -2674,8 +3038,8 @@ function isFreshAgentsMdContract(content) {
2674
3038
  var init_agents_contract = () => {};
2675
3039
 
2676
3040
  // src/wrapper-exit-observability.ts
2677
- import { readFileSync as readFileSync7, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
2678
- import { join as join9 } from "path";
3041
+ import { readFileSync as readFileSync7, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
3042
+ import { join as join10 } from "path";
2679
3043
  function discoverNativeChildPid(launcherPid, run) {
2680
3044
  try {
2681
3045
  const out = run("pgrep", ["-P", String(launcherPid)]);
@@ -2697,16 +3061,6 @@ function readTurnInProgress(statusFilePath, read = (p) => readFileSync7(p, "utf-
2697
3061
  return null;
2698
3062
  }
2699
3063
  }
2700
- function defaultIsPidAlive(pid) {
2701
- if (pid <= 0)
2702
- return false;
2703
- try {
2704
- process.kill(pid, 0);
2705
- return true;
2706
- } catch (err) {
2707
- return err.code === "EPERM";
2708
- }
2709
- }
2710
3064
  function refineCleanExitClassification(turnInProgress) {
2711
3065
  if (turnInProgress === true)
2712
3066
  return "exit_0_during_turn";
@@ -2714,12 +3068,12 @@ function refineCleanExitClassification(turnInProgress) {
2714
3068
  return "exit_0_idle";
2715
3069
  return "exit_0_turn_unknown";
2716
3070
  }
2717
- function findCodexSqliteLog(codexHome, fs = { readdir: readdirSync2, stat: statSync3 }) {
3071
+ function findCodexSqliteLog(codexHome, fs = { readdir: readdirSync3, stat: statSync4 }) {
2718
3072
  try {
2719
3073
  const entries = fs.readdir(codexHome).filter((name) => /^logs.*\.sqlite$/.test(String(name)));
2720
3074
  let best = null;
2721
3075
  for (const name of entries) {
2722
- const path = join9(codexHome, String(name));
3076
+ const path = join10(codexHome, String(name));
2723
3077
  try {
2724
3078
  const mtime = fs.stat(path).mtimeMs;
2725
3079
  if (!best || mtime > best.mtime)
@@ -2753,7 +3107,11 @@ function captureTuiLogTail(options) {
2753
3107
  return `(tui log tail capture failed: ${err instanceof Error ? err.message : String(err)})`;
2754
3108
  }
2755
3109
  }
2756
- var init_wrapper_exit_observability = () => {};
3110
+ var defaultIsPidAlive;
3111
+ var init_wrapper_exit_observability = __esm(() => {
3112
+ init_process_lifecycle();
3113
+ defaultIsPidAlive = pidLooksAlive;
3114
+ });
2757
3115
 
2758
3116
  // src/pair-command.ts
2759
3117
  function pairScopedCommand(cmd) {
@@ -2775,15 +3133,15 @@ var init_pair_command = __esm(() => {
2775
3133
  });
2776
3134
 
2777
3135
  // src/rotating-log.ts
2778
- import { appendFileSync as appendFileSync2, existsSync as existsSync8, renameSync as renameSync2, statSync as statSync4, unlinkSync as unlinkSync3 } from "fs";
3136
+ import { appendFileSync as appendFileSync2, existsSync as existsSync11, renameSync as renameSync2, statSync as statSync5, unlinkSync as unlinkSync4 } from "fs";
2779
3137
  import { dirname as dirname2 } from "path";
2780
- function appendRotatingLog(path, content, options = {}) {
3138
+ function appendRotatingLog(path, content, options = {}, fsOps = REAL_FS_OPS) {
2781
3139
  const maxBytes = options.maxBytes ?? positiveIntFromEnv("AGENTBRIDGE_LOG_MAX_BYTES", DEFAULT_MAX_BYTES);
2782
3140
  const keep = options.keep ?? positiveIntFromEnv("AGENTBRIDGE_LOG_ROTATE_KEEP", DEFAULT_KEEP);
2783
- if (!existsSync8(dirname2(path)))
3141
+ if (!fsOps.existsSync(dirname2(path)))
2784
3142
  return;
2785
- rotateIfNeeded(path, Buffer.byteLength(content), maxBytes, keep);
2786
- appendFileSync2(path, content, "utf-8");
3143
+ rotateIfNeeded(path, Buffer.byteLength(content), maxBytes, keep, fsOps);
3144
+ fsOps.appendFileSync(path, content, "utf-8");
2787
3145
  }
2788
3146
  function positiveIntFromEnv(name, fallback) {
2789
3147
  const value = process.env[name];
@@ -2792,30 +3150,53 @@ function positiveIntFromEnv(name, fallback) {
2792
3150
  const parsed = Number(value);
2793
3151
  return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
2794
3152
  }
2795
- function rotateIfNeeded(path, incomingBytes, maxBytes, keep) {
3153
+ function isEnoent(error) {
3154
+ return !!error && error.code === "ENOENT";
3155
+ }
3156
+ function renameIfPresent(from, to, fsOps) {
3157
+ try {
3158
+ fsOps.renameSync(from, to);
3159
+ } catch (error) {
3160
+ if (!isEnoent(error))
3161
+ throw error;
3162
+ }
3163
+ }
3164
+ function unlinkIfPresent(path, fsOps) {
3165
+ try {
3166
+ fsOps.unlinkSync(path);
3167
+ } catch (error) {
3168
+ if (!isEnoent(error))
3169
+ throw error;
3170
+ }
3171
+ }
3172
+ function rotateIfNeeded(path, incomingBytes, maxBytes, keep, fsOps) {
2796
3173
  if (!Number.isFinite(maxBytes) || maxBytes <= 0 || keep <= 0)
2797
3174
  return;
2798
- if (!existsSync8(path))
2799
- return;
2800
- const size = statSync4(path).size;
3175
+ let size;
3176
+ try {
3177
+ size = fsOps.statSync(path).size;
3178
+ } catch (error) {
3179
+ if (isEnoent(error))
3180
+ return;
3181
+ throw error;
3182
+ }
2801
3183
  if (size + incomingBytes <= maxBytes)
2802
3184
  return;
2803
3185
  for (let index = keep;index >= 1; index--) {
2804
3186
  const current = `${path}.${index}`;
2805
3187
  const next = `${path}.${index + 1}`;
2806
- if (!existsSync8(current))
2807
- continue;
2808
3188
  if (index === keep) {
2809
- unlinkSync3(current);
3189
+ unlinkIfPresent(current, fsOps);
2810
3190
  } else {
2811
- renameSync2(current, next);
3191
+ renameIfPresent(current, next, fsOps);
2812
3192
  }
2813
3193
  }
2814
- renameSync2(path, `${path}.1`);
3194
+ renameIfPresent(path, `${path}.1`, fsOps);
2815
3195
  }
2816
- var DEFAULT_MAX_BYTES, DEFAULT_KEEP = 3;
3196
+ var DEFAULT_MAX_BYTES, DEFAULT_KEEP = 3, REAL_FS_OPS;
2817
3197
  var init_rotating_log = __esm(() => {
2818
3198
  DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
3199
+ REAL_FS_OPS = { statSync: statSync5, renameSync: renameSync2, unlinkSync: unlinkSync4, appendFileSync: appendFileSync2, existsSync: existsSync11 };
2819
3200
  });
2820
3201
 
2821
3202
  // src/stderr-ring-buffer.ts
@@ -2868,20 +3249,20 @@ var init_stderr_ring_buffer = __esm(() => {
2868
3249
 
2869
3250
  // src/thread-state.ts
2870
3251
  import {
2871
- existsSync as existsSync9,
3252
+ existsSync as existsSync12,
2872
3253
  mkdirSync as mkdirSync5,
2873
- readdirSync as readdirSync3,
3254
+ readdirSync as readdirSync4,
2874
3255
  readFileSync as readFileSync8,
2875
3256
  renameSync as renameSync3,
2876
3257
  writeFileSync as writeFileSync6
2877
3258
  } from "fs";
2878
3259
  import { homedir as homedir3 } from "os";
2879
- import { basename as basename2, dirname as dirname3, join as join10 } from "path";
3260
+ import { basename as basename3, dirname as dirname3, join as join11 } from "path";
2880
3261
  function nowIso() {
2881
3262
  return new Date().toISOString();
2882
3263
  }
2883
3264
  function codexHome(env = process.env) {
2884
- return env.CODEX_HOME && env.CODEX_HOME.length > 0 ? env.CODEX_HOME : join10(homedir3(), ".codex");
3265
+ return env.CODEX_HOME && env.CODEX_HOME.length > 0 ? env.CODEX_HOME : join11(homedir3(), ".codex");
2885
3266
  }
2886
3267
  function atomicWriteJson(path, value) {
2887
3268
  mkdirSync5(dirname3(path), { recursive: true });
@@ -2900,8 +3281,8 @@ function readRawCurrentThread(stateDir) {
2900
3281
  return null;
2901
3282
  }
2902
3283
  function findCodexRolloutFile(threadId, env = process.env, maxEntries = 20000) {
2903
- const sessionsDir = join10(codexHome(env), "sessions");
2904
- if (!threadId || !existsSync9(sessionsDir))
3284
+ const sessionsDir = join11(codexHome(env), "sessions");
3285
+ if (!threadId || !existsSync12(sessionsDir))
2905
3286
  return null;
2906
3287
  const exactName = `rollout-${threadId}.jsonl`;
2907
3288
  const stack = [sessionsDir];
@@ -2910,20 +3291,20 @@ function findCodexRolloutFile(threadId, env = process.env, maxEntries = 20000) {
2910
3291
  const dir = stack.pop();
2911
3292
  let entries;
2912
3293
  try {
2913
- entries = readdirSync3(dir, { withFileTypes: true });
3294
+ entries = readdirSync4(dir, { withFileTypes: true });
2914
3295
  } catch {
2915
3296
  continue;
2916
3297
  }
2917
3298
  for (const entry of entries) {
2918
3299
  visited++;
2919
- const path = join10(dir, entry.name);
3300
+ const path = join11(dir, entry.name);
2920
3301
  if (entry.isDirectory()) {
2921
3302
  stack.push(path);
2922
3303
  continue;
2923
3304
  }
2924
3305
  if (!entry.isFile())
2925
3306
  continue;
2926
- const name = basename2(entry.name);
3307
+ const name = basename3(entry.name);
2927
3308
  if (name === exactName || name.startsWith("rollout-") && name.endsWith(".jsonl") && name.includes(threadId)) {
2928
3309
  return path;
2929
3310
  }
@@ -2941,7 +3322,7 @@ function readUsableCurrentThread(identity, env = process.env) {
2941
3322
  return null;
2942
3323
  if (state.cwd !== identity.cwd)
2943
3324
  return null;
2944
- if (state.rolloutPath && existsSync9(state.rolloutPath))
3325
+ if (state.rolloutPath && existsSync12(state.rolloutPath))
2945
3326
  return state;
2946
3327
  const rolloutPath = findCodexRolloutFile(state.threadId, env);
2947
3328
  if (!rolloutPath)
@@ -2957,145 +3338,6 @@ function readUsableCurrentThread(identity, env = process.env) {
2957
3338
  }
2958
3339
  var init_thread_state = () => {};
2959
3340
 
2960
- // src/process-lifecycle.ts
2961
- import { execFileSync as execFileSync6 } from "child_process";
2962
- import { basename as basename3 } from "path";
2963
- function parsePsProcessList(output) {
2964
- const entries = [];
2965
- for (const line of output.split(/\r?\n/)) {
2966
- const match = line.match(/^\s*(\d+)\s+(.+?)\s*$/);
2967
- if (!match)
2968
- continue;
2969
- const pid = Number.parseInt(match[1], 10);
2970
- if (!Number.isFinite(pid))
2971
- continue;
2972
- entries.push({ pid, command: match[2] });
2973
- }
2974
- return entries;
2975
- }
2976
- function invokesCodexBinary(command) {
2977
- const tokens = command.trim().split(/\s+/);
2978
- const exe = tokens[0] ? basename3(tokens[0]) : "";
2979
- if (exe === "codex")
2980
- return true;
2981
- if ((exe === "node" || exe === "bun") && tokens[1]) {
2982
- return basename3(tokens[1]) === "codex";
2983
- }
2984
- return false;
2985
- }
2986
- function commandMatchesManagedCodexTui(command, proxyUrl) {
2987
- if (!invokesCodexBinary(command))
2988
- return false;
2989
- if (!command.includes("tui_app_server"))
2990
- return false;
2991
- const remoteUrl = extractRemoteUrl(command);
2992
- if (!remoteUrl)
2993
- return false;
2994
- if (!proxyUrl)
2995
- return true;
2996
- return remoteTargetsProxy(remoteUrl, proxyUrl);
2997
- }
2998
- function findManagedCodexTuiProcessesFromList(processes, proxyUrl) {
2999
- return processes.filter((entry) => commandMatchesManagedCodexTui(entry.command, proxyUrl));
3000
- }
3001
- function findManagedCodexTuiProcesses(proxyUrl) {
3002
- try {
3003
- const output = execFileSync6("ps", ["-axo", "pid=,command="], { encoding: "utf-8" });
3004
- return findManagedCodexTuiProcessesFromList(parsePsProcessList(output), proxyUrl).filter((entry) => entry.pid !== process.pid);
3005
- } catch {
3006
- return [];
3007
- }
3008
- }
3009
- function listManagedCodexTuiProcessesFromList(processes) {
3010
- return processes.filter((entry) => commandMatchesManagedCodexTui(entry.command)).map((entry) => ({ ...entry, remoteUrl: extractRemoteUrl(entry.command) }));
3011
- }
3012
- function listManagedCodexTuiProcesses() {
3013
- try {
3014
- const output = execFileSync6("ps", ["-axo", "pid=,command="], { encoding: "utf-8" });
3015
- return listManagedCodexTuiProcessesFromList(parsePsProcessList(output)).filter((entry) => entry.pid !== process.pid);
3016
- } catch {
3017
- return [];
3018
- }
3019
- }
3020
- function listBridgeFrontendProcessesFromList(processes) {
3021
- return processes.filter((entry) => /(?:^|[\s/\\])bridge-server\.js(?:\s|$)/.test(entry.command) && (entry.command.includes("agentbridge") || entry.command.includes("agent_bridge")));
3022
- }
3023
- function listBridgeFrontendProcesses() {
3024
- try {
3025
- const output = execFileSync6("ps", ["-axo", "pid=,command="], { encoding: "utf-8" });
3026
- return listBridgeFrontendProcessesFromList(parsePsProcessList(output)).filter((entry) => entry.pid !== process.pid);
3027
- } catch {
3028
- return [];
3029
- }
3030
- }
3031
- function isProcessAlive2(pid) {
3032
- try {
3033
- process.kill(pid, 0);
3034
- return true;
3035
- } catch {
3036
- return false;
3037
- }
3038
- }
3039
- function commandForPid(pid) {
3040
- try {
3041
- return execFileSync6("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf-8" }).trim();
3042
- } catch {
3043
- return null;
3044
- }
3045
- }
3046
- function terminateProcessSync(pid, options = {}) {
3047
- const gracefulTimeoutMs = options.gracefulTimeoutMs ?? 2000;
3048
- const target = options.processGroup && process.platform !== "win32" ? -pid : pid;
3049
- const label = options.processGroup && process.platform !== "win32" ? `process group ${pid}` : `pid ${pid}`;
3050
- try {
3051
- process.kill(target, "SIGTERM");
3052
- options.log?.(`Sent SIGTERM to ${label}`);
3053
- } catch {
3054
- return !isProcessAlive2(pid);
3055
- }
3056
- if (waitForExitSync(pid, gracefulTimeoutMs))
3057
- return true;
3058
- try {
3059
- process.kill(target, "SIGKILL");
3060
- options.log?.(`Sent SIGKILL to ${label}`);
3061
- } catch {}
3062
- return waitForExitSync(pid, 500);
3063
- }
3064
- function waitForExitSync(pid, timeoutMs) {
3065
- const deadline = Date.now() + timeoutMs;
3066
- while (Date.now() < deadline) {
3067
- if (!isProcessAlive2(pid))
3068
- return true;
3069
- sleepSync(50);
3070
- }
3071
- return !isProcessAlive2(pid);
3072
- }
3073
- function sleepSync(ms) {
3074
- const buffer = new SharedArrayBuffer(4);
3075
- const view = new Int32Array(buffer);
3076
- Atomics.wait(view, 0, 0, ms);
3077
- }
3078
- function extractRemoteUrl(command) {
3079
- const equals = command.match(/(?:^|\s)--remote=([^\s]+)/);
3080
- if (equals)
3081
- return equals[1];
3082
- const separate = command.match(/(?:^|\s)--remote\s+([^\s]+)/);
3083
- return separate?.[1] ?? null;
3084
- }
3085
- function remoteTargetsProxy(remoteUrl, proxyUrl) {
3086
- try {
3087
- const remote = new URL(remoteUrl);
3088
- const proxy = new URL(proxyUrl);
3089
- return remote.protocol === proxy.protocol && remote.hostname === proxy.hostname && remote.port === proxy.port && normalizePath(remote.pathname) === normalizePath(proxy.pathname);
3090
- } catch {
3091
- return remoteUrl === proxyUrl;
3092
- }
3093
- }
3094
- function normalizePath(pathname) {
3095
- return pathname === "" ? "/" : pathname;
3096
- }
3097
- var init_process_lifecycle = () => {};
3098
-
3099
3341
  // src/cli/codex.ts
3100
3342
  var exports_codex = {};
3101
3343
  __export(exports_codex, {
@@ -3104,23 +3346,23 @@ __export(exports_codex, {
3104
3346
  parseAgentBridgeCodexArgs: () => parseAgentBridgeCodexArgs,
3105
3347
  buildCodexArgs: () => buildCodexArgs
3106
3348
  });
3107
- import { spawn as spawn3, execSync as execSync2, execFileSync as execFileSync7 } from "child_process";
3349
+ import { spawn as spawn3, execSync as execSync2, execFileSync as execFileSync6 } from "child_process";
3108
3350
  import {
3109
3351
  openSync as openSync3,
3110
3352
  writeSync,
3111
3353
  closeSync as closeSync3,
3112
3354
  writeFileSync as writeFileSync7,
3113
3355
  readFileSync as readFileSync9,
3114
- unlinkSync as unlinkSync4,
3115
- existsSync as existsSync10,
3356
+ unlinkSync as unlinkSync5,
3357
+ existsSync as existsSync13,
3116
3358
  mkdirSync as mkdirSync6
3117
3359
  } from "fs";
3118
3360
  import { homedir as homedir4 } from "os";
3119
- import { dirname as dirname4, join as join11 } from "path";
3361
+ import { dirname as dirname4, join as join12 } from "path";
3120
3362
  function appendWrapperLog(path, entry) {
3121
3363
  try {
3122
3364
  const dir = dirname4(path);
3123
- if (!existsSync10(dir)) {
3365
+ if (!existsSync13(dir)) {
3124
3366
  mkdirSync6(dir, { recursive: true });
3125
3367
  }
3126
3368
  appendRotatingLog(path, `[${new Date().toISOString()}] ${entry}
@@ -3287,7 +3529,7 @@ async function runCodex(args) {
3287
3529
  if (status?.proxyUrl) {
3288
3530
  proxyUrl = status.proxyUrl;
3289
3531
  } else {
3290
- const fallbackProxyPort = process.env.CODEX_PROXY_PORT ?? String(new ConfigService().loadOrDefault().codex.proxyPort);
3532
+ const fallbackProxyPort = process.env.CODEX_PROXY_PORT ?? String(new ConfigService().loadOrDefault((msg) => console.error(`[agentbridge] ${msg}`)).codex.proxyPort);
3291
3533
  proxyUrl = `ws://127.0.0.1:${fallbackProxyPort}`;
3292
3534
  console.error(`[agentbridge] No daemon status found, using fallback proxy port: ${proxyUrl}`);
3293
3535
  }
@@ -3377,7 +3619,7 @@ async function runCodex(args) {
3377
3619
  let attempts = 0;
3378
3620
  const discover = () => {
3379
3621
  attempts += 1;
3380
- nativeChildPid = discoverNativeChildPid(launcherPid, (cmd, args2) => execFileSync7(cmd, args2, { encoding: "utf-8", timeout: 2000 }));
3622
+ nativeChildPid = discoverNativeChildPid(launcherPid, (cmd, args2) => execFileSync6(cmd, args2, { encoding: "utf-8", timeout: 2000 }));
3381
3623
  if (nativeChildPid !== null) {
3382
3624
  appendWrapperLog(wrapperLogPath, `native child pid=${nativeChildPid} (launcher pid=${launcherPid})`);
3383
3625
  return;
@@ -3407,7 +3649,7 @@ async function runCodex(args) {
3407
3649
  return;
3408
3650
  cleanedTuiPid = true;
3409
3651
  try {
3410
- unlinkSync4(stateDir.tuiPidFile);
3652
+ unlinkSync5(stateDir.tuiPidFile);
3411
3653
  } catch {}
3412
3654
  }
3413
3655
  function requestChildTermination(reason) {
@@ -3481,9 +3723,9 @@ async function runCodex(args) {
3481
3723
  classification = refineCleanExitClassification(readTurnInProgress(stateDir.statusFile));
3482
3724
  }
3483
3725
  const tuiLogTail = captureTuiLogTail({
3484
- codexHome: join11(homedir4(), ".codex"),
3726
+ codexHome: join12(homedir4(), ".codex"),
3485
3727
  nativePid: nativeChildPid,
3486
- run: (cmd, args2) => execFileSync7(cmd, args2, { encoding: "utf-8", timeout: 2000 })
3728
+ run: (cmd, args2) => execFileSync6(cmd, args2, { encoding: "utf-8", timeout: 2000 })
3487
3729
  });
3488
3730
  appendWrapperLog(wrapperLogPath, [
3489
3731
  `exit: code=${code ?? "null"} signal=${signal ?? "null"} runtime_ms=${runtimeMs} pid=${child.pid ?? "unknown"} native_pid=${nativeChildPid ?? "unknown"} classification=${classification}`,
@@ -3535,14 +3777,14 @@ function traceCliStart2(event, args, originalEnv, envGuardAction, pair) {
3535
3777
  function guardNoLiveManagedTui(stateDir, proxyUrl) {
3536
3778
  const pid = readTuiPid(stateDir);
3537
3779
  if (pid) {
3538
- if (!isProcessAlive2(pid)) {
3780
+ if (!isProcessAlive(pid)) {
3539
3781
  try {
3540
- unlinkSync4(stateDir.tuiPidFile);
3782
+ unlinkSync5(stateDir.tuiPidFile);
3541
3783
  } catch {}
3542
3784
  } else if (!isManagedCodexTuiProcess(pid, proxyUrl)) {
3543
3785
  appendWrapperLog(stateDir.codexWrapperLogFile, `stale tui pid file pointed at unmanaged live pid=${pid}; removing`);
3544
3786
  try {
3545
- unlinkSync4(stateDir.tuiPidFile);
3787
+ unlinkSync5(stateDir.tuiPidFile);
3546
3788
  } catch {}
3547
3789
  } else {
3548
3790
  console.error(`[agentbridge] This pair already has a managed Codex TUI running (pid ${pid}).`);
@@ -3589,7 +3831,7 @@ async function waitForProxyReady(proxyUrl, maxRetries = 20, delayMs = 100) {
3589
3831
  return;
3590
3832
  }
3591
3833
  } catch {}
3592
- await new Promise((resolve4) => setTimeout(resolve4, delayMs));
3834
+ await new Promise((resolve5) => setTimeout(resolve5, delayMs));
3593
3835
  }
3594
3836
  throw new Error(`Timed out waiting for Codex proxy readiness on ${healthUrl}`);
3595
3837
  }
@@ -3638,17 +3880,17 @@ var init_codex = __esm(() => {
3638
3880
  });
3639
3881
 
3640
3882
  // src/claude-session.ts
3641
- import { readdirSync as readdirSync4, statSync as statSync5 } from "fs";
3883
+ import { readdirSync as readdirSync5, statSync as statSync6 } from "fs";
3642
3884
  import { homedir as homedir5 } from "os";
3643
- import { join as join12 } from "path";
3885
+ import { join as join13 } from "path";
3644
3886
  function encodeClaudeProjectDir(cwd) {
3645
3887
  return cwd.replace(/[^a-zA-Z0-9]/g, "-");
3646
3888
  }
3647
- function findLatestClaudeSession(cwd, claudeHome = process.env.CLAUDE_CONFIG_DIR || join12(homedir5(), ".claude")) {
3648
- const dir = join12(claudeHome, "projects", encodeClaudeProjectDir(cwd));
3889
+ function findLatestClaudeSession(cwd, claudeHome = process.env.CLAUDE_CONFIG_DIR || join13(homedir5(), ".claude")) {
3890
+ const dir = join13(claudeHome, "projects", encodeClaudeProjectDir(cwd));
3649
3891
  let entries;
3650
3892
  try {
3651
- entries = readdirSync4(dir);
3893
+ entries = readdirSync5(dir);
3652
3894
  } catch {
3653
3895
  return null;
3654
3896
  }
@@ -3659,10 +3901,10 @@ function findLatestClaudeSession(cwd, claudeHome = process.env.CLAUDE_CONFIG_DIR
3659
3901
  const sessionId = name.slice(0, -".jsonl".length);
3660
3902
  if (!SESSION_ID_PATTERN.test(sessionId))
3661
3903
  continue;
3662
- const file = join12(dir, name);
3904
+ const file = join13(dir, name);
3663
3905
  let mtimeMs;
3664
3906
  try {
3665
- const st = statSync5(file);
3907
+ const st = statSync6(file);
3666
3908
  if (!st.isFile())
3667
3909
  continue;
3668
3910
  mtimeMs = st.mtimeMs;
@@ -3768,8 +4010,8 @@ __export(exports_kill, {
3768
4010
  runKill: () => runKill,
3769
4011
  formatKillReport: () => formatKillReport
3770
4012
  });
3771
- import { readFileSync as readFileSync10, unlinkSync as unlinkSync5 } from "fs";
3772
- import { join as join13 } from "path";
4013
+ import { readFileSync as readFileSync10, unlinkSync as unlinkSync6 } from "fs";
4014
+ import { join as join14 } from "path";
3773
4015
  async function runKill(args = []) {
3774
4016
  const argError = validateKillArgs(args);
3775
4017
  if (argError === "help") {
@@ -3821,7 +4063,7 @@ async function runKill(args = []) {
3821
4063
  for (const dirName of listPairDirsSafe(base)) {
3822
4064
  if (registeredIds.has(dirName))
3823
4065
  continue;
3824
- const stateDir = new StateDirResolver(join13(base, "pairs", dirName));
4066
+ const stateDir = new StateDirResolver(join14(base, "pairs", dirName));
3825
4067
  results.push(await stopStateDir(`${dirName} (unregistered)`, stateDir, portsFromStateDir(stateDir)));
3826
4068
  }
3827
4069
  const legacy = detectLegacyRootDaemon(base);
@@ -3906,7 +4148,7 @@ No arguments stop this directory's registered pairs and any legacy-root daemon.
3906
4148
  }
3907
4149
  async function stopPairEntry(base, pair) {
3908
4150
  const ports = portsForEntry(pair);
3909
- const stateDir = new StateDirResolver(join13(base, "pairs", pair.pairId));
4151
+ const stateDir = new StateDirResolver(join14(base, "pairs", pair.pairId));
3910
4152
  return stopStateDir(pair.pairId, stateDir, ports);
3911
4153
  }
3912
4154
  function listPairDirsSafe(base) {
@@ -4041,7 +4283,7 @@ async function killManagedCodexTui(stateDir, proxyUrl, log, gracefulTimeoutMs =
4041
4283
  if (!pid) {
4042
4284
  log("No Codex TUI pid file found");
4043
4285
  removeTuiPidFile(stateDir);
4044
- } else if (!isProcessAlive2(pid)) {
4286
+ } else if (!isProcessAlive(pid)) {
4045
4287
  log(`Codex TUI pid ${pid} is not alive, cleaning up stale pid file`);
4046
4288
  removeTuiPidFile(stateDir);
4047
4289
  } else if (!isManagedCodexTuiProcess2(pid, proxyUrl)) {
@@ -4075,7 +4317,7 @@ function readTuiPid2(stateDir) {
4075
4317
  }
4076
4318
  function removeTuiPidFile(stateDir) {
4077
4319
  try {
4078
- unlinkSync5(stateDir.tuiPidFile);
4320
+ unlinkSync6(stateDir.tuiPidFile);
4079
4321
  } catch {}
4080
4322
  }
4081
4323
  function isManagedCodexTuiProcess2(pid, proxyUrl) {
@@ -4095,7 +4337,7 @@ var exports_pairs = {};
4095
4337
  __export(exports_pairs, {
4096
4338
  runPairs: () => runPairs
4097
4339
  });
4098
- import { join as join14 } from "path";
4340
+ import { join as join15 } from "path";
4099
4341
  async function runPairs(args = []) {
4100
4342
  const [command, ...rest] = args;
4101
4343
  if (command === "rm") {
@@ -4261,7 +4503,7 @@ async function collectRows() {
4261
4503
  }
4262
4504
  async function rowForPair(base, pair) {
4263
4505
  const ports = portsForEntry(pair);
4264
- const stateDir = new StateDirResolver(join14(base, "pairs", pair.pairId));
4506
+ const stateDir = new StateDirResolver(join15(base, "pairs", pair.pairId));
4265
4507
  const lifecycle = new DaemonLifecycle({
4266
4508
  stateDir,
4267
4509
  controlPort: ports.controlPort,
@@ -4368,18 +4610,18 @@ var DAEMON_STATUS_FETCH_TIMEOUT_MS = 1000;
4368
4610
  import { Database } from "bun:sqlite";
4369
4611
  import {
4370
4612
  copyFileSync,
4371
- existsSync as existsSync11,
4613
+ existsSync as existsSync14,
4372
4614
  mkdirSync as mkdirSync7,
4373
4615
  readFileSync as readFileSync11
4374
4616
  } from "fs";
4375
- import { dirname as dirname5, join as join15 } from "path";
4617
+ import { dirname as dirname5, join as join16 } from "path";
4376
4618
  function isKickoffText(text) {
4377
4619
  if (!text)
4378
4620
  return false;
4379
4621
  return KICKOFF_FINGERPRINTS.some((fingerprint) => text.includes(fingerprint));
4380
4622
  }
4381
4623
  function extractFirstRealUserMessage(rolloutPath) {
4382
- if (!existsSync11(rolloutPath))
4624
+ if (!existsSync14(rolloutPath))
4383
4625
  return null;
4384
4626
  const raw = readFileSync11(rolloutPath, "utf-8");
4385
4627
  for (const line of raw.split(`
@@ -4403,8 +4645,8 @@ function extractFirstRealUserMessage(rolloutPath) {
4403
4645
  }
4404
4646
  function scanResumePollution(options = {}) {
4405
4647
  const codexHome2 = options.codexHome ?? codexHome();
4406
- const dbPath = options.dbPath ?? join15(codexHome2, "state_5.sqlite");
4407
- if (!existsSync11(dbPath)) {
4648
+ const dbPath = options.dbPath ?? join16(codexHome2, "state_5.sqlite");
4649
+ if (!existsSync14(dbPath)) {
4408
4650
  return { codexHome: codexHome2, dbPath, scanned: 0, candidates: [], applied: 0, renamed: 0, deleted: 0 };
4409
4651
  }
4410
4652
  const db = options.apply ? new Database(dbPath) : new Database(dbPath, { readonly: true });
@@ -4497,12 +4739,12 @@ function scanResumePollution(options = {}) {
4497
4739
  }
4498
4740
  function backupCodexStateFiles(dbPath, now = new Date().toISOString()) {
4499
4741
  const safeStamp = now.replace(/[:.]/g, "-");
4500
- const base = join15(dirname5(dbPath), "agentbridge-backups", `resume-pollution-${safeStamp}`);
4742
+ const base = join16(dirname5(dbPath), "agentbridge-backups", `resume-pollution-${safeStamp}`);
4501
4743
  mkdirSync7(base, { recursive: true });
4502
4744
  for (const path of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
4503
- if (!existsSync11(path))
4745
+ if (!existsSync14(path))
4504
4746
  continue;
4505
- const target = join15(base, path.split("/").pop());
4747
+ const target = join16(base, path.split("/").pop());
4506
4748
  mkdirSync7(dirname5(target), { recursive: true });
4507
4749
  copyFileSync(path, target);
4508
4750
  }
@@ -4550,9 +4792,8 @@ __export(exports_doctor, {
4550
4792
  runDoctor: () => runDoctor,
4551
4793
  formatDoctorReport: () => formatDoctorReport
4552
4794
  });
4553
- import { existsSync as existsSync12, readFileSync as readFileSync12, readdirSync as readdirSync5, realpathSync as realpathSync3, statSync as statSync6 } from "fs";
4554
- import { homedir as homedir6 } from "os";
4555
- import { join as join16 } from "path";
4795
+ import { existsSync as existsSync15, readFileSync as readFileSync12, readdirSync as readdirSync6, realpathSync as realpathSync3, statSync as statSync7 } from "fs";
4796
+ import { join as join17 } from "path";
4556
4797
  async function runDoctor(args = []) {
4557
4798
  if (args[0] === "resume-pollution") {
4558
4799
  runResumePollution(args.slice(1));
@@ -4655,6 +4896,7 @@ async function buildDoctorReport(pair, registered) {
4655
4896
  detail: env.ok ? "AgentBridge env matches cwd" : env.reasons.join("; "),
4656
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"
4657
4898
  });
4899
+ checks.push(configParseabilityCheck(cwd));
4658
4900
  checks.push({
4659
4901
  name: "daemon health",
4660
4902
  status: health ? "ok" : "warn",
@@ -4741,16 +4983,16 @@ function artifactAlignmentCheck() {
4741
4983
  stamps.push({ label: "global-cli", commit });
4742
4984
  } catch {}
4743
4985
  }
4744
- const cacheRoot = join16(homedir6(), ".claude", "plugins", "cache", "agentbridge", "agentbridge");
4986
+ const cacheRoot = pluginCacheRoot();
4745
4987
  try {
4746
- for (const version of readdirSync5(cacheRoot)) {
4747
- const commit = extractBundleCommit(join16(cacheRoot, version, "server", "daemon.js"));
4988
+ for (const version of readdirSync6(cacheRoot)) {
4989
+ const commit = extractBundleCommit(join17(cacheRoot, version, "server", "daemon.js"));
4748
4990
  if (commit)
4749
4991
  stamps.push({ label: `plugin-cache@${version}`, commit });
4750
4992
  }
4751
4993
  } catch {}
4752
- const repoBundle = join16(process.cwd(), "plugins", "agentbridge", "server", "daemon.js");
4753
- if (existsSync12(repoBundle)) {
4994
+ const repoBundle = join17(process.cwd(), "plugins", "agentbridge", "server", "daemon.js");
4995
+ if (existsSync15(repoBundle)) {
4754
4996
  const commit = extractBundleCommit(repoBundle);
4755
4997
  if (commit)
4756
4998
  stamps.push({ label: "repo-bundle", commit });
@@ -4782,8 +5024,31 @@ function extractBundleCommit(path) {
4782
5024
  return null;
4783
5025
  }
4784
5026
  }
5027
+ function configParseabilityCheck(cwd) {
5028
+ const desc = new ConfigService(cwd).describeConfig();
5029
+ if (desc.state === "absent") {
5030
+ return {
5031
+ name: "config.json",
5032
+ status: "ok",
5033
+ detail: `no project config at ${desc.path} \u2014 built-in defaults in effect`
5034
+ };
5035
+ }
5036
+ if (desc.state === "corrupt") {
5037
+ return {
5038
+ name: "config.json",
5039
+ status: "warn",
5040
+ 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"
5042
+ };
5043
+ }
5044
+ return {
5045
+ name: "config.json",
5046
+ status: "ok",
5047
+ detail: desc.customValues ? `parsed at ${desc.path} \u2014 custom values in effect` : `parsed at ${desc.path} \u2014 all values match defaults`
5048
+ };
5049
+ }
4785
5050
  function logCheck(name, path) {
4786
- if (!existsSync12(path)) {
5051
+ if (!existsSync15(path)) {
4787
5052
  return {
4788
5053
  name,
4789
5054
  status: "warn",
@@ -4791,7 +5056,7 @@ function logCheck(name, path) {
4791
5056
  hint: "\u65E5\u5FD7\u4F1A\u5728\u76F8\u5E94\u8FDB\u7A0B\u9996\u6B21\u542F\u52A8\u65F6\u521B\u5EFA\uFF1B\u8FDB\u7A0B\u4ECE\u672A\u542F\u52A8\u8FC7\u65F6\u8FD9\u662F\u6B63\u5E38\u7684\u3002"
4792
5057
  };
4793
5058
  }
4794
- const stat = statSync6(path);
5059
+ const stat = statSync7(path);
4795
5060
  if (stat.size > LARGE_LOG_WARN_BYTES) {
4796
5061
  return {
4797
5062
  name,
@@ -4833,7 +5098,9 @@ function printDoctorReport(report) {
4833
5098
  }
4834
5099
  var LARGE_LOG_WARN_BYTES;
4835
5100
  var init_doctor = __esm(() => {
5101
+ init_plugin_cache();
4836
5102
  init_build_info();
5103
+ init_config_service();
4837
5104
  init_env_guard();
4838
5105
  init_pair_resolver();
4839
5106
  init_thread_state();
@@ -4865,6 +5132,9 @@ function formatAgent(name, usage, snapshotAt) {
4865
5132
  if (usage.rateLimitedUntil > 0) {
4866
5133
  parts.push(`\u9650\u6D41\u81F3 ${formatEpoch(usage.rateLimitedUntil)}`);
4867
5134
  }
5135
+ if (usage.parsedVia === "positional") {
5136
+ parts.push("\u26A0\uFE0F \u7A97\u53E3\u8BC6\u522B\u4F7F\u7528\u4F4D\u7F6E\u515C\u5E95");
5137
+ }
4868
5138
  const ageSec = usage.fetchedAt > 0 ? snapshotAt - usage.fetchedAt : 0;
4869
5139
  if (ageSec > 300) {
4870
5140
  parts.push(`\u26A0\uFE0F \u6570\u636E\u91C7\u96C6\u4E8E ${Math.round(ageSec / 60)} \u5206\u949F\u524D`);
@@ -4976,6 +5246,160 @@ var init_budget = __esm(() => {
4976
5246
  init_render();
4977
5247
  });
4978
5248
 
5249
+ // src/cli/logs.ts
5250
+ var exports_logs = {};
5251
+ __export(exports_logs, {
5252
+ tailLines: () => tailLines,
5253
+ runLogs: () => runLogs,
5254
+ parseLogsArgs: () => parseLogsArgs,
5255
+ followLog: () => followLog
5256
+ });
5257
+ import { existsSync as existsSync16, readFileSync as readFileSync13 } from "fs";
5258
+ import { spawn as spawn4 } from "child_process";
5259
+ function parseLogsArgs(args) {
5260
+ let codex = false;
5261
+ let follow = false;
5262
+ let lines = DEFAULT_LINES;
5263
+ for (let i = 0;i < args.length; i++) {
5264
+ const a = args[i];
5265
+ if (a === "--codex") {
5266
+ codex = true;
5267
+ continue;
5268
+ }
5269
+ if (a === "-f" || a === "--follow") {
5270
+ follow = true;
5271
+ continue;
5272
+ }
5273
+ if (a === "-n" || a === "--lines") {
5274
+ const next = args[i + 1];
5275
+ if (next === undefined) {
5276
+ throw new Error(`${a} requires a positive integer (e.g. ${a} 200)`);
5277
+ }
5278
+ lines = parsePositiveInt(next, a);
5279
+ i++;
5280
+ continue;
5281
+ }
5282
+ if (a.startsWith("-n")) {
5283
+ lines = parsePositiveInt(a.slice(2), "-n");
5284
+ continue;
5285
+ }
5286
+ if (a.startsWith("--lines=")) {
5287
+ lines = parsePositiveInt(a.slice("--lines=".length), "--lines");
5288
+ continue;
5289
+ }
5290
+ throw new Error(`Unknown logs flag: ${a}`);
5291
+ }
5292
+ return { codex, follow, lines };
5293
+ }
5294
+ function parsePositiveInt(raw, flag) {
5295
+ if (!/^\d+$/.test(raw)) {
5296
+ throw new Error(`${flag} must be a positive integer, got "${raw}"`);
5297
+ }
5298
+ const n = Number.parseInt(raw, 10);
5299
+ if (!Number.isInteger(n) || n <= 0) {
5300
+ throw new Error(`${flag} must be a positive integer, got "${raw}"`);
5301
+ }
5302
+ return n;
5303
+ }
5304
+ function tailLines(text, count) {
5305
+ const body = text.endsWith(`
5306
+ `) ? text.slice(0, -1) : text;
5307
+ if (body.length === 0)
5308
+ return [];
5309
+ const all = body.split(`
5310
+ `);
5311
+ return all.length <= count ? all : all.slice(all.length - count);
5312
+ }
5313
+ async function runLogs(args) {
5314
+ const { pairFlag } = parsePairFlag(args);
5315
+ const rest = stripPairTokens(args);
5316
+ let options;
5317
+ try {
5318
+ options = parseLogsArgs(rest);
5319
+ } catch (err) {
5320
+ console.error(`[agentbridge] ${err instanceof Error ? err.message : String(err)}`);
5321
+ process.exit(1);
5322
+ return;
5323
+ }
5324
+ let resolution;
5325
+ try {
5326
+ resolution = resolvePairReadOnly(pairFlag);
5327
+ } catch (err) {
5328
+ console.error(`[agentbridge] ${err instanceof Error ? err.message : String(err)}`);
5329
+ process.exit(1);
5330
+ return;
5331
+ }
5332
+ const { pair } = resolution;
5333
+ const logPath = options.codex ? pair.stateDir.codexWrapperLogFile : pair.stateDir.logFile;
5334
+ const logLabel = options.codex ? "codex wrapper log" : "daemon log";
5335
+ if (!existsSync16(logPath)) {
5336
+ const which = options.codex ? "codex wrapper log" : "daemon log";
5337
+ console.error(`no ${which} for pair ${pair.name} yet \u2014 start it with \`abg claude\` (${logPath})`);
5338
+ process.exit(1);
5339
+ return;
5340
+ }
5341
+ if (options.follow) {
5342
+ await followLog(logPath, options.lines);
5343
+ return;
5344
+ }
5345
+ printTail(logPath, options.lines, logLabel, pair.name);
5346
+ }
5347
+ function printTail(logPath, count, label, pairName) {
5348
+ let text;
5349
+ try {
5350
+ text = readFileSync13(logPath, "utf8");
5351
+ } catch (err) {
5352
+ console.error(`[agentbridge] failed to read ${label} for pair ${pairName}: ` + `${err instanceof Error ? err.message : String(err)} (${logPath})`);
5353
+ process.exit(1);
5354
+ return;
5355
+ }
5356
+ const lines = tailLines(text, count);
5357
+ for (const line of lines)
5358
+ console.log(line);
5359
+ }
5360
+ function followLog(logPath, count) {
5361
+ return new Promise((resolvePromise) => {
5362
+ const child = spawn4("tail", ["-f", "-n", String(count), logPath], {
5363
+ stdio: "inherit"
5364
+ });
5365
+ child.on("error", (err) => {
5366
+ console.error(`[agentbridge] failed to follow log: ${err.message}`);
5367
+ process.exit(1);
5368
+ });
5369
+ child.on("exit", (code, signal) => {
5370
+ if (signal === "SIGINT" || signal === "SIGTERM") {
5371
+ resolvePromise();
5372
+ return;
5373
+ }
5374
+ if (code != null && code !== 0) {
5375
+ process.exit(code);
5376
+ return;
5377
+ }
5378
+ resolvePromise();
5379
+ });
5380
+ });
5381
+ }
5382
+ function stripPairTokens(args) {
5383
+ const out = [];
5384
+ for (let i = 0;i < args.length; i++) {
5385
+ const a = args[i];
5386
+ if (a === "--pair") {
5387
+ const next = args[i + 1];
5388
+ if (next !== undefined && !next.startsWith("-"))
5389
+ i++;
5390
+ continue;
5391
+ }
5392
+ if (a.startsWith("--pair="))
5393
+ continue;
5394
+ out.push(a);
5395
+ }
5396
+ return out;
5397
+ }
5398
+ var DEFAULT_LINES = 100;
5399
+ var init_logs = __esm(() => {
5400
+ init_pair_resolver();
5401
+ });
5402
+
4979
5403
  // src/cli.ts
4980
5404
  function parseTopLevel(args) {
4981
5405
  const pairTokens = [];
@@ -5048,6 +5472,10 @@ async function main(command, restArgs) {
5048
5472
  const { runBudget: runBudget2 } = await Promise.resolve().then(() => (init_budget(), exports_budget));
5049
5473
  await runBudget2(restArgs);
5050
5474
  break;
5475
+ case "logs":
5476
+ const { runLogs: runLogs2 } = await Promise.resolve().then(() => (init_logs(), exports_logs));
5477
+ await runLogs2(restArgs);
5478
+ break;
5051
5479
  case "--help":
5052
5480
  case "-h":
5053
5481
  case undefined:
@@ -5086,6 +5514,9 @@ Commands:
5086
5514
  doctor [--json] Diagnose env, daemon, build drift, logs, and current thread
5087
5515
  doctor resume-pollution [--apply] Find/fix old AgentBridge kickoff metadata
5088
5516
  budget [--json] Show both agents' subscription quota snapshot (5h/weekly, drift, pause state)
5517
+ logs [--codex] [-f] [-n N]
5518
+ Tail this pair's daemon log (or the codex wrapper log with
5519
+ --codex). -n N: last N lines (default 100). -f: follow/stream.
5089
5520
  kill [all | --all | --pair <name|id>]
5090
5521
  Stop this directory's pairs (default), every pair (all/--all), or one (--pair)
5091
5522
 
@@ -5124,6 +5555,10 @@ Examples:
5124
5555
  abg pairs # List all pairs and their ports/status
5125
5556
  abg pairs --threads # Include current thread mapping
5126
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
5127
5562
  abg pairs rm work # Stop this directory's "work" pair and free its slot
5128
5563
  abg pairs rm work-1a2b3c4d # ...or by its full id (from that pair's directory)
5129
5564
  abg pairs prune --dry-run # Preview orphan pair dirs (no registry entry, not live)
@@ -5145,7 +5580,7 @@ var MARKETPLACE_NAME = "agentbridge", PLUGIN_NAME = "agentbridge", REFRESH_COMMA
5145
5580
  var init_cli = __esm(() => {
5146
5581
  REFRESH_COMMANDS = new Set(["claude", "codex", "resume"]);
5147
5582
  NOTIFY_COMMANDS = new Set(["claude", "codex", "init", "dev", "resume"]);
5148
- PAIR_AWARE_COMMANDS = new Set(["claude", "codex", "kill", "doctor", "budget", "resume"]);
5583
+ PAIR_AWARE_COMMANDS = new Set(["claude", "codex", "kill", "doctor", "budget", "resume", "logs"]);
5149
5584
  if (import.meta.main) {
5150
5585
  const { command, restArgs } = parseTopLevel(process.argv.slice(2));
5151
5586
  main(command, restArgs).catch((err) => {
@@ -5158,6 +5593,9 @@ init_cli();
5158
5593
 
5159
5594
  export {
5160
5595
  parseTopLevel,
5596
+ REFRESH_COMMANDS,
5161
5597
  PLUGIN_NAME,
5598
+ PAIR_AWARE_COMMANDS,
5599
+ NOTIFY_COMMANDS,
5162
5600
  MARKETPLACE_NAME
5163
5601
  };