@raysonmeng/agentbridge 0.1.4 → 0.1.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raysonmeng/agentbridge",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Bridge between Claude Code and Codex — bidirectional agent communication via MCP Channel + JSON-RPC",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,13 +19,14 @@
19
19
  "start": "bun run src/bridge.ts",
20
20
  "build:cli": "mkdir -p dist && bun build src/cli.ts --outfile dist/cli.js --target bun && chmod +x dist/cli.js",
21
21
  "build:plugin": "mkdir -p plugins/agentbridge/server && bun build src/bridge.ts --outfile plugins/agentbridge/server/bridge-server.js --target bun && bun build src/daemon.ts --outfile plugins/agentbridge/server/daemon.js --target bun",
22
+ "verify:plugin-sync": "node scripts/verify-plugin-sync.cjs",
22
23
  "postinstall": "node scripts/postinstall.cjs",
23
24
  "prepublishOnly": "bun run build:cli && bun run build:plugin",
24
25
  "validate:plugin": "claude plugin validate plugins/agentbridge && claude plugin validate .claude-plugin/marketplace.json",
25
26
  "test": "bun test src",
26
27
  "typecheck": "tsc --noEmit",
27
28
  "validate:plugin-versions": "bun scripts/check-plugin-versions.js",
28
- "check": "tsc --noEmit && bun test src && bun run build:plugin && bun scripts/check-plugin-versions.js"
29
+ "check": "tsc --noEmit && bun test src && bun run verify:plugin-sync && bun scripts/check-plugin-versions.js"
29
30
  },
