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.
@@ -21,6 +21,8 @@ import { getPendingChannelMessagesFile, getPendingWechatMessagesFile, PendingWec
21
21
  import { createRuntimeHost, } from "../runtime/create-runtime-host.js";
22
22
  import { toChannelInboundMessage } from "../channels/wechat/channel-message.js";
23
23
  import { routeBridgeMessage } from "../core/bridge-message-router.js";
24
+ import { clearTurn, InboundConversationContext, resolveConversationTarget, rollbackTurn, tryBeginTurn, } from "../core/conversation-routing.js";
25
+ import { handleAdapterControl, invalidateModelSnapshot } from "../bridge/adapter-control.js";
24
26
  import { forwardBridgeEvent } from "../core/bridge-event-forwarder.js";
25
27
  import { isDirectModuleRun } from "../core/direct-run.js";
26
28
  import { WechatChannelPort } from "../channels/wechat/wechat-channel-port.js";
@@ -217,8 +219,13 @@ export function parseDaemonSwitchDirective(text) {
217
219
  remainder: match[2]?.trim() ?? "",
218
220
  };
219
221
  }
220
- export function defaultDaemonSessionStartMode(adapter) {
221
- return adapter === "codex" ? "restore" : "new";
222
+ export function defaultDaemonSessionStartMode(_adapter) {
223
+ // Codex 0.155+ persists app-server threads lazily, so resuming a freshly
224
+ // prepared (not yet persisted) thread crashes the visible TUI bootstrap
225
+ // with "no rollout found". Every adapter therefore opens a fresh session
226
+ // by default; explicit --session-start-mode restore and WeChat /resume
227
+ // remain available for deliberate restores of persisted threads.
228
+ return "new";
222
229
  }
223
230
  export function resolveDaemonSessionStartMode(params) {
224
231
  // An explicit --session-start-mode wins over visible-client reuse: the user
@@ -231,9 +238,6 @@ export function resolveDaemonSessionStartMode(params) {
231
238
  if (params.reuseExistingVisible && params.visibleConnected) {
232
239
  return "restore";
233
240
  }
234
- if (params.adapter === "codex") {
235
- return "restore";
236
- }
237
241
  if (params.slotCreated) {
238
242
  return "new";
239
243
  }
@@ -360,9 +364,9 @@ function openVisibleClient(params) {
360
364
  const fullArgs = [process.execPath, ...args];
361
365
  if (process.platform === "darwin") {
362
366
  const cmdLine = fullArgs.map(shellQuotePosix).join(" ");
363
- const script = `tell application "Terminal"
364
- activate
365
- do script "cd ${shellQuotePosix(params.cwd)} && exec ${cmdLine}"
367
+ const script = `tell application "Terminal"
368
+ activate
369
+ do script "cd ${shellQuotePosix(params.cwd)} && exec ${cmdLine}"
366
370
  end tell`;
367
371
  const child = spawn("osascript", ["-e", script], {
368
372
  cwd: params.cwd,
@@ -555,8 +559,8 @@ class WechatDaemon {
555
559
  transport;
556
560
  channelId;
557
561
  wecomTransport;
558
- currentInboundConversation = null;
559
- handlingInboundMessage = false;
562
+ inboundConversationContext = new InboundConversationContext();
563
+ fallbackConversation;
560
564
  slots = new Map();
561
565
  // Per-adapter serialization chains for ensureSlot (see ensureSlot comment).
562
566
  slotEnsureChains = new Map();
@@ -579,14 +583,12 @@ class WechatDaemon {
579
583
  this.transport = params.transport;
580
584
  this.channelId = params.channelId ?? "wechat";
581
585
  this.wecomTransport = params.wecomTransport ?? null;
582
- if (this.channelId === "wecom") {
583
- this.currentInboundConversation = {
584
- channelId: "wecom",
585
- conversationId: params.authorizedUserId,
586
- recipientId: params.authorizedUserId,
587
- metadata: { chatType: "direct" },
588
- };
589
- }
586
+ this.fallbackConversation = {
587
+ channelId: this.channelId,
588
+ conversationId: params.authorizedUserId,
589
+ recipientId: params.authorizedUserId,
590
+ metadata: this.channelId === "wecom" ? { chatType: "direct" } : undefined,
591
+ };
590
592
  this.pendingWechatMessages = new PendingWechatMessageStore(this.channelId === "wechat"
591
593
  ? getPendingWechatMessagesFile(this.cwd)
592
594
  : getPendingChannelMessagesFile(this.cwd, this.channelId));
@@ -681,17 +683,12 @@ class WechatDaemon {
681
683
  if (this.shutdownPromise) {
682
684
  return;
683
685
  }
684
- this.currentInboundConversation = channelMessage.conversation;
685
- this.handlingInboundMessage = true;
686
- try {
686
+ await this.inboundConversationContext.run(channelMessage.conversation, async () => {
687
687
  if (this.pendingWechatMessages.list().length > 0) {
688
688
  await this.flushPendingWechatMessages();
689
689
  }
690
690
  await this.handleInboundMessage(toLegacyInboundWechatMessage(channelMessage));
691
- }
692
- finally {
693
- this.handlingInboundMessage = false;
694
- }
691
+ });
695
692
  },
696
693
  onUnauthorized: async (senderId, chatType) => {
697
694
  appendDaemonLog(`wecom_unauthorized: sender=${senderId} chat_type=${chatType}`);
@@ -828,6 +825,23 @@ class WechatDaemon {
828
825
  }
829
826
  async cleanup() {
830
827
  appendDaemonLog("shutdown_started");
828
+ // Stop accepting new channel work and abort an in-flight long poll before
829
+ // waiting for queued sends or adapter disposal. Otherwise Ctrl+C can leave
830
+ // the process alive until the 35-second poll timeout and retain the daemon
831
+ // endpoint for the next startup to clean up.
832
+ try {
833
+ this.transport.stop();
834
+ }
835
+ catch {
836
+ // Best effort shutdown.
837
+ }
838
+ try {
839
+ this.wecomTransport?.stop();
840
+ }
841
+ catch {
842
+ // Best effort shutdown.
843
+ }
844
+ clearDaemonEndpoint();
831
845
  for (const slot of this.slots.values()) {
832
846
  try {
833
847
  await slot.outputBatcher.flushNow();
@@ -839,12 +853,6 @@ class WechatDaemon {
839
853
  await this.waitForPendingWechatForwardTasks();
840
854
  await this.textSendChain.catch(() => undefined);
841
855
  await this.attachmentSendChain.catch(() => undefined);
842
- try {
843
- this.wecomTransport?.stop();
844
- }
845
- catch {
846
- // Best effort shutdown.
847
- }
848
856
  for (const slot of this.slots.values()) {
849
857
  try {
850
858
  await slot.runtime.dispose();
@@ -966,6 +974,9 @@ class WechatDaemon {
966
974
  const launch = openVisibleClient({
967
975
  adapter,
968
976
  cwd: this.cwd,
977
+ // A freshly prepared blank thread is semantically a new session, but
978
+ // the visible client must resume its id from the endpoint instead of
979
+ // creating a second thread of its own.
969
980
  sessionStartMode,
970
981
  cliArgs: options.cliArgs,
971
982
  channelId: this.channelId,
@@ -1009,6 +1020,9 @@ class WechatDaemon {
1009
1020
  }
1010
1021
  const activated = options.openVisible === false || (visibleConnected && visibleReady);
1011
1022
  if (activated) {
1023
+ const previousSlot = this.getActiveSlot();
1024
+ if (previousSlot && this.activeAdapter !== adapter)
1025
+ invalidateModelSnapshot(previousSlot.runtime);
1012
1026
  this.activeAdapter = adapter;
1013
1027
  }
1014
1028
  appendDaemonLog(`switch_adapter: adapter=${adapter} created=${created} opened_visible=${openedVisible} visible_connected=${visibleConnected} visible_ready=${visibleReady} activated=${activated} previous_active=${previousActiveAdapter ?? "(none)"} session_start_mode=${sessionStartMode}`);
@@ -1039,7 +1053,7 @@ class WechatDaemon {
1039
1053
  runtime,
1040
1054
  controller,
1041
1055
  outputBatcher: new OutputBatcher(async (text) => {
1042
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(adapter, text));
1056
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(adapter, text), "message", this.resolveSlotOutputTarget(slot));
1043
1057
  }),
1044
1058
  pendingConfirmations: [],
1045
1059
  pendingUserInput: null,
@@ -1065,7 +1079,7 @@ class WechatDaemon {
1065
1079
  slot.outputBatcher.clear();
1066
1080
  slot.pendingConfirmations = [];
1067
1081
  slot.pendingUserInput = null;
1068
- slot.activeTask = null;
1082
+ clearTurn(slot);
1069
1083
  if (slot.adapter === "codex" || slot.adapter === "claude") {
1070
1084
  await slot.runtime.reset();
1071
1085
  }
@@ -1107,6 +1121,10 @@ class WechatDaemon {
1107
1121
  slot.controller.syncLocalClientEndpoint();
1108
1122
  const adapterState = slot.runtime.getState();
1109
1123
  const channelPort = this.createWechatChannelPort(slot.adapter);
1124
+ const eventTarget = this.channelId === "wecom"
1125
+ ? this.resolveSlotOutputTarget(slot)
1126
+ : this.fallbackConversation;
1127
+ const eventTask = slot.activeTask;
1110
1128
  if (slot.pendingConfirmations.length > 0 && !adapterState.pendingApproval) {
1111
1129
  slot.pendingConfirmations = [];
1112
1130
  }
@@ -1132,15 +1150,7 @@ class WechatDaemon {
1132
1150
  appendDaemonLog(`final_reply: adapter=${slot.adapter} text=${truncatePreview(next.text)}`);
1133
1151
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1134
1152
  await channelPort.send({
1135
- target: this.channelId === "wecom"
1136
- ? slot.activeConversation ??
1137
- slot.lastConversation ??
1138
- this.currentInboundConversation
1139
- : {
1140
- channelId: "wechat",
1141
- conversationId: this.authorizedUserId,
1142
- recipientId: this.authorizedUserId,
1143
- },
1153
+ target: eventTarget,
1144
1154
  kind: "final_reply",
1145
1155
  text: next.text,
1146
1156
  adapter: slot.adapter,
@@ -1158,7 +1168,7 @@ class WechatDaemon {
1158
1168
  appendDaemonLog(`${slot.adapter}_${next.level}_notice: ${truncatePreview(next.text)}`);
1159
1169
  if (shouldForwardBridgeEventToWechat(slot.adapter, next.type, { text: next.text })) {
1160
1170
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1161
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, next.text), "notice");
1171
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, next.text), "notice", eventTarget);
1162
1172
  }));
1163
1173
  }
1164
1174
  },
@@ -1167,7 +1177,7 @@ class WechatDaemon {
1167
1177
  const pending = toPendingApproval(next);
1168
1178
  slot.pendingConfirmations.push(pending);
1169
1179
  appendDaemonLog(`approval_required: adapter=${slot.adapter} source=${pending.source} command=${truncatePreview(pending.commandPreview)}`);
1170
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatApprovalMessage(pending, adapterState)), "approval_required");
1180
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatApprovalMessage(pending, adapterState)), "approval_required", eventTarget);
1171
1181
  }));
1172
1182
  },
1173
1183
  userInputRequired: (next) => {
@@ -1175,14 +1185,14 @@ class WechatDaemon {
1175
1185
  slot.pendingUserInput = pending;
1176
1186
  appendDaemonLog(`user_input_required: adapter=${slot.adapter} questions=${pending.questions.length}`);
1177
1187
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1178
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserInputRequestMessage(pending, adapterState)), "user_input_required");
1188
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserInputRequestMessage(pending, adapterState)), "user_input_required", eventTarget);
1179
1189
  }));
1180
1190
  },
1181
1191
  mirroredUserInput: (next) => {
1182
1192
  appendDaemonLog(`mirrored_local_input: adapter=${slot.adapter} text=${truncatePreview(next.text)}`);
1183
1193
  if (shouldForwardBridgeEventToWechat(slot.adapter, next.type, { text: next.text })) {
1184
1194
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1185
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatMirroredUserInputMessage(slot.adapter, next.text)), "mirrored_user_input");
1195
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatMirroredUserInputMessage(slot.adapter, next.text)), "mirrored_user_input", eventTarget);
1186
1196
  }));
1187
1197
  }
1188
1198
  },
@@ -1192,7 +1202,7 @@ class WechatDaemon {
1192
1202
  appendDaemonLog(`session_switched: adapter=${slot.adapter} session=${next.sessionId} source=${next.source} reason=${next.reason}`);
1193
1203
  if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(slot.adapter, next.type)) {
1194
1204
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1195
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({ adapter: slot.adapter, sessionId: next.sessionId, source: next.source, reason: next.reason })), "session_switched");
1205
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({ adapter: slot.adapter, sessionId: next.sessionId, source: next.source, reason: next.reason })), "session_switched", eventTarget);
1196
1206
  }));
1197
1207
  }
1198
1208
  },
@@ -1210,7 +1220,7 @@ class WechatDaemon {
1210
1220
  }
1211
1221
  if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(slot.adapter, next.type)) {
1212
1222
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1213
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({ adapter: slot.adapter, sessionId: next.threadId, source: next.source, reason: next.reason })), "thread_switched");
1223
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({ adapter: slot.adapter, sessionId: next.threadId, source: next.source, reason: next.reason })), "thread_switched", eventTarget);
1214
1224
  }));
1215
1225
  }
1216
1226
  },
@@ -1218,17 +1228,19 @@ class WechatDaemon {
1218
1228
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(() => {
1219
1229
  slot.pendingConfirmations = [];
1220
1230
  slot.pendingUserInput = null;
1221
- slot.activeTask = null;
1222
- slot.activeConversation = undefined;
1231
+ if (eventTask) {
1232
+ clearTurn(slot, eventTask);
1233
+ }
1223
1234
  }));
1224
1235
  },
1225
1236
  taskFailed: (next) => {
1226
1237
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1227
1238
  slot.pendingConfirmations = [];
1228
1239
  slot.pendingUserInput = null;
1229
- slot.activeTask = null;
1230
- slot.activeConversation = undefined;
1231
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatTaskFailedMessage(slot.adapter, next.message)), "task_failed");
1240
+ if (eventTask) {
1241
+ clearTurn(slot, eventTask);
1242
+ }
1243
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatTaskFailedMessage(slot.adapter, next.message)), "task_failed", eventTarget);
1232
1244
  }));
1233
1245
  },
1234
1246
  fatalError: (next) => {
@@ -1236,11 +1248,12 @@ class WechatDaemon {
1236
1248
  appendDaemonLog(`fatal_error: adapter=${slot.adapter} message=${next.message}`);
1237
1249
  slot.pendingConfirmations = [];
1238
1250
  slot.pendingUserInput = null;
1239
- slot.activeTask = null;
1240
- slot.activeConversation = undefined;
1251
+ if (eventTask) {
1252
+ clearTurn(slot, eventTask);
1253
+ }
1241
1254
  this.disposeDeadSlot(slot);
1242
1255
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1243
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserFacingBridgeFatalError(next.message)), "fatal_error");
1256
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserFacingBridgeFatalError(next.message)), "fatal_error", eventTarget);
1244
1257
  }));
1245
1258
  },
1246
1259
  shutdownRequested: (next) => {
@@ -1252,11 +1265,19 @@ class WechatDaemon {
1252
1265
  appendDaemonLog(`event_forward_failed: adapter=${slot.adapter} message=${message}`);
1253
1266
  });
1254
1267
  }
1268
+ resolveSlotOutputTarget(slot) {
1269
+ return resolveConversationTarget({
1270
+ active: slot.activeConversation,
1271
+ last: slot.lastConversation,
1272
+ fallback: this.fallbackConversation,
1273
+ });
1274
+ }
1255
1275
  bindCurrentWecomConversation(slot) {
1256
- if (this.channelId !== "wecom" || !slot || !this.currentInboundConversation) {
1276
+ const conversation = this.inboundConversationContext.get();
1277
+ if (this.channelId !== "wecom" || !slot || !conversation) {
1257
1278
  return;
1258
1279
  }
1259
- slot.lastConversation = this.currentInboundConversation;
1280
+ slot.lastConversation = conversation;
1260
1281
  }
1261
1282
  async handleInboundMessage(message) {
1262
1283
  if (message.senderId !== this.authorizedUserId) {
@@ -1363,11 +1384,12 @@ class WechatDaemon {
1363
1384
  },
1364
1385
  });
1365
1386
  if (this.channelId === "wecom" &&
1366
- this.currentInboundConversation &&
1387
+ this.inboundConversationContext.get() &&
1367
1388
  currentSlot.activeTask &&
1368
1389
  currentSlot.activeTask !== previousTask) {
1369
- currentSlot.activeConversation = this.currentInboundConversation;
1370
- currentSlot.lastConversation = this.currentInboundConversation;
1390
+ const conversation = this.inboundConversationContext.get();
1391
+ currentSlot.activeConversation = conversation;
1392
+ currentSlot.lastConversation = conversation;
1371
1393
  }
1372
1394
  };
1373
1395
  const command = parseWechatControlCommand(message.text, {
@@ -1417,6 +1439,10 @@ class WechatDaemon {
1417
1439
  }
1418
1440
  async handleSystemCommand(message, activeSlot, command) {
1419
1441
  switch (command.type) {
1442
+ case "model":
1443
+ case "plan":
1444
+ await this.queueWechatMessage(message.senderId, await handleAdapterControl(activeSlot.runtime, message.senderId, command));
1445
+ return;
1420
1446
  case "status":
1421
1447
  await this.queueWechatMessage(message.senderId, formatDaemonStatus(this.getStatus()));
1422
1448
  return;
@@ -1440,9 +1466,10 @@ class WechatDaemon {
1440
1466
  if (command.target) {
1441
1467
  await resumeSlot.outputBatcher.flushNow();
1442
1468
  }
1469
+ const taskBeforeResume = resumeSlot.activeTask;
1443
1470
  const result = await resumeSlot.resumeCoordinator.execute(command.target);
1444
- if (result.kind === "resumed") {
1445
- resumeSlot.activeTask = null;
1471
+ if (result.kind === "resumed" && taskBeforeResume) {
1472
+ clearTurn(resumeSlot, taskBeforeResume);
1446
1473
  }
1447
1474
  await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(resumeSlot.adapter, result.message));
1448
1475
  }
@@ -1581,14 +1608,28 @@ class WechatDaemon {
1581
1608
  }
1582
1609
  async dispatchInboundWechatText(message, slot) {
1583
1610
  const preview = formatInboundMessagePreview(message);
1584
- slot.activeTask = {
1611
+ const nextTask = {
1585
1612
  startedAt: Date.now(),
1586
1613
  inputPreview: truncatePreview(preview, 180),
1587
1614
  };
1615
+ const inboundConversation = this.channelId === "wecom"
1616
+ ? this.inboundConversationContext.get()
1617
+ : undefined;
1618
+ const lease = tryBeginTurn(slot, nextTask, inboundConversation);
1619
+ if (!lease) {
1620
+ await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, `${slot.adapter} is still working. Wait for the current reply or use /stop.`));
1621
+ return;
1622
+ }
1588
1623
  appendDaemonLog(`forwarded_input: adapter=${slot.adapter} text=${truncatePreview(preview)}`);
1589
- await slot.runtime.sendInput(this.channelId === "wecom"
1590
- ? buildWecomInboundPrompt(message.text, message.attachments)
1591
- : buildWechatInboundPrompt(message.text, message.attachments));
1624
+ try {
1625
+ await slot.runtime.sendInput(this.channelId === "wecom"
1626
+ ? buildWecomInboundPrompt(message.text, message.attachments)
1627
+ : buildWechatInboundPrompt(message.text, message.attachments));
1628
+ }
1629
+ catch (error) {
1630
+ rollbackTurn(slot, lease);
1631
+ throw error;
1632
+ }
1592
1633
  }
1593
1634
  queueWechatTextAction(action) {
1594
1635
  const run = this.textSendChain.then(action);
@@ -1605,12 +1646,8 @@ class WechatDaemon {
1605
1646
  const activeSlot = this.getActiveSlot();
1606
1647
  const target = targetOverride ??
1607
1648
  (senderId === this.authorizedUserId
1608
- ? (this.handlingInboundMessage
1609
- ? this.currentInboundConversation
1610
- : null) ??
1611
- activeSlot?.activeConversation ??
1612
- activeSlot?.lastConversation ??
1613
- this.currentInboundConversation
1649
+ ? this.inboundConversationContext.get() ??
1650
+ (activeSlot ? this.resolveSlotOutputTarget(activeSlot) : this.fallbackConversation)
1614
1651
  : {
1615
1652
  channelId: "wecom",
1616
1653
  conversationId: senderId,
@@ -1678,8 +1715,14 @@ class WechatDaemon {
1678
1715
  return { status: "failed" };
1679
1716
  }
1680
1717
  queueWechatMessage(senderId, text, context = "message", targetOverride) {
1718
+ const activeSlot = this.getActiveSlot();
1719
+ const queuedTarget = this.channelId === "wecom" && senderId === this.authorizedUserId
1720
+ ? targetOverride ??
1721
+ this.inboundConversationContext.get() ??
1722
+ (activeSlot ? this.resolveSlotOutputTarget(activeSlot) : this.fallbackConversation)
1723
+ : targetOverride;
1681
1724
  return this.queueWechatTextAction(async () => {
1682
- const result = await this.sendWechatMessageNow(senderId, text, context, targetOverride);
1725
+ const result = await this.sendWechatMessageNow(senderId, text, context, queuedTarget);
1683
1726
  if (result.status === "context_unavailable") {
1684
1727
  const pending = this.pendingWechatMessages.enqueue(senderId, text, context, result.target);
1685
1728
  if (pending) {
@@ -2066,6 +2109,9 @@ export async function runDaemon(options) {
2066
2109
  }
2067
2110
  shutdownInProgress = true;
2068
2111
  log(`Received ${signal}. Stopping daemon.`);
2112
+ // Remove this process's endpoint synchronously so a new terminal can be
2113
+ // started immediately even if asynchronous adapter cleanup takes time.
2114
+ clearDaemonEndpoint();
2069
2115
  void daemon.shutdown().finally(() => process.exit(0));
2070
2116
  };
2071
2117
  process.on("SIGINT", () => handleSignal("SIGINT"));
@@ -11,7 +11,7 @@ export const messages = {
11
11
  "bridge.fatalError": "Bridge error: {message}",
12
12
  "bridge.backlogIgnored": "Ignored {count} backlog message(s) sent before the bridge started (they will not be processed).\nIf you actually sent them after the bridge started, your system clock is likely skewed by more than {graceSeconds}s — sync the clock and restart the bridge.",
13
13
  // === Daemon welcome & control ===
14
- "daemon.welcome": "WeChat Daemon ready.\nCWD: {cwd}\nActive: {adapter}\n\nCommands: /claude, /codex, /opencode, /pi, /stop, /confirm, /deny, /status\n{bindings}\n\nManage: /bind [emoji] cmd, /unbind [emoji], /bindings",
14
+ "daemon.welcome": "WeChat Daemon ready.\nCWD: {cwd}\nActive: {adapter}\n\nCommands: /claude, /codex, /opencode, /pi, /stop, /confirm, /deny, /status, /model, /plan\n{bindings}\n\nManage: /bind [emoji] cmd, /unbind [emoji], /bindings",
15
15
  "daemon.noActiveAdapter": "No active terminal is selected. Send /codex, /claude, /opencode, or /pi to start one.",
16
16
  "daemon.switchResult.new": "Started a new visible CLI.",
17
17
  "daemon.switchResult.reused": "Reused the existing visible CLI.",
@@ -11,7 +11,7 @@ export const messages = {
11
11
  "bridge.fatalError": "桥接错误: {message}",
12
12
  "bridge.backlogIgnored": "已忽略 {count} 条桥接启动前的积压消息(不会自动处理)。\n如果你是在桥接启动后才发送的消息却看到此提示,说明本机系统时钟可能不准(偏差超过 {graceSeconds} 秒),请校准时间后重启桥接。",
13
13
  // === Daemon 欢迎 & 控制 ===
14
- "daemon.welcome": "微信 Daemon 就绪。\n工作目录: {cwd}\n当前: {adapter}\n\n命令: /claude, /codex, /opencode, /pi, /stop, /confirm, /deny, /status\n{bindings}\n\n管理: /bind [表情] 命令, /unbind [表情], /bindings",
14
+ "daemon.welcome": "微信 Daemon 就绪。\n工作目录: {cwd}\n当前: {adapter}\n\n命令: /claude, /codex, /opencode, /pi, /stop, /confirm, /deny, /status, /model, /plan\n{bindings}\n\n管理: /bind [表情] 命令, /unbind [表情], /bindings",
15
15
  "daemon.noActiveAdapter": "未选择活跃终端。发送 /codex、/claude、/opencode 或 /pi 启动一个。",
16
16
  "daemon.switchResult.new": "已启动新的可见 CLI。",
17
17
  "daemon.switchResult.reused": "已复用现有的可见 CLI。",
@@ -25,6 +25,21 @@ export class LegacyAdapterRuntime {
25
25
  }
26
26
  await this.adapter.createSession();
27
27
  }
28
+ async listModels() {
29
+ if (!this.adapter.listModels)
30
+ throw new Error("Model listing is not supported by this companion.");
31
+ return this.adapter.listModels();
32
+ }
33
+ async selectModel(modelId) {
34
+ if (!this.adapter.selectModel)
35
+ throw new Error("Model selection is not supported by this companion.");
36
+ return this.adapter.selectModel(modelId);
37
+ }
38
+ async setPlanMode(enabled) {
39
+ if (!this.adapter.setPlanMode)
40
+ throw new Error("Plan mode is not supported by this companion.");
41
+ return this.adapter.setPlanMode(enabled);
42
+ }
28
43
  async interrupt() {
29
44
  return await this.adapter.interrupt();
30
45
  }
@@ -3,3 +3,6 @@ export const CODEX_REMOTE_AUTH_TOKEN_ENV = "WECHAT_BRIDGE_CODEX_REMOTE_AUTH_TOKE
3
3
  export function hasLocalClientEndpointProvider(runtime) {
4
4
  return typeof runtime.getLocalClientEndpoint === "function";
5
5
  }
6
+ export function hasVisibleClientSessionPreparer(runtime) {
7
+ return typeof runtime.prepareVisibleClientSession === "function";
8
+ }
@@ -179,16 +179,16 @@ export async function checkForUpdate(forceCheck = false) {
179
179
  */
180
180
  export function formatUpdateMessage(versionInfo) {
181
181
  const { current, latest } = versionInfo;
182
- return `
183
- [Update Available] Version ${latest} is available (current: ${current})
184
-
185
- Update instructions:
186
- cd CLI-WeChat-Bridge
187
- git pull
188
- bun install
189
- npm install -g .
190
-
191
- For more information:
192
- https://github.com/UNLINEARITY/CLI-WeChat-Bridge/releases
182
+ return `
183
+ [Update Available] Version ${latest} is available (current: ${current})
184
+
185
+ Update instructions:
186
+ cd CLI-WeChat-Bridge
187
+ git pull
188
+ bun install
189
+ npm install -g .
190
+
191
+ For more information:
192
+ https://github.com/UNLINEARITY/CLI-WeChat-Bridge/releases
193
193
  `;
194
194
  }