@unlinearity/cli-wechat-bridge 1.2.0 → 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 (50) hide show
  1. package/README.md +25 -10
  2. package/bin/wechat-check-update.mjs +0 -0
  3. package/bin/wechat-claude.mjs +0 -0
  4. package/bin/wechat-codex.mjs +0 -0
  5. package/bin/wechat-daemon.mjs +0 -0
  6. package/bin/wechat-opencode.mjs +0 -0
  7. package/bin/wechat-pi.mjs +0 -0
  8. package/bin/wechat-setup.mjs +0 -0
  9. package/bin/wecom-check-update.mjs +0 -0
  10. package/bin/wecom-claude.mjs +0 -0
  11. package/bin/wecom-codex.mjs +0 -0
  12. package/bin/wecom-daemon.mjs +0 -0
  13. package/bin/wecom-opencode.mjs +0 -0
  14. package/bin/wecom-pi.mjs +0 -0
  15. package/bin/wecom-setup.mjs +0 -0
  16. package/dist/bridge/adapter-control.js +19 -12
  17. package/dist/bridge/bridge-adapters.claude.js +219 -115
  18. package/dist/bridge/bridge-adapters.codex.js +295 -90
  19. package/dist/bridge/bridge-adapters.core.js +84 -7
  20. package/dist/bridge/bridge-adapters.opencode.js +344 -74
  21. package/dist/bridge/bridge-adapters.pi.js +23 -18
  22. package/dist/bridge/bridge-adapters.shared.js +9 -9
  23. package/dist/bridge/bridge-session-resume.js +13 -6
  24. package/dist/bridge/bridge-utils.js +94 -144
  25. package/dist/bridge/claude-hooks.js +60 -3
  26. package/dist/bridge/claude-native-controls.js +32 -13
  27. package/dist/bridge/codex-visible-thread-proxy.js +140 -0
  28. package/dist/bridge/language-control.js +33 -0
  29. package/dist/bridge/native-terminal-control.js +38 -10
  30. package/dist/bridge/opencode-v2-compat.js +458 -0
  31. package/dist/bridge/wechat-bridge.js +57 -42
  32. package/dist/channels/channel-driver-factory.js +44 -0
  33. package/dist/channels/local/local-channel-port.js +40 -0
  34. package/dist/channels/local/local-driver.js +217 -0
  35. package/dist/channels/wechat/wechat-driver.js +11 -2
  36. package/dist/channels/wecom/wecom-driver.js +8 -1
  37. package/dist/companion/local-companion.js +27 -7
  38. package/dist/companion/opencode-v2-tui-bridge-plugin.js +93 -0
  39. package/dist/companion/pi-tui-bridge-extension.js +10 -4
  40. package/dist/core/bridge-control.js +3 -0
  41. package/dist/core/bridge-defer.js +3 -2
  42. package/dist/core/bridge-event-forwarder.js +3 -0
  43. package/dist/core/channel-types.js +5 -1
  44. package/dist/daemon/wechat-daemon.js +202 -124
  45. package/dist/i18n/index.js +25 -11
  46. package/dist/i18n/messages-en.js +244 -1
  47. package/dist/i18n/messages-zh.js +244 -1
  48. package/dist/runtime/legacy-adapter-runtime.js +7 -4
  49. package/dist/wechat/channel-config.js +3 -2
  50. package/package.json +4 -2
@@ -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";
@@ -12,10 +13,11 @@ import { detectCliApproval, } from "./bridge-utils.js";
12
13
  import { normalizeOutput, nowIso, summarizeOutput, truncatePreview, } from "../core/text-utils.js";
13
14
  import { AbstractPtyAdapter } from "./bridge-adapters.core.js";
14
15
  import { killProcessTreeSync } from "./bridge-process-reaper.js";
16
+ import { CodexVisibleThreadProxy } from "./codex-visible-thread-proxy.js";
15
17
  import * as shared from "./bridge-adapters.shared.js";
16
18
  import { requestCodexVisibleClientShutdown, requestCodexVisibleThreadSwitch, } from "../companion/codex-visible-client-link.js";
17
19
  import { readLocalCompanionEndpoint } from "../companion/local-companion-link.js";
