@unlinearity/cli-wechat-bridge 1.1.4 → 1.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.
Files changed (42) hide show
  1. package/README.md +153 -131
  2. package/bin/_run-entry.mjs +20 -17
  3. package/dist/bridge/bridge-adapters.claude.js +429 -11
  4. package/dist/bridge/bridge-adapters.codex.js +441 -22
  5. package/dist/bridge/bridge-adapters.core.js +2 -1
  6. package/dist/bridge/bridge-adapters.opencode.js +392 -53
  7. package/dist/bridge/bridge-adapters.pi.js +33 -7
  8. package/dist/bridge/bridge-adapters.shared.js +2 -1
  9. package/dist/bridge/bridge-controller.js +3 -0
  10. package/dist/bridge/bridge-final-reply.js +2 -65
  11. package/dist/bridge/bridge-session-resume.js +151 -0
  12. package/dist/bridge/bridge-state.js +1 -1
  13. package/dist/bridge/bridge-utils.js +30 -101
  14. package/dist/bridge/claude-hooks.js +1 -0
  15. package/dist/bridge/wechat-bridge.js +256 -211
  16. package/dist/bridge/wechat-forwarding.js +2 -58
  17. package/dist/bridge/wechat-outbound-queue.js +2 -0
  18. package/dist/channels/wechat/bridge-final-reply.js +65 -0
  19. package/dist/channels/wechat/channel-message.js +24 -0
  20. package/dist/channels/wechat/index.js +5 -0
  21. package/dist/channels/wechat/wechat-channel-port.js +67 -0
  22. package/dist/channels/wechat/wechat-forwarding.js +79 -0
  23. package/dist/channels/wechat/wechat-outbound-queue.js +72 -0
  24. package/dist/companion/codex-remote-client.js +291 -32
  25. package/dist/companion/codex-visible-client-link.js +122 -0
  26. package/dist/companion/local-companion-link.js +26 -0
  27. package/dist/companion/local-companion-start.js +0 -1
  28. package/dist/companion/opencode-tui-bridge-plugin.js +73 -0
  29. package/dist/companion/pi-tui-bridge-extension.js +42 -3
  30. package/dist/core/bridge-control.js +32 -0
  31. package/dist/core/bridge-event-forwarder.js +50 -0
  32. package/dist/core/bridge-message-router.js +32 -0
  33. package/dist/core/channel-types.js +1 -0
  34. package/dist/core/text-utils.js +63 -0
  35. package/dist/daemon/wechat-daemon.js +242 -159
  36. package/dist/i18n/messages-en.js +2 -2
  37. package/dist/i18n/messages-zh.js +2 -2
  38. package/package.json +2 -6
  39. package/bin/wechat-claude-start.mjs +0 -10
  40. package/bin/wechat-codex-start.mjs +0 -10
  41. package/bin/wechat-opencode-start.mjs +0 -10
  42. package/bin/wechat-pi-start.mjs +0 -10
@@ -8,17 +8,21 @@ import { resolveDefaultAdapterCommand, } from "../bridge/bridge-adapters.js";
8
8
  import { t } from "../i18n/index.js";
9
9
  import { delay, getSharedSessionIdFromAdapterState, quoteWindowsCommandArg, } from "../bridge/bridge-adapters.shared.js";
10
10
  import { BridgeController } from "../bridge/bridge-controller.js";
11
- import { forwardWechatFinalReply } from "../bridge/bridge-final-reply.js";
12
11
  import { readBridgeLockFile, } from "../bridge/bridge-state.js";
13
12
  import { getProcessRecordByPid, isWechatDaemonCommandLine, killProcessTreeSync, listWechatDaemonProcesses, reapOrphanedOpencodeProcesses, reapPeerBridgeProcesses, } from "../bridge/bridge-process-reaper.js";
14
13
  import { buildOneTimeCode, buildWechatInboundPrompt, formatApprovalMessage, formatDuration, formatMirroredUserInputMessage, formatPendingApprovalReminder, formatPendingUserInputReminder, formatSessionSwitchMessage, formatTaskFailedMessage, formatUserInputRequestMessage, MESSAGE_START_GRACE_MS, nowIso, OutputBatcher, parsePendingUserInputAnswerCommand, parseWechatControlCommand, truncatePreview, } from "../bridge/bridge-utils.js";
15
- import { WECHAT_SEND_MAX_ATTEMPTS, computeWechatSendRetryDelayMs, formatUserFacingBridgeFatalError, formatUserFacingInboundError, formatWechatContextTokenStaleLogEntry, formatWechatSendFailureLogEntry, isRetryableWechatSendError, shouldForwardBridgeEventToWechat, } from "../bridge/wechat-forwarding.js";
14
+ import { ResumeSessionCoordinator, isWechatResumeEnabled, shouldForwardSessionSwitchEvent, } from "../bridge/bridge-session-resume.js";
15
+ import { WECHAT_SEND_MAX_ATTEMPTS, computeWechatSendRetryDelayMs, formatUserFacingBridgeFatalError, formatUserFacingInboundError, formatWechatContextTokenStaleLogEntry, formatWechatSendFailureLogEntry, isWechatContextUnavailableError, isRetryableWechatSendError, shouldForwardBridgeEventToWechat, shouldSuppressCodexLocalThreadNotice, } from "../channels/wechat/wechat-forwarding.js";
16
16
  import { BRIDGE_LOCK_FILE, BRIDGE_LOG_FILE, appendBoundedLog, ensureChannelDataDir, migrateLegacyChannelFiles, } from "../wechat/channel-config.js";
