cli-wechat-bridge 1.1.7 → 1.1.8

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.
@@ -12,6 +12,8 @@ import { getProcessRecordByPid, reapOrphanedOpencodeProcesses, reapPeerBridgePro
12
12
  import { createRuntimeHost } from "../runtime/create-runtime-host.js";
13
13
  import { toChannelInboundMessage } from "../channels/wechat/channel-message.js";
14
14
  import { routeBridgeMessage } from "../core/bridge-message-router.js";
15
+ import { clearTurn, resolveConversationTarget, rollbackTurn, tryBeginTurn, } from "../core/conversation-routing.js";
16
+ import { handleAdapterControl } from "./adapter-control.js";
15
17
  import { forwardBridgeEvent } from "../core/bridge-event-forwarder.js";
16
18
  import { isDirectModuleRun } from "../core/direct-run.js";
17
19
  import { WechatChannelPort } from "../channels/wechat/wechat-channel-port.js";
@@ -361,16 +363,17 @@ async function main() {
361
363
  let textSendChain = Promise.resolve();
362
364
  let attachmentSendChain = Promise.resolve();
363
365
  const pendingWechatForwardTasks = new Set();
364
- let activeTask = null;
365
- let currentWecomConversation = null;
366
- let activeWecomConversation = null;
367
- let lastWecomConversation = {
366
+ const defaultWecomConversation = {
368
367
  channelId: "wecom",
369
368
  accountId: wecomAccount?.botId,
370
369
  conversationId: credentials.userId,
371
370
  recipientId: credentials.userId,
372
371
  metadata: { chatType: "direct" },
373
372
  };
373
+ const turnState = {
374
+ activeTask: null,
375
+ lastConversation: defaultWecomConversation,
376
+ };
374
377
  const deferredInboundMessages = [];
375
378
  let drainingDeferredInboundMessages = false;
376
379
  let consecutivePollFailures = 0;
@@ -389,9 +392,11 @@ async function main() {
389
392
  if (options.channelId === "wecom") {
390
393
  const target = targetOverride ??
391
394
  (senderId === credentials.userId
392
- ? currentWecomConversation ??
393
- activeWecomConversation ??
394
- lastWecomConversation
395
+ ? resolveConversationTarget({
396
+ active: turnState.activeConversation,
397
+ last: turnState.lastConversation,
398
+ fallback: defaultWecomConversation,
399
+ })
395
400
  : {
396
401
  channelId: "wecom",
397
402
  accountId: wecomAccount?.botId,
@@ -469,8 +474,15 @@ async function main() {
469
474
  return { status: "failed" };
470
475
  };
471
476
  const queueWechatMessage = (senderId, text, context = "message", targetOverride) => {
477
+ const queuedTarget = options.channelId === "wecom" && senderId === credentials.userId
478
+ ? targetOverride ?? resolveConversationTarget({
479
+ active: turnState.activeConversation,
480
+ last: turnState.lastConversation,
481
+ fallback: defaultWecomConversation,
482
+ })
483
+ : targetOverride;
472
484
  return queueWechatTextAction(async () => {
473
- const result = await sendWechatMessageNow(senderId, text, context, targetOverride);
485
+ const result = await sendWechatMessageNow(senderId, text, context, queuedTarget);
474
486
  if (result.status === "context_unavailable") {
475
487
  const pending = pendingWechatMessages.enqueue(senderId, text, context, result.target);
476
488
  if (pending) {
@@ -514,7 +526,13 @@ async function main() {
514
526
  }
515
527
  };
516
528
  const outputBatcher = new OutputBatcher(async (text) => {
517
- await queueWechatMessage(stateStore.getState().authorizedUserId, text);
529
+ await queueWechatMessage(stateStore.getState().authorizedUserId, text, "message", options.channelId === "wecom"
530
+ ? resolveConversationTarget({
531
+ active: turnState.activeConversation,
532
+ last: turnState.lastConversation,
533
+ fallback: defaultWecomConversation,
534
+ })
535
+ : undefined);
518
536
  });
519
537
  const maybeDrainDeferredInboundMessages = async () => {
520
538
  if (drainingDeferredInboundMessages || !ensureRuntimeOwnership()) {
@@ -529,7 +547,7 @@ async function main() {
529
547
  hasPendingConfirmation: Boolean(stateStore.getState().pendingConfirmation),
530
548
  hasPendingUserInput: Boolean(stateStore.getState().pendingUserInput),
531
549
  hasPendingApproval: Boolean(adapterState.pendingApproval),
532
- hasActiveTask: Boolean(activeTask),
550
+ hasActiveTask: Boolean(turnState.activeTask),
533
551
  })) {
534
552
  return;
535
553
  }
@@ -540,17 +558,25 @@ async function main() {
540
558
  drainingDeferredInboundMessages = true;
541
559
  try {
542
560
  stateStore.appendLog(`draining_deferred_inbound_input: remaining=${deferredInboundMessages.length} text=${truncatePreview(nextDeferred.message.text)}`);
543
- if (nextDeferred.channelMessage) {
544
- activeWecomConversation = nextDeferred.channelMessage.conversation;
545
- lastWecomConversation = nextDeferred.channelMessage.conversation;
561
+ const nextTask = createActiveTask(nextDeferred.message);
562
+ const lease = tryBeginTurn(turnState, nextTask, nextDeferred.channelMessage?.conversation);
563
+ if (!lease) {
564
+ deferredInboundMessages.unshift(nextDeferred);
565
+ return;
566
+ }
567
+ try {
568
+ await dispatchInboundWechatText({
569
+ message: nextDeferred.message,
570
+ options,
571
+ stateStore,
572
+ adapter,
573
+ activeTask: nextTask,
574
+ });
575
+ }
576
+ catch (error) {
577
+ rollbackTurn(turnState, lease);
578
+ throw error;
546
579
  }
547
- const nextTask = await dispatchInboundWechatText({
548
- message: nextDeferred.message,
549
- options,
550
- stateStore,
551
- adapter,
552
- });
553
- activeTask = nextTask;
554
580
  }
555
581
  catch (err) {
556
582
  const errorText = err instanceof Error ? err.message : String(err);
@@ -565,7 +591,7 @@ async function main() {
565
591
  adapter: options.adapter,
566
592
  cwd: options.cwd,
567
593
  errorText,
568
- }), "inbound_error");
594
+ }), "inbound_error", nextDeferred.channelMessage?.conversation);
569
595
  }
570
596
  finally {
571
597
  drainingDeferredInboundMessages = false;
@@ -715,9 +741,11 @@ async function main() {
715
741
  queueWechatMessage,
716
742
  trackWechatForwardTask,
717
743
  maybeDrainDeferredInboundMessages,
718
- clearActiveTask: () => {
719
- activeTask = null;
720
- activeWecomConversation = null;
744
+ getActiveTask: () => turnState.activeTask,
745
+ clearActiveTask: (expectedTask) => {
746
+ if (expectedTask) {
747
+ clearTurn(turnState, expectedTask);
748
+ }
721
749
  },
722
750
  syncSharedSessionState: () => {
723
751
  syncSharedSessionState(stateStore, adapter);
@@ -727,7 +755,11 @@ async function main() {
727
755
  },
728
756
  requestShutdown,
729
757
  wecomTransport,
730
- getWecomTarget: () => activeWecomConversation ?? lastWecomConversation,
758
+ getWecomTarget: () => resolveConversationTarget({
759
+ active: turnState.activeConversation,
760
+ last: turnState.lastConversation,
761
+ fallback: defaultWecomConversation,
762
+ }),
731
763
  });
732
764
  await adapter.start();
733
765
  if (!ensureRuntimeOwnership()) {
@@ -742,59 +774,73 @@ async function main() {
742
774
  if (shutdownPromise || !ensureRuntimeOwnership()) {
743
775
  return;
744
776
  }
745
- currentWecomConversation = channelMessage.conversation;
746
- lastWecomConversation = channelMessage.conversation;
777
+ turnState.lastConversation = channelMessage.conversation;
778
+ const queueInboundReply = (senderId, text, context) => queueWechatMessage(senderId, text, context, channelMessage.conversation);
779
+ if (pendingWechatMessages.list().length > 0) {
780
+ await flushPendingWechatMessages();
781
+ }
782
+ const message = toLegacyInboundWechatMessage(channelMessage);
783
+ const taskAtMessageStart = turnState.activeTask;
784
+ stateStore.touchActivity(message.createdAt);
747
785
  try {
748
- if (pendingWechatMessages.list().length > 0) {
749
- await flushPendingWechatMessages();
750
- }
751
- const message = toLegacyInboundWechatMessage(channelMessage);
752
- stateStore.touchActivity(message.createdAt);
753
- let nextTask = null;
754
- try {
755
- nextTask = await handleInboundMessage({
756
- message,
757
- channelMessage,
758
- options,
759
- stateStore,
760
- adapter,
761
- resumeCoordinator,
762
- queueWechatMessage,
763
- outputBatcher,
764
- clearActiveTask: () => {
765
- activeTask = null;
766
- activeWecomConversation = null;
767
- },
768
- deferInboundMessage: async (nextMessage) => {
769
- deferredInboundMessages.push({
770
- message: nextMessage,
771
- channelMessage,
786
+ await handleInboundMessage({
787
+ message,
788
+ channelMessage,
789
+ options,
790
+ stateStore,
791
+ adapter,
792
+ resumeCoordinator,
793
+ queueWechatMessage: queueInboundReply,
794
+ outputBatcher,
795
+ clearActiveTask: () => {
796
+ if (taskAtMessageStart) {
797
+ clearTurn(turnState, taskAtMessageStart);
798
+ }
799
+ },
800
+ dispatchInboundText: async () => {
801
+ const nextActiveTask = createActiveTask(message);
802
+ const lease = tryBeginTurn(turnState, nextActiveTask, channelMessage.conversation);
803
+ if (!lease) {
804
+ await queueInboundReply(message.senderId, `${options.adapter} is still working. Wait for the current reply or use /stop.`);
805
+ return null;
806
+ }
807
+ try {
808
+ await dispatchInboundWechatText({
809
+ message,
810
+ options,
811
+ stateStore,
812
+ adapter,
813
+ activeTask: nextActiveTask,
772
814
  });
773
- stateStore.appendLog(`deferred_inbound_input: position=${deferredInboundMessages.length} text=${truncatePreview(nextMessage.text)}`);
774
- await queueWechatMessage(nextMessage.senderId, formatDeferredCodexInboundQueueMessage(deferredInboundMessages.length));
775
- },
776
- });
777
- }
778
- catch (error) {
779
- const errorText = error instanceof Error ? error.message : String(error);
780
- logError(errorText);
781
- stateStore.appendLog(`inbound_error: ${errorText}`);
782
- await queueWechatMessage(message.senderId, formatUserFacingInboundError({
783
- adapter: options.adapter,
784
- cwd: options.cwd,
785
- errorText,
786
- }), "inbound_error");
787
- }
788
- if (nextTask) {
789
- activeTask = nextTask;
790
- activeWecomConversation = channelMessage.conversation;
791
- }
792
- syncSharedSessionState(stateStore, adapter);
793
- await maybeDrainDeferredInboundMessages();
815
+ return nextActiveTask;
816
+ }
817
+ catch (error) {
818
+ rollbackTurn(turnState, lease);
819
+ throw error;
820
+ }
821
+ },
822
+ deferInboundMessage: async (nextMessage) => {
823
+ deferredInboundMessages.push({
824
+ message: nextMessage,
825
+ channelMessage,
826
+ });
827
+ stateStore.appendLog(`deferred_inbound_input: position=${deferredInboundMessages.length} text=${truncatePreview(nextMessage.text)}`);
828
+ await queueInboundReply(nextMessage.senderId, formatDeferredCodexInboundQueueMessage(deferredInboundMessages.length));
829
+ },
830
+ });
794
831
  }
795
- finally {
796
- currentWecomConversation = null;
832
+ catch (error) {
833
+ const errorText = error instanceof Error ? error.message : String(error);
834
+ logError(errorText);
835
+ stateStore.appendLog(`inbound_error: ${errorText}`);
836
+ await queueInboundReply(message.senderId, formatUserFacingInboundError({
837
+ adapter: options.adapter,
838
+ cwd: options.cwd,
839
+ errorText,
840
+ }), "inbound_error");
797
841
  }
842
+ syncSharedSessionState(stateStore, adapter);
843
+ await maybeDrainDeferredInboundMessages();
798
844
  },
799
845
  onUnauthorized: async (senderId, chatType) => {
800
846
  stateStore.appendLog(`wecom_unauthorized: sender=${senderId} chat_type=${chatType}`);
@@ -910,10 +956,10 @@ async function main() {
910
956
  if (!ensureRuntimeOwnership()) {
911
957
  break;
912
958
  }
959
+ const taskAtMessageStart = turnState.activeTask;
913
960
  stateStore.touchActivity(message.createdAt);
914
- let nextTask = null;
915
961
  try {
916
- nextTask = await handleInboundMessage({
962
+ await handleInboundMessage({
917
963
  message,
918
964
  options,
919
965
  stateStore,
@@ -922,7 +968,31 @@ async function main() {
922
968
  queueWechatMessage,
923
969
  outputBatcher,
924
970
  clearActiveTask: () => {
925
- activeTask = null;
971
+ if (taskAtMessageStart) {
972
+ clearTurn(turnState, taskAtMessageStart);
973
+ }
974
+ },
975
+ dispatchInboundText: async () => {
976
+ const nextActiveTask = createActiveTask(message);
977
+ const lease = tryBeginTurn(turnState, nextActiveTask);
978
+ if (!lease) {
979
+ await queueWechatMessage(message.senderId, `${options.adapter} is still working. Wait for the current reply or use /stop.`);
980
+ return null;
981
+ }
982
+ try {
983
+ await dispatchInboundWechatText({
984
+ message,
985
+ options,
986
+ stateStore,
987
+ adapter,
988
+ activeTask: nextActiveTask,
989
+ });
990
+ return nextActiveTask;
991
+ }
992
+ catch (error) {
993
+ rollbackTurn(turnState, lease);
994
+ throw error;
995
+ }
926
996
  },
927
997
  deferInboundMessage: async (nextMessage) => {
928
998
  deferredInboundMessages.push({
@@ -943,9 +1013,6 @@ async function main() {
943
1013
  errorText,
944
1014
  }), "inbound_error");
945
1015
  }
946
- if (nextTask) {
947
- activeTask = nextTask;
948
- }
949
1016
  syncSharedSessionState(stateStore, adapter);
950
1017
  await maybeDrainDeferredInboundMessages();
951
1018
  }
@@ -980,7 +1047,7 @@ function syncSharedSessionState(stateStore, adapter) {
980
1047
  }
981
1048
  }
982
1049
  function wireAdapterEvents(params) {
983
- const { adapter, resumeCoordinator, options, transport, stateStore, outputBatcher, queueWechatAttachmentAction, queueWechatMessage, trackWechatForwardTask, maybeDrainDeferredInboundMessages, clearActiveTask, syncSharedSessionState, syncLocalClientEndpoint, requestShutdown, wecomTransport, getWecomTarget, } = params;
1050
+ const { adapter, resumeCoordinator, options, transport, stateStore, outputBatcher, queueWechatAttachmentAction, queueWechatMessage, trackWechatForwardTask, maybeDrainDeferredInboundMessages, getActiveTask, clearActiveTask, syncSharedSessionState, syncLocalClientEndpoint, requestShutdown, wecomTransport, getWecomTarget, } = params;
984
1051
  const channelPort = options.channelId === "wecom"
985
1052
  ? new WecomChannelPort({
986
1053
  transport: wecomTransport,
@@ -1016,6 +1083,14 @@ function wireAdapterEvents(params) {
1016
1083
  stateStore.clearPendingUserInput();
1017
1084
  }
1018
1085
  const authorizedUserId = stateStore.getState().authorizedUserId;
1086
+ const eventTarget = options.channelId === "wecom"
1087
+ ? getWecomTarget()
1088
+ : {
1089
+ channelId: "wechat",
1090
+ conversationId: authorizedUserId,
1091
+ recipientId: authorizedUserId,
1092
+ };
1093
+ const eventTask = getActiveTask();
1019
1094
  eventForwardChain = eventForwardChain
1020
1095
  .then(() => forwardBridgeEvent(event, {
1021
1096
  stdout: (next) => {
@@ -1033,13 +1108,7 @@ function wireAdapterEvents(params) {
1033
1108
  stateStore.appendLog(`final_reply: ${truncatePreview(next.text)}`);
1034
1109
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
1035
1110
  await channelPort.send({
1036
- target: options.channelId === "wecom"
1037
- ? getWecomTarget()
1038
- : {
1039
- channelId: "wechat",
1040
- conversationId: authorizedUserId,
1041
- recipientId: authorizedUserId,
1042
- },
1111
+ target: eventTarget,
1043
1112
  kind: "final_reply",
1044
1113
  text: next.text,
1045
1114
  adapter: options.adapter,
@@ -1057,7 +1126,7 @@ function wireAdapterEvents(params) {
1057
1126
  stateStore.appendLog(`${next.level}_notice: ${truncatePreview(next.text)}`);
1058
1127
  if (shouldForwardBridgeEventToWechat(options.adapter, next.type, { text: next.text })) {
1059
1128
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
1060
- await queueWechatMessage(authorizedUserId, next.text, "notice");
1129
+ await queueWechatMessage(authorizedUserId, next.text, "notice", eventTarget);
1061
1130
  }));
1062
1131
  }
1063
1132
  },
@@ -1069,7 +1138,7 @@ function wireAdapterEvents(params) {
1069
1138
  trackWechatForwardTask((async () => {
1070
1139
  await queueWechatMessage(authorizedUserId, options.channelId === "wecom"
1071
1140
  ? `Processing: ${thinkingPreview}`
1072
- : `思考: ${thinkingPreview}`, "thinking");
1141
+ : `思考: ${thinkingPreview}`, "thinking", eventTarget);
1073
1142
  })());
1074
1143
  }
1075
1144
  }
@@ -1079,7 +1148,7 @@ function wireAdapterEvents(params) {
1079
1148
  const pending = toPendingApproval(next.request);
1080
1149
  stateStore.setPendingConfirmation(pending);
1081
1150
  stateStore.appendLog(`Approval requested (${pending.source}): ${pending.commandPreview}`);
1082
- await queueWechatMessage(authorizedUserId, formatApprovalMessage(pending, adapterState), "approval_required");
1151
+ await queueWechatMessage(authorizedUserId, formatApprovalMessage(pending, adapterState), "approval_required", eventTarget);
1083
1152
  }));
1084
1153
  },
1085
1154
  userInputRequired: (next) => {
@@ -1087,14 +1156,14 @@ function wireAdapterEvents(params) {
1087
1156
  const pending = toPendingUserInput(next.request);
1088
1157
  stateStore.setPendingUserInput(pending);
1089
1158
  stateStore.appendLog(`User input requested: questions=${pending.questions.length}`);
1090
- await queueWechatMessage(authorizedUserId, formatUserInputRequestMessage(pending, adapterState), "user_input_required");
1159
+ await queueWechatMessage(authorizedUserId, formatUserInputRequestMessage(pending, adapterState), "user_input_required", eventTarget);
1091
1160
  }));
1092
1161
  },
1093
1162
  mirroredUserInput: (next) => {
1094
1163
  stateStore.appendLog(`mirrored_local_input: ${truncatePreview(next.text)}`);
1095
1164
  if (shouldForwardBridgeEventToWechat(options.adapter, next.type, { text: next.text })) {
1096
1165
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
1097
- await queueWechatMessage(authorizedUserId, formatMirroredUserInputMessage(options.adapter, next.text), "mirrored_user_input");
1166
+ await queueWechatMessage(authorizedUserId, formatMirroredUserInputMessage(options.adapter, next.text), "mirrored_user_input", eventTarget);
1098
1167
  }));
1099
1168
  }
1100
1169
  },
@@ -1104,7 +1173,7 @@ function wireAdapterEvents(params) {
1104
1173
  stateStore.appendLog(`session_switched: ${next.sessionId} source=${next.source} reason=${next.reason}`);
1105
1174
  if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(options.adapter, next.type)) {
1106
1175
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
1107
- await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({ adapter: options.adapter, sessionId: next.sessionId, source: next.source, reason: next.reason }), "session_switched");
1176
+ await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({ adapter: options.adapter, sessionId: next.sessionId, source: next.source, reason: next.reason }), "session_switched", eventTarget);
1108
1177
  }));
1109
1178
  }
1110
1179
  },
@@ -1122,7 +1191,7 @@ function wireAdapterEvents(params) {
1122
1191
  }
1123
1192
  if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(options.adapter, next.type)) {
1124
1193
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
1125
- await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({ adapter: options.adapter, sessionId: next.threadId, source: next.source, reason: next.reason }), "thread_switched");
1194
+ await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({ adapter: options.adapter, sessionId: next.threadId, source: next.source, reason: next.reason }), "thread_switched", eventTarget);
1126
1195
  }));
1127
1196
  }
1128
1197
  void maybeDrainDeferredInboundMessages();
@@ -1131,7 +1200,7 @@ function wireAdapterEvents(params) {
1131
1200
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
1132
1201
  stateStore.clearPendingConfirmation();
1133
1202
  stateStore.clearPendingUserInput();
1134
- clearActiveTask();
1203
+ clearActiveTask(eventTask);
1135
1204
  await maybeDrainDeferredInboundMessages();
1136
1205
  }));
1137
1206
  },
@@ -1139,8 +1208,8 @@ function wireAdapterEvents(params) {
1139
1208
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
1140
1209
  stateStore.clearPendingConfirmation();
1141
1210
  stateStore.clearPendingUserInput();
1142
- clearActiveTask();
1143
- await queueWechatMessage(authorizedUserId, formatTaskFailedMessage(options.adapter, next.message), "task_failed");
1211
+ clearActiveTask(eventTask);
1212
+ await queueWechatMessage(authorizedUserId, formatTaskFailedMessage(options.adapter, next.message), "task_failed", eventTarget);
1144
1213
  await maybeDrainDeferredInboundMessages();
1145
1214
  }));
