cli-wechat-bridge 1.2.1 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +23 -9
  2. package/dist/bridge/adapter-control.js +19 -12
  3. package/dist/bridge/bridge-adapters.claude.js +219 -115
  4. package/dist/bridge/bridge-adapters.codex.js +207 -83
  5. package/dist/bridge/bridge-adapters.core.js +84 -7
  6. package/dist/bridge/bridge-adapters.opencode.js +202 -55
  7. package/dist/bridge/bridge-adapters.pi.js +23 -18
  8. package/dist/bridge/bridge-adapters.shared.js +9 -9
  9. package/dist/bridge/bridge-session-resume.js +13 -6
  10. package/dist/bridge/bridge-utils.js +94 -144
  11. package/dist/bridge/claude-hooks.js +60 -3
  12. package/dist/bridge/claude-native-controls.js +19 -12
  13. package/dist/bridge/language-control.js +33 -0
  14. package/dist/bridge/native-terminal-control.js +26 -10
  15. package/dist/bridge/opencode-v2-compat.js +11 -3
  16. package/dist/bridge/wechat-bridge.js +57 -42
  17. package/dist/channels/channel-driver-factory.js +44 -0
  18. package/dist/channels/local/local-channel-port.js +40 -0
  19. package/dist/channels/local/local-driver.js +217 -0
  20. package/dist/channels/wechat/wechat-driver.js +11 -2
  21. package/dist/channels/wecom/wecom-driver.js +8 -1
  22. package/dist/companion/local-companion.js +27 -7
  23. package/dist/companion/pi-tui-bridge-extension.js +10 -4
  24. package/dist/core/bridge-control.js +3 -0
  25. package/dist/core/bridge-defer.js +3 -2
  26. package/dist/core/bridge-event-forwarder.js +3 -0
  27. package/dist/core/channel-types.js +5 -1
  28. package/dist/daemon/wechat-daemon.js +202 -124
  29. package/dist/i18n/index.js +25 -11
  30. package/dist/i18n/messages-en.js +244 -1
  31. package/dist/i18n/messages-zh.js +244 -1
  32. package/dist/runtime/legacy-adapter-runtime.js +7 -4
  33. package/dist/wechat/channel-config.js +3 -2
  34. package/package.json +2 -1
@@ -5,6 +5,7 @@
5
5
  // must be released under AGPL-3.0-or-later with full source code; see
6
6
  // LICENSE.txt. Network services built on it must offer source to users.
7
7
  import fs from "node:fs";
8
+ import { t } from "../i18n/index.js";
8
9
  import path from "node:path";
9
10
  import crypto from "node:crypto";
10
11
  import { spawn as spawnChild, spawnSync } from "node:child_process";
@@ -29,7 +30,7 @@ export function isCodexVersionInCompatibilityRange(version) {
29
30
  }
30
31
  const major = parts[0] ?? -1;
31
32
  const minor = parts[1] ?? -1;
32
- return major === 0 && minor >= 149 && minor <= 157;
33
+ return major === 0 && minor >= 149 && minor <= 161;
33
34
  }
34
35
  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;
35
36
  const CODEX_LOCAL_THREAD_ANNOUNCE_SETTLE_MS = 150;
@@ -95,6 +96,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
95
96
  rpcRequestCounter = 0;
96
97
  pendingRpcRequests = new Map();
97
98
  subscribedThreadIds = new Set();
99
+ backgroundThreadIds = new Set();
98
100
  pendingThreadSubscriptions = new Map();
99
101
  sharedThreadId = null;
100
102
  previousCollaborationMode = null;
@@ -114,6 +116,11 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
114
116
  bridgeResumeReplayThreadId = null;
115
117
  bridgeResumeReplayUntilMs = 0;
116
118
  pendingApprovalRequests = [];
119
+ settledApprovalIds = new Set();
120
+ discoveringApprovalIds = new Set();
121
+ approvalGeneration = 0;
122
+ resolvingApprovalIds = new Set();
123
+ backgroundThreadParents = new Map();
117
124
  pendingUserInputRequest = null;
118
125
  pendingUserInputTimer = null;
119
126
  userInputAutoResolutionDelayMs = CODEX_NON_BLOCKING_USER_INPUT_TIMEOUT_MS;
@@ -145,6 +152,8 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
145
152
  localClientInstanceId = `${process.pid}-${Date.now().toString(36)}`;