17
17
  import { ensureWechatCredentials } from "../wechat/setup.js";
18
18
  import { formatBindCommandUsage, formatBindingsListMessage, isBindCommandPrefix, listBindings, loadEmojiBindings, parseEmojiBindingsCommand, removeBinding, resolveEmojiCommand, setBinding, } from "./emoji-bindings.js";
19
- import { formatResumeSessionList } from "../bridge/bridge-utils.js";
20
19
  import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, describeWechatTransportError, isWechatContextTokenStaleError, WeChatTransport, } from "../wechat/wechat-transport.js";
20
+ import { getPendingWechatMessagesFile, PendingWechatMessageStore, } from "../channels/wechat/wechat-outbound-queue.js";
21
21
  import { createRuntimeHost, } from "../runtime/create-runtime-host.js";
22
+ import { toChannelInboundMessage } from "../channels/wechat/channel-message.js";
23
+ import { routeBridgeMessage } from "../core/bridge-message-router.js";
24
+ import { forwardBridgeEvent } from "../core/bridge-event-forwarder.js";
25
+ import { WechatChannelPort } from "../channels/wechat/wechat-channel-port.js";
22
26
  import { clearLocalCompanionEndpoint, clearLocalCompanionOccupancy, readLocalCompanionEndpoint, } from "../companion/local-companion-link.js";
23
27
  import { attachDaemonRequestListener, buildDaemonToken, clearDaemonEndpoint, DAEMON_PROTOCOL_VERSION, isPidAlive, readDaemonEndpoint, sendDaemonRequest, sendDaemonResponse, writeDaemonEndpoint, } from "./daemon-link.js";
24
28
  const MODULE_FILE = fileURLToPath(import.meta.url);