1146
1215
  },
@@ -1149,9 +1218,9 @@ function wireAdapterEvents(params) {
1149
1218
  stateStore.appendLog(`fatal_error: ${next.message}`);
1150
1219
  stateStore.clearPendingConfirmation();
1151
1220
  stateStore.clearPendingUserInput();
1152
- clearActiveTask();
1221
+ clearActiveTask(eventTask);
1153
1222
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
1154
- await queueWechatMessage(authorizedUserId, formatUserFacingBridgeFatalError(next.message), "fatal_error");
1223
+ await queueWechatMessage(authorizedUserId, formatUserFacingBridgeFatalError(next.message), "fatal_error", eventTarget);
1155
1224
  await maybeDrainDeferredInboundMessages();
1156
1225
  }));
1157
1226
  },
@@ -1180,7 +1249,7 @@ function formatInboundMessagePreview(message) {
1180
1249
  }
1181
1250
  async function handleInboundMessage(params) {
1182
1251
  const { message, channelMessage, } = params;
1183
- const { options, stateStore, adapter, resumeCoordinator, queueWechatMessage, outputBatcher, clearActiveTask, deferInboundMessage, } = params;
1252
+ const { options, stateStore, adapter, resumeCoordinator, queueWechatMessage, outputBatcher, clearActiveTask, dispatchInboundText, deferInboundMessage, } = params;
1184
1253
  const state = stateStore.getState();
1185
1254
  if (message.senderId !== state.authorizedUserId) {
1186
1255
  await queueWechatMessage(message.senderId, options.channelId === "wecom"
@@ -1194,6 +1263,10 @@ async function handleInboundMessage(params) {
1194
1263
  hasPendingUserInput: Boolean(state.pendingUserInput),
1195
1264
  });
1196
1265
  switch (systemCommand?.type) {
1266
+ case "model":
1267
+ case "plan":
1268
+ await queueWechatMessage(message.senderId, await handleAdapterControl(adapter, message.senderId, systemCommand));
1269
+ return null;
1197
1270
  case "status":
1198
1271
  await queueWechatMessage(message.senderId, formatStatusReport(stateStore.getState(), adapter.getState()));
1199
1272
  return null;
@@ -1348,24 +1421,20 @@ async function handleInboundMessage(params) {
1348
1421
  defer: async () => {
1349
1422
  await deferInboundMessage(message);
1350
1423
  },
1351
- dispatch: async () => {
1352
- return await dispatchInboundWechatText({
1353
- message,
1354
- options,
1355
- stateStore,
1356
- adapter,
1357
- });
1358
- },
1424
+ dispatch: dispatchInboundText,
1359
1425
  });
1360
1426
  return routeResult.kind === "dispatched" ? routeResult.result : null;
1361
1427
  }
1428
+ function createActiveTask(message) {
1429
+ return {
1430
+ startedAt: Date.now(),
1431
+ inputPreview: truncatePreview(formatInboundMessagePreview(message), 180),
1432
+ };
1433
+ }
1362
1434
  async function dispatchInboundWechatText(params) {
1363
1435
  const { message, options, stateStore, adapter } = params;
1364
1436
  const preview = formatInboundMessagePreview(message);
1365
- const activeTask = {
1366
- startedAt: Date.now(),
1367
- inputPreview: truncatePreview(preview, 180),
1368
- };
1437
+ const activeTask = params.activeTask ?? createActiveTask(message);
1369
1438
  stateStore.appendLog(`Forwarded input to ${options.adapter}: ${truncatePreview(preview)}`);
1370
1439
  await adapter.sendInput(options.channelId === "wecom"
1371
1440
  ? buildWecomInboundPrompt(message.text, message.attachments)
@@ -5,7 +5,7 @@ import net from "node:net";
5
5
  import path from "node:path";
6
6
  import { assertNoReservedExtraCliArgs, buildCliEnvironment, buildCodexCliArgs, resolveSpawnTarget, } from "../bridge/bridge-adapters.shared.js";
7
7
  import { killProcessTreeSync } from "../bridge/bridge-process-reaper.js";
8
- import { CODEX_VISIBLE_CONTROL_HOST, parseCodexVisibleSwitchRequest, sendCodexVisibleSwitchResponse, } from "./codex-visible-client-link.js";
8
+ import { CODEX_VISIBLE_CONTROL_HOST, parseCodexVisibleControlRequest, sendCodexVisibleShutdownResponse, sendCodexVisibleSwitchResponse, } from "./codex-visible-client-link.js";
9
9
  import { clearLocalCompanionOccupancy, readLocalCompanionEndpoint, updateCodexVisibleControl, updateLocalCompanionOccupancy, } from "./local-companion-link.js";
10
10
  import { migrateLegacyChannelFiles } from "../wechat/channel-config.js";
11
11
  import { CODEX_REMOTE_AUTH_TOKEN_ENV } from "../runtime/runtime-types.js";
@@ -302,11 +302,20 @@ export class CodexVisibleClientSupervisor {
302
302
  socket.destroy();
303
303
  return;
304
304
  }
305
- const request = parseCodexVisibleSwitchRequest(parsed);
305
+ const request = parseCodexVisibleControlRequest(parsed);
306
306
  if (!request || request.token !== this.controlToken) {
307
307
  socket.destroy();
308
308
  return;
309
309
  }
310
+ if (request.type === "shutdown") {
311
+ sendCodexVisibleShutdownResponse(socket, {
312
+ type: "shutdown_result",
313
+ id: request.id,
314
+ ok: true,
315
+ });
316
+ void this.shutdownFromControl();
317
+ return;
318
+ }
310
319
  void this.switchThread(request.threadId).then(() => {
311
320
  sendCodexVisibleSwitchResponse(socket, {
312
321
  type: "switch_thread_result",
@@ -340,6 +349,17 @@ export class CodexVisibleClientSupervisor {
340
349
  this.controlServer = server;
341
350
  this.controlPort = address.port;
342
351
  }
352
+ async shutdownFromControl() {
353
+ await this.switchChain.catch(() => undefined);
354
+ if (this.runSettled) {
355
+ return;
356
+ }
357
+ const child = this.child;
358
+ if (child) {
359
+ await this.stopChild(child);
360
+ }
361
+ this.finishRun(undefined, 0);
362
+ }
343
363
  async closeControlServer() {
344
364
  const server = this.controlServer;
345
365
  this.controlServer = null;