18
- import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
20
+ import { appendBoundedLog as appendBridgeLog, BRIDGE_LOG_FILE, ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
19
21
  import { CODEX_REMOTE_AUTH_TOKEN_ENV, LOCAL_CLIENT_PROTOCOL_VERSION, } from "../runtime/runtime-types.js";
20
22
  export function parseCodexCliVersion(output) {
21
23
  const match = output.match(/\b(\d+\.\d+\.\d+)\b/);
@@ -28,7 +30,7 @@ export function isCodexVersionInCompatibilityRange(version) {
28
30
  }
29
31
  const major = parts[0] ?? -1;
30
32
  const minor = parts[1] ?? -1;
31
- return major === 0 && minor >= 149 && minor <= 155;
33
+ return major === 0 && minor >= 149 && minor <= 161;
32
34
  }
33
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;
34
36
  const CODEX_LOCAL_THREAD_ANNOUNCE_SETTLE_MS = 150;
@@ -80,6 +82,8 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
80
82
  appServer = null;
81
83
  nativeProcess = null;
82
84
  appServerPort = null;
85
+ visibleThreadProxy = null;
86
+ visibleOutOfWorkspaceThreadId = null;
83
87
  appServerShuttingDown = false;
84
88
  appServerLog = "";
85
89
  appServerAuthToken = null;
@@ -92,6 +96,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
92
96
  rpcRequestCounter = 0;
93
97
  pendingRpcRequests = new Map();
94
98
  subscribedThreadIds = new Set();
99
+ backgroundThreadIds = new Set();
95
100
  pendingThreadSubscriptions = new Map();
96
101
  sharedThreadId = null;
97
102
  previousCollaborationMode = null;
@@ -111,6 +116,11 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
111
116
  bridgeResumeReplayThreadId = null;
112
117
  bridgeResumeReplayUntilMs = 0;
113
118
  pendingApprovalRequests = [];
119
+ settledApprovalIds = new Set();
120
+ discoveringApprovalIds = new Set();
121
+ approvalGeneration = 0;
122
+ resolvingApprovalIds = new Set();
123
+ backgroundThreadParents = new Map();
114
124
  pendingUserInputRequest = null;
115
125
  pendingUserInputTimer = null;
116
126
  userInputAutoResolutionDelayMs = CODEX_NON_BLOCKING_USER_INPUT_TIMEOUT_MS;
@@ -142,6 +152,8 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
142
152
  localClientInstanceId = `${process.pid}-${Date.now().toString(36)}`;
143
153
  constructor(options) {
144
154
  super(options);
155
+ this.state.supportsApprovalRequestIds = true;
156
+ this.state.pendingApprovals = [];
145
157
  this.resumeThreadId = options.sessionStartMode === "new"
146
158
  ? null
147
159
  : options.initialSharedSessionId ?? options.initialSharedThreadId ?? null;
@@ -166,8 +178,18 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
166
178
  this.setStatus("starting", `Codex protocol compatibility: ${compatibility}.`);
167
179
  await this.startAppServer();
168
180
  await this.connectRpcClient();
169
- await this.restoreInitialSharedThreadIfNeeded();
170
181
  try {
182
+ if (this.isHeadlessRuntimeMode() && this.appServerPort && this.appServerAuthToken) {
183
+ const trace = (message) => appendBridgeLog(BRIDGE_LOG_FILE, `[${nowIso()}] codex_visible_proxy: ${message}\n`);
184
+ this.visibleThreadProxy = await CodexVisibleThreadProxy.start({
185
+ upstreamUrl: `ws://${CODEX_APP_SERVER_HOST}:${this.appServerPort}`,
186
+ token: this.appServerAuthToken,
187
+ onTrace: trace,
188
+ onThreadOpened: (threadId, cwd) => this.handleVisibleCodexThreadOpened(threadId, cwd, trace),
189
+ onVisibleTurn: (threadId) => this.handleVisibleCodexTurn(threadId, trace),
190
+ });
191
+ }
192
+ await this.restoreInitialSharedThreadIfNeeded();
171
193
  if (this.isNativePanelMode()) {
172
194
  await this.startNativeClient();
173
195
  }
@@ -223,23 +245,23 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
223
245
  return;
224
246
  }
225
247
  if (!this.pty) {
226
- throw new Error("codex adapter is not running.");
248
+ throw new Error(t("adapter.codex.notRunning"));
227
249
  }
228
250
  if (this.state.status === "busy") {
229
- throw new Error("codex is still working. Wait for the current reply or use /stop.");
251
+ throw new Error(t("adapter.codex.busy"));
230
252
  }
231
253
  if (this.pendingApproval) {
232
- throw new Error("A Codex approval request is pending. Reply with /confirm or /deny.");
254
+ throw new Error(t("adapter.codex.approvalPending"));
233
255
  }
234
256
  if (this.startupBlocker) {
235
- throw new Error("Codex is waiting for local terminal input before the session can continue.");
257
+ throw new Error(t("adapter.codex.localInputPending"));
236
258
  }
237
259
  await delay(this.warmupUntilMs - Date.now());
238
260
  if (!this.pty) {
239
- throw new Error("codex adapter is not running.");
261
+ throw new Error(t("adapter.codex.notRunning"));
240
262
  }
241
263
  if (this.startupBlocker) {
242
- throw new Error("Codex is waiting for local terminal input before the session can continue.");
264
+ throw new Error(t("adapter.codex.localInputPending"));
243
265
  }
244
266
  this.clearInterruptTimer();
245
267
  this.hasAcceptedInput = true;
@@ -266,7 +288,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
266
288
  }
267
289
  async listModels() {
268
290
  if (!this.usesRpcTurnTransport() || !this.sharedThreadId) {
269
- throw new Error("Codex model selection requires an active app-server thread.");
291
+ throw new Error(t("adapter.codex.modelThreadRequired"));
270
292
  }
271
293
  const response = await this.sendRpcRequest("model/list", { includeHidden: false });
272
294
  const data = isRecord(response) && Array.isArray(response.data) ? response.data : [];
@@ -286,16 +308,18 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
286
308
  return models;
287
309
  }
288
310
  async selectModel(modelId) {
311
+ this.assertVisibleCodexWorkspace();
289
312
  const models = await this.listModels();
290
313
  const selected = models.find((model) => model.id === modelId);
291
314
  if (!selected)
292
- throw new Error(`Codex model ${modelId} is not available.`);
315
+ throw new Error(t("adapter.codex.modelUnavailable", { model: modelId }));
293
316
  await this.sendRpcRequest("thread/settings/update", { threadId: this.sharedThreadId, model: selected.id });
294
317
  return selected;
295
318
  }
296
319
  async setPlanMode(enabled) {
320
+ this.assertVisibleCodexWorkspace();
297
321
  if (!this.usesRpcTurnTransport() || !this.sharedThreadId) {
298
- throw new Error("Codex plan mode requires an active app-server thread.");
322
+ throw new Error(t("adapter.codex.planThreadRequired"));
299
323
  }
300
324
  const settings = await this.readCodexThreadSettings();
301
325
  if (enabled) {
@@ -303,7 +327,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
303
327
  const modes = isRecord(response) && Array.isArray(response.data) ? response.data : [];
304
328
  const plan = modes.find((item) => isRecord(item) && (item.mode === "plan" || item.name?.toString().toLowerCase() === "plan"));
305
329
  if (!isRecord(plan))
306
- throw new Error("Codex plan mode is not available in this app-server.");
330
+ throw new Error(t("adapter.codex.planUnavailable"));
307
331
  this.previousCollaborationMode = isRecord(settings.collaborationMode) ? settings.collaborationMode : null;
308
332
  const mode = typeof plan.mode === "string" ? plan.mode : "plan";
309
333
  const model = typeof plan.model === "string" ? plan.model : settings.model;
@@ -329,7 +353,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
329
353
  const response = await this.sendRpcRequest("thread/read", { threadId: this.sharedThreadId, includeTurns: false });
330
354
  const thread = isRecord(response) && isRecord(response.thread) ? response.thread : null;
331
355
  if (!thread || typeof thread.model !== "string")
332
- throw new Error("Codex did not return the current thread model.");
356
+ throw new Error(t("adapter.codex.modelMissing"));
333
357
  return { model: thread.model, effort: thread.reasoningEffort, collaborationMode: thread.collaborationMode };
334
358
  }
335
359
  async interrupt() {
@@ -347,35 +371,38 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
347
371
  this.armInterruptFallback();
348
372
  return true;
349
373
  }
350
- async resolveApproval(action) {
351
- if (!this.pendingApproval && this.pendingApprovalRequests.length === 0) {
352
- 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);
353
380
  }
354
- if (this.pendingApprovalRequests.length > 0 && this.rpcSocket) {
355
- for (const request of this.pendingApprovalRequests) {
356
- await this.respondToApprovalRequest(request, action);
357
- }
358
- this.clearPendingApprovalState();
359
- 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();
360
390
  return true;
361
391
  }
362
- return await super.resolveApproval(action);
392
+ finally {
393
+ this.resolvingApprovalIds.delete(id);
394
+ }
363
395
  }
364
396
  async resolveAllApprovals(action) {
365
- if (this.pendingApprovalRequests.length === 0 && !this.pendingApproval) {
366
- return 0;
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++;
367
404
  }
368
- if (this.pendingApprovalRequests.length > 0 && this.rpcSocket) {
369
- const count = this.pendingApprovalRequests.length;
370
- for (const request of this.pendingApprovalRequests) {
371
- await this.respondToApprovalRequest(request, action);
372
- }
373
- this.clearPendingApprovalState();
374
- this.setStatus("busy");
375
- return count;
376
- }
377
- const ok = await super.resolveApproval(action);
378
- return ok ? 1 : 0;
405
+ return count;
379
406
  }
380
407
  async submitUserInput(answers) {
381
408
  if (!this.pendingUserInputRequest) {
@@ -395,7 +422,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
395
422
  },
396
423
  });
