cli-wechat-bridge 1.2.1 → 1.2.3

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.
Files changed (54) hide show
  1. package/README.md +146 -73
  2. package/README_EN.md +544 -0
  3. package/bin/telegram-check-update.mjs +10 -0
  4. package/bin/telegram-claude.mjs +10 -0
  5. package/bin/telegram-codex.mjs +10 -0
  6. package/bin/telegram-daemon.mjs +10 -0
  7. package/bin/telegram-opencode.mjs +10 -0
  8. package/bin/telegram-pi.mjs +10 -0
  9. package/bin/telegram-setup.mjs +10 -0
  10. package/dist/bridge/adapter-control.js +19 -12
  11. package/dist/bridge/bridge-adapters.claude.js +219 -115
  12. package/dist/bridge/bridge-adapters.codex.js +532 -99
  13. package/dist/bridge/bridge-adapters.core.js +84 -7
  14. package/dist/bridge/bridge-adapters.opencode.js +202 -55
  15. package/dist/bridge/bridge-adapters.pi.js +23 -18
  16. package/dist/bridge/bridge-adapters.shared.js +9 -9
  17. package/dist/bridge/bridge-session-resume.js +13 -6
  18. package/dist/bridge/bridge-state.js +3 -1
  19. package/dist/bridge/bridge-utils.js +94 -144
  20. package/dist/bridge/claude-hooks.js +60 -3
  21. package/dist/bridge/claude-native-controls.js +19 -12
  22. package/dist/bridge/language-control.js +33 -0
  23. package/dist/bridge/native-terminal-control.js +26 -10
  24. package/dist/bridge/opencode-v2-compat.js +11 -3
  25. package/dist/bridge/wechat-bridge.js +154 -72
  26. package/dist/channels/channel-driver-factory.js +56 -0
  27. package/dist/channels/local/local-channel-port.js +40 -0
  28. package/dist/channels/local/local-driver.js +217 -0
  29. package/dist/channels/telegram/setup-cli.js +12 -0
  30. package/dist/channels/telegram/setup.js +163 -0
  31. package/dist/channels/telegram/telegram-channel-port.js +47 -0
  32. package/dist/channels/telegram/telegram-config.js +66 -0
  33. package/dist/channels/telegram/telegram-driver.js +106 -0
  34. package/dist/channels/telegram/telegram-message.js +41 -0
  35. package/dist/channels/telegram/telegram-transport.js +378 -0
  36. package/dist/channels/wechat/wechat-driver.js +11 -2
  37. package/dist/channels/wecom/wecom-driver.js +8 -1
  38. package/dist/companion/local-companion-start.js +180 -61
  39. package/dist/companion/local-companion.js +27 -7
  40. package/dist/companion/pi-tui-bridge-extension.js +10 -4
  41. package/dist/core/bridge-control.js +3 -0
  42. package/dist/core/bridge-defer.js +3 -2
  43. package/dist/core/bridge-event-forwarder.js +3 -0
  44. package/dist/core/channel-types.js +13 -1
  45. package/dist/daemon/wechat-daemon.js +239 -126
  46. package/dist/i18n/index.js +25 -11
  47. package/dist/i18n/messages-en.js +245 -1
  48. package/dist/i18n/messages-zh.js +245 -1
  49. package/dist/runtime/legacy-adapter-runtime.js +7 -4
  50. package/dist/wechat/channel-config.js +3 -2
  51. package/package.json +17 -5
  52. package/skills/codex-bridge/SKILL.md +35 -0
  53. package/skills/codex-bridge/agents/openai.yaml +4 -0
  54. package/skills/codex-bridge/scripts/install.mjs +28 -0
@@ -12,6 +12,9 @@ import { execFileSync, spawn } from "node:child_process";
12
12
  import { fileURLToPath } from "node:url";
13
13
  import { resolveDefaultAdapterCommand, } from "../bridge/bridge-adapters.js";
14
14
  import { t } from "../i18n/index.js";
15
+ import { handleLanguageControl } from "../bridge/language-control.js";
16
+ import { parseBridgeControlCommand } from "../core/bridge-control.js";
17
+ import { LANGUAGE_FILE } from "../wechat/channel-config.js";
15
18
  import { delay, getSharedSessionIdFromAdapterState, quoteWindowsCommandArg, } from "../bridge/bridge-adapters.shared.js";
16
19
  import { BridgeController } from "../bridge/bridge-controller.js";
17
20
  import { readBridgeLockFile, } from "../bridge/bridge-state.js";
@@ -36,10 +39,15 @@ import { forwardBridgeEvent } from "../core/bridge-event-forwarder.js";
36
39
  import { isDirectModuleRun } from "../core/direct-run.js";
37
40
  import { WechatChannelPort } from "../channels/wechat/wechat-channel-port.js";
38
41
  import { ensureWecomAccount } from "../channels/wecom/setup.js";
42
+ import { ensureTelegramAccount } from "../channels/telegram/setup.js";
43
+ import { TelegramTransport } from "../channels/telegram/telegram-transport.js";
44
+ import { TelegramChannelPort } from "../channels/telegram/telegram-channel-port.js";
39
45
  import { WecomChannelPort } from "../channels/wecom/wecom-channel-port.js";
40
46
  import { WecomTransport } from "../channels/wecom/wecom-transport.js";
41
- import { WecomChannelDriver } from "../channels/wecom/wecom-driver.js";
42
- import { WechatChannelDriver } from "../channels/wechat/wechat-driver.js";
47
+ import { createChannelDriver } from "../channels/channel-driver-factory.js";
48
+ import { LocalChannelPort } from "../channels/local/local-channel-port.js";
49
+ import { getLocalChannelFiles, LOCAL_CHANNEL_OPERATOR_ID, } from "../channels/local/local-driver.js";
50
+ import { getBridgeChannelDisplayName, isBridgeChannelId, normalizeBridgeChannelId, } from "../core/channel-types.js";
43
51
  import { clearLocalCompanionEndpoint, clearLocalCompanionOccupancy, readLocalCompanionEndpoint, } from "../companion/local-companion-link.js";