@@ -493,6 +497,7 @@ class WechatDaemon {
493
497
  textSendChain = Promise.resolve();
494
498
  attachmentSendChain = Promise.resolve();
495
499
  pendingWechatForwardTasks = new Set();
500
+ pendingWechatMessages;
496
501
  shutdownPromise = null;
497
502
  ipcServer = null;
498
503
  endpointToken = "";
@@ -501,6 +506,7 @@ class WechatDaemon {
501
506
  this.profile = params.profile;
502
507
  this.authorizedUserId = params.authorizedUserId;
503
508
  this.transport = params.transport;
509
+ this.pendingWechatMessages = new PendingWechatMessageStore(getPendingWechatMessagesFile(this.cwd));
504
510
  }
505
511
  async startIpcServer() {
506
512
  this.endpointToken = buildDaemonToken();
@@ -624,6 +630,9 @@ class WechatDaemon {
624
630
  appendDaemonLog(`poll_recovered: failures=${consecutivePollFailures}`);
625
631
  consecutivePollFailures = 0;
626
632
  }
633
+ if (pollResult.messages.length > 0 && this.pendingWechatMessages.list().length > 0) {
634
+ await this.flushPendingWechatMessages();
635
+ }
627
636
  if (pollResult.ignoredBacklogCount > 0) {
628
637
  appendDaemonLog(`ignored_startup_backlog: count=${pollResult.ignoredBacklogCount}`);
629
638
  if (!this.backlogNoticeSent) {
@@ -893,8 +902,14 @@ class WechatDaemon {
893
902
  }),
894
903
  pendingConfirmations: [],
895
904
  pendingUserInput: null,
905
+ resumeCoordinator: new ResumeSessionCoordinator({
906
+ adapter,
907
+ runtime,
908
+ }),
896
909
  activeTask: null,
897
910
  lastOutputAt: 0,
911
+ lastFinalReplyAtMs: 0,
912
+ eventForwardChain: Promise.resolve(),
898
913
  };
899
914
  runtime.setEventSink((event) => {
900
915
  this.handleSlotEvent(slot, event);
@@ -921,154 +936,163 @@ class WechatDaemon {
921
936
  }
922
937
  slot.controller.syncLocalClientEndpoint();
923
938
  }
939
+ createWechatChannelPort(adapter) {
940
+ return new WechatChannelPort({
941
+ sendText: (recipientId, text, context) => this.queueWechatMessage(recipientId, text, context),
942
+ sendImage: (recipientId, filePath) => this.queueWechatAttachmentAction(() => this.transport.sendImage(filePath, { recipientId })),
943
+ sendFile: (recipientId, filePath) => this.queueWechatAttachmentAction(() => this.transport.sendFile(filePath, { recipientId })),
944
+ sendVoice: (recipientId, filePath) => this.queueWechatAttachmentAction(() => this.transport.sendVoice(filePath, recipientId)),
945
+ sendVideo: (recipientId, filePath) => this.queueWechatAttachmentAction(() => this.transport.sendVideo(filePath, { recipientId })),
946
+ prefixText: (currentAdapter, text) => prefixDaemonAdapterMessage(currentAdapter ?? adapter, text),
947
+ onEmptyVisibleReply: (currentAdapter, rawText) => {
948
+ appendDaemonLog(`empty_visible_final_reply: adapter=${currentAdapter ?? adapter} raw=${truncatePreview(rawText)}`);
949
+ },
950
+ onTextSent: (currentAdapter, text) => {
951
+ appendDaemonLog(`final_reply_sent: adapter=${currentAdapter ?? adapter} chars=${Array.from(text).length}`);
952
+ },
953
+ });
954
+ }
924
955
  handleSlotEvent(slot, event) {
925
956
  slot.controller.syncLocalClientEndpoint();
926
957
  const adapterState = slot.runtime.getState();
958
+ const channelPort = this.createWechatChannelPort(slot.adapter);
927
959
  if (slot.pendingConfirmations.length > 0 && !adapterState.pendingApproval) {
928
960
  slot.pendingConfirmations = [];
929
961
  }
930
962
  if (slot.pendingUserInput && !adapterState.pendingUserInput) {
931
963
  slot.pendingUserInput = null;
932
964
  }
933
- switch (event.type) {
934
- case "stdout":
935
- case "stderr":
965
+ slot.eventForwardChain = slot.eventForwardChain
966
+ .then(() => forwardBridgeEvent(event, {
967
+ stdout: (next) => {
936
968
  slot.lastOutputAt = Date.now();
937
- if (shouldForwardBridgeEventToWechat(slot.adapter, event.type)) {
938
- slot.outputBatcher.push(event.text);
969
+ if (shouldForwardBridgeEventToWechat(slot.adapter, next.type)) {
970
+ slot.outputBatcher.push(next.text);
939
971
  }
940
- break;
941
- case "final_reply":
942
- appendDaemonLog(`final_reply: adapter=${slot.adapter} text=${truncatePreview(event.text)}`);
972
+ },
973
+ stderr: (next) => {
974
+ slot.lastOutputAt = Date.now();
975
+ if (shouldForwardBridgeEventToWechat(slot.adapter, next.type)) {
976
+ slot.outputBatcher.push(next.text);
977
+ }
978
+ },
979
+ finalReply: (next) => {
980
+ slot.lastFinalReplyAtMs = Date.now();
981
+ appendDaemonLog(`final_reply: adapter=${slot.adapter} text=${truncatePreview(next.text)}`);
943
982
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
944
- await forwardWechatFinalReply({
945
- adapter: slot.adapter,
946
- rawText: event.text,
947
- onEmptyVisibleReply: ({ rawVisibleText }) => {
948
- appendDaemonLog(`empty_visible_final_reply: adapter=${slot.adapter} raw=${truncatePreview(rawVisibleText)}`);
949
- },
950
- sender: {
951
- sendText: async (text) => {
952
- const sent = await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, text), "final_reply");
953
- if (sent) {
954
- appendDaemonLog(`final_reply_sent: adapter=${slot.adapter} chars=${Array.from(text).length}`);
955
- }
956
- return sent;
957
- },
958
- sendImage: (imagePath) => this.queueWechatAttachmentAction(() => this.transport.sendImage(imagePath, {
959
- recipientId: this.authorizedUserId,
960
- })),
961
- sendFile: (filePath) => this.queueWechatAttachmentAction(() => this.transport.sendFile(filePath, {
962
- recipientId: this.authorizedUserId,
963
- })),
964
- sendVoice: (voicePath) => this.queueWechatAttachmentAction(() => this.transport.sendVoice(voicePath, this.authorizedUserId)),
965
- sendVideo: (videoPath) => this.queueWechatAttachmentAction(() => this.transport.sendVideo(videoPath, {
966
- recipientId: this.authorizedUserId,
967
- })),
983
+ await channelPort.send({
984
+ target: {
985
+ channelId: "wechat",
986
+ conversationId: this.authorizedUserId,
987
+ recipientId: this.authorizedUserId,
968
988
  },
989
+ kind: "final_reply",
990
+ text: next.text,
991
+ adapter: slot.adapter,
969
992
  });
970
993
  }));
971
- break;
972
- case "status":
973
- if (event.message) {
974
- log(`${slot.adapter} ${event.status}: ${event.message}`);
975
- appendDaemonLog(`${slot.adapter}_${event.status}: ${event.message}`);
994
+ },
995
+ status: (next) => {
996
+ if (next.message) {
997
+ log(`${slot.adapter} ${next.status}: ${next.message}`);
998
+ appendDaemonLog(`${slot.adapter}_${next.status}: ${next.message}`);
976
999
  }
977
- break;
978
- case "notice":
1000
+ },
1001
+ notice: (next) => {
979
1002
  slot.lastOutputAt = Date.now();
980
- appendDaemonLog(`${slot.adapter}_${event.level}_notice: ${truncatePreview(event.text)}`);
981
- if (shouldForwardBridgeEventToWechat(slot.adapter, event.type, { text: event.text })) {
1003
+ appendDaemonLog(`${slot.adapter}_${next.level}_notice: ${truncatePreview(next.text)}`);
1004
+ if (shouldForwardBridgeEventToWechat(slot.adapter, next.type, { text: next.text })) {
982
1005
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
983
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, event.text), "notice");
1006
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, next.text), "notice");
984
1007
  }));
985
1008
  }
986
- break;
987
- case "approval_required":
1009
+ },
1010
+ approvalRequired: (next) => {
988
1011
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
989
- const pending = toPendingApproval(event);
1012
+ const pending = toPendingApproval(next);
990
1013
  slot.pendingConfirmations.push(pending);
991
1014
  appendDaemonLog(`approval_required: adapter=${slot.adapter} source=${pending.source} command=${truncatePreview(pending.commandPreview)}`);
992
1015
  await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatApprovalMessage(pending, adapterState)), "approval_required");
993
1016
  }));