30
31
  "repository": {
31
32
  "type": "git",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentbridge",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Bridge Claude Code and Codex with a shared daemon, push channel delivery, and bidirectional reply tooling.",
5
5
  "author": {
6
6
  "name": "AgentBridge Contributors",
@@ -13663,6 +13663,60 @@ class StdioServerTransport {
13663
13663
  import { EventEmitter } from "events";
13664
13664
  import { randomUUID } from "crypto";
13665
13665
  import { appendFileSync } from "fs";
13666
+
13667
+ // src/state-dir.ts
13668
+ import { mkdirSync, existsSync } from "fs";
13669
+ import { join } from "path";
13670
+ import { homedir, platform } from "os";
13671
+
13672
+ class StateDirResolver {
13673
+ stateDir;
13674
+ constructor(envOverride) {
13675
+ const override = envOverride ?? process.env.AGENTBRIDGE_STATE_DIR;
13676
+ if (override) {
13677
+ this.stateDir = override;
13678
+ } else if (platform() === "darwin") {
13679
+ this.stateDir = join(homedir(), "Library", "Application Support", "AgentBridge");
13680
+ } else {
13681
+ const xdgState = process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state");
13682
+ this.stateDir = join(xdgState, "agentbridge");
13683
+ }
13684
+ }
13685
+ ensure() {
13686
+ if (!existsSync(this.stateDir)) {
13687
+ mkdirSync(this.stateDir, { recursive: true });
13688
+ }
13689
+ }
13690
+ get dir() {
13691
+ return this.stateDir;
13692
+ }
13693
+ get pidFile() {
13694
+ return join(this.stateDir, "daemon.pid");
13695
+ }
13696
+ get tuiPidFile() {
13697
+ return join(this.stateDir, "codex-tui.pid");
13698
+ }
13699
+ get lockFile() {
13700
+ return join(this.stateDir, "daemon.lock");
13701
+ }
13702
+ get statusFile() {
13703
+ return join(this.stateDir, "status.json");
13704
+ }
13705
+ get portsFile() {
13706
+ return join(this.stateDir, "ports.json");
13707
+ }
13708
+ get logFile() {
13709
+ return join(this.stateDir, "agentbridge.log");
13710
+ }
13711
+ get codexWrapperLogFile() {
13712
+ return join(this.stateDir, "codex-wrapper.log");
13713
+ }
13714
+ get killedFile() {
13715
+ return join(this.stateDir, "killed");
13716
+ }
13717
+ }
13718
+
13719
+ // src/claude-adapter.ts
13666
13720
  var CLAUDE_INSTRUCTIONS = [
13667
13721
  "Codex is an AI coding agent (OpenAI) running in a separate session on the same machine.",
13668
13722
  "",
@@ -13697,23 +13751,27 @@ var CLAUDE_INSTRUCTIONS = [
13697
13751
  "- If the reply tool returns a busy error, Codex is still executing \u2014 wait and try again later."
13698
13752
  ].join(`
13699
13753
  `);
13700
- var LOG_FILE = "/tmp/agentbridge.log";
13701
13754
 
13702
13755
  class ClaudeAdapter extends EventEmitter {
13703
13756
  server;
13704
13757
  notificationSeq = 0;
13705
13758
  sessionId;
13706
13759
  notificationIdPrefix;
13760
+ instanceId;
13707
13761
  replySender = null;
13762
+ logFile;
13708
13763
  configuredMode;
13709
13764
  resolvedMode = null;
13710
13765
  pendingMessages = [];
13711
13766
  maxBufferedMessages;
13712
13767
  droppedMessageCount = 0;
13713
- constructor() {
13768
+ constructor(logFile = new StateDirResolver().logFile) {
13714
13769
  super();
13770
+ this.logFile = logFile;
13771
+ this.instanceId = randomUUID().slice(0, 8);
13715
13772
  this.sessionId = `codex_${Date.now()}`;
13716
13773
  this.notificationIdPrefix = randomUUID().replace(/-/g, "").slice(0, 12);
13774
+ this.log(`ClaudeAdapter created (instance=${this.instanceId})`);
13717
13775
  const envMode = process.env.AGENTBRIDGE_MODE;
13718
13776
  this.configuredMode = envMode && ["push", "pull", "auto"].includes(envMode) ? envMode : "auto";
13719
13777
  this.maxBufferedMessages = parseInt(process.env.AGENTBRIDGE_MAX_BUFFERED_MESSAGES ?? "100", 10);
@@ -13750,10 +13808,11 @@ class ClaudeAdapter extends EventEmitter {
13750
13808
  this.log(`Delivery mode set by AGENTBRIDGE_MODE: ${this.resolvedMode}`);
13751
13809
  } else {
13752
13810
  this.resolvedMode = "push";
13753
- this.log("Delivery mode defaulting to push (set AGENTBRIDGE_MODE=pull for API key mode)");
13811
+ this.log("Delivery mode defaulting to push (set AGENTBRIDGE_MODE=pull to use polling instead)");
13754
13812
  }
13755
13813
  }
13756
13814
  async pushNotification(message) {
13815
+ this.log(`pushNotification (instance=${this.instanceId}, mode=${this.resolvedMode}, msgId=${message.id}, len=${message.content.length})`);
13757
13816
  if (this.resolvedMode === "push") {
13758
13817
  await this.pushViaChannel(message);
13759
13818
  } else {
@@ -13781,6 +13840,7 @@ class ClaudeAdapter extends EventEmitter {
13781
13840
  this.log(`Pushed notification: ${msgId}`);
13782
13841
  } catch (e) {
13783
13842
  this.log(`Push notification failed: ${e.message}`);
13843
+ this.queueForPull(message);
13784
13844
  }
13785
13845
  }
13786
13846
  queueForPull(message) {
@@ -13790,9 +13850,10 @@ class ClaudeAdapter extends EventEmitter {
13790
13850
  this.log(`Message queue full, dropped oldest message (total dropped: ${this.droppedMessageCount})`);
13791
13851
  }
13792
13852
  this.pendingMessages.push(message);
13793
- this.log(`Queued message for pull (${this.pendingMessages.length} pending)`);
13853
+ this.log(`Queued message for pull (${this.pendingMessages.length} pending, instance=${this.instanceId})`);
13794
13854
  }
13795
13855
  drainMessages() {
13856
+ this.log(`get_messages called (instance=${this.instanceId}, pending=${this.pendingMessages.length}, dropped=${this.droppedMessageCount})`);
13796
13857
  if (this.pendingMessages.length === 0 && this.droppedMessageCount === 0) {
13797
13858
  return {
13798
13859
  content: [{ type: "text", text: "No new messages from Codex." }]
@@ -13817,6 +13878,7 @@ Codex: ${msg.content}`;
13817
13878
  }).join(`
13818
13879
 
13819
13880
  `);
13881
+ this.log(`get_messages returning ${count} message(s) (instance=${this.instanceId}, dropped=${dropped})`);
13820
13882
  return {
13821
13883
  content: [
13822
13884
  {
@@ -13922,13 +13984,18 @@ ${formatted}`
13922
13984
  `;
13923
13985
  process.stderr.write(line);
13924
13986
  try {
13925
- appendFileSync(LOG_FILE, line);
13987
+ appendFileSync(this.logFile, line);
13926
13988
  } catch {}
13927
13989
  }
13928
13990
  }
13929
13991
 
13930
13992
  // src/daemon-client.ts
13931
13993
  import { EventEmitter as EventEmitter2 } from "events";
13994
+
13995
+ // src/control-protocol.ts
13996
+ var CLOSE_CODE_REPLACED = 4001;
13997
+
13998
+ // src/daemon-client.ts
13932
13999
  var nextSocketId = 0;
13933
14000
 
13934
14001
  class DaemonClient extends EventEmitter2 {
@@ -14047,7 +14114,11 @@ class DaemonClient extends EventEmitter2 {
14047
14114
  if (isCurrent) {
14048
14115
  this.ws = null;
14049
14116
  this.rejectPendingReplies("AgentBridge daemon disconnected.");
14050
- this.emit("disconnect");
14117
+ if (event.code === CLOSE_CODE_REPLACED) {
14118
+ this.emit("rejected");
14119
+ } else {
14120
+ this.emit("disconnect");
14121
+ }
14051
14122
  }
14052
14123
  };
14053
14124
  ws.onerror = () => {};
@@ -14073,7 +14144,7 @@ class DaemonClient extends EventEmitter2 {
14073
14144
 
14074
14145
  // src/daemon-lifecycle.ts
14075
14146
  import { spawn, execFileSync } from "child_process";
14076
- import { existsSync, readFileSync, unlinkSync, writeFileSync, openSync, closeSync, constants } from "fs";
14147
+ import { existsSync as existsSync2, readFileSync, unlinkSync, writeFileSync, openSync, closeSync, constants } from "fs";
14077
14148
  import { fileURLToPath } from "url";
14078
14149
  var DAEMON_ENTRY = process.env.AGENTBRIDGE_DAEMON_ENTRY ?? "./daemon.ts";
14079
14150
  var DAEMON_PATH = fileURLToPath(new URL(DAEMON_ENTRY, import.meta.url));
@@ -14211,7 +14282,7 @@ class DaemonLifecycle {
14211
14282
  } catch {}
14212
14283
  }
14213
14284
  wasKilled() {
14214
- return existsSync(this.stateDir.killedFile);
14285
+ return existsSync2(this.stateDir.killedFile);
14215
14286
  }
14216
14287
  launch() {
14217
14288
  this.stateDir.ensure();
@@ -14332,116 +14403,62 @@ function isProcessAlive(pid) {
14332
14403
  }
14333
14404
  }
14334
14405
 
14335
- // src/state-dir.ts
14336
- import { mkdirSync, existsSync as existsSync2 } from "fs";
14337
- import { join } from "path";
14338
- import { homedir, platform } from "os";
14339
-
14340
- class StateDirResolver {
14341
- stateDir;
14342
- constructor(envOverride) {
14343
- const override = envOverride ?? process.env.AGENTBRIDGE_STATE_DIR;
14344
- if (override) {
14345
- this.stateDir = override;
14346
- } else if (platform() === "darwin") {
14347
- this.stateDir = join(homedir(), "Library", "Application Support", "AgentBridge");
14348
- } else {
14349
- const xdgState = process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state");
14350
- this.stateDir = join(xdgState, "agentbridge");
14351
- }
14352
- }
14353
- ensure() {
14354
- if (!existsSync2(this.stateDir)) {
14355
- mkdirSync(this.stateDir, { recursive: true });
14356
- }
14357
- }
14358
- get dir() {
14359
- return this.stateDir;
14360
- }
14361
- get pidFile() {
14362
- return join(this.stateDir, "daemon.pid");
14363
- }
14364
- get tuiPidFile() {
14365
- return join(this.stateDir, "codex-tui.pid");
14366
- }
14367
- get lockFile() {
14368
- return join(this.stateDir, "daemon.lock");
14369
- }
14370
- get statusFile() {
14371
- return join(this.stateDir, "status.json");
14372
- }
14373
- get portsFile() {
14374
- return join(this.stateDir, "ports.json");
14375
- }
14376
- get logFile() {
14377
- return join(this.stateDir, "agentbridge.log");
14378
- }
14379
- get killedFile() {
14380
- return join(this.stateDir, "killed");
14381
- }
14382
- }
14383
-
14384
14406
  // src/config-service.ts
14385
14407
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
14386
14408
  import { join as join2 } from "path";
14387
14409
  var DEFAULT_CONFIG = {
14388
14410
  version: "1.0",
14389
- daemon: {
14390
- port: 4500,
14411
+ codex: {
14412
+ appPort: 4500,
14391
14413
  proxyPort: 4501
14392
14414
  },
14393
- agents: {
14394
- claude: {
14395
- role: "Reviewer, Planner",
14396
- mode: "push"
14397
- },
14398
- codex: {
14399
- role: "Implementer, Executor"
14400
- }
14401
- },
14402
- markers: ["IMPORTANT", "STATUS", "FYI"],
14403
14415
  turnCoordination: {
14404
- attentionWindowSeconds: 15,
14405
- busyGuard: true
14416
+ attentionWindowSeconds: 15
14406
14417
  },
14407
14418
  idleShutdownSeconds: 30
14408
14419
  };
14409
- var DEFAULT_COLLABORATION_MD = `# Collaboration Rules
14410
-
14411
- ## Roles
14412
- - Claude: Reviewer, Planner, Hypothesis Challenger
14413
- - Codex: Implementer, Executor, Reproducer/Verifier
14414
-
14415
- ## Thinking Patterns
14416
- - Analytical/review tasks: Independent Analysis & Convergence
14417
- - Implementation tasks: Architect -> Builder -> Critic
14418
- - Debugging tasks: Hypothesis -> Experiment -> Interpretation
14419
-
14420
- ## Communication
14421
- - Use explicit phrases: "My independent view is:", "I agree on:", "I disagree on:", "Current consensus:"
14422
- - Tag messages with [IMPORTANT], [STATUS], or [FYI]
14423
-
14424
- ## Review Process
14425
- - Cross-review: author never reviews their own code
14426
- - All changes go through feature/fix branches + PR
14427
- - Merge via squash merge
14428
-
14429
- ## Custom Rules
14430
- <!-- Add your project-specific collaboration rules here -->
14431
- `;
14432
14420
  var CONFIG_DIR = ".agentbridge";
14433
14421
  var CONFIG_FILE = "config.json";
14434
- var COLLABORATION_FILE = "collaboration.md";
14422
+ function isRecord(value) {
14423
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14424
+ }
14425
+ function normalizeInteger(value, fallback) {
14426
+ if (typeof value === "number" && Number.isFinite(value))
14427
+ return value;
14428
+ if (typeof value === "string") {
14429
+ const parsed = Number(value);
14430
+ if (Number.isFinite(parsed))
14431
+ return parsed;
14432
+ }
14433
+ return fallback;
14434
+ }
14435
+ function normalizeConfig(raw) {
14436
+ if (!isRecord(raw))
14437
+ return null;
14438
+ const config2 = raw;
14439
+ const codex = isRecord(config2.codex) ? config2.codex : {};
14440
+ const daemon = isRecord(config2.daemon) ? config2.daemon : {};
14441
+ const turnCoordination = isRecord(config2.turnCoordination) ? config2.turnCoordination : {};
14442
+ return {
14443
+ version: typeof config2.version === "string" ? config2.version : DEFAULT_CONFIG.version,
14444
+ codex: {
14445
+ appPort: normalizeInteger(codex.appPort ?? daemon.port, DEFAULT_CONFIG.codex.appPort),
14446
+ proxyPort: normalizeInteger(codex.proxyPort ?? daemon.proxyPort, DEFAULT_CONFIG.codex.proxyPort)
14447
+ },
14448
+ turnCoordination: {
14449
+ attentionWindowSeconds: normalizeInteger(turnCoordination.attentionWindowSeconds, DEFAULT_CONFIG.turnCoordination.attentionWindowSeconds)
14450
+ },
14451
+ idleShutdownSeconds: normalizeInteger(config2.idleShutdownSeconds, DEFAULT_CONFIG.idleShutdownSeconds)
14452
+ };
14453
+ }
14435
14454
 
14436
14455
  class ConfigService {
14437
14456
  configDir;
14438
14457
  configPath;
14439
- collaborationPath;
14440
14458
  constructor(projectRoot) {
14441
14459
  const root = projectRoot ?? process.cwd();
14442
14460
  this.configDir = join2(root, CONFIG_DIR);
14443
14461
  this.configPath = join2(this.configDir, CONFIG_FILE);
14444
- this.collaborationPath = join2(this.configDir, COLLABORATION_FILE);
14445
14462
  }
14446
14463
  hasConfig() {
14447
14464
  return existsSync3(this.configPath);
@@ -14449,7 +14466,7 @@ class ConfigService {
14449
14466
  load() {
14450
14467
  try {
14451
14468
  const raw = readFileSync2(this.configPath, "utf-8");
14452
- return JSON.parse(raw);
14469
+ return normalizeConfig(JSON.parse(raw));
14453
14470
  } catch {
14454
14471
  return null;
14455
14472
  }
@@ -14462,17 +14479,6 @@ class ConfigService {
14462
14479
  writeFileSync2(this.configPath, JSON.stringify(config2, null, 2) + `
14463
14480
  `, "utf-8");
14464
14481
  }
14465
- loadCollaboration() {
14466
- try {
14467
- return readFileSync2(this.collaborationPath, "utf-8");
14468
- } catch {
14469
- return null;
14470
- }
14471
- }
14472
- saveCollaboration(content) {
14473
- this.ensureConfigDir();
14474
- writeFileSync2(this.collaborationPath, content, "utf-8");
14475
- }
14476
14482
  initDefaults() {
14477
14483
  this.ensureConfigDir();
14478
14484
  const created = [];
@@ -14480,18 +14486,11 @@ class ConfigService {
14480
14486
  this.save(DEFAULT_CONFIG);
14481
14487
  created.push(this.configPath);
14482
14488
  }
14483
- if (!existsSync3(this.collaborationPath)) {
14484
- this.saveCollaboration(DEFAULT_COLLABORATION_MD);
14485
- created.push(this.collaborationPath);
14486
- }
14487
14489
  return created;
14488
14490
  }
14489
14491
  get configFilePath() {
14490
14492
  return this.configPath;
14491
14493
  }
14492
- get collaborationFilePath() {
14493
- return this.collaborationPath;
14494
- }
14495
14494
  ensureConfigDir() {
14496
14495
  if (!existsSync3(this.configDir)) {
14497
14496
  mkdirSync2(this.configDir, { recursive: true });
@@ -14499,17 +14498,31 @@ class ConfigService {
14499
14498
  }
14500
14499
  }
14501
14500
 
14501
+ // src/bridge-disabled-state.ts
14502
+ function disabledReplyError(reason) {
14503
+ switch (reason) {
14504
+ case "rejected":
14505
+ return "AgentBridge rejected this session \u2014 another Claude Code session is already connected. Close the other session first, or run `agentbridge kill` to reset.";
14506
+ case "killed":
14507
+ return "AgentBridge is disabled by `agentbridge kill`. Restart Claude Code (`agentbridge claude`), switch to a new conversation, or run `/resume` to reconnect.";
14508
+ }
14509
+ }
14510
+
14502
14511
  // src/bridge.ts
14503
14512
  var stateDir = new StateDirResolver;
14513
+ stateDir.ensure();
14504
14514
  var configService = new ConfigService;
14505
14515
  var config2 = configService.loadOrDefault();
14506
14516
  var CONTROL_PORT = parseInt(process.env.AGENTBRIDGE_CONTROL_PORT ?? "4502", 10);
14507
14517
  var daemonLifecycle = new DaemonLifecycle({ stateDir, controlPort: CONTROL_PORT, log });
14508
14518
  var CONTROL_WS_URL = daemonLifecycle.controlWsUrl;
14509
- var claude = new ClaudeAdapter;
14519
+ var claude = new ClaudeAdapter(stateDir.logFile);
14510
14520
  var daemonClient = new DaemonClient(CONTROL_WS_URL);
14511
14521
  var shuttingDown = false;
14512
14522
  var daemonDisabled = false;
14523
+ var daemonDisabledReason = null;
14524
+ var hasSeenTuiConnect = false;
14525
+ var previousTuiConnected = false;
14513
14526
  var RECONNECT_NOTIFY_COOLDOWN_MS = 30000;
14514
14527
  var DISABLED_RECOVERY_INTERVAL_MS = 5000;
14515
14528
  var lastDisconnectNotifyTs = 0;
@@ -14523,7 +14536,7 @@ claude.setReplySender(async (msg, requireReply) => {
14523
14536
  if (daemonDisabled) {
14524
14537
  return {
14525
14538
  success: false,
14526
- error: "AgentBridge is disabled by `agentbridge kill`. Restart Claude Code (`agentbridge claude`), switch to a new conversation, or run `/resume` to reconnect."
14539
+ error: disabledReplyError(daemonDisabledReason ?? "killed")
14527
14540
  };
14528
14541
  }
14529
14542
  return daemonClient.sendReply(msg, requireReply);
@@ -14534,6 +14547,18 @@ daemonClient.on("codexMessage", (message) => {
14534
14547
  });
14535
14548
  daemonClient.on("status", (status) => {
14536
14549
  log(`Daemon status: ready=${status.bridgeReady} tui=${status.tuiConnected} thread=${status.threadId ?? "none"} queued=${status.queuedMessageCount}`);
14550
+ if (!hasSeenTuiConnect && status.tuiConnected && !previousTuiConnected) {
14551
+ hasSeenTuiConnect = true;
14552
+ log("First TUI connect detected \u2014 sending kickoff message to Claude");
14553
+ claude.pushNotification(systemMessage("system_tui_kickoff", [
14554
+ "\uD83E\uDD1D Codex has connected via AgentBridge.",
14555
+ "You are now in a multi-agent collaboration session.",
14556
+ "When you receive a complex task, propose a division of labor to Codex.",
14557
+ "Use `reply` to send messages and `get_messages` to check for responses."
14558
+ ].join(`
14559
+ `)));
14560
+ }
14561
+ previousTuiConnected = status.tuiConnected;
14537
14562
  });
14538
14563
  daemonClient.on("disconnect", () => {
14539
14564
  if (shuttingDown || daemonDisabled)
@@ -14548,6 +14573,15 @@ daemonClient.on("disconnect", () => {
14548
14573
  }
14549
14574
  reconnectToDaemon();
14550
14575
  });
14576
+ daemonClient.on("rejected", async () => {
14577
+ if (shuttingDown || daemonDisabled)
14578
+ return;
14579
+ log("Daemon rejected this session (close code 4001) \u2014 another Claude session is already connected");
14580
+ daemonDisabled = true;
14581
+ daemonDisabledReason = "rejected";
14582
+ await claude.pushNotification(systemMessage("system_bridge_replaced", "\u26A0\uFE0F AgentBridge daemon rejected this session \u2014 another Claude Code session is already connected. Close the other session first, or run `agentbridge kill` to reset. AgentBridge \u5B88\u62A4\u8FDB\u7A0B\u62D2\u7EDD\u4E86\u6B64\u4F1A\u8BDD\u2014\u2014\u53E6\u4E00\u4E2A Claude Code \u4F1A\u8BDD\u5DF2\u5728\u8FDE\u63A5\u4E2D\u3002\u8BF7\u5148\u5173\u95ED\u53E6\u4E00\u4E2A\u4F1A\u8BDD\uFF0C\u6216\u8FD0\u884C `agentbridge kill` \u91CD\u7F6E\u3002"));
14583
+ await daemonClient.disconnect();
14584
+ });
14551
14585
  claude.on("ready", async () => {
14552
14586
  log(`MCP server ready (delivery mode: ${claude.getDeliveryMode()}) \u2014 ensuring AgentBridge daemon...`);
14553
14587
  if (daemonLifecycle.wasKilled()) {
@@ -14565,6 +14599,7 @@ async function connectToDaemon(isReconnect = false) {
14565
14599
  await daemonLifecycle.ensureRunning();
14566
14600
  await daemonClient.connect();
14567
14601
  daemonClient.attachClaude();
14602
+ daemonDisabledReason = null;
14568
14603
  if (!isReconnect) {
14569
14604
  claude.pushNotification(systemMessage("system_bridge_ready", "\u2705 AgentBridge bridge is ready. Daemon connected. Start Codex in another terminal with: agentbridge codex"));
14570
14605
  }
@@ -14578,6 +14613,7 @@ async function enterDisabledState(logMessage, notificationContent) {
14578
14613
  if (daemonDisabled)
14579
14614
  return;
14580
14615
  daemonDisabled = true;
14616
+ daemonDisabledReason = "killed";
14581
14617
  log(logMessage);
14582
14618
  await claude.pushNotification(systemMessage("system_bridge_disabled", notificationContent));
14583
14619
  await daemonClient.disconnect();
@@ -14666,11 +14702,13 @@ async function pollDisabledRecovery() {
14666
14702
  await daemonClient.connect();
14667
14703
  daemonClient.attachClaude();
14668
14704
  daemonDisabled = false;
14705
+ daemonDisabledReason = null;
14669
14706
  stopDisabledRecoveryPoller();
14670
14707
  claude.pushNotification(systemMessage("system_bridge_recovered", "\u2705 AgentBridge recovered after the killed sentinel was cleared. Daemon reconnected."));
14671
14708
  } catch (err) {
14672
14709
  log(`Disabled-state direct reconnect failed: ${err.message}`);
14673
14710
  daemonDisabled = false;
14711
+ daemonDisabledReason = null;
14674
14712
  stopDisabledRecoveryPoller();
14675
14713
  reconnectToDaemon();
14676
14714
  }