cli-wechat-bridge 1.0.5 → 1.0.7

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.
@@ -1,16 +1,83 @@
1
1
  import fs from "node:fs";
2
2
  import net from "node:net";
3
+ import os from "node:os";
3
4
  import path from "node:path";
4
5
  import { buildLocalCompanionToken } from "../companion/local-companion-link.js";
5
6
  import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
6
- import { buildClaudeFailureMessage, buildClaudeHookScript, buildClaudeHookSettings, buildClaudePermissionDecisionHookOutput, buildClaudePermissionApprovalRequest, extractClaudeAssistantMessageText, extractClaudeResumeConversationId, extractClaudeTranscriptFinalReply, findInjectedClaudePromptIndex, normalizeClaudeAssistantMessage, parseClaudeHookPayload, } from "./claude-hooks.js";
7
+ import { buildClaudeFailureMessage, buildClaudeHookScript, buildClaudeHookSettings, buildClaudePermissionDecisionHookOutput, buildClaudePermissionApprovalRequest, extractClaudeAssistantMessageText, extractClaudeResumeConversationId, extractClaudeTranscriptFinalReply, findInjectedClaudePromptIndex, getClaudePermissionAutoResponse, getClaudeWechatOutboundAttachmentDenyMessage, normalizeClaudeAssistantMessage, parseClaudeHookPayload, } from "./claude-hooks.js";
7
8
  import { detectCliApproval, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
8
9
  import { AbstractPtyAdapter } from "./bridge-adapters.core.js";
9
10
  import * as shared from "./bridge-adapters.shared.js";
10
- const { CLAUDE_HOOK_LISTEN_HOST, CLAUDE_WECHAT_WORKING_NOTICE_DELAY_MS, DEFAULT_COLS, DEFAULT_ROWS, MODULE_DIR, buildClaudeCliArgs, isClaudeInvalidResumeError, quotePosixCommandArg, quoteWindowsCommandArg, shouldIncludeClaudeNoAltScreen, } = shared;
11
+ const { CLAUDE_HOOK_LISTEN_HOST, CLAUDE_WECHAT_WORKING_NOTICE_DELAY_MS, DEFAULT_COLS, DEFAULT_ROWS, MODULE_DIR, buildClaudeCliArgs, delay, isClaudeInvalidResumeError, quotePosixCommandArg, quoteWindowsCommandArg, shouldIncludeClaudeNoAltScreen, } = shared;
11
12
  const CLAUDE_COMPACT_OUTPUT_LINE_RE = /^Compacted(?:\s*\(.*full summary.*\))?$/i;
12
13
  const CLAUDE_COMPACT_FAILURE_RE = /Error:\s*Error during compaction:|(?:^|\b)API Error:|\b(?:compact|compaction)\s+failed\b|^Error:/i;
13
14
  const CLAUDE_COMPACT_DEDUP_MS = 2_000;
15
+ const CLAUDE_BRACKETED_PASTE_START = "\u001b[200~";
16
+ const CLAUDE_BRACKETED_PASTE_END = "\u001b[201~";
17
+ const CLAUDE_REMOTE_ENTER_DELAY_MS = 40;
18
+ const CLAUDE_STARTUP_OUTPUT_BUFFER_LIMIT = 4_000;
19
+ function isClaudeWorkspaceTrustPrompt(text) {
20
+ const compact = text.replace(/\s+/g, " ").trim();
21
+ return (/Accessing workspace:/i.test(compact) &&
22
+ /Quick safety check:/i.test(compact) &&
23
+ /project you created or one you trust/i.test(compact) &&
24
+ /I trust this folder/i.test(compact));
25
+ }
26
+ function isRecord(value) {
27
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
28
+ }
29
+ export function normalizeClaudeProjectConfigKey(cwd) {
30
+ return path.resolve(cwd).replace(/\\/g, "/");
31
+ }
32
+ export function ensureClaudeWorkspaceTrustAccepted(cwd, homeDir = process.env.USERPROFILE || process.env.HOME || os.homedir()) {
33
+ const configPath = path.join(homeDir, ".claude.json");
34
+ let config = {};
35
+ try {
36
+ if (fs.existsSync(configPath)) {
37
+ const raw = fs.readFileSync(configPath, "utf8");
38
+ config = raw.trim() ? JSON.parse(raw) : {};
39
+ }
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ if (!isRecord(config)) {
45
+ return false;
46
+ }
47
+ const projectKey = normalizeClaudeProjectConfigKey(cwd);
48
+ const projects = isRecord(config.projects) ? config.projects : {};
49
+ const currentProject = isRecord(projects[projectKey])
50
+ ? projects[projectKey]
51
+ : {};
52
+ if (currentProject.hasTrustDialogAccepted === true) {
53
+ return false;
54
+ }
55
+ const nextConfig = {
56
+ ...config,
57
+ projects: {
58
+ ...projects,
59
+ [projectKey]: {
60
+ ...currentProject,
61
+ hasTrustDialogAccepted: true,
62
+ },
63
+ },
64
+ };
65
+ const tempPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
66
+ try {
67
+ fs.writeFileSync(tempPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
68
+ fs.renameSync(tempPath, configPath);
69
+ return true;
70
+ }
71
+ catch {
72
+ try {
73
+ fs.rmSync(tempPath, { force: true });
74
+ }
75
+ catch {
76
+ // Best effort cleanup.
77
+ }
78
+ return false;
79
+ }
80
+ }
14
81
  export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
15
82
  hookServer = null;
16
83
  hookPort = null;
@@ -29,11 +96,20 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
29
96
  workingNoticeSent = false;
30
97
  workingNoticeDelayMs = CLAUDE_WECHAT_WORKING_NOTICE_DELAY_MS;
31
98
  lastCompactCompletionAtMs = 0;
99
+ startupOutputBuffer = "";
100
+ hasAutoConfirmedWorkspaceTrustPrompt = false;
32
101
  constructor(options) {
33
102
  super(options);
34
- this.runtimeSessionId = options.initialSharedSessionId ?? options.initialSharedThreadId ?? null;
35
- this.resumeConversationId = options.initialResumeConversationId ?? null;
36
- this.transcriptPath = options.initialTranscriptPath ?? null;
103
+ const shouldRestoreInitialSession = options.sessionStartMode !== "new";
104
+ this.runtimeSessionId = shouldRestoreInitialSession
105
+ ? options.initialSharedSessionId ?? options.initialSharedThreadId ?? null
106
+ : null;
107
+ this.resumeConversationId = shouldRestoreInitialSession
108
+ ? options.initialResumeConversationId ?? null
109
+ : null;
110
+ this.transcriptPath = shouldRestoreInitialSession
111
+ ? options.initialTranscriptPath ?? null
112
+ : null;
37
113
  if (this.runtimeSessionId) {
38
114
  this.state.sharedSessionId = this.runtimeSessionId;
39
115
  this.state.activeRuntimeSessionId = this.runtimeSessionId;
@@ -49,6 +125,9 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
49
125
  if (this.pty) {
50
126
  return;
51
127
  }
128
+ this.startupOutputBuffer = "";
129
+ this.hasAutoConfirmedWorkspaceTrustPrompt = false;
130
+ ensureClaudeWorkspaceTrustAccepted(this.options.cwd);
52
131
  // Validate transcript file exists before launching Claude CLI.
53
132
  // After a compact, the old transcript is deleted and the persisted
54
133
  // resumeConversationId becomes invalid, causing --resume to crash.
@@ -99,7 +178,8 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
99
178
  this.pendingCliApprovalHints = null;
100
179
  this.clearWechatWorkingNotice(true);
101
180
  this.setStatus("busy");
102
- this.writeToPty(text.replace(/\r?\n/g, "\r"));
181
+ this.writeToPty(this.buildRemoteInputPayload(text));
182
+ await delay(CLAUDE_REMOTE_ENTER_DELAY_MS);
103
183
  this.writeToPty("\r");
104
184
  this.armWechatWorkingNotice();
105
185
  }
@@ -205,6 +285,9 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
205
285
  return;
206
286
  }
207
287
  this.state.lastOutputAt = nowIso();
288
+ if (this.maybeAutoConfirmWorkspaceTrustPrompt(text)) {
289
+ return;
290
+ }
208
291
  if (this.shouldTreatClaudeOutputAsCompactCompletion(text)) {
209
292
  this.completeClaudeCompact();
210
293
  return;
@@ -237,6 +320,18 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
237
320
  return;
238
321
  }
239
322
  }
323
+ maybeAutoConfirmWorkspaceTrustPrompt(text) {
324
+ if (this.hasAcceptedInput || this.hasAutoConfirmedWorkspaceTrustPrompt) {
325
+ return false;
326
+ }
327
+ this.startupOutputBuffer = `${this.startupOutputBuffer}${text}`.slice(-CLAUDE_STARTUP_OUTPUT_BUFFER_LIMIT);
328
+ if (!isClaudeWorkspaceTrustPrompt(this.startupOutputBuffer)) {
329
+ return false;
330
+ }
331
+ this.hasAutoConfirmedWorkspaceTrustPrompt = true;
332
+ this.writeToPty("\r");
333
+ return true;
334
+ }
240
335
  handleExit(exitCode) {
241
336
  this.detachLocalTerminal();
242
337
  this.clearWechatWorkingNotice(true);
@@ -464,6 +559,13 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
464
559
  timestamp: nowIso(),
465
560
  });
466
561
  }