994
- break;
995
- case "user_input_required": {
996
- // Mirror the pending request synchronously, before the async WeChat
997
- // send (which retries with backoff). Otherwise a fast user reply can
998
- // race past the still-unwritten mirror and be dispatched as plain
999
- // input into a CLI that is waiting for a structured form answer.
1000
- const pending = toPendingUserInput(event.request);
1017
+ },
1018
+ userInputRequired: (next) => {
1019
+ const pending = toPendingUserInput(next.request);
1001
1020
  slot.pendingUserInput = pending;
1002
1021
  appendDaemonLog(`user_input_required: adapter=${slot.adapter} questions=${pending.questions.length}`);
1003
1022
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1004
1023
  await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserInputRequestMessage(pending, adapterState)), "user_input_required");
1005
1024
  }));
1006
- break;
1007
- }
1008
- case "mirrored_user_input":
1009
- appendDaemonLog(`mirrored_local_input: adapter=${slot.adapter} text=${truncatePreview(event.text)}`);
1010
- if (shouldForwardBridgeEventToWechat(slot.adapter, event.type, { text: event.text })) {
1025
+ },
1026
+ mirroredUserInput: (next) => {
1027
+ appendDaemonLog(`mirrored_local_input: adapter=${slot.adapter} text=${truncatePreview(next.text)}`);
1028
+ if (shouldForwardBridgeEventToWechat(slot.adapter, next.type, { text: next.text })) {
1011
1029
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1012
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatMirroredUserInputMessage(slot.adapter, event.text)), "mirrored_user_input");
1030
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatMirroredUserInputMessage(slot.adapter, next.text)), "mirrored_user_input");
1013
1031
  }));
1014
1032
  }
1015
- break;
1016
- case "session_switched":
1017
- appendDaemonLog(`session_switched: adapter=${slot.adapter} session=${event.sessionId} source=${event.source} reason=${event.reason}`);
1018
- if (shouldForwardBridgeEventToWechat(slot.adapter, event.type)) {
1033
+ },
1034
+ sessionSwitched: (next) => {
1035
+ if (next.source === "local")
1036
+ slot.resumeCoordinator.clear();
1037
+ appendDaemonLog(`session_switched: adapter=${slot.adapter} session=${next.sessionId} source=${next.source} reason=${next.reason}`);
1038
+ if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(slot.adapter, next.type)) {
1019
1039
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1020
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({
1021
- adapter: slot.adapter,
1022
- sessionId: event.sessionId,
1023
- source: event.source,
1024
- reason: event.reason,
1025
- })), "session_switched");
1040
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({ adapter: slot.adapter, sessionId: next.sessionId, source: next.source, reason: next.reason })), "session_switched");
1026
1041
  }));
1027
1042
  }
1028
- break;
1029
- case "thread_switched":
1030
- appendDaemonLog(`thread_switched: adapter=${slot.adapter} thread=${event.threadId} source=${event.source} reason=${event.reason}`);
1031
- if (shouldForwardBridgeEventToWechat(slot.adapter, event.type)) {
1043
+ },
1044
+ threadSwitched: (next) => {
1045
+ if (next.source === "local")
1046
+ slot.resumeCoordinator.clear();
1047
+ appendDaemonLog(`thread_switched: adapter=${slot.adapter} thread=${next.threadId} source=${next.source} reason=${next.reason}`);
1048
+ if (shouldSuppressCodexLocalThreadNotice({
1049
+ adapter: slot.adapter,
1050
+ source: next.source,
1051
+ activeTurnOrigin: slot.runtime.getState().activeTurnOrigin,
1052
+ lastFinalReplyAtMs: slot.lastFinalReplyAtMs,
1053
+ })) {
1054
+ return;
1055
+ }
1056
+ if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(slot.adapter, next.type)) {
1032
1057
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1033
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({
1034
- adapter: slot.adapter,
1035
- sessionId: event.threadId,
1036
- source: event.source,
1037
- reason: event.reason,
1038
- })), "thread_switched");
1058
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({ adapter: slot.adapter, sessionId: next.threadId, source: next.source, reason: next.reason })), "thread_switched");
1039
1059
  }));
1040
1060
  }
1041
- break;
1042
- case "task_complete":
1061
+ },
1062
+ taskComplete: () => {
1043
1063
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(() => {
1044
1064
  slot.pendingConfirmations = [];
1045
1065
  slot.pendingUserInput = null;
1046
1066
  slot.activeTask = null;
1047
1067
  }));