44
52
  import { attachDaemonRequestListener, buildDaemonToken, clearDaemonEndpoint, DAEMON_PROTOCOL_VERSION, isPidAlive, readDaemonEndpoint, sendDaemonRequest, sendDaemonResponse, writeDaemonEndpoint, } from "./daemon-link.js";
45
53
  const MODULE_FILE = fileURLToPath(import.meta.url);
@@ -60,7 +68,8 @@ const MAX_DAEMON_DEFERRED_INPUTS = 32;
60
68
  const DAEMON_ADAPTERS = ["codex", "claude", "opencode", "pi"];
61
69
  function getCliChannelId(argv = process.argv.slice(2)) {
62
70
  const index = argv.indexOf("--channel");
63
- return index >= 0 && argv[index + 1] === "wecom" ? "wecom" : "wechat";
71
+ const value = index >= 0 ? argv[index + 1] : undefined;
72
+ return normalizeBridgeChannelId(value);
64
73
  }
65
74
  function log(message) {
66
75
  process.stderr.write(`[${getCliChannelId()}-daemon] ${message}\n`);
@@ -132,11 +141,12 @@ export function parseDaemonCliArgs(argv) {
132
141
  }
133
142
  const next = argv[i + 1];
134
143
  if (arg === "--help" || arg === "-h") {
144
+ const channelDisplayName = getBridgeChannelDisplayName(channelId);
135
145
  process.stdout.write([
136
146
  `Usage: ${channelId}-daemon [--cwd <path>] [--adapter <codex|claude|opencode|pi>] [--profile <name-or-path>] [--no-open]`,
137
147
  "",
138
- `Keeps one ${channelId === "wecom" ? "WeCom" : "WeChat"} connection alive and switches between Codex, Claude Code, OpenCode, and Pi.`,
139
- `Send /codex, /claude, /opencode, or /pi in ${channelId === "wecom" ? "WeCom" : "WeChat"} to switch the active terminal.`,
148
+ `Keeps one ${channelDisplayName} connection alive and switches between Codex, Claude Code, OpenCode, and Pi.`,
149
+ `Send /codex, /claude, /opencode, or /pi in ${channelDisplayName} to switch the active terminal.`,
140
150
  "",
141
151
  ].join("\n"));
142
152
  process.exit(0);
@@ -150,7 +160,7 @@ export function parseDaemonCliArgs(argv) {
150
160
  continue;
151
161
  }
152
162
  if (arg === "--channel") {
153
- if (!next || (next !== "wechat" && next !== "wecom")) {
163
+ if (!isBridgeChannelId(next)) {
154
164
  throw new Error(`Invalid channel: ${next ?? "(missing)"}`);
155
165
  }
156
166
  channelId = next;
@@ -184,7 +194,7 @@ export function parseDaemonCliArgs(argv) {
184
194
  profile,
185
195
  initialAdapter,
186
196
  openVisible,
187
- ...(channelId === "wecom" ? { channelId } : {}),
197
+ ...(channelId === "wechat" ? {} : { channelId }),
188
198
  };
189
199
  }
190
200
  export function parseDaemonSwitchCommand(text) {
@@ -323,8 +333,29 @@ function shellQuotePosix(s) {
323
333
  function formatLaunchPreview(launch) {
324
334
  return [launch.command, ...launch.args].join(" ");
325
335
  }
326
- function openVisibleClient(params) {
336
+ export function openVisibleClient(params) {
327
337
  const args = buildVisibleClientLaunchArgs(params);
338
+ // E2E harness override: host the visible client in a PTY owned by an
339
+ // external launcher script instead of a terminal window, so automated
340
+ // runs can type and read the TUI while every other step stays identical.
341
+ const launcherOverride = process.env.CLI_BRIDGE_VISIBLE_LAUNCHER;
342
+ if (launcherOverride) {
343
+ const child = spawn(process.execPath, [launcherOverride, ...args], {
344
+ cwd: params.cwd,
345
+ env: process.env,
346
+ detached: true,
347
+ stdio: "ignore",
348
+ });
349
+ child.once("error", (error) => {
350
+ params.onError?.(error instanceof Error ? error : new Error(String(error)));
351
+ });
352
+ child.unref();
353
+ return {
354
+ command: process.execPath,
355
+ args: [launcherOverride, ...args],
356
+ pid: child.pid,
357
+ };
358
+ }
328
359
  if (process.platform === "win32") {
329
360
  const command = process.env.ComSpec || "cmd.exe";
330
361
  const launchArgs = [
@@ -490,57 +521,44 @@ function formatInboundMessagePreview(message) {
490
521
  return "(empty)";
491
522
  }
492
523
  function formatNoActiveAdapterMessage() {
493
- return [
494
- "No active terminal is selected.",
495
- "Send /codex, /claude, /opencode, or /pi to choose one.",
496
- ].join("\n");
524
+ return t("daemon.noActiveAdapter");
497
525
  }
498
526
  export function formatDaemonSwitchResultDetail(result) {
499
- if (result.activated === false) {
500
- const previous = result.previousActiveAdapter
501
- ? ` Active terminal remains ${result.previousActiveAdapter}.`
502
- : " No terminal is active yet.";
503
- if (result.visibleConnected && result.visibleReady === false) {
504
- return `The visible Codex CLI connected, but its active thread is not ready yet.${previous} Check ${BRIDGE_LOG_FILE}.`;
505
- }
506
- if (result.openedVisible) {
507
- return result.created
508
- ? `Started the bridge slot and tried to open the visible CLI, but it has not connected yet.${previous} Check ${BRIDGE_LOG_FILE}.`
509
- : `Tried to open a visible CLI for the existing slot, but it has not connected yet.${previous} Check ${BRIDGE_LOG_FILE}.`;
510
- }
511
- return `The visible CLI is not connected yet.${previous} Check ${BRIDGE_LOG_FILE}.`;
512
- }
513
- if (result.openedVisible && result.visibleConnected) {
514
- return result.created
515
- ? "Started a new visible CLI."
516
- : "Opened a visible CLI for the existing slot.";
517
- }
518
- if (result.openedVisible) {
519
- return result.created
520
- ? `Started the bridge slot and tried to open the visible CLI, but it has not connected yet. Check ${BRIDGE_LOG_FILE}.`
521
- : `Tried to open a visible CLI for the existing slot, but it has not connected yet. Check ${BRIDGE_LOG_FILE}.`;
522
- }
523
- if (result.visibleConnected) {
524
- return "Reused the existing visible CLI.";
525
- }
526
- return result.created ? "Started the bridge slot." : "Reused the bridge slot.";
527
+ const previous = result.activated === false
528
+ ? result.previousActiveAdapter
529
+ ? t("daemon.switch.previous", { adapter: result.previousActiveAdapter })
530
+ : t("daemon.switch.none")
531
+ : "";
532
+ if (result.activated === false && result.visibleConnected && result.visibleReady === false) {
533
+ return t("daemon.switch.threadNotReady", { previous, log: BRIDGE_LOG_FILE });
534
+ }
535
+ if (result.openedVisible && (result.activated === false || !result.visibleConnected)) {
536
+ return t(result.created ? "daemon.switch.newNotConnected" : "daemon.switch.existingNotConnected", { previous, log: BRIDGE_LOG_FILE });
537
+ }
538
+ if (result.activated === false)
539
+ return t("daemon.switch.notConnected", { previous, log: BRIDGE_LOG_FILE });
540
+ if (result.openedVisible && result.visibleConnected)
541
+ return t(result.created ? "daemon.switchResult.new" : "daemon.switch.opened");
542
+ if (result.visibleConnected)
543
+ return t("daemon.switchResult.reused");
544
+ return t(result.created ? "daemon.switch.slotStarted" : "daemon.switch.slotReused");
527
545
  }
528
546
  export function formatDaemonStatus(status) {
529
547
  const lines = [
530
- "wechat-daemon status",
531
- `cwd: ${status.cwd}`,
532
- `active: ${status.activeAdapter ?? "(none)"}`,
533
- `started_at: ${status.startedAt}`,
548
+ t("status.daemon", { channel: status.channelId ?? "wechat" }),
549
+ `${t("status.label.cwd")}: ${status.cwd}`,
550
+ `${t("status.label.active")}: ${status.activeAdapter ?? t("status.none")}`,
551
+ `${t("status.label.started_at")}: ${status.startedAt}`,
534
552
  ];
535
553
  for (const adapter of DAEMON_ADAPTERS) {
536
554
  const slot = status.slots.find((entry) => entry.adapter === adapter);
537
555
  if (!slot) {
538
- lines.push(`${adapter}: not started`);
556
+ lines.push(`${adapter}: ${t("status.notStarted")}`);
539
557
  continue;
540
558
  }
541
559
  const flags = [
542
- slot.pendingApproval ? "pending_approval" : "",
543
- slot.pendingUserInput ? "pending_input" : "",
560
+ slot.pendingApproval ? t("status.pendingApproval") : "",
561
+ slot.pendingUserInput ? t("status.pendingInput") : "",
544
562
  slot.companionPid ? `companion_pid=${slot.companionPid}` : "",
545
563
  ].filter(Boolean);
546
564
  lines.push(`${adapter}: ${slot.status}${flags.length ? ` (${flags.join(", ")})` : ""}`);
@@ -555,6 +573,8 @@ export class WechatDaemon {
555
573
  channelDriver;
556
574
  channelId;
557
575
  wecomTransport;
576
+ telegramTransport;
577
+ localFiles;
558
578
  inboundConversationContext = new InboundConversationContext();
559
579
  fallbackConversation;
560
580
  slots = new Map();
@@ -582,19 +602,21 @@ export class WechatDaemon {
582
602
  this.transport = params.transport;
583
603
  this.channelId = params.channelId ?? "wechat";
584
604
  this.wecomTransport = params.wecomTransport ?? null;
605
+ this.telegramTransport = params.telegramTransport ?? null;
606
+ this.localFiles = params.localFiles ?? null;
585
607
  this.deps = params.deps ?? {};
586
- this.channelDriver = this.wecomTransport
587
- ? new WecomChannelDriver({
588
- transport: this.wecomTransport,
589
- accountId: params.accountId,
590
- operatorId: params.authorizedUserId,
591
- logError,
592
- })
593
- : new WechatChannelDriver({
594
- transport: this.transport,
595
- logError,
596
- buildInboundPrompt: (text, attachments) => buildWechatInboundPrompt(text, attachments.filter((attachment) => attachment.kind === "image" || attachment.kind === "file")),
597
- });
608
+ this.channelDriver = createChannelDriver({
609
+ channelId: this.channelId,
610
+ authorizedUserId: params.authorizedUserId,
611
+ accountId: params.accountId,
612
+ wechatTransport: this.transport,
613
+ wecomTransport: this.wecomTransport,
614
+ telegramTransport: this.telegramTransport,
615
+ localFiles: this.localFiles,
616
+ buildWechatInboundPrompt: (text, attachments) => buildWechatInboundPrompt(text, attachments.filter((attachment) => attachment.kind === "image" || attachment.kind === "file")),
617
+ log: (message) => this.daemonLog(message),
618
+ logError,
619
+ });
598
620
  this.fallbackConversation = this.channelDriver.defaultConversation()
599
621
  ?? this.channelDriver.directConversation(params.authorizedUserId);
600
622
  this.pendingWechatMessages = this.deps.pendingWechatMessages ??
@@ -716,7 +738,7 @@ export class WechatDaemon {
716
738
  if (chatType === "direct") {
717
739
  const result = await this.channelDriver.sendText({
718
740
  target: this.channelDriver.directConversation(senderId),
719
- text: "Unauthorized.",
741
+ text: t("channel.unauthorized"),
720
742
  context: "notice",
721
743
  log: appendDaemonLog,
722
744
  });
@@ -743,6 +765,7 @@ export class WechatDaemon {
743
765
  appendDaemonLog(`started: channel=${this.channelId} cwd=${this.cwd}`);
744
766
  const activeSlot = this.getActiveSlot();
745
767
  const welcomeText = t("daemon.welcome", {
768
+ channel: this.channelDriver.displayName,
746
769
  cwd: this.cwd,
747
770
  adapter: activeSlot?.adapter ?? "none",
748
771
  bindings: formatBindingsListMessage(listBindings()),
@@ -756,6 +779,9 @@ export class WechatDaemon {
756
779
  return;
757
780
  }
758
781
  while (!this.shutdownPromise) {
782
+ if (!this.transport) {
783
+ throw new Error("The wechat poll loop requires a WeChat transport.");
784
+ }
759
785
  let pollResult;
760
786
  try {
761
787
  pollResult = await this.transport.pollMessages({
@@ -853,7 +879,7 @@ export class WechatDaemon {
853
879
  // the process alive until the 35-second poll timeout and retain the daemon
854
880
  // endpoint for the next startup to clean up.
855
881
  try {
856
- this.transport.stop();
882
+ this.transport?.stop();
857
883
  }
858
884
  catch {
859
885
  // Best effort shutdown.
@@ -870,6 +896,12 @@ export class WechatDaemon {
870
896
  catch {
871
897
  // Best effort shutdown.
872
898
  }
899
+ try {
900
+ await this.telegramTransport?.stop();
901
+ }
902
+ catch {
903
+ // Best effort shutdown.
904
+ }
873
905
  clearDaemonEndpoint();
874
906
  for (const slot of this.slots.values()) {
875
907
  try {
@@ -1081,12 +1113,12 @@ export class WechatDaemon {
1081
1113
  remindPendingUserInput: async () => {
1082
1114
  routeMessage = prefixDaemonAdapterMessage(slot.adapter, slot.pendingUserInput
1083
1115
  ? formatPendingUserInputReminder(slot.pendingUserInput)
1084
- : `${slot.adapter} is waiting for structured input. Reply with /answer <key>=<value> ...`);
1116
+ : t("bridge.input.waiting", { adapter: slot.adapter }));
1085
1117
  reason = "pending_user_input";
1086
1118
  await sendReminder(routeMessage, "user_input_required");
1087
1119
  },
1088
1120
  remindBusy: async () => {
1089
- routeMessage = prefixDaemonAdapterMessage(slot.adapter, `${slot.adapter} is still working. Wait for the current reply or use /stop.`);
1121
+ routeMessage = prefixDaemonAdapterMessage(slot.adapter, t("bridge.busy.remote", { adapter: slot.adapter }));
1090
1122
  reason = "busy";
1091
1123
  await sendReminder(routeMessage, "notice");
1092
1124
  },
@@ -1362,7 +1394,7 @@ export class WechatDaemon {
1362
1394
  }
1363
1395
  else if (slot.adapter === "opencode" || slot.adapter === "pi") {
1364
1396
  if (!slot.runtime.createSession) {
1365
- throw new Error(`/new is not available in ${slot.adapter} mode.`);
1397
+ throw new Error(t("bridge.newSession.unavailable", { adapter: slot.adapter }));
1366
1398
  }
1367
1399
  await slot.runtime.createSession();
1368
1400
  }
@@ -1379,12 +1411,38 @@ export class WechatDaemon {
1379
1411
  },
1380
1412
  });
1381
1413
  }
1414
+ if (this.channelId === "telegram") {
1415
+ return new TelegramChannelPort({
1416
+ transport: this.telegramTransport,
1417
+ sendText: (target, text, kind) => this.queueWechatMessage(this.authorizedUserId, text, toWechatSendContext(kind), target),
1418
+ prefixText: (currentAdapter, text) => prefixDaemonAdapterMessage(currentAdapter ?? adapter, text),
1419
+ onEmptyVisibleReply: (currentAdapter, rawText) => {
1420
+ appendDaemonLog(`empty_visible_final_reply: adapter=${currentAdapter ?? adapter} raw=${truncatePreview(rawText)}`);
1421
+ },
1422
+ });
1423
+ }
1424
+ if (this.channelId === "local") {
1425
+ return new LocalChannelPort({
1426
+ sendText: (recipientId, text, context) => this.queueWechatMessage(recipientId, text, context),
1427
+ prefixText: (currentAdapter, text) => prefixDaemonAdapterMessage(currentAdapter ?? adapter, text),
1428
+ onEmptyVisibleReply: (currentAdapter, rawText) => {
1429
+ appendDaemonLog(`empty_visible_final_reply: adapter=${currentAdapter ?? adapter} raw=${truncatePreview(rawText)}`);
1430
+ },
1431
+ onTextSent: (currentAdapter, text) => {
1432
+ appendDaemonLog(`final_reply_sent: adapter=${currentAdapter ?? adapter} chars=${Array.from(text).length}`);
1433
+ },
1434
+ });
1435
+ }
1436
+ if (!this.transport) {
1437
+ throw new Error("The wechat channel port requires a WeChat transport.");
1438
+ }
1439
+ const transport = this.transport;
1382
1440
  return new WechatChannelPort({
1383
1441
  sendText: (recipientId, text, context) => this.queueWechatMessage(recipientId, text, context),
1384
- sendImage: (recipientId, filePath) => this.queueWechatAttachmentAction(() => this.transport.sendImage(filePath, { recipientId })),
1385
- sendFile: (recipientId, filePath) => this.queueWechatAttachmentAction(() => this.transport.sendFile(filePath, { recipientId })),
1386
- sendVoice: (recipientId, filePath) => this.queueWechatAttachmentAction(() => this.transport.sendVoice(filePath, recipientId)),
1387
- sendVideo: (recipientId, filePath) => this.queueWechatAttachmentAction(() => this.transport.sendVideo(filePath, { recipientId })),
1442
+ sendImage: (recipientId, filePath) => this.queueWechatAttachmentAction(() => transport.sendImage(filePath, { recipientId })),
1443
+ sendFile: (recipientId, filePath) => this.queueWechatAttachmentAction(() => transport.sendFile(filePath, { recipientId })),
1444
+ sendVoice: (recipientId, filePath) => this.queueWechatAttachmentAction(() => transport.sendVoice(filePath, recipientId)),
1445
+ sendVideo: (recipientId, filePath) => this.queueWechatAttachmentAction(() => transport.sendVideo(filePath, { recipientId })),
1388
1446
  prefixText: (currentAdapter, text) => prefixDaemonAdapterMessage(currentAdapter ?? adapter, text),
1389
1447
  onEmptyVisibleReply: (currentAdapter, rawText) => {
1390
1448
  appendDaemonLog(`empty_visible_final_reply: adapter=${currentAdapter ?? adapter} raw=${truncatePreview(rawText)}`);
@@ -1402,10 +1460,18 @@ export class WechatDaemon {
1402
1460
  ? this.resolveSlotOutputTarget(slot)
1403
1461
  : this.fallbackConversation;
1404
1462
  const eventTask = slot.turns.activeTask;
1463
+ if (adapterState.pendingApprovals !== undefined) {
1464
+ const previous = new Map(slot.pendingConfirmations.map((pending) => [pending.requestId, pending]));
1465
+ slot.pendingConfirmations = adapterState.pendingApprovals.map((request) => {
1466
+ const existing = previous.get(request.requestId);
1467
+ return existing ? { ...existing, ...request } : toPendingApproval({ type: "approval_required", request, timestamp: nowIso() });
1468
+ });
1469
+ }
1405
1470
  if (slot.pendingConfirmations.length > 0 && !adapterState.pendingApproval) {
1406
1471
  slot.pendingConfirmations = [];
1407
1472
  }
1408
- if (slot.pendingUserInput && !adapterState.pendingUserInput) {
1473
+ if (slot.pendingUserInput && (!adapterState.pendingUserInput
1474
+ || (slot.pendingUserInput.requestId !== undefined && slot.pendingUserInput.requestId !== adapterState.pendingUserInput.requestId))) {
1409
1475
  slot.pendingUserInput = null;
1410
1476
  }
1411
1477
  slot.eventForwardChain = slot.eventForwardChain
@@ -1452,17 +1518,29 @@ export class WechatDaemon {
1452
1518
  },
1453
1519
  approvalRequired: (next) => {
1454
1520
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1521
+ const liveState = slot.runtime.getState();
1522
+ if (liveState.pendingApprovals !== undefined && liveState.pendingApproval?.requestId !== next.request.requestId)
1523
+ return;
1455
1524
  const pending = toPendingApproval(next);
1456
- slot.pendingConfirmations.push(pending);
1525
+ if (liveState.pendingApprovals === undefined) {
1526
+ slot.pendingConfirmations = [pending];
1527
+ }
1457
1528
  appendDaemonLog(`approval_required: adapter=${slot.adapter} source=${pending.source} command=${truncatePreview(pending.commandPreview)}`);
1458
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatApprovalMessage(pending, adapterState)), "approval_required", eventTarget);
1529
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatApprovalMessage(pending, liveState)), "approval_required", eventTarget);
1530
+ if (slot.runtime.getState().pendingApproval?.requestId === pending.requestId) {
1531
+ slot.displayedApprovalRequestId = pending.requestId;
1532
+ }
1459
1533
  }));
1460
1534
  },
1461
1535
  userInputRequired: (next) => {
1536
+ if (next.request.requestId !== undefined && slot.runtime.getState().pendingUserInput?.requestId !== next.request.requestId)
1537
+ return;
1462
1538
  const pending = toPendingUserInput(next.request);
1463
1539
  slot.pendingUserInput = pending;
1464
1540
  appendDaemonLog(`user_input_required: adapter=${slot.adapter} questions=${pending.questions.length}`);
1465
1541
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1542
+ if (pending.requestId !== undefined && slot.runtime.getState().pendingUserInput?.requestId !== pending.requestId)
1543
+ return;
1466
1544
  await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserInputRequestMessage(pending, adapterState)), "user_input_required", eventTarget);
1467
1545
  }));
1468
1546
  },
@@ -1561,7 +1639,12 @@ export class WechatDaemon {
1561
1639
  }
1562
1640
  async handleInboundMessage(message) {
1563
1641
  if (message.senderId !== this.authorizedUserId) {
1564
- await this.queueWechatMessage(message.senderId, `Unauthorized. This daemon only accepts messages from ${this.channelDriver.operatorDescription}.`);
1642
+ await this.queueWechatMessage(message.senderId, t("bridge.unauthorized", { mode: t("bridge.mode.daemon"), operator: this.channelDriver.operatorDescription }));
1643
+ return;
1644
+ }
1645
+ const languageCommand = parseBridgeControlCommand(message.text);
1646
+ if (languageCommand?.type === "language") {
1647
+ await this.handleLanguageCommand(message.senderId, languageCommand.target);
1565
1648
  return;
1566
1649
  }
1567
1650
  const emojiMatch = resolveEmojiCommand(message.text);
@@ -1578,7 +1661,7 @@ export class WechatDaemon {
1578
1661
  }
1579
1662
  else {
1580
1663
  const detail = formatDaemonSwitchResultDetail(result);
1581
- await this.queueWechatMessage(message.senderId, `Could not activate terminal: ${switchTarget}.\n${detail}`);
1664
+ await this.queueWechatMessage(message.senderId, t("daemon.activateFailed", { adapter: switchTarget, detail }));
1582
1665
  return;
1583
1666
  }
1584
1667
  }
@@ -1597,7 +1680,7 @@ export class WechatDaemon {
1597
1680
  });
1598
1681
  const detail = formatDaemonSwitchResultDetail(result);
1599
1682
  if (!result.activated) {
1600
- await this.queueWechatMessage(message.senderId, `Could not activate terminal: ${switchDirective.adapter}.\n${detail}`);
1683
+ await this.queueWechatMessage(message.senderId, t("daemon.activateFailed", { adapter: switchDirective.adapter, detail }));
1601
1684
  return;
1602
1685
  }
1603
1686
  this.bindCurrentWecomConversation(this.getActiveSlot());
@@ -1605,12 +1688,12 @@ export class WechatDaemon {
1605
1688
  message = { ...message, text: switchDirective.remainder };
1606
1689
  }
1607
1690
  else {
1608
- await this.queueWechatMessage(message.senderId, `Active terminal: ${switchDirective.adapter}.\n${detail}`);
1691
+ await this.queueWechatMessage(message.senderId, t("daemon.activeTerminal", { adapter: switchDirective.adapter, detail }));
1609
1692
  return;
1610
1693
  }
1611
1694
  }
1612
1695
  if (message.text.trim().toLowerCase() === "/daemon-stop") {
1613
- await this.queueWechatMessage(message.senderId, `Stopping ${this.channelId}-daemon...`);
1696
+ await this.queueWechatMessage(message.senderId, t("daemon.stopping", { channel: this.channelId }));
1614
1697
  setTimeout(() => {
1615
1698
  void this.shutdown().finally(() => process.exit(0));
1616
1699
  }, 0);
@@ -1651,10 +1734,10 @@ export class WechatDaemon {
1651
1734
  remindPendingUserInput: async () => {
1652
1735
  await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(currentSlot.adapter, currentSlot.pendingUserInput
1653
1736
  ? formatPendingUserInputReminder(currentSlot.pendingUserInput)
1654
- : `${currentSlot.adapter} is waiting for structured input. Reply with /answer <key>=<value> ...`));
1737
+ : t("bridge.input.waiting", { adapter: currentSlot.adapter })));
1655
1738
  },
1656
1739
  remindBusy: async () => {
1657
- await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(currentSlot.adapter, `${currentSlot.adapter} is still working. Wait for the current reply or use /stop.`));
1740
+ await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(currentSlot.adapter, t("bridge.busy.remote", { adapter: currentSlot.adapter })));
1658
1741
  },
1659
1742
  defer: async () => undefined,
1660
1743
  dispatch: async () => {
@@ -1702,20 +1785,23 @@ export class WechatDaemon {
1702
1785
  }
1703
1786
  case "bind": {
1704
1787
  setBinding(cmd.emoji, cmd.command);
1705
- await this.queueWechatMessage(senderId, `Bound ${cmd.emoji} → ${cmd.command}`);
1788
+ await this.queueWechatMessage(senderId, t("binding.bound", { emoji: cmd.emoji, command: cmd.command }));
1706
1789
  return;
1707
1790
  }
1708
1791
  case "unbind": {
1709
1792
  const removed = removeBinding(cmd.emoji);
1710
1793
  await this.queueWechatMessage(senderId, removed
1711
- ? `Unbound ${cmd.emoji}`
1712
- : `No binding found for ${cmd.emoji}`);
1794
+ ? t("binding.unbound", { emoji: cmd.emoji })
1795
+ : t("binding.notFound", { emoji: cmd.emoji }));
1713
1796
  return;
1714
1797
  }
1715
1798
  }
1716
1799
  }
1717
1800
  async handleSystemCommand(message, activeSlot, command) {
1718
1801
  switch (command.type) {
1802
+ case "language":
1803
+ await this.handleLanguageCommand(message.senderId, command.target);
1804
+ return;
1719
1805
  case "model":
1720
1806
  case "plan":
1721
1807
  await this.queueWechatMessage(message.senderId, await handleAdapterControl(activeSlot.runtime, message.senderId, command));
@@ -1725,7 +1811,7 @@ export class WechatDaemon {
1725
1811
  return;
1726
1812
  case "resume": {
1727
1813
  if (!isWechatResumeEnabled(activeSlot.adapter)) {
1728
- await this.queueWechatMessage(message.senderId, `${this.channelDriver.displayName} /resume is disabled for ${activeSlot.adapter} in daemon mode. Use /resume directly inside the visible terminal; the remote channel will follow that local session.`);
1814
+ await this.queueWechatMessage(message.senderId, t("daemon.resume.disabled", { channel: this.channelDriver.displayName, adapter: activeSlot.adapter }));
1729
1815
  return;
1730
1816
  }
1731
1817
  try {
@@ -1757,7 +1843,7 @@ export class WechatDaemon {
1757
1843
  }
1758
1844
  case "new_session":
1759
1845
  if (!activeSlot.runtime.createSession) {
1760
- await this.queueWechatMessage(message.senderId, `/new is not available in ${activeSlot.adapter} mode.`);
1846
+ await this.queueWechatMessage(message.senderId, t("bridge.newSession.unavailable", { adapter: activeSlot.adapter }));
1761
1847
  return;
1762
1848
  }
1763
1849
  await activeSlot.outputBatcher.flushNow();
@@ -1774,8 +1860,8 @@ export class WechatDaemon {
1774
1860
  case "stop": {
1775
1861
  const interrupted = await activeSlot.runtime.interrupt();
1776
1862
  await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(activeSlot.adapter, interrupted
1777
- ? "Interrupt signal sent to the active worker."
1778
- : "No running worker was available to interrupt."));
1863
+ ? t("bridge.interrupt.workerSent")
1864
+ : t("bridge.interrupt.workerIdle")));
1779
1865
  return;
1780
1866
  }
1781
1867
  case "reset":
@@ -1786,7 +1872,7 @@ export class WechatDaemon {
1786
1872
  activeSlot.resumeCoordinator.clear();
1787
1873
  await activeSlot.runtime.reset();
1788
1874
  appendDaemonLog(`reset: adapter=${activeSlot.adapter}`);
1789
- await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(activeSlot.adapter, "Worker session has been reset."));
1875
+ await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(activeSlot.adapter, t("bridge.reset.done")));
1790
1876
  return;
1791
1877
  case "confirm":
1792
1878
  await this.confirmPendingApproval(message, activeSlot);
@@ -1799,42 +1885,49 @@ export class WechatDaemon {
1799
1885
  return;
1800
1886
  }
1801
1887
  }
1888
+ async handleLanguageCommand(senderId, target) {
1889
+ await this.queueWechatMessage(senderId, await handleLanguageControl({
1890
+ target,
1891
+ settingsFile: this.deps.localeSettingsFile ?? LANGUAGE_FILE,
1892
+ adapters: [...this.slots.values()].map((slot) => slot.runtime),
1893
+ }));
1894
+ }
1802
1895
  async confirmPendingApproval(message, activeSlot) {
1803
1896
  const slot = this.resolvePendingApprovalSlot(activeSlot);
1804
1897
  if (!slot || slot.pendingConfirmations.length === 0) {
1805
- await this.queueWechatMessage(message.senderId, "No pending approval request.");
1898
+ await this.queueWechatMessage(message.senderId, t("approval.noPending"));
1806
1899
  return;
1807
1900
  }
1808
- const count = await slot.runtime.resolveAllApprovals("confirm");
1901
+ const pending = slot.pendingConfirmations[0];
1902
+ const count = await slot.runtime.resolveApproval("confirm", slot.runtime.getState().pendingApprovals !== undefined ? slot.displayedApprovalRequestId ?? "" : pending.requestId) ? 1 : 0;
1809
1903
  if (!count) {
1810
- await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, "The worker could not apply this approval request."));
1904
+ await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, t("bridge.approval.applyFailed")));
1811
1905
  return;
1812
1906
  }
1813
- const preview = slot.pendingConfirmations[0]?.commandPreview ?? "";
1814
- slot.pendingConfirmations = [];
1815
- slot.turns.setActiveTask({
1816
- startedAt: Date.now(),
1817
- inputPreview: preview,
1818
- });
1907
+ const preview = pending.commandPreview;
1908
+ const adapterState = slot.runtime.getState();
1909
+ slot.pendingConfirmations = (adapterState.pendingApprovals ?? (adapterState.pendingApproval ? [adapterState.pendingApproval] : []))
1910
+ .map((request) => toPendingApproval({ type: "approval_required", request, timestamp: nowIso() }));
1819
1911
  appendDaemonLog(`approval_confirmed: adapter=${slot.adapter} count=${count} command=${truncatePreview(preview)}`);
1820
- await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, count > 1
1821
- ? `${count} approvals confirmed. Continuing...`
1822
- : "Approval confirmed. Continuing..."));
1912
+ await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, t("approval.confirmed")));
1823
1913
  }
1824
1914
  async denyPendingApproval(message, activeSlot) {
1825
1915
  const slot = this.resolvePendingApprovalSlot(activeSlot);
1826
1916
  if (!slot || slot.pendingConfirmations.length === 0) {
1827
- await this.queueWechatMessage(message.senderId, "No pending approval request.");
1917
+ await this.queueWechatMessage(message.senderId, t("approval.noPending"));
1828
1918
  return;
1829
1919
  }
1830
- const count = await slot.runtime.resolveAllApprovals("deny");
1920
+ const pending = slot.pendingConfirmations[0];
1921
+ const count = await slot.runtime.resolveApproval("deny", slot.runtime.getState().pendingApprovals !== undefined ? slot.displayedApprovalRequestId ?? "" : pending.requestId) ? 1 : 0;
1831
1922
  if (!count) {
1832
- await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, "The worker could not deny this approval request cleanly."));
1923
+ await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, t("bridge.approval.denyFailed")));
1833
1924
  return;
1834
1925
  }