397
424
  this.clearPendingUserInputState();
398
- this.setStatus("busy", "Codex user input submitted.");
425
+ this.setStatus(this.pendingApprovalRequests.length ? "awaiting_approval" : "busy", "Codex user input submitted.");
399
426
  return true;
400
427
  }
401
428
  async dispose() {
@@ -464,7 +491,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
464
491
  renderMode: "headless",
465
492
  bridgeOwnerPid: process.pid,
466
493
  serverPort: this.appServerPort,
467
- serverUrl: `ws://${CODEX_APP_SERVER_HOST}:${this.appServerPort}`,
494
+ serverUrl: this.visibleThreadProxy?.url ?? `ws://${CODEX_APP_SERVER_HOST}:${this.appServerPort}`,
468
495
  remoteAuthTokenEnv: CODEX_REMOTE_AUTH_TOKEN_ENV,
469
496
  cwd: this.options.cwd,
470
497
  command: this.options.command,
@@ -484,11 +511,13 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
484
511
  return;
485
512
  }
486
513
  this.state.lastOutputAt = nowIso();
487
- const approval = detectCliApproval(text);
514
+ const detectedApproval = detectCliApproval(text);
515
+ const approval = detectedApproval ? { ...detectedApproval, requestId: crypto.randomUUID() } : null;
488
516
  if (this.hasAcceptedInput) {
489
517
  if (approval && !this.pendingApproval) {
490
518
  this.pendingApproval = approval;
491
519
  this.state.pendingApproval = approval;
520
+ this.state.pendingApprovals = [approval];
492
521
  this.state.pendingApprovalOrigin = this.state.activeTurnOrigin;
493
522
  this.setStatus("awaiting_approval", "Codex approval is required.");
494
523
  this.emit({
@@ -1020,23 +1049,24 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1020
1049
  }
1021
1050
  }
1022
1051
  async sendPanelTurn(text) {
1052
+ this.assertVisibleCodexWorkspace();
1023
1053
  if (this.isNativePanelMode() && !this.nativeProcess) {
1024
- throw new Error("codex panel is not running.");
1054
+ throw new Error(t("adapter.codex.panelNotRunning"));
1025
1055
  }
1026
1056
  this.recoverStaleBusyStateIfNeeded();
1027
1057
  this.recoverStaleActiveTurnStateIfNeeded();
1028
1058
  if (this.pendingApproval) {
1029
- throw new Error("A Codex approval request is pending. Reply with /confirm or /deny.");
1059
+ throw new Error(t("adapter.codex.approvalPending"));
1030
1060
  }
1031
1061
  if (this.pendingUserInputRequest || this.state.pendingUserInput) {
1032
- 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"));
1033
1063
  }
1034
1064
  if (this.pendingTurnStart || this.activeTurn || this.state.status === "busy") {
1035
1065
  const origin = this.state.activeTurnOrigin;
1036
1066
  if (origin === "local") {
1037
- 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"));
1038
1068
  }
1039
- throw new Error("codex is still working. Wait for the current reply or use /stop.");
1069
+ throw new Error(t("adapter.codex.busy"));
1040
1070
  }
1041
1071
  this.clearInterruptTimer();
1042
1072
  this.hasAcceptedInput = true;
@@ -1168,6 +1198,8 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1168
1198
  });
1169
1199
  this.appServer = null;
1170
1200
  this.appServerPort = null;
1201
+ void this.visibleThreadProxy?.close();
1202
+ this.visibleThreadProxy = null;
1171
1203
  this.appServerShuttingDown = false;
1172
1204
  this.deleteAppServerAuthTokenFile();
1173
1205
  this.appServerAuthToken = null;
@@ -1190,6 +1222,8 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1190
1222
  });