146
153
  constructor(options) {
147
154
  super(options);
155
+ this.state.supportsApprovalRequestIds = true;
156
+ this.state.pendingApprovals = [];
148
157
  this.resumeThreadId = options.sessionStartMode === "new"
149
158
  ? null
150
159
  : options.initialSharedSessionId ?? options.initialSharedThreadId ?? null;
@@ -236,23 +245,23 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
236
245
  return;
237
246
  }
238
247
  if (!this.pty) {
239
- throw new Error("codex adapter is not running.");
248
+ throw new Error(t("adapter.codex.notRunning"));
240
249
  }
241
250
  if (this.state.status === "busy") {
242
- throw new Error("codex is still working. Wait for the current reply or use /stop.");
251
+ throw new Error(t("adapter.codex.busy"));
243
252
  }
244
253
  if (this.pendingApproval) {
245
- throw new Error("A Codex approval request is pending. Reply with /confirm or /deny.");
254
+ throw new Error(t("adapter.codex.approvalPending"));
246
255
  }
247
256
  if (this.startupBlocker) {
248
- throw new Error("Codex is waiting for local terminal input before the session can continue.");
257
+ throw new Error(t("adapter.codex.localInputPending"));
249
258
  }
250
259
  await delay(this.warmupUntilMs - Date.now());
251
260
  if (!this.pty) {
252
- throw new Error("codex adapter is not running.");
261
+ throw new Error(t("adapter.codex.notRunning"));
253
262
  }
254
263
  if (this.startupBlocker) {
255
- throw new Error("Codex is waiting for local terminal input before the session can continue.");
264
+ throw new Error(t("adapter.codex.localInputPending"));
256
265
  }
257
266
  this.clearInterruptTimer();
258
267
  this.hasAcceptedInput = true;
@@ -279,7 +288,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
279
288
  }
280
289
  async listModels() {
281
290
  if (!this.usesRpcTurnTransport() || !this.sharedThreadId) {
282
- throw new Error("Codex model selection requires an active app-server thread.");
291
+ throw new Error(t("adapter.codex.modelThreadRequired"));
283
292
  }
284
293
  const response = await this.sendRpcRequest("model/list", { includeHidden: false });
285
294
  const data = isRecord(response) && Array.isArray(response.data) ? response.data : [];
@@ -303,14 +312,14 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
303
312
  const models = await this.listModels();
304
313
  const selected = models.find((model) => model.id === modelId);
305
314
  if (!selected)
306
- throw new Error(`Codex model ${modelId} is not available.`);
315
+ throw new Error(t("adapter.codex.modelUnavailable", { model: modelId }));
307
316
  await this.sendRpcRequest("thread/settings/update", { threadId: this.sharedThreadId, model: selected.id });
308
317
  return selected;
309
318
  }
