@unlinearity/cli-wechat-bridge 1.0.4 → 1.0.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.
@@ -7,7 +7,7 @@ import { AbstractPtyAdapter } from "./bridge-adapters.core.js";
7
7
  import * as shared from "./bridge-adapters.shared.js";
8
8
  import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
9
9
  import { CODEX_REMOTE_AUTH_TOKEN_ENV, LOCAL_CLIENT_PROTOCOL_VERSION, } from "../runtime/runtime-types.js";
10
- const { CODEX_APP_SERVER_HOST, CODEX_APP_SERVER_LOG_LIMIT, CODEX_APP_SERVER_READY_TIMEOUT_MS, CODEX_FINAL_REPLY_SETTLE_DELAY_MS, CODEX_RECENT_SESSION_KEY_LIMIT, CODEX_RPC_CONNECT_RETRY_MS, CODEX_RPC_RECONNECT_TIMEOUT_MS, CODEX_SESSION_FALLBACK_SCAN_INTERVAL_MS, CODEX_SESSION_LOCAL_MIRROR_FALLBACK_WINDOW_MS, CODEX_SESSION_POLL_INTERVAL_MS, CODEX_STARTUP_WARMUP_MS, CODEX_THREAD_SIGNAL_TTL_MS, INTERRUPT_SETTLE_DELAY_MS, appendBoundedLog, buildCliEnvironment, buildCodexApprovalRequest, buildCodexCliArgs, buildCodexUserInputRequest, coerceWebSocketMessageData, delay, describeUnknownError, extractCodexFinalTextFromItem, extractCodexThreadFollowIdFromStatusChanged, extractCodexThreadStartedThreadId, extractCodexUserMessageText, findCodexSessionFile, findRecentCodexSessionFileForCwd, getCodexRpcRequestId, getNotificationThreadId, getNotificationTurnId, isRecord, isRecentIsoTimestamp, listCodexResumeSessions, normalizeComparablePath, normalizeCodexRpcError, reserveLocalPort, resolveSpawnTarget, shouldAutoCompleteCodexWechatTurnAfterFinalReply, shouldIgnoreCodexSessionReplayEntry, shouldRecoverCodexStaleBusyState, waitForTcpPort, } = shared;
10
+ const { CODEX_APP_SERVER_HOST, CODEX_APP_SERVER_LOG_LIMIT, CODEX_APP_SERVER_READY_TIMEOUT_MS, CODEX_FINAL_REPLY_SETTLE_DELAY_MS, CODEX_RECENT_SESSION_KEY_LIMIT, CODEX_RPC_CONNECT_RETRY_MS, CODEX_RPC_RECONNECT_TIMEOUT_MS, CODEX_SESSION_FALLBACK_SCAN_INTERVAL_MS, CODEX_SESSION_LOCAL_MIRROR_FALLBACK_WINDOW_MS, CODEX_SESSION_POLL_INTERVAL_MS, CODEX_STARTUP_WARMUP_MS, CODEX_THREAD_SIGNAL_TTL_MS, INTERRUPT_SETTLE_DELAY_MS, appendBoundedLog, buildCliEnvironment, buildCodexApprovalRequest, buildCodexCliArgs, buildCodexDynamicToolCallFailureResponse, buildCodexMcpServerElicitationDeclineResponse, buildCodexPermissionsRequestApprovalResponse, buildCodexUserInputRequest, coerceWebSocketMessageData, delay, describeUnknownError, extractCodexFinalTextFromItem, extractCodexThreadFollowIdFromStatusChanged, extractCodexThreadStartedThreadId, extractCodexUserMessageText, findCodexSessionFile, findRecentCodexSessionFileForCwd, getCodexRpcRequestId, getCodexApprovalAutoResponse, getCodexWechatOutboundAttachmentDenyMessage, getNotificationThreadId, getNotificationTurnId, isRecord, isRecentIsoTimestamp, listCodexResumeSessions, normalizeComparablePath, normalizeCodexRpcError, reserveLocalPort, resolveSpawnTarget, shouldAutoCompleteCodexWechatTurnAfterFinalReply, shouldIgnoreCodexSessionReplayEntry, shouldRecoverCodexStaleBusyState, waitForTcpPort, } = shared;
11
11
  const CODEX_LOCAL_THREAD_ANNOUNCE_SETTLE_MS = 150;