1191
1223
  this.appServer = null;
1192
1224
  this.appServerPort = null;
1225
+ void this.visibleThreadProxy?.close();
1226
+ this.visibleThreadProxy = null;
1193
1227
  this.appServerShuttingDown = false;
1194
1228
  this.deleteAppServerAuthTokenFile();
1195
1229
  this.appServerAuthToken = null;
@@ -1342,6 +1376,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1342
1376
  cleanPanelExitInProgress: this.cleanPanelExitInProgress,
1343
1377
  });
1344
1378
  this.rpcSocket = null;
1379
+ this.clearPendingApprovalState();
1345
1380
  this.lastRpcCloseDetail = `close_code=${code ?? "unknown"}${reason ? ` reason=${truncatePreview(reason, 240)}` : ""}`;
1346
1381
  this.subscribedThreadIds.clear();
1347
1382
  this.rejectPendingRpcRequests("Codex websocket connection closed.");
@@ -1604,20 +1639,20 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1604
1639
  throw new Error("A thread id is required to resume a Codex thread.");
1605
1640
  }
1606
1641
  if (this.pendingVisibleResume) {
1607
- throw new Error("Codex is already switching visible threads.");
1642
+ throw new Error(t("adapter.codex.switching"));
1608
1643
  }
1609
1644
  if (this.pendingApproval || this.pendingApprovalRequests.length > 0) {
1610
- throw new Error("A Codex approval request is pending. Reply with /confirm or /deny.");
1645
+ throw new Error(t("adapter.codex.approvalPending"));
1611
1646
  }