310
319
  async setPlanMode(enabled) {
311
320
  this.assertVisibleCodexWorkspace();
312
321
  if (!this.usesRpcTurnTransport() || !this.sharedThreadId) {
313
- throw new Error("Codex plan mode requires an active app-server thread.");
322
+ throw new Error(t("adapter.codex.planThreadRequired"));
314
323
  }
315
324
  const settings = await this.readCodexThreadSettings();
316
325
  if (enabled) {
@@ -318,7 +327,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
318
327
  const modes = isRecord(response) && Array.isArray(response.data) ? response.data : [];
319
328
  const plan = modes.find((item) => isRecord(item) && (item.mode === "plan" || item.name?.toString().toLowerCase() === "plan"));
320
329
  if (!isRecord(plan))
321
- throw new Error("Codex plan mode is not available in this app-server.");
330
+ throw new Error(t("adapter.codex.planUnavailable"));
322
331
  this.previousCollaborationMode = isRecord(settings.collaborationMode) ? settings.collaborationMode : null;
323
332
  const mode = typeof plan.mode === "string" ? plan.mode : "plan";
324
333
  const model = typeof plan.model === "string" ? plan.model : settings.model;
@@ -344,7 +353,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
344
353
  const response = await this.sendRpcRequest("thread/read", { threadId: this.sharedThreadId, includeTurns: false });
345
354
  const thread = isRecord(response) && isRecord(response.thread) ? response.thread : null;
346
355
  if (!thread || typeof thread.model !== "string")
347
- throw new Error("Codex did not return the current thread model.");
356
+ throw new Error(t("adapter.codex.modelMissing"));
348
357
  return { model: thread.model, effort: thread.reasoningEffort, collaborationMode: thread.collaborationMode };
349
358
  }
350
359
  async interrupt() {
@@ -362,35 +371,38 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
362
371
  this.armInterruptFallback();
363
372
  return true;
364
373
  }
365
- async resolveApproval(action) {
366
- if (!this.pendingApproval && this.pendingApprovalRequests.length === 0) {
367
- return false;
374
+ async resolveApproval(action, requestId) {
375
+ const pending = requestId === undefined
376
+ ? this.pendingApprovalRequests[0]
377
+ : this.pendingApprovalRequests.find((request) => this.rpcRequestKey(request.requestId) === requestId);
378
+ if (!pending) {
379
+ return this.pendingApprovalRequests.length ? false : await super.resolveApproval(action, requestId);
368
380
  }
369
- if (this.pendingApprovalRequests.length > 0 && this.rpcSocket) {
370
- for (const request of this.pendingApprovalRequests) {
371
- await this.respondToApprovalRequest(request, action);
372
- }
373
- this.clearPendingApprovalState();
374
- this.setStatus("busy");
381
+ const id = this.rpcRequestKey(pending.requestId);
382
+ if (this.resolvingApprovalIds.has(id))
383
+ return false;
384
+ this.resolvingApprovalIds.add(id);
385
+ try {
386
+ await this.respondToApprovalRequest(pending, action);
387
+ this.rememberSettledApproval(pending.requestId);
388
+ this.pendingApprovalRequests = this.pendingApprovalRequests.filter((request) => request !== pending);
389
+ this.syncPendingApprovals();
375
390
  return true;
376
391
  }
377
- return await super.resolveApproval(action);
392
+ finally {
393
+ this.resolvingApprovalIds.delete(id);
394
+ }
378
395
  }
379
396
  async resolveAllApprovals(action) {
380
- if (this.pendingApprovalRequests.length === 0 && !this.pendingApproval) {
381
- return 0;
382
- }
383
- if (this.pendingApprovalRequests.length > 0 && this.rpcSocket) {
384
- const count = this.pendingApprovalRequests.length;
385
- for (const request of this.pendingApprovalRequests) {
386
- await this.respondToApprovalRequest(request, action);
387
- }
388
- this.clearPendingApprovalState();
389
- this.setStatus("busy");
390
- return count;
397
+ if (!this.pendingApprovalRequests.length)
398
+ return await super.resolveAllApprovals(action);
399
+ let count = 0;
400
+ const ids = this.pendingApprovalRequests.map((request) => this.rpcRequestKey(request.requestId));
401
+ for (const id of ids) {
402
+ if (await this.resolveApproval(action, id))
403
+ count++;
391
404
  }
392
- const ok = await super.resolveApproval(action);
393
- return ok ? 1 : 0;
405
+ return count;
394
406
  }
395
407
  async submitUserInput(answers) {
396
408
  if (!this.pendingUserInputRequest) {
@@ -410,7 +422,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
410
422
  },
411
423
  });
412
424
  this.clearPendingUserInputState();
413
- this.setStatus("busy", "Codex user input submitted.");
425
+ this.setStatus(this.pendingApprovalRequests.length ? "awaiting_approval" : "busy", "Codex user input submitted.");
414
426
  return true;
415
427
  }
416
428
  async dispose() {
@@ -499,11 +511,13 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
499
511
  return;
500
512
  }
501
513
  this.state.lastOutputAt = nowIso();
502
- const approval = detectCliApproval(text);
514
+ const detectedApproval = detectCliApproval(text);
515
+ const approval = detectedApproval ? { ...detectedApproval, requestId: crypto.randomUUID() } : null;
503
516
  if (this.hasAcceptedInput) {
504
517
  if (approval && !this.pendingApproval) {
505
518
  this.pendingApproval = approval;
506
519
  this.state.pendingApproval = approval;
520
+ this.state.pendingApprovals = [approval];
507
521
  this.state.pendingApprovalOrigin = this.state.activeTurnOrigin;
508
522
  this.setStatus("awaiting_approval", "Codex approval is required.");
509
523
  this.emit({
@@ -1037,22 +1051,22 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1037
1051
  async sendPanelTurn(text) {
1038
1052
  this.assertVisibleCodexWorkspace();
1039
1053
  if (this.isNativePanelMode() && !this.nativeProcess) {
1040
- throw new Error("codex panel is not running.");
1054
+ throw new Error(t("adapter.codex.panelNotRunning"));
1041
1055
  }
1042
1056
  this.recoverStaleBusyStateIfNeeded();
1043
1057
  this.recoverStaleActiveTurnStateIfNeeded();
1044
1058
  if (this.pendingApproval) {
1045
- throw new Error("A Codex approval request is pending. Reply with /confirm or /deny.");
1059
+ throw new Error(t("adapter.codex.approvalPending"));
1046
1060
  }
1047
1061
  if (this.pendingUserInputRequest || this.state.pendingUserInput) {
1048
- throw new Error("Codex is waiting for user input. Reply with /answer and your response, or use /stop.");
1062
+ throw new Error(t("adapter.codex.inputPending"));
1049
1063
  }
1050
1064
  if (this.pendingTurnStart || this.activeTurn || this.state.status === "busy") {
1051
1065
  const origin = this.state.activeTurnOrigin;
1052
1066
  if (origin === "local") {
1053
- throw new Error("The local Codex panel is still working. Wait for the current reply or use /stop.");
1067
+ throw new Error(t("adapter.codex.localBusy"));
1054
1068
  }
1055
- throw new Error("codex is still working. Wait for the current reply or use /stop.");
1069
+ throw new Error(t("adapter.codex.busy"));
1056
1070
  }
1057
1071
  this.clearInterruptTimer();
1058
1072
  this.hasAcceptedInput = true;
@@ -1362,6 +1376,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1362
1376
  cleanPanelExitInProgress: this.cleanPanelExitInProgress,
1363
1377
  });
1364
1378
  this.rpcSocket = null;
1379
+ this.clearPendingApprovalState();
1365
1380
  this.lastRpcCloseDetail = `close_code=${code ?? "unknown"}${reason ? ` reason=${truncatePreview(reason, 240)}` : ""}`;
1366
1381
  this.subscribedThreadIds.clear();
1367
1382
  this.rejectPendingRpcRequests("Codex websocket connection closed.");
@@ -1624,20 +1639,20 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1624
1639
  throw new Error("A thread id is required to resume a Codex thread.");
1625
1640
  }
1626
1641
  if (this.pendingVisibleResume) {
1627
- throw new Error("Codex is already switching visible threads.");
1642
+ throw new Error(t("adapter.codex.switching"));
1628
1643
  }
1629
1644
  if (this.pendingApproval || this.pendingApprovalRequests.length > 0) {
1630
- throw new Error("A Codex approval request is pending. Reply with /confirm or /deny.");
1645
+ throw new Error(t("adapter.codex.approvalPending"));
1631
1646
  }
1632
1647
  if (this.pendingUserInputRequest || this.state.pendingUserInput) {
1633
- throw new Error("Codex is waiting for user input. Reply with /answer or use /stop.");
1648
+ throw new Error(t("adapter.codex.inputPendingShort"));
1634
1649
  }
1635
1650
  if (this.pendingTurnStart ||
1636
1651
  this.activeTurn ||
1637
1652
  this.state.status === "busy" ||
1638
1653
  this.state.status === "awaiting_approval" ||
1639
1654
  this.state.status === "awaiting_input") {
1640
- throw new Error("codex is still working. Wait for the current reply or use /stop.");
1655
+ throw new Error(t("adapter.codex.busy"));
1641
1656
  }
1642
1657
  if (this.state.status !== "idle") {
1643
1658
  throw new Error(`Codex cannot switch threads while its status is ${this.state.status}.`);
@@ -1722,14 +1737,14 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1722
1737
  throw new Error("A thread id is required to resume a Codex thread.");
1723
1738
  }
1724
1739
  if (this.pendingApproval) {
1725
- throw new Error("A Codex approval request is pending. Reply with /confirm or /deny.");
1740
+ throw new Error(t("adapter.codex.approvalPending"));
1726
1741
  }
1727
1742
  if (!options.startup &&
1728
1743
  (this.pendingTurnStart ||
1729
1744
  this.activeTurn ||
1730
1745
  this.state.status === "busy" ||
1731
1746
  this.state.status === "awaiting_approval")) {
1732
- throw new Error("codex is still working. Wait for the current reply or use /stop.");
1747
+ throw new Error(t("adapter.codex.busy"));
1733
1748
  }
1734
1749
  const response = await this.sendRpcRequest("thread/resume", {
1735
1750
  threadId: trimmedThreadId,
@@ -1820,7 +1835,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1820
1835
  this.setStatus("idle", "Codex task interrupted.");
1821
1836
  this.emit({
1822
1837
  type: "task_complete",
1823
- summary: "Interrupted",
1838
+ summary: t("task.interrupted"),
1824
1839
  timestamp: nowIso(),
1825
1840
  });
1826
1841
  }, INTERRUPT_SETTLE_DELAY_MS);
@@ -2079,6 +2094,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2079
2094
  }
2080
2095
  }
2081
2096
  trackLocalSharedThread(threadId, options) {
2097
+ if (this.backgroundThreadIds.has(threadId)) {
2098
+ return;
2099
+ }
2082
2100
  const weakSignal = options.signal === "status_changed" ||
2083
2101
  options.signal === "thread_started" ||
2084
2102
  options.signal === "session_fallback";
@@ -2200,10 +2218,33 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2200
2218
  this.bridgeResumeReplayUntilMs = 0;
2201
2219
  }
2202
2220
  clearPendingApprovalState() {
2221
+ this.approvalGeneration++;
2222
+ const hadPending = this.pendingApprovalRequests.length > 0;
2223
+ for (const pending of this.pendingApprovalRequests)
2224
+ this.rememberSettledApproval(pending.requestId);
2203
2225
  this.pendingApprovalRequests = [];
2226
+ this.state.pendingApprovals = [];
2204
2227
  this.pendingApproval = null;
2205
2228
  this.state.pendingApproval = null;
2206
2229
  this.state.pendingApprovalOrigin = undefined;
2230
+ if (hadPending)
2231
+ this.emit({ type: "approval_state_changed", requests: [], timestamp: nowIso() });
2232
+ }
2233
+ syncPendingApprovals() {
2234
+ const previousId = this.pendingApproval?.requestId;
2235
+ const requests = this.pendingApprovalRequests.flatMap((pending) => {
2236
+ const request = buildCodexApprovalRequest(pending.method, pending.params);
2237
+ return request ? [{ ...request, requestId: this.rpcRequestKey(pending.requestId) }] : [];
2238
+ });
2239
+ this.pendingApproval = requests[0] ?? null;
2240
+ this.state.pendingApproval = this.pendingApproval;
2241
+ this.state.pendingApprovals = requests;
2242
+ this.state.pendingApprovalOrigin = this.pendingApprovalRequests[0]?.origin;
2243
+ this.setStatus(requests.length ? "awaiting_approval" : this.state.pendingUserInput ? "awaiting_input" : "busy");
2244
+ this.emit({ type: "approval_state_changed", requests, timestamp: nowIso() });
2245
+ if (requests[0] && requests[0].requestId !== previousId) {
2246
+ this.emit({ type: "approval_required", request: requests[0], timestamp: nowIso() });
2247
+ }
2207
2248
  }
2208
2249
  clearPendingUserInputState() {
2209
2250
  if (this.pendingUserInputTimer) {
@@ -2344,6 +2385,18 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2344
2385
  if (!isRecord(params)) {
2345
2386
  return;
2346
2387
  }
2388
+ if (method === "serverRequest/resolved") {
2389
+ const requestId = getCodexRpcRequestId(params.requestId);
2390
+ if (requestId !== null)
2391
+ this.rememberSettledApproval(requestId);
2392
+ const pending = this.pendingApprovalRequests.find((request) => request.requestId === requestId
2393
+ && (!getNotificationThreadId(params) || request.threadId === getNotificationThreadId(params))
2394
+ && (!getNotificationTurnId(params) || request.turnId === getNotificationTurnId(params)));
2395
+ if (pending) {
2396
+ this.pendingApprovalRequests = this.pendingApprovalRequests.filter((request) => request !== pending);
2397
+ this.syncPendingApprovals();
2398
+ }
2399
+ }
2347
2400
  if (method === "thread/started") {
2348
2401
  this.handleThreadStarted(params);
2349
2402
  return;
@@ -2386,6 +2439,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2386
2439
  if (!threadId || !turnId) {
2387
2440
  return null;
2388
2441
  }
2442
+ if (this.backgroundThreadIds.has(threadId)) {
2443
+ return null;
2444
+ }
2389
2445
  if (this.hasCompletedTurn(turnId)) {
2390
2446
  return null;
2391
2447
  }
@@ -2470,16 +2526,6 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2470
2526
  }
2471
2527
  case "serverRequest/resolved": {
2472
2528
  const requestId = getCodexRpcRequestId(params.requestId);
2473
- if (requestId !== null &&
2474
- this.pendingApprovalRequests.some((r) => r.requestId === requestId && r.turnId === trackedTurn.turnId)) {
2475
- this.pendingApprovalRequests = this.pendingApprovalRequests.filter((r) => r.requestId !== requestId);
2476
- if (this.pendingApprovalRequests.length === 0) {
2477
- this.clearPendingApprovalState();
2478
- }
2479
- if (this.state.status === "awaiting_approval") {
2480
- this.setStatus("busy", "Codex approval resolved.");
2481
- }
2482
- }
2483
2529
  if (requestId !== null &&
2484
2530
  this.pendingUserInputRequest &&
2485
2531
  requestId === this.pendingUserInputRequest.requestId &&
@@ -2553,9 +2599,14 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2553
2599
  });
2554
2600
  return;
2555
2601
  }
2556
- const trackedTurn = this.identifyTrackedTurn("server/request", params) ??
2602
+ const trackedTurn = (method !== "item/tool/requestUserInput" ? this.approvalOwnerForBackgroundThread(params) : null) ??
2603
+ this.identifyTrackedTurn("server/request", params) ??
2557
2604
  this.fallbackTrackedTurnForServerRequest(params);
2558
2605
  if (!trackedTurn) {
2606
+ if (method !== "item/tool/requestUserInput" && this.activeTurn && this.rpcSocket) {
2607
+ this.discoverBackgroundApprovalOwner(requestId, method, params);
2608
+ return;
2609
+ }
2559
2610
  this.sendRpcMessage({
2560
2611
  id: requestId,
2561
2612
  error: {
@@ -2582,7 +2633,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2582
2633
  : { decision: "decline" },
2583
2634
  });
2584
2635
  this.state.lastOutputAt = nowIso();
2585
- this.setStatus("busy", `Codex approval auto-denied: ${truncatePreview(denyMessage, 180)}`);
2636
+ this.setStatus(this.pendingApprovalRequests.length ? "awaiting_approval" : "busy", `Codex approval auto-denied: ${truncatePreview(denyMessage, 180)}`);
2586
2637
  return;
2587
2638
  }
2588
2639
  const autoResponse = getCodexApprovalAutoResponse(method, params);
@@ -2592,7 +2643,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2592
2643
  result: autoResponse.result,
2593
2644
  });
2594
2645
  this.state.lastOutputAt = nowIso();
2595
- this.setStatus("busy", `Codex approval auto-approved: ${truncatePreview(autoResponse.reason, 180)}`);
2646
+ this.setStatus(this.pendingApprovalRequests.length ? "awaiting_approval" : "busy", `Codex approval auto-approved: ${truncatePreview(autoResponse.reason, 180)}`);
2596
2647
  return;
2597
2648
  }
2598
2649
  const request = buildCodexApprovalRequest(method, params);
@@ -2606,24 +2657,20 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2606
2657
  });