1835
- slot.pendingConfirmations = [];
1926
+ const adapterState = slot.runtime.getState();
1927
+ slot.pendingConfirmations = (adapterState.pendingApprovals ?? (adapterState.pendingApproval ? [adapterState.pendingApproval] : []))
1928
+ .map((request) => toPendingApproval({ type: "approval_required", request, timestamp: nowIso() }));
1836
1929
  appendDaemonLog(`approval_denied: adapter=${slot.adapter} count=${count}`);
1837
- await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, count > 1 ? `${count} approvals denied.` : "Approval denied."));
1930
+ await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, t("approval.denied")));
1838
1931
  }
1839
1932
  async answerPendingUserInput(message, activeSlot, raw) {
1840
1933
  // Mirror the /confirm and /deny behavior: search every slot for a pending
@@ -1842,12 +1935,12 @@ export class WechatDaemon {
1842
1935
  // when the user has since switched the active adapter.
1843
1936
  const slot = this.resolvePendingUserInputSlot(activeSlot);
1844
1937
  if (!slot) {
1845
- await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(activeSlot.adapter, "No pending user input request."));
1938
+ await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(activeSlot.adapter, t("bridge.answer.none")));
1846
1939
  return;
1847
1940
  }
1848
1941
  const pending = slot.pendingUserInput;