1612
1647
  if (this.pendingUserInputRequest || this.state.pendingUserInput) {
1613
- throw new Error("Codex is waiting for user input. Reply with /answer or use /stop.");
1648
+ throw new Error(t("adapter.codex.inputPendingShort"));
1614
1649
  }
1615
1650
  if (this.pendingTurnStart ||
1616
1651
  this.activeTurn ||
1617
1652
  this.state.status === "busy" ||
1618
1653
  this.state.status === "awaiting_approval" ||
1619
1654
  this.state.status === "awaiting_input") {
1620
- throw new Error("codex is still working. Wait for the current reply or use /stop.");
1655
+ throw new Error(t("adapter.codex.busy"));
1621
1656
  }
1622
1657
  if (this.state.status !== "idle") {
1623
1658
  throw new Error(`Codex cannot switch threads while its status is ${this.state.status}.`);
@@ -1660,6 +1695,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1660
1695
  reason: "wechat_resume",
1661
1696
  notify: true,
1662
1697
  });
1698
+ this.clearVisibleCodexWorkspaceBlock();
1663
1699
  this.bridgeResumeReplayThreadId = targetThreadId;
1664
1700
  this.bridgeResumeReplayUntilMs = Date.now() + CODEX_RESUME_REPLAY_SETTLE_MS;
1665
1701
  this.setStatus("starting", `Codex resume committed thread ${targetThreadId.slice(0, 12)}.`);
@@ -1701,14 +1737,14 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1701
1737
  throw new Error("A thread id is required to resume a Codex thread.");
1702
1738
  }
1703
1739
  if (this.pendingApproval) {
1704
- throw new Error("A Codex approval request is pending. Reply with /confirm or /deny.");
1740
+ throw new Error(t("adapter.codex.approvalPending"));
1705
1741
  }
1706
1742
  if (!options.startup &&
1707
1743
  (this.pendingTurnStart ||
1708
1744
  this.activeTurn ||
1709
1745
  this.state.status === "busy" ||
1710
1746
  this.state.status === "awaiting_approval")) {
1711
- throw new Error("codex is still working. Wait for the current reply or use /stop.");
1747
+ throw new Error(t("adapter.codex.busy"));
1712
1748
  }
1713
1749
  const response = await this.sendRpcRequest("thread/resume", {
1714
1750
  threadId: trimmedThreadId,
@@ -1799,7 +1835,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1799
1835
  this.setStatus("idle", "Codex task interrupted.");
1800
1836
  this.emit({
1801
1837
  type: "task_complete",
1802
- summary: "Interrupted",
1838
+ summary: t("task.interrupted"),
1803
1839
  timestamp: nowIso(),
1804
1840
  });
1805
1841
  }, INTERRUPT_SETTLE_DELAY_MS);
@@ -1881,6 +1917,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
1881
1917
  if (!options.preserveThread) {
1882
1918
  this.clearPendingThreadAnnouncement();
1883
1919
  this.announcedThreadId = null;
1920
+ this.visibleOutOfWorkspaceThreadId = null;
1884
1921
  }
1885
1922
  if (!options.preserveThread) {
1886
1923
  this.updateSharedThread(null);
@@ -2000,14 +2037,73 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2000
2037
  }, CODEX_LOCAL_THREAD_ANNOUNCE_SETTLE_MS);
2001
2038
  pending.timer.unref?.();
2002
2039
  }