1048
- break;
1049
- case "task_failed":
1068
+ },
1069
+ taskFailed: (next) => {
1050
1070
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1051
1071
  slot.pendingConfirmations = [];
1052
1072
  slot.pendingUserInput = null;
1053
1073
  slot.activeTask = null;
1054
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatTaskFailedMessage(slot.adapter, event.message)), "task_failed");
1074
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatTaskFailedMessage(slot.adapter, next.message)), "task_failed");
1055
1075
  }));
1056
- break;
1057
- case "fatal_error":
1058
- logError(`${slot.adapter}: ${event.message}`);
1059
- appendDaemonLog(`fatal_error: adapter=${slot.adapter} message=${event.message}`);
1076
+ },
1077
+ fatalError: (next) => {
1078
+ logError(`${slot.adapter}: ${next.message}`);
1079
+ appendDaemonLog(`fatal_error: adapter=${slot.adapter} message=${next.message}`);
1060
1080
  slot.pendingConfirmations = [];
1061
1081
  slot.pendingUserInput = null;
1062
1082
  slot.activeTask = null;
1063
1083
  this.disposeDeadSlot(slot);
1064
1084
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1065
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserFacingBridgeFatalError(event.message)), "fatal_error");
1085
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserFacingBridgeFatalError(next.message)), "fatal_error");
1066
1086
  }));
1067
- break;
1068
- case "shutdown_requested":
1069
- appendDaemonLog(`slot_shutdown_requested: adapter=${slot.adapter} reason=${event.reason}`);
1070
- break;
1071
- }
1087
+ },
1088
+ shutdownRequested: (next) => {
1089
+ appendDaemonLog(`slot_shutdown_requested: adapter=${slot.adapter} reason=${next.reason}`);
1090
+ },
1091
+ }))
1092
+ .catch((error) => {
1093
+ const message = error instanceof Error ? error.message : String(error);
1094
+ appendDaemonLog(`event_forward_failed: adapter=${slot.adapter} message=${message}`);
1095
+ });
1072
1096
  }
1073
1097
  async handleInboundMessage(message) {
1074
1098
  if (message.senderId !== this.authorizedUserId) {
@@ -1139,21 +1163,47 @@ class WechatDaemon {
1139
1163
  await this.queueWechatMessage(message.senderId, formatNoActiveAdapterMessage());
1140
1164
  return;
1141
1165
  }
1166
+ const routeCurrentMessage = async (currentSlot, command) => {
1167
+ await routeBridgeMessage({
1168
+ message: toChannelInboundMessage(message),
1169
+ authorized: true,
1170
+ command,
1171
+ adapterState: currentSlot.runtime.getState(),
1172
+ hasPendingApproval: currentSlot.pendingConfirmations.length > 0,
1173
+ hasPendingUserInput: Boolean(currentSlot.pendingUserInput),
1174
+ onUnauthorized: async () => undefined,
1175
+ handleCommand: async (nextCommand) => {
1176
+ await this.handleSystemCommand(message, currentSlot, nextCommand);
1177
+ return true;
1178
+ },
1179
+ remindPendingApproval: async () => {
1180
+ await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(currentSlot.adapter, formatPendingApprovalReminder(currentSlot.pendingConfirmations[0], currentSlot.runtime.getState())));
1181
+ },
1182
+ remindPendingUserInput: async () => {
1183
+ await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(currentSlot.adapter, currentSlot.pendingUserInput
1184
+ ? formatPendingUserInputReminder(currentSlot.pendingUserInput)
1185
+ : `${currentSlot.adapter} is waiting for structured input. Reply with /answer <key>=<value> ...`));
1186
+ },
1187
+ remindBusy: async () => {
1188
+ await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(currentSlot.adapter, `${currentSlot.adapter} is still working. Wait for the current reply or use /stop.`));
1189
+ },
1190
+ defer: async () => undefined,
1191
+ dispatch: async () => {
1192
+ await this.dispatchInboundWechatText(message, currentSlot);
1193
+ },
1194
+ });
1195
+ };
1142
1196
  const command = parseWechatControlCommand(message.text, {
1143
1197
  adapter: slot.adapter,
1144
1198
  hasPendingConfirmation: slot.pendingConfirmations.length > 0,
1145
1199
  hasPendingUserInput: Boolean(slot.pendingUserInput),
1146
1200
  });
1147
- if (command) {
1148
- await this.handleSystemCommand(message, slot, command);
1149
- return;
1150
- }
1151
- if (slot.pendingConfirmations.length > 0) {
1152
- await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, formatPendingApprovalReminder(slot.pendingConfirmations[0], slot.runtime.getState())));
1153
- return;
1154
- }
1155
- if (slot.pendingUserInput) {
1156
- await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, formatPendingUserInputReminder(slot.pendingUserInput)));
1201
+ const slotState = slot.runtime.getState();
1202
+ if (command ||
1203
+ slot.pendingConfirmations.length > 0 ||
1204
+ slot.pendingUserInput ||
1205
+ slotState.status === "awaiting_input") {
1206
+ await routeCurrentMessage(slot, command);
1157
1207
  return;
1158
1208
  }