1849
1942
  if (!pending) {
1850
- await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(activeSlot.adapter, "No pending user input request."));
1943
+ await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(activeSlot.adapter, t("bridge.answer.none")));
1851
1944
  return;
1852
1945
  }
1853
1946
  const parsed = parsePendingUserInputAnswerCommand(raw, pending);
@@ -1855,18 +1948,15 @@ export class WechatDaemon {
1855
1948
  await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, parsed.error));
1856
1949
  return;
1857
1950
  }
1858
- const submitted = await slot.runtime.submitUserInput(parsed.answers);
1951
+ const submitted = await slot.runtime.submitUserInput(parsed.answers, pending.requestId);
1859
1952
  if (!submitted) {
1860
- await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, "The worker could not apply this answer."));
1953
+ await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, t("bridge.answer.failed")));
1861
1954
  return;
1862
1955
  }
1863
- slot.pendingUserInput = null;
1864
- slot.turns.setActiveTask({
1865
- startedAt: Date.now(),
1866
- inputPreview: parsed.preview,
1867
- });
1956
+ if (slot.pendingUserInput?.requestId === pending.requestId)
1957
+ slot.pendingUserInput = null;
1868
1958
  appendDaemonLog(`user_input_answered: adapter=${slot.adapter} preview=${parsed.preview}`);