2040
+ handleVisibleCodexThreadOpened(threadId, cwd, trace) {
2041
+ if (this.pendingVisibleResume || this.shuttingDown) {
2042
+ trace?.(`ignored thread=${threadId.slice(0, 12)} reason=bridge_switch`);
2043
+ return;
2044
+ }
2045
+ if (cwd && normalizeComparablePath(cwd) !== normalizeComparablePath(this.options.cwd)) {
2046
+ trace?.(`paused thread=${threadId.slice(0, 12)} reason=workspace`);
2047
+ this.interruptWechatTurnForLocalThreadSwitch(threadId);
2048
+ this.clearPendingThreadAnnouncement();
2049
+ this.pendingThreadFollowId = null;
2050
+ if (this.visibleOutOfWorkspaceThreadId !== threadId) {
2051
+ this.visibleOutOfWorkspaceThreadId = threadId;
2052
+ this.emit({
2053
+ type: "notice",
2054
+ level: "warning",
2055
+ text: `The visible Codex terminal switched to ${cwd}, outside the bridge workspace ${this.options.cwd}. WeChat input is paused instead of being sent to the previous session. Resume a session from the bridge workspace, or restart the bridge in ${cwd}.`,
2056
+ timestamp: nowIso(),
2057
+ });
2058
+ }
2059
+ return;
2060
+ }
2061
+ this.clearVisibleCodexWorkspaceBlock();
2062
+ trace?.(`follow thread=${threadId.slice(0, 12)} source=visible_open`);
2063
+ this.trackLocalSharedThread(threadId, { reason: "local_follow", signal: "visible_request" });
2064
+ }
2065
+ handleVisibleCodexTurn(threadId, trace) {
2066
+ if (this.pendingVisibleResume || this.shuttingDown)
2067
+ return;
2068
+ if (threadId === this.sharedThreadId) {
2069
+ this.clearVisibleCodexWorkspaceBlock();
2070
+ return;
2071
+ }
2072
+ if (!findCodexSessionFile(this.options.cwd, Date.now(), { threadId })) {
2073
+ trace?.(`ignored thread=${threadId.slice(0, 12)} reason=workspace_or_unpersisted`);
2074
+ return;
2075
+ }
2076
+ this.clearVisibleCodexWorkspaceBlock();
2077
+ trace?.(`follow thread=${threadId.slice(0, 12)} source=visible_turn`);
2078
+ this.trackLocalSharedThread(threadId, { reason: "local_follow", signal: "visible_request" });
2079
+ }
2080
+ clearVisibleCodexWorkspaceBlock() {
2081
+ if (!this.visibleOutOfWorkspaceThreadId)
2082
+ return;
2083
+ this.visibleOutOfWorkspaceThreadId = null;
2084
+ this.emit({
2085
+ type: "notice",
2086
+ level: "info",
2087
+ text: `The visible Codex terminal is back in the bridge workspace ${this.options.cwd}. WeChat input has resumed.`,
2088
+ timestamp: nowIso(),
2089
+ });
2090
+ }
2091
+ assertVisibleCodexWorkspace() {
2092
+ if (this.visibleOutOfWorkspaceThreadId) {
2093
+ throw new Error(`The visible Codex terminal is outside the bridge workspace ${this.options.cwd}. WeChat input is paused; resume a session from this workspace or restart the bridge in the terminal's workspace.`);
2094
+ }
2095
+ }
2003
2096
  trackLocalSharedThread(threadId, options) {
2097
+ if (this.backgroundThreadIds.has(threadId)) {
2098
+ return;
2099
+ }
2004
2100
  const weakSignal = options.signal === "status_changed" ||
2005
2101
  options.signal === "thread_started" ||
2006
2102
  options.signal === "session_fallback";
2007
2103
  if (weakSignal && Date.now() < this.localThreadFollowBlockedUntilMs) {
2008
2104
  return;
2009
2105
  }
2010
- if (options.signal === "user_message" || options.signal === "turn_started") {
2106
+ if (options.signal === "user_message" || options.signal === "turn_started" || options.signal === "visible_request") {
2011
2107
  this.localThreadFollowBlockedUntilMs = 0;
2012
2108
  }
2013
2109
  if (this.pendingVisibleResume) {
@@ -2122,10 +2218,33 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2122
2218
  this.bridgeResumeReplayUntilMs = 0;
2123
2219
  }
2124
2220
  clearPendingApprovalState() {
2221
+ this.approvalGeneration++;
2222
+ const hadPending = this.pendingApprovalRequests.length > 0;
2223
+ for (const pending of this.pendingApprovalRequests)
2224
+ this.rememberSettledApproval(pending.requestId);
2125
2225
  this.pendingApprovalRequests = [];
2226
+ this.state.pendingApprovals = [];
2126
2227
  this.pendingApproval = null;
2127
2228
  this.state.pendingApproval = null;
2128
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
+ }
2129
2248
  }
2130
2249
  clearPendingUserInputState() {
2131
2250
  if (this.pendingUserInputTimer) {
@@ -2266,6 +2385,18 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2266
2385
  if (!isRecord(params)) {
2267
2386
  return;
2268
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
+ }
2269
2400
  if (method === "thread/started") {
2270
2401
  this.handleThreadStarted(params);
2271
2402
  return;
@@ -2308,6 +2439,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2308
2439
  if (!threadId || !turnId) {
2309
2440
  return null;
2310
2441
  }
2442
+ if (this.backgroundThreadIds.has(threadId)) {
2443
+ return null;
2444
+ }
2311
2445
  if (this.hasCompletedTurn(turnId)) {
2312
2446
  return null;
2313
2447
  }
@@ -2343,7 +2477,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2343
2477
  origin: "local",
2344
2478
  };
2345
2479
  }
2346
- if (method === "turn/started" && !this.activeTurn) {
2480
+ if (method === "turn/started" && !this.activeTurn && !this.visibleThreadProxy) {
2347
2481
  return {
2348
2482
  threadId,
2349
2483
  turnId,
@@ -2392,16 +2526,6 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2392
2526
  }
2393
2527
  case "serverRequest/resolved": {
2394
2528
  const requestId = getCodexRpcRequestId(params.requestId);
2395
- if (requestId !== null &&
2396
- this.pendingApprovalRequests.some((r) => r.requestId === requestId && r.turnId === trackedTurn.turnId)) {
2397
- this.pendingApprovalRequests = this.pendingApprovalRequests.filter((r) => r.requestId !== requestId);
2398
- if (this.pendingApprovalRequests.length === 0) {
2399
- this.clearPendingApprovalState();
2400
- }
2401
- if (this.state.status === "awaiting_approval") {
2402
- this.setStatus("busy", "Codex approval resolved.");
2403
- }
2404
- }
2405
2529
  if (requestId !== null &&
2406
2530
  this.pendingUserInputRequest &&
2407
2531
  requestId === this.pendingUserInputRequest.requestId &&
@@ -2475,9 +2599,14 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2475
2599
  });
2476
2600
  return;
2477
2601
  }
2478
- const trackedTurn = this.identifyTrackedTurn("server/request", params) ??
2602
+ const trackedTurn = (method !== "item/tool/requestUserInput" ? this.approvalOwnerForBackgroundThread(params) : null) ??
2603
+ this.identifyTrackedTurn("server/request", params) ??
2479
2604
  this.fallbackTrackedTurnForServerRequest(params);
2480
2605
  if (!trackedTurn) {
2606
+ if (method !== "item/tool/requestUserInput" && this.activeTurn && this.rpcSocket) {
2607
+ this.discoverBackgroundApprovalOwner(requestId, method, params);
2608
+ return;
2609
+ }
2481
2610
  this.sendRpcMessage({
2482
2611
  id: requestId,
2483
2612
  error: {
@@ -2504,7 +2633,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2504
2633
  : { decision: "decline" },
2505
2634
  });
2506
2635
  this.state.lastOutputAt = nowIso();
2507
- 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)}`);
2508
2637
  return;
2509
2638
  }
2510
2639
  const autoResponse = getCodexApprovalAutoResponse(method, params);
@@ -2514,7 +2643,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2514
2643
  result: autoResponse.result,
2515
2644
  });
2516
2645
  this.state.lastOutputAt = nowIso();
2517
- 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)}`);
2518
2647
  return;
2519
2648
  }
2520
2649
  const request = buildCodexApprovalRequest(method, params);
@@ -2528,24 +2657,20 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2528
2657
  });