1159
1209
  const visibleResult = await this.ensureSlot(slot.adapter, {
@@ -1165,21 +1215,7 @@ class WechatDaemon {
1165
1215
  return;
1166
1216
  }
1167
1217
  slot = this.getActiveSlot() ?? slot;
1168
- const adapterState = slot.runtime.getState();
1169
- if (adapterState.status === "awaiting_input") {
1170
- // Belt-and-braces with the pendingUserInput mirror above: the adapter
1171
- // state is authoritative even when the mirror was cleared or the WeChat
1172
- // notification is still queued.
1173
- await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, slot.pendingUserInput
1174
- ? formatPendingUserInputReminder(slot.pendingUserInput)
1175
- : `${slot.adapter} is waiting for structured input. Reply with /answer <key>=<value> ...`));
1176
- return;
1177
- }
1178
- if (adapterState.status === "busy" || adapterState.status === "awaiting_approval") {
1179
- await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, `${slot.adapter} is still working. Wait for the current reply or use /stop.`));
1180
- return;
1181
- }
1182
- await this.dispatchInboundWechatText(message, slot);
1218
+ await routeCurrentMessage(slot, null);
1183
1219
  }
1184
1220
  async handleEmojiBindingsCommand(senderId, cmd) {
1185
1221
  switch (cmd.type) {
@@ -1207,29 +1243,33 @@ class WechatDaemon {
1207
1243
  await this.queueWechatMessage(message.senderId, formatDaemonStatus(this.getStatus()));
1208
1244
  return;
1209
1245
  case "resume": {
1210
- if (activeSlot.adapter !== "opencode") {
1246
+ if (!isWechatResumeEnabled(activeSlot.adapter)) {
1211
1247
  await this.queueWechatMessage(message.senderId, `WeChat /resume is disabled for ${activeSlot.adapter} in daemon mode. Use /resume directly inside the visible terminal; WeChat will follow that local session.`);
1212
1248
  return;
1213
1249
  }
1214
- if (command.type !== "resume") {
1215
- return;
1216
- }
1217
1250
  try {
1251
+ let resumeSlot = activeSlot;
1252
+ if (activeSlot.adapter === "codex" && command.target) {
1253
+ const visible = await this.ensureSlot("codex", {
1254
+ openVisible: true,
1255
+ reuseExistingVisible: true,
1256
+ });
1257
+ if (!visible.activated) {
1258
+ throw new Error(formatDaemonSwitchResultDetail(visible));
1259
+ }
1260
+ resumeSlot = this.slots.get("codex") ?? activeSlot;
1261
+ }
1218
1262
  if (command.target) {
1219
- await activeSlot.runtime.resumeSession(command.target);
1220
- await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(activeSlot.adapter, `Resumed OpenCode session ${command.target}.`));
1263
+ await resumeSlot.outputBatcher.flushNow();
1221
1264
  }
1222
- else {
1223
- const candidates = await activeSlot.runtime.listResumeSessions(8);
1224
- await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(activeSlot.adapter, formatResumeSessionList({
1225
- adapter: activeSlot.adapter,
1226
- candidates,
1227
- currentSessionId: activeSlot.runtime.getState().sharedSessionId,
1228
- })));
1265
+ const result = await resumeSlot.resumeCoordinator.execute(command.target);
1266
+ if (result.kind === "resumed") {
1267
+ resumeSlot.activeTask = null;
1229
1268
  }
1269
+ await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(resumeSlot.adapter, result.message));
1230
1270
  }
1231
1271
  catch (error) {
1232
- await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(activeSlot.adapter, `Failed to resume OpenCode session: ${error instanceof Error ? error.message : String(error)}`));
1272
+ await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(activeSlot.adapter, error instanceof Error ? error.message : String(error)));
1233
1273
  }
1234
1274
  return;
1235
1275
  }