1869
- await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, "Answer submitted. Continuing..."));
1959
+ await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, t("bridge.answer.sent")));
1870
1960
  }
1871
1961
  resolvePendingUserInputSlot(activeSlot) {
1872
1962
  if (activeSlot.pendingUserInput) {
@@ -1904,14 +1994,14 @@ export class WechatDaemon {
1904
1994
  .map((adapter) => this.slots.get(adapter))
1905
1995
  .filter((slot) => Boolean(slot));
1906
1996
  if (startedSlots.length === 0) {
1907
- await this.queueWechatMessage(message.senderId, "/all needs at least one running adapter. Open one first with /codex, /claude, /opencode, or /pi.");
1997
+ await this.queueWechatMessage(message.senderId, t("daemon.broadcast.empty"));
1908
1998
  return;
1909
1999
  }
1910
2000
  const busySlots = startedSlots.filter((slot) => this.isSlotBusyForBroadcast(slot));
1911
2001
  if (busySlots.length > 0) {
1912
2002
  const names = busySlots.map((slot) => slot.adapter).join(", ");
1913
2003
  appendDaemonLog(`broadcast_canceled_busy: busy=[${names}]`);
1914
- await this.queueWechatMessage(message.senderId, `/all canceled: ${busySlots.length > 1 ? "these workers are" : "this worker is"} still busy — ${names}. Wait for the current replies or use /stop first.`);
2004
+ await this.queueWechatMessage(message.senderId, t("daemon.broadcast.busy", { workers: t(busySlots.length > 1 ? "daemon.broadcast.multipleBusy" : "daemon.broadcast.singleBusy"), names }));
1915
2005
  return;
1916
2006
  }
1917
2007
  const broadcastMessage = { ...message, text };
@@ -1928,10 +2018,10 @@ export class WechatDaemon {
1928
2018
  dispatched.push(adapter);
1929
2019
  }
1930
2020
  const lines = [
1931
- `Broadcast dispatched to ${dispatched.length} ${dispatched.length > 1 ? "workers" : "worker"}: ${dispatched.join(", ")}. Each reply arrives prefixed with its adapter name.`,
2021
+ t("daemon.broadcast.sent", { count: dispatched.length, workers: t(dispatched.length > 1 ? "daemon.broadcast.multipleWorkers" : "daemon.broadcast.singleWorker"), names: dispatched.join(", ") }),
1932
2022
  ];
1933
2023
  if (skipped.length > 0) {
1934
- lines.push(`Not started (skipped): ${skipped.join(", ")}. Start them with their /command to include them next time.`);
2024
+ lines.push(t("daemon.broadcast.skipped", { names: skipped.join(", ") }));
1935
2025
  }
1936
2026
  await this.queueWechatMessage(message.senderId, lines.join("\n"));
1937
2027
  }
@@ -1948,7 +2038,7 @@ export class WechatDaemon {
1948
2038
  task: nextTask,
1949
2039
  conversation: inboundConversation,
1950
2040
  onBusy: async () => {
1951
- await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, `${slot.adapter} is still working. Wait for the current reply or use /stop.`), "notice", conversationOverride);
2041
+ await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, t("bridge.busy.remote", { adapter: slot.adapter })), "notice", conversationOverride);
1952
2042
  },