2529
2658
  return;
2530
2659
  }
2660
+ if (this.pendingApprovalRequests.some((pending) => pending.requestId === requestId)
2661
+ || this.settledApprovalIds.has(this.rpcRequestKey(requestId)))
2662
+ return;
2531
2663
  this.pendingApprovalRequests.push({
2664
+ ownerTurnId: trackedTurn.turnId,
2532
2665
  requestId,
2533
2666
  method,
2534
- threadId: trackedTurn.threadId,
2535
- turnId: trackedTurn.turnId,
2667
+ threadId: getNotificationThreadId(params) ?? trackedTurn.threadId,
2668
+ turnId: getNotificationTurnId(params) ?? trackedTurn.turnId,
2536
2669
  origin: trackedTurn.origin,
2537
2670
  params,
2538
2671
  });
2539
- this.pendingApproval = request;
2540
- this.state.pendingApproval = request;
2541
- this.state.pendingApprovalOrigin = trackedTurn.origin;
2542
2672
  this.state.lastOutputAt = nowIso();
2543
- this.setStatus("awaiting_approval", `Codex approval is required: ${truncatePreview(request.commandPreview, 180)}`);
2544
- this.emit({
2545
- type: "approval_required",
2546
- request,
2547
- timestamp: nowIso(),
2548
- });
2673
+ this.syncPendingApprovals();
2549
2674
  }