@@ -1242,6 +1282,7 @@ class WechatDaemon {
1242
1282
  activeSlot.outputBatcher.clear();
1243
1283
  activeSlot.pendingConfirmations = [];
1244
1284
  activeSlot.pendingUserInput = null;
1285
+ activeSlot.resumeCoordinator.clear();
1245
1286
  await activeSlot.runtime.createSession();
1246
1287
  appendDaemonLog(`new_session: adapter=${activeSlot.adapter}`);
1247
1288
  return;
@@ -1257,6 +1298,7 @@ class WechatDaemon {
1257
1298
  activeSlot.outputBatcher.clear();
1258
1299
  activeSlot.pendingConfirmations = [];
1259
1300
  activeSlot.pendingUserInput = null;
1301
+ activeSlot.resumeCoordinator.clear();
1260
1302
  await activeSlot.runtime.reset();
1261
1303
  appendDaemonLog(`reset: adapter=${activeSlot.adapter}`);
1262
1304
  await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(activeSlot.adapter, "Worker session has been reset."));
@@ -1378,39 +1420,80 @@ class WechatDaemon {
1378
1420
  this.attachmentSendChain = run.then(() => undefined, () => undefined);
1379
1421
  return run;
1380
1422
  }
1381
- queueWechatMessage(senderId, text, context = "message") {
1382
- return this.queueWechatTextAction(async () => {
1383
- for (let attempt = 1; attempt <= WECHAT_SEND_MAX_ATTEMPTS; attempt += 1) {
1384
- try {
1385
- await this.transport.sendText(senderId, text);
1386
- return true;
1423
+ async sendWechatMessageNow(senderId, text, context = "message") {
1424
+ const startedAtMs = Date.now();
1425
+ appendDaemonLog(`wechat_send_started: context=${context} recipient=${senderId} chars=${Array.from(text).length}`);
1426
+ for (let attempt = 1; attempt <= WECHAT_SEND_MAX_ATTEMPTS; attempt += 1) {
1427
+ try {
1428
+ await this.transport.sendText(senderId, text);
1429
+ appendDaemonLog(`wechat_send_completed: context=${context} recipient=${senderId} attempt=${attempt} elapsed_ms=${Date.now() - startedAtMs}`);
1430
+ return { status: "sent" };
1431
+ }
1432
+ catch (error) {
1433
+ if (error instanceof Error && error.name === "AbortError") {
1434
+ appendDaemonLog(`wechat_send_timeout: context=${context} recipient=${senderId} attempt=${attempt} elapsed_ms=${Date.now() - startedAtMs}`);
1387
1435
  }
1388
- catch (error) {
1436
+ if (isWechatContextUnavailableError(error)) {
1389
1437
  if (isWechatContextTokenStaleError(error)) {
1390
1438
  this.transport.clearCachedContextToken(senderId);
1391
- appendDaemonLog(formatWechatContextTokenStaleLogEntry({
1439
+ }
1440
+ appendDaemonLog(isWechatContextTokenStaleError(error)
1441
+ ? formatWechatContextTokenStaleLogEntry({
1442
+ context,
1443
+ recipientId: senderId,
1444
+ error,
1445
+ })
1446
+ : formatWechatSendFailureLogEntry({
1392
1447
  context,
1393
1448
  recipientId: senderId,
1394
1449
  error,
1395
1450
  }));
1396
- return false;
1397
- }
1398
- if (attempt < WECHAT_SEND_MAX_ATTEMPTS && isRetryableWechatSendError(error)) {
1399
- const delayMs = computeWechatSendRetryDelayMs(attempt);
1400
- appendDaemonLog(`wechat_send_retry: context=${context} recipient=${senderId} attempt=${attempt} delay_ms=${delayMs} error=${truncatePreview(describeWechatTransportError(error), 400)}`);
1401
- await delay(delayMs);
1402
- continue;
1403
- }
1404
- logError(`Failed to send WeChat ${context}: ${describeWechatTransportError(error)}`);
1405
- appendDaemonLog(formatWechatSendFailureLogEntry({
1406
- context,
1407
- recipientId: senderId,
1408
- error,
1409
- }));
1410
- return false;
1451
+ return { status: "context_unavailable", error };
1452
+ }
1453
+ if (attempt < WECHAT_SEND_MAX_ATTEMPTS && isRetryableWechatSendError(error)) {
1454
+ const delayMs = computeWechatSendRetryDelayMs(attempt);
1455
+ appendDaemonLog(`wechat_send_retry: context=${context} recipient=${senderId} attempt=${attempt} delay_ms=${delayMs} error=${truncatePreview(describeWechatTransportError(error), 400)}`);
1456
+ await delay(delayMs);
1457
+ continue;
1411
1458
  }
1459
+ logError(`Failed to send WeChat ${context}: ${describeWechatTransportError(error)}`);
1460
+ appendDaemonLog(formatWechatSendFailureLogEntry({
1461
+ context,
1462
+ recipientId: senderId,
1463
+ error,
1464
+ }));
1465
+ return { status: "failed" };
1466
+ }
1467
+ }
1468
+ return { status: "failed" };
1469
+ }
1470
+ queueWechatMessage(senderId, text, context = "message") {
1471
+ return this.queueWechatTextAction(async () => {
1472
+ const result = await this.sendWechatMessageNow(senderId, text, context);
1473
+ if (result.status === "context_unavailable") {
1474
+ const pending = this.pendingWechatMessages.enqueue(senderId, text, context);
1475
+ if (pending) {
1476
+ appendDaemonLog(`wechat_send_queued: id=${pending.id} context=${context} recipient=${senderId} pending=${this.pendingWechatMessages.list().length}`);
1477
+ }
1478
+ }
1479
+ return result.status === "sent";
1480
+ });
1481
+ }
1482
+ flushPendingWechatMessages() {
1483
+ return this.queueWechatTextAction(async () => {
1484
+ for (const pending of this.pendingWechatMessages.list()) {
1485
+ const result = await this.sendWechatMessageNow(pending.recipientId, pending.text, pending.context);
1486
+ if (result.status === "sent") {
1487
+ this.pendingWechatMessages.remove(pending.id);
1488
+ appendDaemonLog(`wechat_pending_sent: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
1489
+ continue;
1490
+ }
1491
+ if (result.status === "context_unavailable") {
1492
+ break;
1493
+ }
1494
+ appendDaemonLog(`wechat_pending_retryable_failure: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
1495
+ break;
1412
1496
  }
1413
- return false;
1414
1497
  });
1415
1498
  }
1416
1499
  trackWechatForwardTask(task) {
@@ -25,7 +25,7 @@ export const messages = {
25
25
  // === Hook health check ===
26
26
  "hook.healthCheck.warning": "[Warning] No hook events received from Claude after 15s.\nThe hook system may not be working — Claude output will not reach WeChat.",
27
27
  "hook.healthCheck.logHint": "\nCheck: {logPath}",
28
- "hook.healthCheck.fixes": "\nCommon fixes:\n- Ensure Node.js >= 22.6.0: node --version\n- Reinstall: npm install -g cli-wechat-bridge@latest\n- Check firewall: allow localhost TCP connections",
28
+ "hook.healthCheck.fixes": "\nCommon fixes:\n- Ensure Node.js >= 22.13.0: node --version\n- Reinstall: npm install -g cli-wechat-bridge@latest\n- Check firewall: allow localhost TCP connections",
29
29
  // === PTY fallback ===
30
30
  "pty.fallback.warning": "[Warning] PTY unavailable — using fallback mode (TERM=dumb).\nCore features (messaging, approvals, session management) work normally; terminal rendering (colors, progress bars) may be degraded.\nFix: Linux install build-essential python3 | Windows run npm rebuild node-pty | macOS run xcode-select --install\nRun --doctor for detailed environment info.\n",
31
31
  // === Spawn diagnostic ===
@@ -36,7 +36,7 @@ export const messages = {
36
36
  "spawn.diagnostic.linuxBuildTools": "- Linux requires build tools for node-pty: sudo apt install build-essential python3\n- Then reinstall: npm install -g cli-wechat-bridge@latest",
37
37
  "spawn.diagnostic.notFound": "- The command \"{target}\" was not found on PATH.\n- Verify it is installed and accessible from your terminal.",
38
38
  "spawn.diagnostic.generic": "- Reinstall: npm install -g cli-wechat-bridge@latest",
39
- "spawn.diagnostic.nodeVersion": "- Ensure Node.js >= 22.6.0: node --version",
39
+ "spawn.diagnostic.nodeVersion": "- Ensure Node.js >= 22.13.0: node --version",
40
40
  "spawn.diagnostic.winFull": "- Ensure Windows 10 build 18309+ (run 'winver' to check)\n- Install Visual C++ Redistributable: https://aka.ms/vs/17/release/vc_redist.x64.exe\n- Run: npm rebuild node-pty\n- Try running as Administrator",
41
41
  // === Emoji bindings ===
42
42
  "binding.usage": "Invalid format.\nUsage: /bind [emoji] command\nExample: /bind 🚀 deploy --prod",
@@ -25,7 +25,7 @@ export const messages = {
25
25
  // === Hook 健康检查 ===
26
26
  "hook.healthCheck.warning": "[警告] 15 秒内未收到 Claude hook 事件。\nhook 系统可能未正常工作 — Claude 的输出将无法到达微信。",
27
27
  "hook.healthCheck.logHint": "\n查看日志: {logPath}",
28
- "hook.healthCheck.fixes": "\n常见修复方法:\n- 确保 Node.js >= 22.6.0: node --version\n- 重新安装: npm install -g cli-wechat-bridge@latest\n- 检查防火墙: 允许 localhost TCP 连接",
28
+ "hook.healthCheck.fixes": "\n常见修复方法:\n- 确保 Node.js >= 22.13.0: node --version\n- 重新安装: npm install -g cli-wechat-bridge@latest\n- 检查防火墙: 允许 localhost TCP 连接",
29
29
  // === PTY 回退 ===
30
30
  "pty.fallback.warning": "[警告] PTY 不可用 — 已切换到回退模式(TERM=dumb)。\n核心功能(消息收发、审批、会话管理)正常工作;终端渲染(颜色、进度条)可能受限。\n修复方法: Linux 安装 build-essential python3 | Windows 运行 npm rebuild node-pty | macOS 运行 xcode-select --install\n运行 --doctor 查看详细环境信息。\n",
31
31
  // === 启动诊断 ===
@@ -36,7 +36,7 @@ export const messages = {
36
36
  "spawn.diagnostic.linuxBuildTools": "- Linux 需要编译工具来构建 node-pty: sudo apt install build-essential python3\n- 然后重新安装: npm install -g cli-wechat-bridge@latest",
37
37
  "spawn.diagnostic.notFound": "- 命令 \"{target}\" 未在 PATH 中找到。\n- 请确认已安装并可从终端访问。",
38
38
  "spawn.diagnostic.generic": "- 重新安装: npm install -g cli-wechat-bridge@latest",
39
- "spawn.diagnostic.nodeVersion": "- 确保 Node.js >= 22.6.0: node --version",
39
+ "spawn.diagnostic.nodeVersion": "- 确保 Node.js >= 22.13.0: node --version",
40
40
  "spawn.diagnostic.winFull": "- 确保 Windows 10 build 18309+(运行 winver 查看)\n- 安装 Visual C++ Redistributable: https://aka.ms/vs/17/release/vc_redist.x64.exe\n- 运行: npm rebuild node-pty\n- 尝试以管理员身份运行",
41
41
  // === Emoji 绑定 ===
42
42
  "binding.usage": "格式错误。\n用法: /bind [表情] 命令\n示例: /bind 🚀 deploy --prod",