@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.
- package/README.md +25 -10
- package/bin/wechat-check-update.mjs +0 -0
- package/bin/wechat-claude.mjs +0 -0
- package/bin/wechat-codex.mjs +0 -0
- package/bin/wechat-daemon.mjs +0 -0
- package/bin/wechat-opencode.mjs +0 -0
- package/bin/wechat-pi.mjs +0 -0
- package/bin/wechat-setup.mjs +0 -0
- package/bin/wecom-check-update.mjs +0 -0
- package/bin/wecom-claude.mjs +0 -0
- package/bin/wecom-codex.mjs +0 -0
- package/bin/wecom-daemon.mjs +0 -0
- package/bin/wecom-opencode.mjs +0 -0
- package/bin/wecom-pi.mjs +0 -0
- package/bin/wecom-setup.mjs +0 -0
- package/dist/bridge/adapter-control.js +19 -12
- package/dist/bridge/bridge-adapters.claude.js +219 -115
- package/dist/bridge/bridge-adapters.codex.js +295 -90
- package/dist/bridge/bridge-adapters.core.js +84 -7
- package/dist/bridge/bridge-adapters.opencode.js +344 -74
- package/dist/bridge/bridge-adapters.pi.js +23 -18
- package/dist/bridge/bridge-adapters.shared.js +9 -9
- package/dist/bridge/bridge-session-resume.js +13 -6
- package/dist/bridge/bridge-utils.js +94 -144
- package/dist/bridge/claude-hooks.js +60 -3
- package/dist/bridge/claude-native-controls.js +32 -13
- package/dist/bridge/codex-visible-thread-proxy.js +140 -0
- package/dist/bridge/language-control.js +33 -0
- package/dist/bridge/native-terminal-control.js +38 -10
- package/dist/bridge/opencode-v2-compat.js +458 -0
- package/dist/bridge/wechat-bridge.js +57 -42
- package/dist/channels/channel-driver-factory.js +44 -0
- package/dist/channels/local/local-channel-port.js +40 -0
- package/dist/channels/local/local-driver.js +217 -0
- package/dist/channels/wechat/wechat-driver.js +11 -2
- package/dist/channels/wecom/wecom-driver.js +8 -1
- package/dist/companion/local-companion.js +27 -7
- package/dist/companion/opencode-v2-tui-bridge-plugin.js +93 -0
- package/dist/companion/pi-tui-bridge-extension.js +10 -4
- package/dist/core/bridge-control.js +3 -0
- package/dist/core/bridge-defer.js +3 -2
- package/dist/core/bridge-event-forwarder.js +3 -0
- package/dist/core/channel-types.js +5 -1
- package/dist/daemon/wechat-daemon.js +202 -124
- package/dist/i18n/index.js +25 -11
- package/dist/i18n/messages-en.js +244 -1
- package/dist/i18n/messages-zh.js +244 -1
- package/dist/runtime/legacy-adapter-runtime.js +7 -4
- package/dist/wechat/channel-config.js +3 -2
- 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 <=
|
|
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("
|
|
248
|
+
throw new Error(t("adapter.codex.notRunning"));
|
|
227
249
|
}
|
|
228
250
|
if (this.state.status === "busy") {
|
|
229
|
-
throw new Error("codex
|
|
251
|
+
throw new Error(t("adapter.codex.busy"));
|
|
230
252
|
}
|
|
231
253
|
if (this.pendingApproval) {
|
|
232
|
-
throw new Error("
|
|
254
|
+
throw new Error(t("adapter.codex.approvalPending"));
|
|
233
255
|
}
|
|
234
256
|
if (this.startupBlocker) {
|
|
235
|
-
throw new Error("
|
|
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("
|
|
261
|
+
throw new Error(t("adapter.codex.notRunning"));
|
|
240
262
|
}
|
|
241
263
|
if (this.startupBlocker) {
|
|
242
|
-
throw new Error("
|
|
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("
|
|
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(
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
-
|
|
352
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
this.
|
|
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
|
-
|
|
392
|
+
finally {
|
|
393
|
+
this.resolvingApprovalIds.delete(id);
|
|
394
|
+
}
|
|
363
395
|
}
|
|
364
396
|
async resolveAllApprovals(action) {
|
|
365
|
-
if (this.pendingApprovalRequests.length
|
|
366
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
|
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("
|
|
1059
|
+
throw new Error(t("adapter.codex.approvalPending"));
|
|
1030
1060
|
}
|
|
1031
1061
|
if (this.pendingUserInputRequest || this.state.pendingUserInput) {
|
|
1032
|
-
throw new Error("
|
|
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("
|
|
1067
|
+
throw new Error(t("adapter.codex.localBusy"));
|
|
1038
1068
|
}
|
|
1039
|
-
throw new Error("codex
|
|
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("
|
|
1642
|
+
throw new Error(t("adapter.codex.switching"));
|
|
1608
1643
|
}
|
|
1609
1644
|
if (this.pendingApproval || this.pendingApprovalRequests.length > 0) {
|
|
1610
|
-
throw new Error("
|
|
1645
|
+
throw new Error(t("adapter.codex.approvalPending"));
|
|
1611
1646
|
}
|
|
1612
1647
|
if (this.pendingUserInputRequest || this.state.pendingUserInput) {
|
|
1613
|
-
throw new Error("
|
|
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
|
|
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("
|
|
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
|
|
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: "
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
2708
|
-
|
|
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;
|