2550
2675
  fallbackTrackedTurnForServerRequest(params) {
2551
2676
  if (this.activeTurn) {
@@ -2553,7 +2678,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2553
2678
  }
2554
2679
  const threadId = getNotificationThreadId(params);
2555
2680
  const turnId = getNotificationTurnId(params);
2556
- if (!threadId || !turnId) {
2681
+ if (!threadId || !turnId || this.backgroundThreadIds.has(threadId)) {
2557
2682
  return null;
2558
2683
  }
2559
2684
  return {
@@ -2585,7 +2710,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2585
2710
  this.state.pendingUserInput = request;
2586
2711
  this.state.pendingUserInputOrigin = trackedTurn.origin;
2587
2712
  this.state.lastOutputAt = nowIso();
2588
- 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.");
2589
2714
  this.emit({
2590
2715
  type: "user_input_required",
2591
2716
  request,
@@ -2612,12 +2737,84 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2612
2737
  }, this.userInputAutoResolutionDelayMs);
2613
2738
  this.pendingUserInputTimer.unref?.();
2614
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
+ }
2615
2809
  handleThreadStatusChanged(params) {
2616
2810
  const threadId = extractCodexThreadFollowIdFromStatusChanged(params);
2617
2811
  if (!threadId) {
2618
2812
  return;
2619
2813
  }
2620
- if (this.pendingVisibleResume) {
2814
+ if (this.rememberBackgroundThread(params.thread) || this.backgroundThreadIds.has(threadId)) {
2815
+ return;
2816
+ }
2817
+ if (this.pendingVisibleResume || this.visibleThreadProxy) {
2621
2818
  return;
2622
2819
  }
2623
2820
  if (this.isBridgeResumeReplay(threadId)) {
@@ -2664,6 +2861,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2664
2861
  return;
2665
2862
  }
2666
2863
  const thread = response.thread;
2864
+ if (this.rememberBackgroundThread(thread) || this.backgroundThreadIds.has(threadId)) {
2865
+ return;
2866
+ }
2667
2867
  if (thread.id === threadId &&
2668
2868
  typeof thread.cwd === "string" &&
2669
2869
  normalizeComparablePath(thread.cwd) === normalizeComparablePath(this.options.cwd) &&
@@ -2696,20 +2896,19 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2696
2896
  if (!threadId) {
2697
2897
  return;
2698
2898
  }
2699
- if (this.isBridgeResumeReplay(threadId)) {
2899
+ if (this.rememberBackgroundThread(params.thread) || this.backgroundThreadIds.has(threadId)) {
2900
+ return;
2901
+ }
2902
+ if (this.visibleThreadProxy || this.isBridgeResumeReplay(threadId)) {
2700
2903
  return;
2701
2904
  }
2702
2905
  if (this.isRecentlyBridgeOwnedThread(threadId)) {
2703
2906
  return;
2704
2907
  }
2705
2908
  const thread = isRecord(params.thread) ? params.thread : null;
2706
- if (thread) {
2707
- if ((typeof thread.cwd === "string" &&
2708
- normalizeComparablePath(thread.cwd) !== normalizeComparablePath(this.options.cwd)) ||
2709
- typeof thread.parentThreadId === "string" ||
2710
- thread.ephemeral === true) {
2711
- return;
2712
- }
2909
+ if (typeof thread?.cwd === "string" &&
2910
+ normalizeComparablePath(thread.cwd) !== normalizeComparablePath(this.options.cwd)) {
2911
+ return;
2713
2912
  }
2714
2913
  if (!this.activeTurn ||
2715
2914
  this.activeTurn.threadId === threadId ||
@@ -2798,7 +2997,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2798
2997
  ? "Local terminal turn completed."
2799
2998
  : this.currentPreview;
2800
2999
  if (this.pendingApprovalRequests.length > 0 &&
2801
- this.pendingApprovalRequests.some((r) => r.turnId === trackedTurn.turnId)) {
3000
+ this.pendingApprovalRequests.some((r) => (r.ownerTurnId ?? r.turnId) === trackedTurn.turnId)) {
2802
3001
  this.clearPendingApprovalState();
2803
3002
  }
2804
3003
  if (this.pendingUserInputRequest &&
@@ -2974,6 +3173,12 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
2974
3173
  this.rememberCompletedTurn(turnId);
2975
3174
  }
2976
3175
  async stopAppServer() {
3176
+ const visibleThreadProxy = this.visibleThreadProxy;
3177
+ this.visibleThreadProxy = null;
3178
+ await visibleThreadProxy?.close();
3179
+ this.backgroundThreadIds.clear();
3180
+ this.backgroundThreadParents.clear();
3181
+ this.settledApprovalIds.clear();
2977
3182
  if (!this.appServer) {
2978
3183
  this.appServerPort = null;
2979
3184
  this.appServerShuttingDown = false;