562
+ buildRemoteInputPayload(text) {
563
+ const normalizedText = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
564
+ if (!normalizedText.includes("\n")) {
565
+ return normalizedText;
566
+ }
567
+ return `${CLAUDE_BRACKETED_PASTE_START}${normalizedText}${CLAUDE_BRACKETED_PASTE_END}`;
568
+ }
467
569
  shouldTreatClaudeOutputAsCompactCompletion(text) {
468
570
  if (this.state.status !== "busy" &&
469
571
  this.state.status !== "awaiting_approval" &&
@@ -707,6 +809,25 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
707
809
  }
708
810
  handleClaudePermissionRequest(requestId, payload, socket) {
709
811
  this.clearWechatWorkingNotice();
812
+ const denyMessage = getClaudeWechatOutboundAttachmentDenyMessage(payload);
813
+ if (denyMessage) {
814
+ this.pendingApproval = null;
815
+ this.state.pendingApproval = null;
816
+ this.state.pendingApprovalOrigin = undefined;
817
+ if (this.state.status === "awaiting_approval") {
818
+ this.setStatus("busy");
819
+ }
820
+ this.respondToClaudeHook(socket, requestId, buildClaudePermissionDecisionHookOutput("deny", denyMessage));
821
+ return;
822
+ }
823
+ const autoResponse = getClaudePermissionAutoResponse(payload);
824
+ if (autoResponse) {
825
+ if (!this.pendingApproval && !this.state.pendingApproval) {
826
+ this.setStatus("busy", `Claude approval auto-approved: ${truncatePreview(autoResponse.reason, 180)}`);
827
+ }
828
+ this.respondToClaudeHook(socket, requestId, buildClaudePermissionDecisionHookOutput(autoResponse.action));
829
+ return;
830
+ }
710
831
  this.flushPendingClaudeHookApprovals();
711
832
  this.pendingHookApprovals.set(requestId, {
712
833
  requestId,
@@ -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_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, 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;
@@ -63,7 +64,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
63
64
  localClientInstanceId = `${process.pid}-${Date.now().toString(36)}`;
64
65
  constructor(options) {
65
66
  super(options);
66
- this.resumeThreadId = options.initialSharedSessionId ?? options.initialSharedThreadId ?? null;
67
+ this.resumeThreadId = options.sessionStartMode === "new"
68
+ ? null
69
+ : options.initialSharedSessionId ?? options.initialSharedThreadId ?? null;
67
70
  if (this.resumeThreadId && options.renderMode !== "panel") {
68
71
  this.state.sharedSessionId = this.resumeThreadId;
69
72
  this.state.sharedThreadId = this.resumeThreadId;
@@ -194,6 +197,27 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
194
197
  }
195
198
  return await super.resolveApproval(action);
196
199
  }
200
+ async submitUserInput(answers) {
201
+ if (!this.pendingUserInputRequest) {
202
+ return false;
203
+ }
204
+ const request = this.pendingUserInputRequest;
205
+ const responseAnswers = {};
206
+ for (const [questionId, values] of Object.entries(answers)) {
207
+ responseAnswers[questionId] = {
208
+ answers: values,
209
+ };
210
+ }
211
+ this.sendRpcMessage({
212
+ id: request.requestId,
213
+ result: {
214
+ answers: responseAnswers,
215
+ },
216
+ });
217
+ this.clearPendingUserInputState();
218
+ this.setStatus("busy", "Codex user input submitted.");
219
+ return true;
220
+ }
197
221
  async dispose() {
198
222
  this.resetTurnTracking({ preserveThread: false });
199
223
  if (this.isEmbeddedCliMode()) {
@@ -752,6 +776,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
752
776
  if (this.pendingApproval) {
753
777
  throw new Error("A Codex approval request is pending. Reply with /confirm <code> or /deny.");
754
778
  }
779
+ if (this.pendingUserInputRequest || this.state.pendingUserInput) {
780
+ throw new Error("Codex is waiting for user input. Reply with /answer and your response, or use /stop.");
781
+ }
755
782
  if (this.pendingTurnStart || this.activeTurn || this.state.status === "busy") {
756
783
  const origin = this.state.activeTurnOrigin;
757
784
  if (origin === "local") {
@@ -766,6 +793,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
766
793
  this.rememberInjectedInput(text);
767
794
  this.clearPendingApprovalState();
768
795
  const threadId = await this.ensureThreadStarted();
796
+ const subscribedBeforeTurnStart = await this.tryEnsureSharedThreadSubscribed(threadId);
769
797
  this.pendingTurnStart = true;
770
798
  this.pendingTurnThreadId = threadId;
771
799
  this.interruptPendingTurnStart = false;
@@ -793,6 +821,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
793
821
  turnId,
794
822
  origin: "wechat",
795
823
  });
824
+ if (!subscribedBeforeTurnStart) {
825
+ await this.tryEnsureSharedThreadSubscribed(threadId);
826
+ }
796
827
  if (this.interruptPendingTurnStart) {
797
828
  await this.requestActiveTurnInterrupt();
798
829
  this.armInterruptFallback();
@@ -813,11 +844,15 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
813
844
  if (this.isNativePanelMode() && !this.nativeProcess) {
814
845
  return false;
815
846
  }
816
- const turnPending = this.pendingTurnStart || this.state.status === "busy" || this.state.status === "awaiting_approval";
847
+ const turnPending = this.pendingTurnStart ||
848
+ this.state.status === "busy" ||
849
+ this.state.status === "awaiting_approval" ||
850
+ this.state.status === "awaiting_input";
817
851
  if (!turnPending) {
818
852
  return false;
819
853
  }
820
854
  this.clearPendingApprovalState();
855
+ this.clearPendingUserInputState();
821
856
  if (this.pendingTurnStart && !this.activeTurn) {
822
857
  this.interruptPendingTurnStart = true;
823
858
  this.armInterruptFallback();
@@ -980,6 +1015,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
980
1015
  attachRpcSocket(socket) {
981
1016
  this.rpcSocket = socket;
982
1017
  this.rpcShuttingDown = false;
1018
+ this.subscribedThreadIds.clear();
983
1019
  socket.addEventListener("message", (event) => {
984
1020
  this.handleRpcMessageData(event.data);
985
1021
  });
@@ -991,6 +1027,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
991
1027
  const socket = this.rpcSocket;
992
1028
  this.rpcSocket = null;
993
1029
  this.rpcShuttingDown = true;
1030
+ this.subscribedThreadIds.clear();
994
1031
  this.rejectPendingRpcRequests("Codex websocket connection closed.");
995
1032
  if (!socket) {
996
1033
  this.rpcShuttingDown = false;
@@ -1028,6 +1065,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1028
1065
  cleanPanelExitInProgress: this.cleanPanelExitInProgress,
1029
1066
  });
1030
1067
  this.rpcSocket = null;
1068
+ this.subscribedThreadIds.clear();
1031
1069
  this.rejectPendingRpcRequests("Codex websocket connection closed.");
1032
1070
  this.rpcShuttingDown = false;
1033
1071
  if (expectedShutdown) {
@@ -1156,9 +1194,44 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1156
1194
  throw new Error("Codex did not return a thread id for the bridge session.");
1157
1195
  }
1158
1196
  this.rememberBridgeOwnedThreadSignal(threadId);
1197
+ this.subscribedThreadIds.add(threadId);
1159
1198
  this.updateSharedThread(threadId);
1160
1199
  return threadId;
1161
1200
  }
1201
+ async tryEnsureSharedThreadSubscribed(threadId) {
1202
+ if (this.subscribedThreadIds.has(threadId)) {
1203
+ return true;
1204
+ }
1205
+ try {
1206
+ await this.ensureSharedThreadSubscribed(threadId);
1207
+ return true;
1208
+ }
1209
+ catch {
1210
+ return false;
1211
+ }
1212
+ }
1213
+ async ensureSharedThreadSubscribed(threadId) {
1214
+ if (this.subscribedThreadIds.has(threadId)) {
1215
+ return;
1216
+ }
1217
+ const response = await this.sendRpcRequest("thread/resume", {
1218
+ threadId,
1219
+ cwd: this.options.cwd,
1220
+ approvalPolicy: "on-request",
1221
+ approvalsReviewer: "user",
1222
+ sandbox: "workspace-write",
1223
+ excludeTurns: true,
1224
+ });
1225
+ const resumedThreadId = this.extractThreadIdFromResponse(response);
1226
+ if (!resumedThreadId) {
1227
+ throw new Error("Codex did not return a thread id while subscribing the bridge client.");
1228
+ }
1229
+ this.rememberBridgeOwnedThreadSignal(resumedThreadId);
1230
+ this.subscribedThreadIds.add(resumedThreadId);
1231
+ if (resumedThreadId !== this.sharedThreadId) {
1232
+ this.updateSharedThread(resumedThreadId);
1233
+ }
1234
+ }
1162
1235
  async resumeSharedThread(threadId, options = {}) {
1163
1236
  const trimmedThreadId = threadId.trim();
1164
1237
  if (!trimmedThreadId) {
@@ -1180,12 +1253,14 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1180
1253
  approvalPolicy: "on-request",
1181
1254
  approvalsReviewer: "user",
1182
1255
  sandbox: "workspace-write",
1256
+ excludeTurns: true,
1183
1257
  });
1184
1258
  const resumedThreadId = this.extractThreadIdFromResponse(response);
1185
1259
  if (!resumedThreadId) {
1186
1260
  throw new Error("Codex did not return a thread id while resuming the saved thread.");
1187
1261
  }
1188
1262
  this.rememberBridgeOwnedThreadSignal(resumedThreadId);
1263
+ this.subscribedThreadIds.add(resumedThreadId);
1189
1264
  this.sessionFilePath = null;
1190
1265
  this.sessionReadOffset = 0;
1191
1266
  this.sessionPartialLine = "";
@@ -1238,7 +1313,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1238
1313
  this.clearInterruptTimer();
1239
1314
  this.interruptTimer = setTimeout(() => {
1240
1315
  this.interruptTimer = null;
1241
- if (this.state.status !== "busy" && this.state.status !== "awaiting_approval") {
1316
+ if (this.state.status !== "busy" &&
1317
+ this.state.status !== "awaiting_approval" &&
1318
+ this.state.status !== "awaiting_input") {
1242
1319
  return;
1243
1320
  }
1244
1321
  this.resetTurnTracking({ preserveThread: true });
@@ -1263,6 +1340,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1263
1340
  pendingTurnStart: this.pendingTurnStart,
1264
1341
  hasActiveTurn: Boolean(this.activeTurn),
1265
1342
  hasPendingApproval: Boolean(this.pendingApproval || this.pendingApprovalRequest),
1343
+ hasPendingUserInput: Boolean(this.pendingUserInputRequest || this.state.pendingUserInput),
1266
1344
  activeTurnId: this.state.activeTurnId,
1267
1345
  })) {
1268
1346
  return;
@@ -1301,6 +1379,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1301
1379
  this.interruptPendingTurnStart = false;
1302
1380
  this.pendingThreadFollowId = null;
1303
1381
  this.clearPendingApprovalState();
1382
+ this.clearPendingUserInputState();
1304
1383
  this.queuedTurnNotifications = [];
1305
1384
  this.queuedTurnServerRequests = [];
1306
1385
  this.turnFinalMessages.clear();
@@ -1503,6 +1582,11 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1503
1582
  this.state.pendingApproval = null;
1504
1583
  this.state.pendingApprovalOrigin = undefined;
1505
1584
  }
1585
+ clearPendingUserInputState() {
1586
+ this.pendingUserInputRequest = null;
1587
+ this.state.pendingUserInput = null;
1588
+ this.state.pendingUserInputOrigin = undefined;
1589
+ }
1506
1590
  cleanupTurnArtifacts(turnId) {
1507
1591
  this.clearFinalReplyCompletionTimerForTurn(turnId);
1508
1592
  this.turnFinalMessages.delete(turnId);
@@ -1570,6 +1654,13 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1570
1654
  socket.send(JSON.stringify(payload));
1571
1655
  }
1572
1656
  async respondToApprovalRequest(request, action) {
1657
+ if (request.method === "item/permissions/requestApproval") {
1658
+ this.sendRpcMessage({
1659
+ id: request.requestId,
1660
+ result: buildCodexPermissionsRequestApprovalResponse(request.params, action),
1661
+ });
1662
+ return;
1663
+ }
1573
1664
  const decision = action === "confirm" ? "accept" : "decline";
1574
1665
  this.sendRpcMessage({
1575
1666
  id: request.requestId,
@@ -1754,6 +1845,15 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1754
1845
  this.setStatus("busy", "Codex approval resolved.");
1755
1846
  }
1756
1847
  }
1848
+ if (requestId !== null &&
1849
+ this.pendingUserInputRequest &&
1850
+ requestId === this.pendingUserInputRequest.requestId &&
1851
+ trackedTurn.turnId === this.pendingUserInputRequest.turnId) {
1852
+ this.clearPendingUserInputState();
1853
+ if (this.state.status === "awaiting_input") {
1854
+ this.setStatus("busy", "Codex user input resolved.");
1855
+ }
1856
+ }
1757
1857
  return;
1758
1858
  }
1759
1859
  case "turn/completed": {
@@ -1764,8 +1864,24 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1764
1864
  }
1765
1865
  }
1766
1866
  handleRpcServerRequest(requestId, method, params) {
1867
+ if (method === "mcpServer/elicitation/request") {
1868
+ this.sendRpcMessage({
1869
+ id: requestId,
1870
+ result: buildCodexMcpServerElicitationDeclineResponse(),
1871
+ });
1872
+ return;
1873
+ }
1874
+ if (method === "item/tool/call") {
1875
+ this.sendRpcMessage({
1876
+ id: requestId,
1877
+ result: buildCodexDynamicToolCallFailureResponse(),
1878
+ });
1879
+ return;
1880
+ }
1767
1881
  if (method !== "item/commandExecution/requestApproval" &&
1768
- method !== "item/fileChange/requestApproval") {
1882
+ method !== "item/fileChange/requestApproval" &&
1883
+ method !== "item/permissions/requestApproval" &&
1884
+ method !== "item/tool/requestUserInput") {
1769
1885
  this.sendRpcMessage({
1770
1886
  id: requestId,
1771
1887
  error: {
@@ -1793,16 +1909,57 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1793
1909
  });
1794
1910
  return;
1795
1911
  }
1796
- const trackedTurn = this.identifyTrackedTurn("server/request", params);
1912
+ const trackedTurn = this.identifyTrackedTurn("server/request", params) ??
1913
+ this.fallbackTrackedTurnForServerRequest(params);
1797
1914
  if (!trackedTurn) {
1915
+ this.sendRpcMessage({
1916
+ id: requestId,
1917
+ error: {
1918
+ code: -32602,
1919
+ message: "Invalid Codex server request payload: missing threadId or turnId.",
1920
+ },
1921
+ });
1798
1922
  return;
1799
1923
  }
1800
1924
  this.handleTrackedTurnStarted(trackedTurn);
1801
1925
  this.handleTrackedTurnServerRequest(requestId, method, params, trackedTurn);
1802
1926
  }
1803
1927
  handleTrackedTurnServerRequest(requestId, method, params, trackedTurn) {
1928
+ if (method === "item/tool/requestUserInput") {
1929
+ this.handleTrackedTurnUserInputRequest(requestId, params, trackedTurn);
1930
+ return;
1931
+ }
1932
+ const denyMessage = getCodexWechatOutboundAttachmentDenyMessage(method, params);
1933
+ if (denyMessage) {
1934
+ this.sendRpcMessage({
1935
+ id: requestId,
1936
+ result: method === "item/permissions/requestApproval"
1937
+ ? buildCodexPermissionsRequestApprovalResponse(params, "deny")
1938
+ : { decision: "decline" },
1939
+ });
1940
+ this.state.lastOutputAt = nowIso();
1941
+ this.setStatus("busy", `Codex approval auto-denied: ${truncatePreview(denyMessage, 180)}`);
1942
+ return;
1943
+ }
1944
+ const autoResponse = getCodexApprovalAutoResponse(method, params);
1945
+ if (autoResponse) {
1946
+ this.sendRpcMessage({
1947
+ id: requestId,
1948
+ result: autoResponse.result,
1949
+ });
1950
+ this.state.lastOutputAt = nowIso();
1951
+ this.setStatus("busy", `Codex approval auto-approved: ${truncatePreview(autoResponse.reason, 180)}`);
1952
+ return;
1953
+ }
1804
1954
  const request = buildCodexApprovalRequest(method, params);
1805
1955
  if (!request) {
1956
+ this.sendRpcMessage({
1957
+ id: requestId,
1958
+ error: {
1959
+ code: -32602,
1960
+ message: "Invalid Codex approval request payload.",
1961
+ },
1962
+ });
1806
1963
  return;
1807
1964
  }
1808
1965
  this.pendingApprovalRequest = {
@@ -1811,18 +1968,63 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1811
1968
  threadId: trackedTurn.threadId,
1812
1969
  turnId: trackedTurn.turnId,
1813
1970
  origin: trackedTurn.origin,
1971
+ params,
1814
1972
  };
1815
1973
  this.pendingApproval = request;
1816
1974
  this.state.pendingApproval = request;
1817
1975
  this.state.pendingApprovalOrigin = trackedTurn.origin;
1818
1976
  this.state.lastOutputAt = nowIso();
1819
- this.setStatus("awaiting_approval", "Codex approval is required.");
1977
+ this.setStatus("awaiting_approval", `Codex approval is required: ${truncatePreview(request.commandPreview, 180)}`);
1820
1978
  this.emit({
1821
1979
  type: "approval_required",
1822
1980
  request,
1823
1981
  timestamp: nowIso(),
1824
1982
  });
1825
1983
  }
1984
+ fallbackTrackedTurnForServerRequest(params) {
1985
+ if (this.activeTurn) {
1986
+ return null;
1987
+ }
1988
+ const threadId = getNotificationThreadId(params);
1989
+ const turnId = getNotificationTurnId(params);
1990
+ if (!threadId || !turnId) {
1991
+ return null;
1992
+ }
1993
+ return {
1994
+ threadId,
1995
+ turnId,
1996
+ origin: this.bridgeOwnedTurnIds.has(turnId) ? "wechat" : "local",
1997
+ };
1998
+ }
1999
+ handleTrackedTurnUserInputRequest(requestId, params, trackedTurn) {
2000
+ const request = buildCodexUserInputRequest(params);
2001
+ if (!request) {
2002
+ this.sendRpcMessage({
2003
+ id: requestId,
2004
+ error: {
2005
+ code: -32602,
2006
+ message: "Invalid Codex user input request payload.",
2007
+ },
2008
+ });
2009
+ return;
2010
+ }
2011
+ this.pendingUserInputRequest = {
2012
+ requestId,
2013
+ method: "item/tool/requestUserInput",
2014
+ threadId: trackedTurn.threadId,
2015
+ turnId: trackedTurn.turnId,
2016
+ origin: trackedTurn.origin,
2017
+ };
2018
+ this.state.pendingUserInput = request;
2019
+ this.state.pendingUserInputOrigin = trackedTurn.origin;
2020
+ this.state.lastOutputAt = nowIso();
2021
+ this.setStatus("awaiting_input", "Codex is waiting for user input.");
2022
+ this.emit({
2023
+ type: "user_input_required",
2024
+ request,
2025
+ timestamp: nowIso(),
2026
+ });
2027
+ }
1826
2028
  handleThreadStatusChanged(params) {
1827
2029
  const threadId = extractCodexThreadFollowIdFromStatusChanged(params);
1828
2030
  if (!threadId) {
@@ -1935,6 +2137,10 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1935
2137
  this.pendingApprovalRequest.turnId === trackedTurn.turnId) {
1936
2138
  this.clearPendingApprovalState();
1937
2139
  }
2140
+ if (this.pendingUserInputRequest &&
2141
+ this.pendingUserInputRequest.turnId === trackedTurn.turnId) {
2142
+ this.clearPendingUserInputState();
2143
+ }
1938
2144
  if (this.activeTurn?.turnId === trackedTurn.turnId) {
1939
2145
  this.setActiveTurn(null);
1940
2146
  }
@@ -2023,6 +2229,8 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2023
2229
  this.pendingTurnStart ||
2024
2230
  this.pendingApproval ||
2025
2231
  this.pendingApprovalRequest ||
2232
+ this.pendingUserInputRequest ||
2233
+ this.state.pendingUserInput ||
2026
2234
  !this.collectTurnOutput(turnId)) {
2027
2235
  return;
2028
2236
  }
@@ -2039,6 +2247,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2039
2247
  const finalText = this.collectTurnOutput(turnId);
2040
2248
  const lastActivityAtMs = this.turnLastActivityAtMs.get(turnId) ?? null;
2041
2249
  const pendingApproval = Boolean(this.pendingApproval || this.pendingApprovalRequest);
2250
+ const pendingUserInput = Boolean(this.pendingUserInputRequest || this.state.pendingUserInput);
2042
2251
  const nowMs = Date.now();
2043
2252
  if (!shouldAutoCompleteCodexWechatTurnAfterFinalReply({
2044
2253
  candidateTurnId: turnId,
@@ -2046,6 +2255,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2046
2255
  activeTurnOrigin: activeTurn?.origin,
2047
2256
  pendingTurnStart: this.pendingTurnStart,
2048
2257
  hasPendingApproval: pendingApproval,
2258
+ hasPendingUserInput: pendingUserInput,
2049
2259
  hasFinalOutput: Boolean(finalText),
2050
2260
  hasCompletedTurn: this.hasCompletedTurn(turnId),
2051
2261
  lastActivityAtMs,
@@ -2056,6 +2266,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2056
2266
  activeTurn.origin === "wechat" &&
2057
2267
  !this.pendingTurnStart &&
2058
2268
  !pendingApproval &&
2269
+ !pendingUserInput &&
2059
2270
  finalText &&
2060
2271
  typeof lastActivityAtMs === "number") {
2061
2272
  const remainingMs = CODEX_FINAL_REPLY_SETTLE_DELAY_MS - (nowMs - lastActivityAtMs);
@@ -2073,6 +2284,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2073
2284
  return;
2074
2285
  }
2075
2286
  this.clearPendingApprovalState();
2287
+ this.clearPendingUserInputState();
2076
2288
  this.setActiveTurn(null);
2077
2289
  this.cleanupTurnArtifacts(turnId);
2078
2290
  this.state.lastOutputAt = nowIso();
@@ -20,21 +20,29 @@ export class LocalCompanionProxyAdapter {
20
20
  reconnectShutdownTimer = null;
21
21
  constructor(options) {
22
22
  this.options = options;
23
+ const shouldRestoreInitialSession = options.sessionStartMode !== "new";
24
+ const initialSharedSessionId = shouldRestoreInitialSession
25
+ ? options.initialSharedSessionId ?? options.initialSharedThreadId
26
+ : undefined;
23
27
  this.state = {
24
28
  kind: options.kind,
25
29
  status: "stopped",
26
30
  cwd: options.cwd,
27
31
  command: options.command,
28
32
  profile: options.profile,
29
- sharedSessionId: options.initialSharedSessionId ?? options.initialSharedThreadId,
33
+ sharedSessionId: initialSharedSessionId,
30
34
  sharedThreadId: options.kind === "codex" || options.kind === "opencode"
31
- ? options.initialSharedSessionId ?? options.initialSharedThreadId
35
+ ? initialSharedSessionId
32
36
  : undefined,
33
37
  activeRuntimeSessionId: options.kind === "claude" || options.kind === "opencode"
34
- ? options.initialSharedSessionId ?? options.initialSharedThreadId
38
+ ? initialSharedSessionId
39
+ : undefined,
40
+ resumeConversationId: shouldRestoreInitialSession && options.kind === "claude"
41
+ ? options.initialResumeConversationId
42
+ : undefined,
43
+ transcriptPath: shouldRestoreInitialSession && options.kind === "claude"
44
+ ? options.initialTranscriptPath
35
45
  : undefined,
36
- resumeConversationId: options.kind === "claude" ? options.initialResumeConversationId : undefined,
37
- transcriptPath: options.kind === "claude" ? options.initialTranscriptPath : undefined,
38
46
  };
39
47
  }
40
48
  setEventSink(sink) {