2607
2658
  return;
2608
2659
  }
2660
+ if (this.pendingApprovalRequests.some((pending) => pending.requestId === requestId)
2661
+ || this.settledApprovalIds.has(this.rpcRequestKey(requestId)))
2662
+ return;
2609
2663
  this.pendingApprovalRequests.push({
2664
+ ownerTurnId: trackedTurn.turnId,
2610
2665
  requestId,
2611
2666
  method,
2612
- threadId: trackedTurn.threadId,
2613
- turnId: trackedTurn.turnId,
2667
+ threadId: getNotificationThreadId(params) ?? trackedTurn.threadId,
2668
+ turnId: getNotificationTurnId(params) ?? trackedTurn.turnId,
2614
2669
  origin: trackedTurn.origin,
2615
2670
  params,
2616
2671
  });
2617
- this.pendingApproval = request;
2618
- this.state.pendingApproval = request;
2619
- this.state.pendingApprovalOrigin = trackedTurn.origin;
2620
2672
  this.state.lastOutputAt = nowIso();
2621
- this.setStatus("awaiting_approval", `Codex approval is required: ${truncatePreview(request.commandPreview, 180)}`);
2622
- this.emit({
2623
- type: "approval_required",
2624
- request,
2625
- timestamp: nowIso(),
2626
- });
2673
+ this.syncPendingApprovals();
2627
2674
  }