12
12
  export class CodexPtyAdapter extends AbstractPtyAdapter {
13
13
  runtimeKind = "codex_runtime_host";
@@ -24,6 +24,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
24
24
  cleanPanelExitInProgress = false;
25
25
  rpcRequestCounter = 0;
26
26
  pendingRpcRequests = new Map();
27
+ subscribedThreadIds = new Set();
27
28
  sharedThreadId = null;
28
29
  announcedThreadId = null;
29
30
  pendingThreadAnnouncement = null;
@@ -194,6 +195,27 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
194
195
  }
195
196
  return await super.resolveApproval(action);
196
197
  }
198
+ async submitUserInput(answers) {
199
+ if (!this.pendingUserInputRequest) {
200
+ return false;
201
+ }
202
+ const request = this.pendingUserInputRequest;
203
+ const responseAnswers = {};
204
+ for (const [questionId, values] of Object.entries(answers)) {
205
+ responseAnswers[questionId] = {
206
+ answers: values,
207
+ };
208
+ }
209
+ this.sendRpcMessage({
210
+ id: request.requestId,
211
+ result: {
212
+ answers: responseAnswers,
213
+ },
214
+ });
215
+ this.clearPendingUserInputState();
216
+ this.setStatus("busy", "Codex user input submitted.");
217
+ return true;
218
+ }
197
219
  async dispose() {
198
220
  this.resetTurnTracking({ preserveThread: false });
199
221
  if (this.isEmbeddedCliMode()) {
@@ -752,6 +774,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
752
774
  if (this.pendingApproval) {
753
775
  throw new Error("A Codex approval request is pending. Reply with /confirm <code> or /deny.");
754
776
  }
777
+ if (this.pendingUserInputRequest || this.state.pendingUserInput) {
778
+ throw new Error("Codex is waiting for user input. Reply with /answer and your response, or use /stop.");
779
+ }
755
780
  if (this.pendingTurnStart || this.activeTurn || this.state.status === "busy") {
756
781
  const origin = this.state.activeTurnOrigin;
757
782
  if (origin === "local") {
@@ -766,6 +791,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
766
791
  this.rememberInjectedInput(text);
767
792
  this.clearPendingApprovalState();
768
793
  const threadId = await this.ensureThreadStarted();
794
+ const subscribedBeforeTurnStart = await this.tryEnsureSharedThreadSubscribed(threadId);
769
795
  this.pendingTurnStart = true;
770
796
  this.pendingTurnThreadId = threadId;
771
797
  this.interruptPendingTurnStart = false;
@@ -793,6 +819,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
793
819
  turnId,
794
820
  origin: "wechat",
795
821
  });
822
+ if (!subscribedBeforeTurnStart) {
823
+ await this.tryEnsureSharedThreadSubscribed(threadId);
824
+ }
796
825
  if (this.interruptPendingTurnStart) {
797
826
  await this.requestActiveTurnInterrupt();
798
827
  this.armInterruptFallback();
@@ -813,11 +842,15 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
813
842
  if (this.isNativePanelMode() && !this.nativeProcess) {
814
843
  return false;
815
844
  }
816
- const turnPending = this.pendingTurnStart || this.state.status === "busy" || this.state.status === "awaiting_approval";
845
+ const turnPending = this.pendingTurnStart ||
846
+ this.state.status === "busy" ||
847
+ this.state.status === "awaiting_approval" ||
848
+ this.state.status === "awaiting_input";
817
849
  if (!turnPending) {
818
850
  return false;
819
851
  }
820
852
  this.clearPendingApprovalState();
853
+ this.clearPendingUserInputState();
821
854
  if (this.pendingTurnStart && !this.activeTurn) {
822
855
  this.interruptPendingTurnStart = true;
823
856
  this.armInterruptFallback();
@@ -980,6 +1013,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
980
1013
  attachRpcSocket(socket) {
981
1014
  this.rpcSocket = socket;
982
1015
  this.rpcShuttingDown = false;
1016
+ this.subscribedThreadIds.clear();
983
1017
  socket.addEventListener("message", (event) => {
984
1018
  this.handleRpcMessageData(event.data);
985
1019
  });
@@ -991,6 +1025,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
991
1025
  const socket = this.rpcSocket;
992
1026
  this.rpcSocket = null;
993
1027
  this.rpcShuttingDown = true;
1028
+ this.subscribedThreadIds.clear();
994
1029
  this.rejectPendingRpcRequests("Codex websocket connection closed.");
995
1030
  if (!socket) {
996
1031
  this.rpcShuttingDown = false;
@@ -1028,6 +1063,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1028
1063
  cleanPanelExitInProgress: this.cleanPanelExitInProgress,
1029
1064
  });
1030
1065
  this.rpcSocket = null;
1066
+ this.subscribedThreadIds.clear();
1031
1067
  this.rejectPendingRpcRequests("Codex websocket connection closed.");
1032
1068
  this.rpcShuttingDown = false;
1033
1069
  if (expectedShutdown) {
@@ -1156,9 +1192,44 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1156
1192
  throw new Error("Codex did not return a thread id for the bridge session.");
1157
1193
  }
1158
1194
  this.rememberBridgeOwnedThreadSignal(threadId);
1195
+ this.subscribedThreadIds.add(threadId);
1159
1196
  this.updateSharedThread(threadId);
1160
1197
  return threadId;
1161
1198
  }
1199
+ async tryEnsureSharedThreadSubscribed(threadId) {
1200
+ if (this.subscribedThreadIds.has(threadId)) {
1201
+ return true;
1202
+ }
1203
+ try {
1204
+ await this.ensureSharedThreadSubscribed(threadId);
1205
+ return true;
1206
+ }
1207
+ catch {
1208
+ return false;
1209
+ }
1210
+ }
1211
+ async ensureSharedThreadSubscribed(threadId) {
1212
+ if (this.subscribedThreadIds.has(threadId)) {
1213
+ return;
1214
+ }
1215
+ const response = await this.sendRpcRequest("thread/resume", {
1216
+ threadId,
1217
+ cwd: this.options.cwd,
1218
+ approvalPolicy: "on-request",
1219
+ approvalsReviewer: "user",
1220
+ sandbox: "workspace-write",
1221
+ excludeTurns: true,
1222
+ });
1223
+ const resumedThreadId = this.extractThreadIdFromResponse(response);
1224
+ if (!resumedThreadId) {
1225
+ throw new Error("Codex did not return a thread id while subscribing the bridge client.");
1226
+ }
1227
+ this.rememberBridgeOwnedThreadSignal(resumedThreadId);
1228
+ this.subscribedThreadIds.add(resumedThreadId);
1229
+ if (resumedThreadId !== this.sharedThreadId) {
1230
+ this.updateSharedThread(resumedThreadId);
1231
+ }
1232
+ }
1162
1233
  async resumeSharedThread(threadId, options = {}) {
1163
1234
  const trimmedThreadId = threadId.trim();
1164
1235
  if (!trimmedThreadId) {
@@ -1180,12 +1251,14 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1180
1251
  approvalPolicy: "on-request",
1181
1252
  approvalsReviewer: "user",
1182
1253
  sandbox: "workspace-write",
1254
+ excludeTurns: true,
1183
1255
  });
1184
1256
  const resumedThreadId = this.extractThreadIdFromResponse(response);
1185
1257
  if (!resumedThreadId) {
1186
1258
  throw new Error("Codex did not return a thread id while resuming the saved thread.");
1187
1259
  }
1188
1260
  this.rememberBridgeOwnedThreadSignal(resumedThreadId);
1261
+ this.subscribedThreadIds.add(resumedThreadId);
1189
1262
  this.sessionFilePath = null;
1190
1263
  this.sessionReadOffset = 0;
1191
1264
  this.sessionPartialLine = "";
@@ -1238,7 +1311,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1238
1311
  this.clearInterruptTimer();
1239
1312
  this.interruptTimer = setTimeout(() => {
1240
1313
  this.interruptTimer = null;
1241
- if (this.state.status !== "busy" && this.state.status !== "awaiting_approval") {
1314
+ if (this.state.status !== "busy" &&
1315
+ this.state.status !== "awaiting_approval" &&
1316
+ this.state.status !== "awaiting_input") {
1242
1317
  return;
1243
1318
  }
1244
1319
  this.resetTurnTracking({ preserveThread: true });
@@ -1263,6 +1338,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1263
1338
  pendingTurnStart: this.pendingTurnStart,
1264
1339
  hasActiveTurn: Boolean(this.activeTurn),
1265
1340
  hasPendingApproval: Boolean(this.pendingApproval || this.pendingApprovalRequest),
1341
+ hasPendingUserInput: Boolean(this.pendingUserInputRequest || this.state.pendingUserInput),
1266
1342
  activeTurnId: this.state.activeTurnId,
1267
1343
  })) {
1268
1344
  return;
@@ -1301,6 +1377,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1301
1377
  this.interruptPendingTurnStart = false;
1302
1378
  this.pendingThreadFollowId = null;
1303
1379
  this.clearPendingApprovalState();
1380
+ this.clearPendingUserInputState();
1304
1381
  this.queuedTurnNotifications = [];
1305
1382
  this.queuedTurnServerRequests = [];
1306
1383
  this.turnFinalMessages.clear();
@@ -1503,6 +1580,11 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1503
1580
  this.state.pendingApproval = null;
1504
1581
  this.state.pendingApprovalOrigin = undefined;
1505
1582
  }
1583
+ clearPendingUserInputState() {
1584
+ this.pendingUserInputRequest = null;
1585
+ this.state.pendingUserInput = null;
1586
+ this.state.pendingUserInputOrigin = undefined;
1587
+ }
1506
1588
  cleanupTurnArtifacts(turnId) {
1507
1589
  this.clearFinalReplyCompletionTimerForTurn(turnId);
1508
1590
  this.turnFinalMessages.delete(turnId);
@@ -1570,6 +1652,13 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1570
1652
  socket.send(JSON.stringify(payload));
1571
1653
  }
1572
1654
  async respondToApprovalRequest(request, action) {
1655
+ if (request.method === "item/permissions/requestApproval") {
1656
+ this.sendRpcMessage({
1657
+ id: request.requestId,
1658
+ result: buildCodexPermissionsRequestApprovalResponse(request.params, action),
1659
+ });
1660
+ return;
1661
+ }
1573
1662
  const decision = action === "confirm" ? "accept" : "decline";
1574
1663
  this.sendRpcMessage({
1575
1664
  id: request.requestId,
@@ -1754,6 +1843,15 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1754
1843
  this.setStatus("busy", "Codex approval resolved.");
1755
1844
  }
1756
1845
  }
1846
+ if (requestId !== null &&
1847
+ this.pendingUserInputRequest &&
1848
+ requestId === this.pendingUserInputRequest.requestId &&
1849
+ trackedTurn.turnId === this.pendingUserInputRequest.turnId) {
1850
+ this.clearPendingUserInputState();
1851
+ if (this.state.status === "awaiting_input") {
1852
+ this.setStatus("busy", "Codex user input resolved.");
1853
+ }
1854
+ }
1757
1855
  return;
1758
1856
  }
1759
1857
  case "turn/completed": {
@@ -1764,8 +1862,24 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1764
1862
  }
1765
1863
  }
1766
1864
  handleRpcServerRequest(requestId, method, params) {
1865
+ if (method === "mcpServer/elicitation/request") {
1866
+ this.sendRpcMessage({
1867
+ id: requestId,
1868
+ result: buildCodexMcpServerElicitationDeclineResponse(),
1869
+ });
1870
+ return;
1871
+ }
1872
+ if (method === "item/tool/call") {
1873
+ this.sendRpcMessage({
1874
+ id: requestId,
1875
+ result: buildCodexDynamicToolCallFailureResponse(),
1876
+ });
1877
+ return;
1878
+ }
1767
1879
  if (method !== "item/commandExecution/requestApproval" &&
1768
- method !== "item/fileChange/requestApproval") {
1880
+ method !== "item/fileChange/requestApproval" &&
1881
+ method !== "item/permissions/requestApproval" &&
1882
+ method !== "item/tool/requestUserInput") {
1769
1883
  this.sendRpcMessage({
1770
1884
  id: requestId,
1771
1885
  error: {
@@ -1793,16 +1907,57 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1793
1907
  });
1794
1908
  return;
1795
1909
  }
1796
- const trackedTurn = this.identifyTrackedTurn("server/request", params);
1910
+ const trackedTurn = this.identifyTrackedTurn("server/request", params) ??
1911
+ this.fallbackTrackedTurnForServerRequest(params);
1797
1912
  if (!trackedTurn) {
1913
+ this.sendRpcMessage({
1914
+ id: requestId,
1915
+ error: {
1916
+ code: -32602,
1917
+ message: "Invalid Codex server request payload: missing threadId or turnId.",
1918
+ },
1919
+ });
1798
1920
  return;
1799
1921
  }
1800
1922
  this.handleTrackedTurnStarted(trackedTurn);
1801
1923
  this.handleTrackedTurnServerRequest(requestId, method, params, trackedTurn);
1802
1924
  }
1803
1925
  handleTrackedTurnServerRequest(requestId, method, params, trackedTurn) {
1926
+ if (method === "item/tool/requestUserInput") {
1927
+ this.handleTrackedTurnUserInputRequest(requestId, params, trackedTurn);
1928
+ return;
1929
+ }
1930
+ const denyMessage = getCodexWechatOutboundAttachmentDenyMessage(method, params);
1931
+ if (denyMessage) {
1932
+ this.sendRpcMessage({
1933
+ id: requestId,
1934
+ result: method === "item/permissions/requestApproval"
1935
+ ? buildCodexPermissionsRequestApprovalResponse(params, "deny")
1936
+ : { decision: "decline" },
1937
+ });
1938
+ this.state.lastOutputAt = nowIso();
1939
+ this.setStatus("busy", `Codex approval auto-denied: ${truncatePreview(denyMessage, 180)}`);
1940
+ return;
1941
+ }
1942
+ const autoResponse = getCodexApprovalAutoResponse(method, params);
1943
+ if (autoResponse) {
1944
+ this.sendRpcMessage({
1945
+ id: requestId,
1946
+ result: autoResponse.result,
1947
+ });
1948
+ this.state.lastOutputAt = nowIso();
1949
+ this.setStatus("busy", `Codex approval auto-approved: ${truncatePreview(autoResponse.reason, 180)}`);
1950
+ return;
1951
+ }
1804
1952
  const request = buildCodexApprovalRequest(method, params);
1805
1953
  if (!request) {
1954
+ this.sendRpcMessage({
1955
+ id: requestId,
1956
+ error: {
1957
+ code: -32602,
1958
+ message: "Invalid Codex approval request payload.",
1959
+ },
1960
+ });
1806
1961
  return;
1807
1962
  }
1808
1963
  this.pendingApprovalRequest = {
@@ -1811,18 +1966,63 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1811
1966
  threadId: trackedTurn.threadId,
1812
1967
  turnId: trackedTurn.turnId,
1813
1968
  origin: trackedTurn.origin,
1969
+ params,
1814
1970
  };
1815
1971
  this.pendingApproval = request;
1816
1972
  this.state.pendingApproval = request;
1817
1973
  this.state.pendingApprovalOrigin = trackedTurn.origin;
1818
1974
  this.state.lastOutputAt = nowIso();
1819
- this.setStatus("awaiting_approval", "Codex approval is required.");
1975
+ this.setStatus("awaiting_approval", `Codex approval is required: ${truncatePreview(request.commandPreview, 180)}`);
1820
1976
  this.emit({
1821
1977
  type: "approval_required",
1822
1978
  request,
1823
1979
  timestamp: nowIso(),
1824
1980
  });
1825
1981
  }
1982
+ fallbackTrackedTurnForServerRequest(params) {
1983
+ if (this.activeTurn) {
1984
+ return null;
1985
+ }
1986
+ const threadId = getNotificationThreadId(params);
1987
+ const turnId = getNotificationTurnId(params);
1988
+ if (!threadId || !turnId) {
1989
+ return null;
1990
+ }
1991
+ return {
1992
+ threadId,
1993
+ turnId,
1994
+ origin: this.bridgeOwnedTurnIds.has(turnId) ? "wechat" : "local",
1995
+ };
1996
+ }
1997
+ handleTrackedTurnUserInputRequest(requestId, params, trackedTurn) {
1998
+ const request = buildCodexUserInputRequest(params);
1999
+ if (!request) {
2000
+ this.sendRpcMessage({
2001
+ id: requestId,
2002
+ error: {
2003
+ code: -32602,
2004
+ message: "Invalid Codex user input request payload.",
2005
+ },
2006
+ });
2007
+ return;
2008
+ }
2009
+ this.pendingUserInputRequest = {
2010
+ requestId,
2011
+ method: "item/tool/requestUserInput",
2012
+ threadId: trackedTurn.threadId,
2013
+ turnId: trackedTurn.turnId,
2014
+ origin: trackedTurn.origin,
2015
+ };
2016
+ this.state.pendingUserInput = request;
2017
+ this.state.pendingUserInputOrigin = trackedTurn.origin;
2018
+ this.state.lastOutputAt = nowIso();
2019
+ this.setStatus("awaiting_input", "Codex is waiting for user input.");
2020
+ this.emit({
2021
+ type: "user_input_required",
2022
+ request,
2023
+ timestamp: nowIso(),
2024
+ });
2025
+ }
1826
2026
  handleThreadStatusChanged(params) {
1827
2027
  const threadId = extractCodexThreadFollowIdFromStatusChanged(params);
1828
2028
  if (!threadId) {
@@ -1935,6 +2135,10 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1935
2135
  this.pendingApprovalRequest.turnId === trackedTurn.turnId) {
1936
2136
  this.clearPendingApprovalState();
1937
2137
  }
2138
+ if (this.pendingUserInputRequest &&
2139
+ this.pendingUserInputRequest.turnId === trackedTurn.turnId) {
2140
+ this.clearPendingUserInputState();
2141
+ }
1938
2142
  if (this.activeTurn?.turnId === trackedTurn.turnId) {
1939
2143
  this.setActiveTurn(null);
1940
2144
  }
@@ -2023,6 +2227,8 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2023
2227
  this.pendingTurnStart ||
2024
2228
  this.pendingApproval ||
2025
2229
  this.pendingApprovalRequest ||
2230
+ this.pendingUserInputRequest ||
2231
+ this.state.pendingUserInput ||
2026
2232
  !this.collectTurnOutput(turnId)) {
2027
2233
  return;
2028
2234
  }
@@ -2039,6 +2245,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2039
2245
  const finalText = this.collectTurnOutput(turnId);
2040
2246
  const lastActivityAtMs = this.turnLastActivityAtMs.get(turnId) ?? null;
2041
2247
  const pendingApproval = Boolean(this.pendingApproval || this.pendingApprovalRequest);
2248
+ const pendingUserInput = Boolean(this.pendingUserInputRequest || this.state.pendingUserInput);
2042
2249
  const nowMs = Date.now();
2043
2250
  if (!shouldAutoCompleteCodexWechatTurnAfterFinalReply({
2044
2251
  candidateTurnId: turnId,
@@ -2046,6 +2253,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2046
2253
  activeTurnOrigin: activeTurn?.origin,
2047
2254
  pendingTurnStart: this.pendingTurnStart,
2048
2255
  hasPendingApproval: pendingApproval,
2256
+ hasPendingUserInput: pendingUserInput,
2049
2257
  hasFinalOutput: Boolean(finalText),
2050
2258
  hasCompletedTurn: this.hasCompletedTurn(turnId),
2051
2259
  lastActivityAtMs,
@@ -2056,6 +2264,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2056
2264
  activeTurn.origin === "wechat" &&
2057
2265
  !this.pendingTurnStart &&
2058
2266
  !pendingApproval &&
2267
+ !pendingUserInput &&
2059
2268
  finalText &&
2060
2269
  typeof lastActivityAtMs === "number") {
2061
2270
  const remainingMs = CODEX_FINAL_REPLY_SETTLE_DELAY_MS - (nowMs - lastActivityAtMs);
@@ -2073,6 +2282,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2073
2282
  return;
2074
2283
  }
2075
2284
  this.clearPendingApprovalState();
2285
+ this.clearPendingUserInputState();
2076
2286
  this.setActiveTurn(null);
2077
2287
  this.cleanupTurnArtifacts(turnId);
2078
2288
  this.state.lastOutputAt = nowIso();
@@ -47,7 +47,10 @@ export class LocalCompanionProxyAdapter {
47
47
  this.shuttingDown = false;
48
48
  this.expectedCloseReason = null;
49
49
  this.clearReconnectShutdownTimer();
50
- this.setStatus("starting", `Waiting for manual ${this.options.kind} companion connection. Run "${getLocalCompanionCommandName(this.options.kind)}" in a second terminal for this directory.`);
50
+ this.setStatus("starting", formatLocalCompanionStartupMessage({
51
+ kind: this.options.kind,
52
+ launchMode: this.options.companionLaunchMode,
53
+ }));
51
54
  await new Promise((resolve, reject) => {
52
55
  const server = net.createServer((socket) => {
53
56
  this.handlePanelSocket(socket);
@@ -133,7 +136,9 @@ export class LocalCompanionProxyAdapter {
133
136
  this.expectedCloseReason = null;
134
137
  this.clearReconnectShutdownTimer();
135
138
  this.rejectPendingRequests(`${this.options.kind} companion proxy is shutting down.`);
136
- clearLocalCompanionEndpoint(this.options.cwd, this.endpoint?.instanceId);
139
+ clearLocalCompanionEndpoint(this.options.cwd, this.endpoint?.instanceId, {
140
+ adapter: this.options.kind,
141
+ });
137
142
  if (this.socket) {
138
143
  try {
139
144
  sendLocalCompanionMessage(this.socket, {
@@ -185,10 +190,15 @@ export class LocalCompanionProxyAdapter {
185
190
  this.expectedCloseReason = null;
186
191
  this.socket = socket;
187
192
  this.detachMessageListener = detachListener;
193
+ const companionConnectedAt = nowIso();
194
+ if (this.endpoint) {
195
+ this.endpoint.companionPid = message.companionPid;
196
+ this.endpoint.companionConnectedAt = companionConnectedAt;
197
+ }
188
198
  updateLocalCompanionOccupancy(this.options.cwd, {
189
199
  companionPid: message.companionPid,
190
- companionConnectedAt: nowIso(),
191
- }, this.endpoint?.instanceId);
200
+ companionConnectedAt,
201
+ }, this.endpoint?.instanceId, { adapter: this.options.kind });
192
202
  sendLocalCompanionMessage(socket, { type: "hello_ack" });
193
203
  return;
194
204
  }
@@ -198,7 +208,9 @@ export class LocalCompanionProxyAdapter {
198
208
  if (this.socket === socket) {
199
209
  const expectedCloseReason = this.expectedCloseReason;
200
210
  this.expectedCloseReason = null;
201
- clearLocalCompanionOccupancy(this.options.cwd, this.endpoint?.instanceId);
211
+ clearLocalCompanionOccupancy(this.options.cwd, this.endpoint?.instanceId, {
212
+ adapter: this.options.kind,
213
+ });
202
214
  this.detachPanelSocket();
203
215
  if (!this.shuttingDown) {
204
216
  this.handleCompanionDisconnect(expectedCloseReason);
@@ -296,6 +308,7 @@ export class LocalCompanionProxyAdapter {
296
308
  lifecycle: this.options.lifecycle,
297
309
  expectedClose: isExpectedLocalCompanionClose(expectedCloseReason),
298
310
  reconnectGraceMs: LOCAL_COMPANION_RECONNECT_GRACE_MS,
311
+ launchMode: this.options.companionLaunchMode,
299
312
  });
300
313
  if (disposition.action === "shutdown") {
301
314
  this.clearReconnectShutdownTimer();
@@ -362,7 +375,10 @@ export class LocalCompanionProxyAdapter {
362
375
  async sendRequest(payload) {
363
376
  const socket = this.socket;
364
377
  if (!socket) {
365
- throw new Error(`${this.options.kind} companion is not connected. Run "${getLocalCompanionCommandName(this.options.kind)}" in a second terminal for this directory.`);
378
+ throw new Error(formatCompanionNotConnectedMessage({
379
+ kind: this.options.kind,
380
+ launchMode: this.options.companionLaunchMode,
381
+ }));
366
382
  }
367
383
  if (!this.state.pid && payload.command !== "dispose") {
368
384
  throw new Error(`${this.options.kind} companion is connected but not ready yet. Wait for it to finish starting.`);
@@ -389,6 +405,18 @@ export function buildCompanionHealthPatch(state, timestamp) {
389
405
  companionWorkerPid: state.pid,
390
406
  };
391
407
  }
408
+ export function formatLocalCompanionStartupMessage(params) {
409
+ if (params.launchMode === "daemon_auto") {
410
+ return `Waiting for daemon-managed ${params.kind} companion connection. The daemon will open or reuse the visible CLI automatically.`;
411
+ }
412
+ return `Waiting for manual ${params.kind} companion connection. Run "${getLocalCompanionCommandName(params.kind)}" in a second terminal for this directory.`;
413
+ }
414
+ export function formatCompanionNotConnectedMessage(params) {
415
+ if (params.launchMode === "daemon_auto") {
416
+ return `${params.kind} companion is not connected yet. Send /${params.kind} in WeChat to open or reconnect the visible CLI automatically.`;
417
+ }
418
+ return `${params.kind} companion is not connected. Run "${getLocalCompanionCommandName(params.kind)}" in a second terminal for this directory.`;
419
+ }
392
420
  export function isExpectedLocalCompanionClose(reason) {
393
421
  return typeof reason === "string" && reason.length > 0;
394
422
  }
@@ -411,11 +439,23 @@ export function getCompanionDisconnectDisposition(params) {
411
439
  };
412
440
  }
413
441
  if (params.expectedClose) {
442
+ if (params.launchMode === "daemon_auto") {
443
+ return {
444
+ action: "await_manual_reconnect",
445
+ message: `${params.kind} companion closed. Send /${params.kind} in WeChat to reopen the visible CLI automatically.`,
446
+ };
447
+ }
414
448
  return {
415
449
  action: "await_manual_reconnect",
416
450
  message: `${params.kind} companion closed. Run "${commandName}" again in a second terminal for this directory.`,
417
451
  };
418
452
  }
453
+ if (params.launchMode === "daemon_auto") {
454
+ return {
455
+ action: "await_manual_reconnect",
456
+ message: `${params.kind} companion disconnected unexpectedly. Send /${params.kind} in WeChat to reopen the visible CLI automatically.`,
457
+ };
458
+ }
419
459
  return {
420
460
  action: "await_manual_reconnect",
421
461
  message: `${params.kind} companion disconnected unexpectedly. Run "${commandName}" again in a second terminal for this directory to reconnect.`,