1953
2043
  forward: async () => {
1954
2044
  appendDaemonLog(`forwarded_input: adapter=${slot.adapter} text=${truncatePreview(preview)}`);
@@ -2344,25 +2434,36 @@ export async function runDaemon(options) {
2344
2434
  const wecomAccount = options.channelId === "wecom"
2345
2435
  ? await ensureWecomAccount({ log })
2346
2436
  : null;
2437
+ const telegramAccount = options.channelId === "telegram"
2438
+ ? await ensureTelegramAccount({ log })
2439
+ : null;
2347
2440
  const credentials = wecomAccount
2348
2441
  ? { userId: wecomAccount.operatorUserId }
2349
- : await ensureWechatCredentials({
2350
- requireUserId: true,
2351
- validateExisting: true,
2352
- log,
2353
- });
2442
+ : telegramAccount
2443
+ ? { userId: telegramAccount.operatorUserId }
2444
+ : options.channelId === "local"
2445
+ ? { userId: LOCAL_CHANNEL_OPERATOR_ID }
2446
+ : await ensureWechatCredentials({
2447
+ requireUserId: true,
2448
+ validateExisting: true,
2449
+ log,
2450
+ });
2354
2451
  if (!credentials.userId) {
2355
2452
  throw new Error(options.channelId === "wecom"
2356
2453
  ? "Saved WeCom credentials are missing operatorUserId."
2357
- : "Saved WeChat credentials are missing userId.");
2454
+ : options.channelId === "telegram"
2455
+ ? "Saved Telegram credentials are missing operatorUserId."
2456
+ : "Saved WeChat credentials are missing userId.");
2358
2457
  }
2359
2458
  const daemon = new WechatDaemon({
2360
2459
  cwd: options.cwd,
2361
2460
  profile: options.profile,
2362
2461
  authorizedUserId: credentials.userId,
2363
- transport: new WeChatTransport({ log, logError }),
2462
+ transport: (options.channelId ?? "wechat") === "wechat"
2463
+ ? new WeChatTransport({ log, logError })
2464
+ : null,
2364
2465
  channelId: options.channelId,
2365
- accountId: wecomAccount?.botId,
2466
+ accountId: wecomAccount?.botId ?? telegramAccount?.botUsername,
2366
2467
  wecomTransport: wecomAccount
2367
2468
  ? new WecomTransport({
2368
2469
  account: wecomAccount,
@@ -2372,6 +2473,18 @@ export async function runDaemon(options) {
2372
2473
  },
2373
2474
  })
2374
2475
  : null,
2476
+ telegramTransport: telegramAccount
2477
+ ? new TelegramTransport({
2478
+ account: telegramAccount,
2479
+ logger: {
2480
+ log: (message) => log(message),
2481
+ error: (message) => logError(message),
2482
+ },
2483
+ })
2484
+ : null,
2485
+ localFiles: options.channelId === "local"
2486
+ ? getLocalChannelFiles(options.cwd)
2487
+ : null,
2375
2488
  });
2376
2489
  if (cleanupResult.action === "stopped" && isDaemonAdapterKind(cleanupResult.lock.adapter)) {
2377
2490
  daemon.takenOverAdapter = cleanupResult.lock.adapter;