2628
2675
  fallbackTrackedTurnForServerRequest(params) {
2629
2676
  if (this.activeTurn) {
@@ -2631,7 +2678,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2631
2678
  }
2632
2679
  const threadId = getNotificationThreadId(params);
2633
2680
  const turnId = getNotificationTurnId(params);
2634
- if (!threadId || !turnId) {
2681
+ if (!threadId || !turnId || this.backgroundThreadIds.has(threadId)) {
2635
2682
  return null;
2636
2683
  }
2637
2684
  return {
@@ -2663,7 +2710,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2663
2710
  this.state.pendingUserInput = request;
2664
2711
  this.state.pendingUserInputOrigin = trackedTurn.origin;
2665
2712
  this.state.lastOutputAt = nowIso();
2666
- this.setStatus("awaiting_input", "Codex is waiting for user input.");
2713
+ this.setStatus(this.pendingApprovalRequests.length ? "awaiting_approval" : "awaiting_input", "Codex is waiting for user input.");
2667
2714
  this.emit({
2668
2715
  type: "user_input_required",
2669
2716
  request,
@@ -2690,11 +2737,83 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2690
2737
  }, this.userInputAutoResolutionDelayMs);
2691
2738
  this.pendingUserInputTimer.unref?.();
2692
2739
  }
2740
+ rememberSettledApproval(requestId) {
2741
+ this.settledApprovalIds.add(this.rpcRequestKey(requestId));
2742
+ if (this.settledApprovalIds.size > 512)
2743
+ this.settledApprovalIds.delete(this.settledApprovalIds.values().next().value);
2744
+ }
2745
+ discoverBackgroundApprovalOwner(requestId, method, params) {
2746
+ const id = this.rpcRequestKey(requestId);
2747
+ if (this.discoveringApprovalIds.has(id) || this.settledApprovalIds.has(id))
2748
+ return;
2749
+ this.discoveringApprovalIds.add(id);
2750
+ const owner = this.activeTurn;
2751
+ const generation = this.approvalGeneration;
2752
+ void (async () => {
2753
+ let threadId = getNotificationThreadId(params);
2754
+ const seen = new Set();
2755
+ while (threadId && owner && !seen.has(threadId) && seen.size < 32) {
2756
+ if (this.approvalOwnerForBackgroundThread(params))
2757
+ break;
2758
+ seen.add(threadId);
2759
+ const response = await this.sendRpcRequest("thread/read", { threadId, includeTurns: false });
2760
+ if (!isRecord(response) || !isRecord(response.thread))
2761
+ break;
2762
+ const thread = response.thread;
2763
+ if (thread.id !== threadId || typeof thread.cwd !== "string"
2764
+ || normalizeComparablePath(thread.cwd) !== normalizeComparablePath(this.options.cwd)
2765
+ || typeof thread.parentThreadId !== "string")
2766
+ break;
2767
+ this.rememberBackgroundThread(thread);
2768
+ threadId = thread.parentThreadId;
2769
+ }
2770
+ if (generation !== this.approvalGeneration || this.activeTurn !== owner || this.settledApprovalIds.has(id))
2771
+ return;
2772
+ const trackedTurn = this.approvalOwnerForBackgroundThread(params);
2773
+ if (!trackedTurn)
2774
+ throw new Error("Codex approval does not belong to the active parent turn.");
2775
+ this.handleTrackedTurnServerRequest(requestId, method, params, trackedTurn);
2776
+ })().catch((error) => {
2777
+ if (generation === this.approvalGeneration && !this.settledApprovalIds.has(id) && this.rpcSocket) {
2778
+ this.sendRpcMessage({ id: requestId, error: { code: -32602, message: describeUnknownError(error) } });
2779
+ }
2780
+ }).finally(() => this.discoveringApprovalIds.delete(id));
2781
+ }
2782
+ approvalOwnerForBackgroundThread(params) {
2783
+ if (!this.activeTurn)
2784
+ return null;
2785
+ let threadId = getNotificationThreadId(params);
2786
+ const seen = new Set();
2787
+ while (threadId && !seen.has(threadId)) {
2788
+ seen.add(threadId);
2789
+ threadId = this.backgroundThreadParents.get(threadId) ?? null;
2790
+ if (threadId === this.activeTurn.threadId)
2791
+ return this.activeTurn;
2792
+ }
2793
+ return null;
2794
+ }
2795
+ rememberBackgroundThread(thread) {
2796
+ if (!isRecord(thread) || typeof thread.id !== "string" ||
2797
+ (typeof thread.parentThreadId !== "string" && thread.ephemeral !== true)) {
2798
+ return false;
2799
+ }
2800
+ // Later notifications may carry only an id. Keep subagent/ephemeral
2801
+ // threads out of local follow and turn ownership for the server lifetime.
2802
+ this.backgroundThreadIds.add(thread.id);
2803
+ if (typeof thread.parentThreadId === "string"
2804
+ && (typeof thread.cwd !== "string" || normalizeComparablePath(thread.cwd) === normalizeComparablePath(this.options.cwd))) {
2805
+ this.backgroundThreadParents.set(thread.id, thread.parentThreadId);
2806
+ }
2807
+ return true;
2808
+ }
2693
2809
  handleThreadStatusChanged(params) {
2694
2810
  const threadId = extractCodexThreadFollowIdFromStatusChanged(params);
2695
2811
  if (!threadId) {
2696
2812
  return;
2697
2813
  }
2814
+ if (this.rememberBackgroundThread(params.thread) || this.backgroundThreadIds.has(threadId)) {
2815
+ return;
2816
+ }
2698
2817
  if (this.pendingVisibleResume || this.visibleThreadProxy) {
2699
2818
  return;
2700
2819
  }
@@ -2742,6 +2861,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2742
2861
  return;
2743
2862
  }
2744
2863
  const thread = response.thread;
2864
+ if (this.rememberBackgroundThread(thread) || this.backgroundThreadIds.has(threadId)) {
2865
+ return;
2866
+ }
2745
2867
  if (thread.id === threadId &&
2746
2868
  typeof thread.cwd === "string" &&
2747
2869
  normalizeComparablePath(thread.cwd) === normalizeComparablePath(this.options.cwd) &&
@@ -2774,6 +2896,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2774
2896
  if (!threadId) {
2775
2897
  return;
2776
2898
  }
2899
+ if (this.rememberBackgroundThread(params.thread) || this.backgroundThreadIds.has(threadId)) {
2900
+ return;
2901
+ }
2777
2902
  if (this.visibleThreadProxy || this.isBridgeResumeReplay(threadId)) {
2778
2903
  return;
2779
2904
  }
@@ -2781,13 +2906,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2781
2906
  return;
2782
2907
  }
2783
2908
  const thread = isRecord(params.thread) ? params.thread : null;
2784
- if (thread) {
2785
- if ((typeof thread.cwd === "string" &&
2786
- normalizeComparablePath(thread.cwd) !== normalizeComparablePath(this.options.cwd)) ||
2787
- typeof thread.parentThreadId === "string" ||
2788
- thread.ephemeral === true) {
2789
- return;
2790
- }
2909
+ if (typeof thread?.cwd === "string" &&
2910
+ normalizeComparablePath(thread.cwd) !== normalizeComparablePath(this.options.cwd)) {
2911
+ return;
2791
2912
  }
2792
2913
  if (!this.activeTurn ||
2793
2914
  this.activeTurn.threadId === threadId ||
@@ -2876,7 +2997,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2876
2997
  ? "Local terminal turn completed."
2877
2998
  : this.currentPreview;
2878
2999
  if (this.pendingApprovalRequests.length > 0 &&
2879
- this.pendingApprovalRequests.some((r) => r.turnId === trackedTurn.turnId)) {
3000
+ this.pendingApprovalRequests.some((r) => (r.ownerTurnId ?? r.turnId) === trackedTurn.turnId)) {
2880
3001
  this.clearPendingApprovalState();
2881
3002
  }
2882
3003
  if (this.pendingUserInputRequest &&
@@ -3055,6 +3176,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
3055
3176
  const visibleThreadProxy = this.visibleThreadProxy;
3056
3177
  this.visibleThreadProxy = null;
3057
3178
  await visibleThreadProxy?.close();
3179
+ this.backgroundThreadIds.clear();
3180
+ this.backgroundThreadParents.clear();
3181
+ this.settledApprovalIds.clear();
3058
3182
  if (!this.appServer) {
3059
3183
  this.appServerPort = null;
3060
3184
  this.appServerShuttingDown = false;