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.
- package/README.md +23 -9
- package/dist/bridge/adapter-control.js +19 -12
- package/dist/bridge/bridge-adapters.claude.js +219 -115
- package/dist/bridge/bridge-adapters.codex.js +207 -83
- package/dist/bridge/bridge-adapters.core.js +84 -7
- package/dist/bridge/bridge-adapters.opencode.js +202 -55
- 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 +19 -12
- package/dist/bridge/language-control.js +33 -0
- package/dist/bridge/native-terminal-control.js +26 -10
- package/dist/bridge/opencode-v2-compat.js +11 -3
- 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/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 +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 <=
|
|
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("
|
|
248
|
+
throw new Error(t("adapter.codex.notRunning"));
|
|
240
249
|
}
|
|
241
250
|
if (this.state.status === "busy") {
|
|
242
|
-
throw new Error("codex
|
|
251
|
+
throw new Error(t("adapter.codex.busy"));
|
|
243
252
|
}
|
|
244
253
|
if (this.pendingApproval) {
|
|
245
|
-
throw new Error("
|
|
254
|
+
throw new Error(t("adapter.codex.approvalPending"));
|
|
246
255
|
}
|
|
247
256
|
if (this.startupBlocker) {
|
|
248
|
-
throw new Error("
|
|
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("
|
|
261
|
+
throw new Error(t("adapter.codex.notRunning"));
|
|
253
262
|
}
|
|
254
263
|
if (this.startupBlocker) {
|
|
255
|
-
throw new Error("
|
|
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("
|
|
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(
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
-
|
|
367
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
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();
|
|
375
390
|
return true;
|
|
376
391
|
}
|
|
377
|
-
|
|
392
|
+
finally {
|
|
393
|
+
this.resolvingApprovalIds.delete(id);
|
|
394
|
+
}
|
|
378
395
|
}
|
|
379
396
|
async resolveAllApprovals(action) {
|
|
380
|
-
if (this.pendingApprovalRequests.length
|
|
381
|
-
return
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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("
|
|
1059
|
+
throw new Error(t("adapter.codex.approvalPending"));
|
|
1046
1060
|
}
|
|
1047
1061
|
if (this.pendingUserInputRequest || this.state.pendingUserInput) {
|
|
1048
|
-
throw new Error("
|
|
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("
|
|
1067
|
+
throw new Error(t("adapter.codex.localBusy"));
|
|
1054
1068
|
}
|
|
1055
|
-
throw new Error("codex
|
|
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("
|
|
1642
|
+
throw new Error(t("adapter.codex.switching"));
|
|
1628
1643
|
}
|
|
1629
1644
|
if (this.pendingApproval || this.pendingApprovalRequests.length > 0) {
|
|
1630
|
-
throw new Error("
|
|
1645
|
+
throw new Error(t("adapter.codex.approvalPending"));
|
|
1631
1646
|
}
|
|
1632
1647
|
if (this.pendingUserInputRequest || this.state.pendingUserInput) {
|
|
1633
|
-
throw new Error("
|
|
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
|
|
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("
|
|
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
|
|
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: "
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
2786
|
-
|
|
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;
|