@unlinearity/cli-wechat-bridge 1.2.1 → 1.2.3
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 +146 -73
- package/README_EN.md +544 -0
- package/bin/telegram-check-update.mjs +10 -0
- package/bin/telegram-claude.mjs +10 -0
- package/bin/telegram-codex.mjs +10 -0
- package/bin/telegram-daemon.mjs +10 -0
- package/bin/telegram-opencode.mjs +10 -0
- package/bin/telegram-pi.mjs +10 -0
- package/bin/telegram-setup.mjs +10 -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 +532 -99
- 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-state.js +3 -1
- 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 +154 -72
- package/dist/channels/channel-driver-factory.js +56 -0
- package/dist/channels/local/local-channel-port.js +40 -0
- package/dist/channels/local/local-driver.js +217 -0
- package/dist/channels/telegram/setup-cli.js +12 -0
- package/dist/channels/telegram/setup.js +163 -0
- package/dist/channels/telegram/telegram-channel-port.js +47 -0
- package/dist/channels/telegram/telegram-config.js +66 -0
- package/dist/channels/telegram/telegram-driver.js +106 -0
- package/dist/channels/telegram/telegram-message.js +41 -0
- package/dist/channels/telegram/telegram-transport.js +378 -0
- package/dist/channels/wechat/wechat-driver.js +11 -2
- package/dist/channels/wecom/wecom-driver.js +8 -1
- package/dist/companion/local-companion-start.js +180 -61
- 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 +13 -1
- package/dist/daemon/wechat-daemon.js +239 -126
- package/dist/i18n/index.js +25 -11
- package/dist/i18n/messages-en.js +245 -1
- package/dist/i18n/messages-zh.js +245 -1
- package/dist/runtime/legacy-adapter-runtime.js +7 -4
- package/dist/wechat/channel-config.js +3 -2
- package/package.json +14 -4
- package/skills/codex-bridge/SKILL.md +35 -0
- package/skills/codex-bridge/agents/openai.yaml +4 -0
- package/skills/codex-bridge/scripts/install.mjs +28 -0
|
@@ -5,9 +5,13 @@
|
|
|
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";
|
|
9
|
+
import net from "node:net";
|
|
10
|
+
import os from "node:os";
|
|
8
11
|
import path from "node:path";
|
|
9
12
|
import crypto from "node:crypto";
|
|
10
13
|
import { spawn as spawnChild, spawnSync } from "node:child_process";
|
|
14
|
+
import { WebSocket as WsPackageWebSocket } from "ws";
|
|
11
15
|
import { detectCliApproval, } from "./bridge-utils.js";
|
|
12
16
|
import { normalizeOutput, nowIso, summarizeOutput, truncatePreview, } from "../core/text-utils.js";
|
|
13
17
|
import { AbstractPtyAdapter } from "./bridge-adapters.core.js";
|
|
@@ -29,7 +33,87 @@ export function isCodexVersionInCompatibilityRange(version) {
|
|
|
29
33
|
}
|
|
30
34
|
const major = parts[0] ?? -1;
|
|
31
35
|
const minor = parts[1] ?? -1;
|
|
32
|
-
return major === 0 && minor >= 149 && minor <=
|
|
36
|
+
return major === 0 && minor >= 149 && minor <= 161;
|
|
37
|
+
}
|
|
38
|
+
/** Resolve the shared Codex app-server daemon control socket path. */
|
|
39
|
+
export function resolveCodexDaemonControlSocketPath(codexHomeEnv = process.env.CODEX_HOME) {
|
|
40
|
+
const codexHome = codexHomeEnv && codexHomeEnv.trim()
|
|
41
|
+
? codexHomeEnv
|
|
42
|
+
: path.join(os.homedir(), ".codex");
|
|
43
|
+
return path.join(codexHome, "app-server-control", "app-server-control.sock");
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Pick attachable threads from a `thread/list` response: only threads that are
|
|
47
|
+
* live in the shared daemon (`status.type !== "notLoaded"`), belong to the
|
|
48
|
+
* requested cwd, are root threads, accept direct input, and were not started
|
|
49
|
+
* by a bridge (`originator === "wechat-bridge"`). Newest activity wins.
|
|
50
|
+
*/
|
|
51
|
+
export function selectCodexDaemonAttachCandidates(response, cwd) {
|
|
52
|
+
if (!isRecord(response) || !Array.isArray(response.data)) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
const candidates = [];
|
|
56
|
+
for (const entry of response.data) {
|
|
57
|
+
if (!isRecord(entry))
|
|
58
|
+
continue;
|
|
59
|
+
const id = typeof entry.id === "string" ? entry.id : null;
|
|
60
|
+
if (!id)
|
|
61
|
+
continue;
|
|
62
|
+
const threadCwd = typeof entry.cwd === "string" ? entry.cwd : null;
|
|
63
|
+
if (!threadCwd || normalizeComparablePath(threadCwd) !== normalizeComparablePath(cwd)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (entry.parentThreadId !== null && entry.parentThreadId !== undefined)
|
|
67
|
+
continue;
|
|
68
|
+
if (entry.canAcceptDirectInput === false)
|
|
69
|
+
continue;
|
|
70
|
+
const status = isRecord(entry.status) ? entry.status.type : undefined;
|
|
71
|
+
if (typeof status !== "string" || status === "notLoaded")
|
|
72
|
+
continue;
|
|
73
|
+
const originator = typeof entry.originator === "string" ? entry.originator : null;
|
|
74
|
+
if (originator === "wechat-bridge")
|
|
75
|
+
continue;
|
|
76
|
+
const updatedAt = typeof entry.updatedAt === "number"
|
|
77
|
+
? entry.updatedAt
|
|
78
|
+
: typeof entry.lastUpdatedAt === "number"
|
|
79
|
+
? entry.lastUpdatedAt
|
|
80
|
+
: 0;
|
|
81
|
+
candidates.push({ id, updatedAt, originator });
|
|
82
|
+
}
|
|
83
|
+
return candidates.sort((left, right) => right.updatedAt - left.updatedAt);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Resume params for the bridge's app-server client. In daemon-attach mode the
|
|
87
|
+
* thread already has an owner with its own settings; sending cwd/approval/
|
|
88
|
+
* sandbox overrides would rewrite the shared thread's settings
|
|
89
|
+
* (`thread/settings/updated`), so attach mode resumes without overrides.
|
|
90
|
+
*/
|
|
91
|
+
export function buildCodexThreadResumeParams(params) {
|
|
92
|
+
if (params.attachDaemon) {
|
|
93
|
+
return { threadId: params.threadId, excludeTurns: true };
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
threadId: params.threadId,
|
|
97
|
+
cwd: params.cwd,
|
|
98
|
+
approvalPolicy: "on-request",
|
|
99
|
+
approvalsReviewer: "user",
|
|
100
|
+
sandbox: "workspace-write",
|
|
101
|
+
excludeTurns: true,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** Turn params; daemon-attach mode inherits the thread's existing settings. */
|
|
105
|
+
export function buildCodexTurnStartParams(params) {
|
|
106
|
+
const input = [{ type: "text", text: params.text }];
|
|
107
|
+
if (params.attachDaemon) {
|
|
108
|
+
return { threadId: params.threadId, input };
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
threadId: params.threadId,
|
|
112
|
+
cwd: params.cwd,
|
|
113
|
+
approvalPolicy: "on-request",
|
|
114
|
+
approvalsReviewer: "user",
|
|
115
|
+
input,
|
|
116
|
+
};
|
|
33
117
|
}
|
|
34
118
|
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
119
|
const CODEX_LOCAL_THREAD_ANNOUNCE_SETTLE_MS = 150;
|
|
@@ -95,6 +179,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
95
179
|
rpcRequestCounter = 0;
|
|
96
180
|
pendingRpcRequests = new Map();
|
|
97
181
|
subscribedThreadIds = new Set();
|
|
182
|
+
backgroundThreadIds = new Set();
|
|
98
183
|
pendingThreadSubscriptions = new Map();
|
|
99
184
|
sharedThreadId = null;
|
|
100
185
|
previousCollaborationMode = null;
|
|
@@ -114,6 +199,11 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
114
199
|
bridgeResumeReplayThreadId = null;
|
|
115
200
|
bridgeResumeReplayUntilMs = 0;
|
|
116
201
|
pendingApprovalRequests = [];
|
|
202
|
+
settledApprovalIds = new Set();
|
|
203
|
+
discoveringApprovalIds = new Set();
|
|
204
|
+
approvalGeneration = 0;
|
|
205
|
+
resolvingApprovalIds = new Set();
|
|
206
|
+
backgroundThreadParents = new Map();
|
|
117
207
|
pendingUserInputRequest = null;
|
|
118
208
|
pendingUserInputTimer = null;
|
|
119
209
|
userInputAutoResolutionDelayMs = CODEX_NON_BLOCKING_USER_INPUT_TIMEOUT_MS;
|
|
@@ -145,9 +235,16 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
145
235
|
localClientInstanceId = `${process.pid}-${Date.now().toString(36)}`;
|
|
146
236
|
constructor(options) {
|
|
147
237
|
super(options);
|
|
238
|
+
this.state.supportsApprovalRequestIds = true;
|
|
239
|
+
this.state.pendingApprovals = [];
|
|
148
240
|
this.resumeThreadId = options.sessionStartMode === "new"
|
|
149
241
|
? null
|
|
150
242
|
: options.initialSharedSessionId ?? options.initialSharedThreadId ?? null;
|
|
243
|
+
if (options.codexAttachDaemon) {
|
|
244
|
+
// Daemon attach binds an explicit (or discovered) live thread; stale
|
|
245
|
+
// persisted session ids must not leak into the attach target.
|
|
246
|
+
this.resumeThreadId = options.codexAttachThreadId?.trim() || null;
|
|
247
|
+
}
|
|
151
248
|
if (this.resumeThreadId && options.renderMode !== "panel") {
|
|
152
249
|
this.state.sharedSessionId = this.resumeThreadId;
|
|
153
250
|
this.state.sharedThreadId = this.resumeThreadId;
|
|
@@ -167,6 +264,10 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
167
264
|
: `outside-supported-range=${this.codexCliVersion}`
|
|
168
265
|
: "unknown-version";
|
|
169
266
|
this.setStatus("starting", `Codex protocol compatibility: ${compatibility}.`);
|
|
267
|
+
if (this.options.codexAttachDaemon) {
|
|
268
|
+
await this.startDaemonAttach();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
170
271
|
await this.startAppServer();
|
|
171
272
|
await this.connectRpcClient();
|
|
172
273
|
try {
|
|
@@ -236,23 +337,23 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
236
337
|
return;
|
|
237
338
|
}
|
|
238
339
|
if (!this.pty) {
|
|
239
|
-
throw new Error("
|
|
340
|
+
throw new Error(t("adapter.codex.notRunning"));
|
|
240
341
|
}
|
|
241
342
|
if (this.state.status === "busy") {
|
|
242
|
-
throw new Error("codex
|
|
343
|
+
throw new Error(t("adapter.codex.busy"));
|
|
243
344
|
}
|
|
244
345
|
if (this.pendingApproval) {
|
|
245
|
-
throw new Error("
|
|
346
|
+
throw new Error(t("adapter.codex.approvalPending"));
|
|
246
347
|
}
|
|
247
348
|
if (this.startupBlocker) {
|
|
248
|
-
throw new Error("
|
|
349
|
+
throw new Error(t("adapter.codex.localInputPending"));
|
|
249
350
|
}
|
|
250
351
|
await delay(this.warmupUntilMs - Date.now());
|
|
251
352
|
if (!this.pty) {
|
|
252
|
-
throw new Error("
|
|
353
|
+
throw new Error(t("adapter.codex.notRunning"));
|
|
253
354
|
}
|
|
254
355
|
if (this.startupBlocker) {
|
|
255
|
-
throw new Error("
|
|
356
|
+
throw new Error(t("adapter.codex.localInputPending"));
|
|
256
357
|
}
|
|
257
358
|
this.clearInterruptTimer();
|
|
258
359
|
this.hasAcceptedInput = true;
|
|
@@ -275,11 +376,59 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
275
376
|
if (!this.isHeadlessRuntimeMode()) {
|
|
276
377
|
throw new Error('WeChat /resume requires the managed "wechat-codex" visible client.');
|
|
277
378
|
}
|
|
379
|
+
if (this.options.codexAttachDaemon) {
|
|
380
|
+
await this.resumeDaemonAttachedThread(threadId);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
278
383
|
await this.resumeVisibleSharedThread(threadId);
|
|
279
384
|
}
|
|
385
|
+
/**
|
|
386
|
+
* WeChat /resume in daemon-attach mode: rejoin another thread on the shared
|
|
387
|
+
* daemon. Live threads rejoin in place; cold threads are loaded by the
|
|
388
|
+
* daemon (the bridge never becomes the rollout writer for an embedded
|
|
389
|
+
* desktop session — that resume fails with an active-writer error).
|
|
390
|
+
*/
|
|
391
|
+
async resumeDaemonAttachedThread(threadId) {
|
|
392
|
+
const trimmedThreadId = threadId.trim();
|
|
393
|
+
if (!trimmedThreadId) {
|
|
394
|
+
throw new Error("A thread id is required to resume a Codex thread.");
|
|
395
|
+
}
|
|
396
|
+
if (this.pendingApproval) {
|
|
397
|
+
throw new Error(t("adapter.codex.approvalPending"));
|
|
398
|
+
}
|
|
399
|
+
if (this.pendingTurnStart ||
|
|
400
|
+
this.activeTurn ||
|
|
401
|
+
this.state.status === "busy" ||
|
|
402
|
+
this.state.status === "awaiting_approval") {
|
|
403
|
+
throw new Error(t("adapter.codex.busy"));
|
|
404
|
+
}
|
|
405
|
+
const response = await this.sendRpcRequest("thread/resume", buildCodexThreadResumeParams({
|
|
406
|
+
threadId: trimmedThreadId,
|
|
407
|
+
cwd: this.options.cwd,
|
|
408
|
+
attachDaemon: true,
|
|
409
|
+
}));
|
|
410
|
+
this.validateCodexResumeThread(response, trimmedThreadId);
|
|
411
|
+
if (isRecord(response) && isRecord(response.thread) && typeof response.thread.path === "string") {
|
|
412
|
+
this.sessionFilePath = response.thread.path;
|
|
413
|
+
}
|
|
414
|
+
const previousThreadId = this.sharedThreadId;
|
|
415
|
+
this.subscribedThreadIds.add(trimmedThreadId);
|
|
416
|
+
this.sessionReadOffset = 0;
|
|
417
|
+
this.sessionPartialLine = "";
|
|
418
|
+
this.sessionFinalText = null;
|
|
419
|
+
this.pendingThreadFollowId = null;
|
|
420
|
+
this.updateSharedThread(trimmedThreadId, {
|
|
421
|
+
source: "wechat",
|
|
422
|
+
reason: "wechat_resume",
|
|
423
|
+
notify: true,
|
|
424
|
+
});
|
|
425
|
+
if (previousThreadId && previousThreadId !== trimmedThreadId) {
|
|
426
|
+
await this.unsubscribeCodexThreadBestEffort(previousThreadId);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
280
429
|
async listModels() {
|
|
281
430
|
if (!this.usesRpcTurnTransport() || !this.sharedThreadId) {
|
|
282
|
-
throw new Error("
|
|
431
|
+
throw new Error(t("adapter.codex.modelThreadRequired"));
|
|
283
432
|
}
|
|
284
433
|
const response = await this.sendRpcRequest("model/list", { includeHidden: false });
|
|
285
434
|
const data = isRecord(response) && Array.isArray(response.data) ? response.data : [];
|
|
@@ -303,14 +452,14 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
303
452
|
const models = await this.listModels();
|
|
304
453
|
const selected = models.find((model) => model.id === modelId);
|
|
305
454
|
if (!selected)
|
|
306
|
-
throw new Error(
|
|
455
|
+
throw new Error(t("adapter.codex.modelUnavailable", { model: modelId }));
|
|
307
456
|
await this.sendRpcRequest("thread/settings/update", { threadId: this.sharedThreadId, model: selected.id });
|
|
308
457
|
return selected;
|
|
309
458
|
}
|
|
310
459
|
async setPlanMode(enabled) {
|
|
311
460
|
this.assertVisibleCodexWorkspace();
|
|
312
461
|
if (!this.usesRpcTurnTransport() || !this.sharedThreadId) {
|
|
313
|
-
throw new Error("
|
|
462
|
+
throw new Error(t("adapter.codex.planThreadRequired"));
|
|
314
463
|
}
|
|
315
464
|
const settings = await this.readCodexThreadSettings();
|
|
316
465
|
if (enabled) {
|
|
@@ -318,7 +467,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
318
467
|
const modes = isRecord(response) && Array.isArray(response.data) ? response.data : [];
|
|
319
468
|
const plan = modes.find((item) => isRecord(item) && (item.mode === "plan" || item.name?.toString().toLowerCase() === "plan"));
|
|
320
469
|
if (!isRecord(plan))
|
|
321
|
-
throw new Error("
|
|
470
|
+
throw new Error(t("adapter.codex.planUnavailable"));
|
|
322
471
|
this.previousCollaborationMode = isRecord(settings.collaborationMode) ? settings.collaborationMode : null;
|
|
323
472
|
const mode = typeof plan.mode === "string" ? plan.mode : "plan";
|
|
324
473
|
const model = typeof plan.model === "string" ? plan.model : settings.model;
|
|
@@ -344,7 +493,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
344
493
|
const response = await this.sendRpcRequest("thread/read", { threadId: this.sharedThreadId, includeTurns: false });
|
|
345
494
|
const thread = isRecord(response) && isRecord(response.thread) ? response.thread : null;
|
|
346
495
|
if (!thread || typeof thread.model !== "string")
|
|
347
|
-
throw new Error("
|
|
496
|
+
throw new Error(t("adapter.codex.modelMissing"));
|
|
348
497
|
return { model: thread.model, effort: thread.reasoningEffort, collaborationMode: thread.collaborationMode };
|
|
349
498
|
}
|
|
350
499
|
async interrupt() {
|
|
@@ -362,35 +511,38 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
362
511
|
this.armInterruptFallback();
|
|
363
512
|
return true;
|
|
364
513
|
}
|
|
365
|
-
async resolveApproval(action) {
|
|
366
|
-
|
|
367
|
-
|
|
514
|
+
async resolveApproval(action, requestId) {
|
|
515
|
+
const pending = requestId === undefined
|
|
516
|
+
? this.pendingApprovalRequests[0]
|
|
517
|
+
: this.pendingApprovalRequests.find((request) => this.rpcRequestKey(request.requestId) === requestId);
|
|
518
|
+
if (!pending) {
|
|
519
|
+
return this.pendingApprovalRequests.length ? false : await super.resolveApproval(action, requestId);
|
|
368
520
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
this.
|
|
521
|
+
const id = this.rpcRequestKey(pending.requestId);
|
|
522
|
+
if (this.resolvingApprovalIds.has(id))
|
|
523
|
+
return false;
|
|
524
|
+
this.resolvingApprovalIds.add(id);
|
|
525
|
+
try {
|
|
526
|
+
await this.respondToApprovalRequest(pending, action);
|
|
527
|
+
this.rememberSettledApproval(pending.requestId);
|
|
528
|
+
this.pendingApprovalRequests = this.pendingApprovalRequests.filter((request) => request !== pending);
|
|
529
|
+
this.syncPendingApprovals();
|
|
375
530
|
return true;
|
|
376
531
|
}
|
|
377
|
-
|
|
532
|
+
finally {
|
|
533
|
+
this.resolvingApprovalIds.delete(id);
|
|
534
|
+
}
|
|
378
535
|
}
|
|
379
536
|
async resolveAllApprovals(action) {
|
|
380
|
-
if (this.pendingApprovalRequests.length
|
|
381
|
-
return
|
|
537
|
+
if (!this.pendingApprovalRequests.length)
|
|
538
|
+
return await super.resolveAllApprovals(action);
|
|
539
|
+
let count = 0;
|
|
540
|
+
const ids = this.pendingApprovalRequests.map((request) => this.rpcRequestKey(request.requestId));
|
|
541
|
+
for (const id of ids) {
|
|
542
|
+
if (await this.resolveApproval(action, id))
|
|
543
|
+
count++;
|
|
382
544
|
}
|
|
383
|
-
|
|
384
|
-
const count = this.pendingApprovalRequests.length;
|
|
385
|
-
for (const request of this.pendingApprovalRequests) {
|
|
386
|
-
await this.respondToApprovalRequest(request, action);
|
|
387
|
-
}
|
|
388
|
-
this.clearPendingApprovalState();
|
|
389
|
-
this.setStatus("busy");
|
|
390
|
-
return count;
|
|
391
|
-
}
|
|
392
|
-
const ok = await super.resolveApproval(action);
|
|
393
|
-
return ok ? 1 : 0;
|
|
545
|
+
return count;
|
|
394
546
|
}
|
|
395
547
|
async submitUserInput(answers) {
|
|
396
548
|
if (!this.pendingUserInputRequest) {
|
|
@@ -410,7 +562,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
410
562
|
},
|
|
411
563
|
});
|
|
412
564
|
this.clearPendingUserInputState();
|
|
413
|
-
this.setStatus("busy", "Codex user input submitted.");
|
|
565
|
+
this.setStatus(this.pendingApprovalRequests.length ? "awaiting_approval" : "busy", "Codex user input submitted.");
|
|
414
566
|
return true;
|
|
415
567
|
}
|
|
416
568
|
async dispose() {
|
|
@@ -499,11 +651,13 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
499
651
|
return;
|
|
500
652
|
}
|
|
501
653
|
this.state.lastOutputAt = nowIso();
|
|
502
|
-
const
|
|
654
|
+
const detectedApproval = detectCliApproval(text);
|
|
655
|
+
const approval = detectedApproval ? { ...detectedApproval, requestId: crypto.randomUUID() } : null;
|
|
503
656
|
if (this.hasAcceptedInput) {
|
|
504
657
|
if (approval && !this.pendingApproval) {
|
|
505
658
|
this.pendingApproval = approval;
|
|
506
659
|
this.state.pendingApproval = approval;
|
|
660
|
+
this.state.pendingApprovals = [approval];
|
|
507
661
|
this.state.pendingApprovalOrigin = this.state.activeTurnOrigin;
|
|
508
662
|
this.setStatus("awaiting_approval", "Codex approval is required.");
|
|
509
663
|
this.emit({
|
|
@@ -550,6 +704,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
550
704
|
}
|
|
551
705
|
isCodexClientRunning() {
|
|
552
706
|
if (this.isHeadlessRuntimeMode()) {
|
|
707
|
+
if (this.options.codexAttachDaemon) {
|
|
708
|
+
return Boolean(this.rpcSocket);
|
|
709
|
+
}
|
|
553
710
|
return Boolean(this.appServer);
|
|
554
711
|
}
|
|
555
712
|
return this.isNativePanelMode() ? Boolean(this.nativeProcess) : Boolean(this.pty);
|
|
@@ -1037,22 +1194,22 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1037
1194
|
async sendPanelTurn(text) {
|
|
1038
1195
|
this.assertVisibleCodexWorkspace();
|
|
1039
1196
|
if (this.isNativePanelMode() && !this.nativeProcess) {
|
|
1040
|
-
throw new Error("codex
|
|
1197
|
+
throw new Error(t("adapter.codex.panelNotRunning"));
|
|
1041
1198
|
}
|
|
1042
1199
|
this.recoverStaleBusyStateIfNeeded();
|
|
1043
1200
|
this.recoverStaleActiveTurnStateIfNeeded();
|
|
1044
1201
|
if (this.pendingApproval) {
|
|
1045
|
-
throw new Error("
|
|
1202
|
+
throw new Error(t("adapter.codex.approvalPending"));
|
|
1046
1203
|
}
|
|
1047
1204
|
if (this.pendingUserInputRequest || this.state.pendingUserInput) {
|
|
1048
|
-
throw new Error("
|
|
1205
|
+
throw new Error(t("adapter.codex.inputPending"));
|
|
1049
1206
|
}
|
|
1050
1207
|
if (this.pendingTurnStart || this.activeTurn || this.state.status === "busy") {
|
|
1051
1208
|
const origin = this.state.activeTurnOrigin;
|
|
1052
1209
|
if (origin === "local") {
|
|
1053
|
-
throw new Error("
|
|
1210
|
+
throw new Error(t("adapter.codex.localBusy"));
|
|
1054
1211
|
}
|
|
1055
|
-
throw new Error("codex
|
|
1212
|
+
throw new Error(t("adapter.codex.busy"));
|
|
1056
1213
|
}
|
|
1057
1214
|
this.clearInterruptTimer();
|
|
1058
1215
|
this.hasAcceptedInput = true;
|
|
@@ -1068,18 +1225,12 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1068
1225
|
this.state.activeTurnOrigin = "wechat";
|
|
1069
1226
|
this.setStatus("busy");
|
|
1070
1227
|
try {
|
|
1071
|
-
const response = await this.sendRpcRequest("turn/start", {
|
|
1228
|
+
const response = await this.sendRpcRequest("turn/start", buildCodexTurnStartParams({
|
|
1072
1229
|
threadId,
|
|
1230
|
+
text,
|
|
1073
1231
|
cwd: this.options.cwd,
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
input: [
|
|
1077
|
-
{
|
|
1078
|
-
type: "text",
|
|
1079
|
-
text,
|
|
1080
|
-
},
|
|
1081
|
-
],
|
|
1082
|
-
});
|
|
1232
|
+
attachDaemon: Boolean(this.options.codexAttachDaemon),
|
|
1233
|
+
}));
|
|
1083
1234
|
const turnId = this.extractTurnIdFromResponse(response);
|
|
1084
1235
|
if (!turnId) {
|
|
1085
1236
|
throw new Error("Codex did not return a turn id for the requested turn.");
|
|
@@ -1133,6 +1284,178 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1133
1284
|
this.armInterruptFallback();
|
|
1134
1285
|
return true;
|
|
1135
1286
|
}
|
|
1287
|
+
/**
|
|
1288
|
+
* Daemon-attach mode: instead of spawning an owned app-server, join the
|
|
1289
|
+
* shared Codex app-server daemon (codex >= 0.162) as an additional client
|
|
1290
|
+
* and rebind the bridge to a live thread in this workspace. The daemon and
|
|
1291
|
+
* the owning TUI keep their lifecycle; the bridge is a guest connection.
|
|
1292
|
+
*/
|
|
1293
|
+
async startDaemonAttach() {
|
|
1294
|
+
this.setStatus("starting", "Attaching to the shared Codex app-server daemon...");
|
|
1295
|
+
await this.connectDaemonRpcClient();
|
|
1296
|
+
try {
|
|
1297
|
+
await this.attachDaemonThread();
|
|
1298
|
+
}
|
|
1299
|
+
catch (error) {
|
|
1300
|
+
await this.disconnectRpcClient();
|
|
1301
|
+
throw error;
|
|
1302
|
+
}
|
|
1303
|
+
this.shuttingDown = false;
|
|
1304
|
+
this.cleanPanelExitInProgress = false;
|
|
1305
|
+
this.hasAcceptedInput = true;
|
|
1306
|
+
this.state.startedAt = nowIso();
|
|
1307
|
+
this.state.pendingApproval = null;
|
|
1308
|
+
this.afterStart();
|
|
1309
|
+
this.setStatus("idle", "Attached to the live Codex daemon session.");
|
|
1310
|
+
}
|
|
1311
|
+
async connectDaemonRpcClient() {
|
|
1312
|
+
if (this.rpcSocket) {
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
const socket = await this.openDaemonRpcSocket();
|
|
1316
|
+
this.attachRpcSocket(socket);
|
|
1317
|
+
await this.initializeRpcClient();
|
|
1318
|
+
}
|
|
1319
|
+
async openDaemonRpcSocket() {
|
|
1320
|
+
const socketPath = resolveCodexDaemonControlSocketPath();
|
|
1321
|
+
if (!fs.existsSync(socketPath)) {
|
|
1322
|
+
throw new Error(`No shared Codex app-server daemon socket at ${socketPath}. Attach mode requires Codex >= 0.162 with its shared background server (the default). Sessions started with --no-daemon, --oss, or a --profile, and desktop-app embedded sessions, cannot be attached.`);
|
|
1323
|
+
}
|
|
1324
|
+
return await new Promise((resolve, reject) => {
|
|
1325
|
+
// ws 8.22 ignores the `agent` option and installs its own TCP
|
|
1326
|
+
// createConnection; passing createConnection at the top level is
|
|
1327
|
+
// forwarded to http.request and takes precedence, which is how the
|
|
1328
|
+
// Unix control socket is dialed.
|
|
1329
|
+
const socket = new WsPackageWebSocket("ws://localhost/", {
|
|
1330
|
+
createConnection: () => net.connect(socketPath),
|
|
1331
|
+
});
|
|
1332
|
+
let settled = false;
|
|
1333
|
+
const timer = setTimeout(() => {
|
|
1334
|
+
if (settled) {
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
settled = true;
|
|
1338
|
+
try {
|
|
1339
|
+
socket.close();
|
|
1340
|
+
}
|
|
1341
|
+
catch {
|
|
1342
|
+
// Best effort cleanup after timeout.
|
|
1343
|
+
}
|
|
1344
|
+
reject(new Error(`Timed out connecting to the Codex daemon socket ${socketPath}.`));
|
|
1345
|
+
}, CODEX_APP_SERVER_READY_TIMEOUT_MS);
|
|
1346
|
+
const cleanup = () => {
|
|
1347
|
+
clearTimeout(timer);
|
|
1348
|
+
};
|
|
1349
|
+
socket.once("open", () => {
|
|
1350
|
+
if (settled) {
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
settled = true;
|
|
1354
|
+
cleanup();
|
|
1355
|
+
resolve(socket);
|
|
1356
|
+
});
|
|
1357
|
+
socket.once("error", (error) => {
|
|
1358
|
+
if (settled) {
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
settled = true;
|
|
1362
|
+
cleanup();
|
|
1363
|
+
reject(new Error(`Failed to connect to the Codex daemon socket ${socketPath}: ${String(error)}`));
|
|
1364
|
+
});
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Bind the bridge to a live daemon thread. An explicit thread id wins;
|
|
1369
|
+
* otherwise the newest live interactive thread in the workspace cwd is
|
|
1370
|
+
* selected. `thread/resume` on a running thread rejoins it (history plus a
|
|
1371
|
+
* live subscription) without taking the rollout writer lock, which stays
|
|
1372
|
+
* with the owning client.
|
|
1373
|
+
*/
|
|
1374
|
+
async attachDaemonThread() {
|
|
1375
|
+
const targetThreadId = this.resumeThreadId ?? await this.discoverDaemonAttachThreadId();
|
|
1376
|
+
this.setStatus("starting", `Attaching Codex daemon thread ${targetThreadId.slice(0, 12)}...`);
|
|
1377
|
+
const response = await this.sendRpcRequest("thread/resume", buildCodexThreadResumeParams({
|
|
1378
|
+
threadId: targetThreadId,
|
|
1379
|
+
cwd: this.options.cwd,
|
|
1380
|
+
attachDaemon: true,
|
|
1381
|
+
}));
|
|
1382
|
+
const resumedThreadId = this.extractThreadIdFromResponse(response);
|
|
1383
|
+
if (!resumedThreadId) {
|
|
1384
|
+
throw new Error("Codex daemon did not confirm the attached thread id.");
|
|
1385
|
+
}
|
|
1386
|
+
if (resumedThreadId !== targetThreadId) {
|
|
1387
|
+
throw new Error(`Codex daemon opened ${resumedThreadId}, expected ${targetThreadId}.`);
|
|
1388
|
+
}
|
|
1389
|
+
this.validateCodexThreadWorkspaceIfPresent(response, resumedThreadId);
|
|
1390
|
+
if (isRecord(response) && isRecord(response.thread) && typeof response.thread.path === "string") {
|
|
1391
|
+
this.sessionFilePath = response.thread.path;
|
|
1392
|
+
}
|
|
1393
|
+
this.subscribedThreadIds.add(resumedThreadId);
|
|
1394
|
+
this.resumeThreadId = null;
|
|
1395
|
+
this.updateSharedThread(resumedThreadId, {
|
|
1396
|
+
source: "restore",
|
|
1397
|
+
reason: "startup_restore",
|
|
1398
|
+
notify: true,
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
async discoverDaemonAttachThreadId() {
|
|
1402
|
+
const response = await this.sendRpcRequest("thread/list", {
|
|
1403
|
+
limit: 50,
|
|
1404
|
+
sortKey: "updated_at",
|
|
1405
|
+
sortDirection: "desc",
|
|
1406
|
+
sourceKinds: ["cli", "vscode"],
|
|
1407
|
+
archived: false,
|
|
1408
|
+
cwd: this.options.cwd,
|
|
1409
|
+
});
|
|
1410
|
+
const candidates = selectCodexDaemonAttachCandidates(response, this.options.cwd);
|
|
1411
|
+
if (candidates.length === 0) {
|
|
1412
|
+
throw new Error(`No live Codex daemon session found for ${this.options.cwd}. Open codex (>= 0.162, default shared-server mode) in this directory, then retry. Embedded sessions (desktop app, --no-daemon, --oss, --profile) cannot be attached.`);
|
|
1413
|
+
}
|
|
1414
|
+
if (candidates.length > 1) {
|
|
1415
|
+
const listed = candidates
|
|
1416
|
+
.slice(0, 5)
|
|
1417
|
+
.map((candidate) => candidate.id.slice(0, 8))
|
|
1418
|
+
.join(", ");
|
|
1419
|
+
this.setStatus("starting", `Multiple live Codex daemon sessions for ${this.options.cwd}; attaching the newest (${listed}).`);
|
|
1420
|
+
}
|
|
1421
|
+
return candidates[0].id;
|
|
1422
|
+
}
|
|
1423
|
+
/** Reconnect after the daemon socket dropped unexpectedly. */
|
|
1424
|
+
async reconnectDaemonRpcClient() {
|
|
1425
|
+
const reconnectDeadline = Date.now() + CODEX_RPC_RECONNECT_TIMEOUT_MS;
|
|
1426
|
+
let lastError = "Codex daemon connection closed.";
|
|
1427
|
+
while (!this.shuttingDown &&
|
|
1428
|
+
!this.cleanPanelExitInProgress &&
|
|
1429
|
+
Date.now() < reconnectDeadline) {
|
|
1430
|
+
try {
|
|
1431
|
+
await this.connectDaemonRpcClient();
|
|
1432
|
+
// The daemon may have restarted; rejoin the bound thread. Cold threads
|
|
1433
|
+
// are reloaded from disk, so this also recovers after daemon restarts.
|
|
1434
|
+
if (this.state.sharedThreadId) {
|
|
1435
|
+
await this.ensureSharedThreadSubscribed(this.state.sharedThreadId);
|
|
1436
|
+
}
|
|
1437
|
+
return true;
|
|
1438
|
+
}
|
|
1439
|
+
catch (error) {
|
|
1440
|
+
lastError = describeUnknownError(error);
|
|
1441
|
+
await delay(CODEX_RPC_CONNECT_RETRY_MS);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
if (shouldSuppressCodexTransportFatalError({
|
|
1445
|
+
transportShuttingDown: this.rpcShuttingDown,
|
|
1446
|
+
shuttingDown: this.shuttingDown,
|
|
1447
|
+
cleanPanelExitInProgress: this.cleanPanelExitInProgress,
|
|
1448
|
+
})) {
|
|
1449
|
+
return false;
|
|
1450
|
+
}
|
|
1451
|
+
this.emit({
|
|
1452
|
+
type: "fatal_error",
|
|
1453
|
+
message: `Lost the connection to the shared Codex app-server daemon (${this.lastRpcCloseDetail || lastError}). The attached session may have closed; restart the attach bridge.`,
|
|
1454
|
+
timestamp: nowIso(),
|
|
1455
|
+
});
|
|
1456
|
+
this.terminateCodexClient();
|
|
1457
|
+
return false;
|
|
1458
|
+
}
|
|
1136
1459
|
async startAppServer() {
|
|
1137
1460
|
if (this.appServer) {
|
|
1138
1461
|
return;
|
|
@@ -1362,6 +1685,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1362
1685
|
cleanPanelExitInProgress: this.cleanPanelExitInProgress,
|
|
1363
1686
|
});
|
|
1364
1687
|
this.rpcSocket = null;
|
|
1688
|
+
this.clearPendingApprovalState();
|
|
1365
1689
|
this.lastRpcCloseDetail = `close_code=${code ?? "unknown"}${reason ? ` reason=${truncatePreview(reason, 240)}` : ""}`;
|
|
1366
1690
|
this.subscribedThreadIds.clear();
|
|
1367
1691
|
this.rejectPendingRpcRequests("Codex websocket connection closed.");
|
|
@@ -1383,6 +1707,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1383
1707
|
})) {
|
|
1384
1708
|
return false;
|
|
1385
1709
|
}
|
|
1710
|
+
if (this.options.codexAttachDaemon) {
|
|
1711
|
+
return await this.reconnectDaemonRpcClient();
|
|
1712
|
+
}
|
|
1386
1713
|
if (!this.appServer || !this.appServerPort) {
|
|
1387
1714
|
if (shouldSuppressCodexTransportFatalError({
|
|
1388
1715
|
transportShuttingDown: this.appServerShuttingDown,
|
|
@@ -1524,14 +1851,11 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1524
1851
|
if (this.subscribedThreadIds.has(threadId)) {
|
|
1525
1852
|
return;
|
|
1526
1853
|
}
|
|
1527
|
-
const response = await this.sendRpcRequest("thread/resume", {
|
|
1854
|
+
const response = await this.sendRpcRequest("thread/resume", buildCodexThreadResumeParams({
|
|
1528
1855
|
threadId,
|
|
1529
1856
|
cwd: this.options.cwd,
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
sandbox: "workspace-write",
|
|
1533
|
-
excludeTurns: true,
|
|
1534
|
-
});
|
|
1857
|
+
attachDaemon: Boolean(this.options.codexAttachDaemon),
|
|
1858
|
+
}));
|
|
1535
1859
|
const resumedThreadId = this.extractThreadIdFromResponse(response);
|
|
1536
1860
|
if (!resumedThreadId) {
|
|
1537
1861
|
throw new Error("Codex did not return a thread id while subscribing the bridge client.");
|
|
@@ -1624,20 +1948,20 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1624
1948
|
throw new Error("A thread id is required to resume a Codex thread.");
|
|
1625
1949
|
}
|
|
1626
1950
|
if (this.pendingVisibleResume) {
|
|
1627
|
-
throw new Error("
|
|
1951
|
+
throw new Error(t("adapter.codex.switching"));
|
|
1628
1952
|
}
|
|
1629
1953
|
if (this.pendingApproval || this.pendingApprovalRequests.length > 0) {
|
|
1630
|
-
throw new Error("
|
|
1954
|
+
throw new Error(t("adapter.codex.approvalPending"));
|
|
1631
1955
|
}
|
|
1632
1956
|
if (this.pendingUserInputRequest || this.state.pendingUserInput) {
|
|
1633
|
-
throw new Error("
|
|
1957
|
+
throw new Error(t("adapter.codex.inputPendingShort"));
|
|
1634
1958
|
}
|
|
1635
1959
|
if (this.pendingTurnStart ||
|
|
1636
1960
|
this.activeTurn ||
|
|
1637
1961
|
this.state.status === "busy" ||
|
|
1638
1962
|
this.state.status === "awaiting_approval" ||
|
|
1639
1963
|
this.state.status === "awaiting_input") {
|
|
1640
|
-
throw new Error("codex
|
|
1964
|
+
throw new Error(t("adapter.codex.busy"));
|
|
1641
1965
|
}
|
|
1642
1966
|
if (this.state.status !== "idle") {
|
|
1643
1967
|
throw new Error(`Codex cannot switch threads while its status is ${this.state.status}.`);
|
|
@@ -1722,14 +2046,14 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1722
2046
|
throw new Error("A thread id is required to resume a Codex thread.");
|
|
1723
2047
|
}
|
|
1724
2048
|
if (this.pendingApproval) {
|
|
1725
|
-
throw new Error("
|
|
2049
|
+
throw new Error(t("adapter.codex.approvalPending"));
|
|
1726
2050
|
}
|
|
1727
2051
|
if (!options.startup &&
|
|
1728
2052
|
(this.pendingTurnStart ||
|
|
1729
2053
|
this.activeTurn ||
|
|
1730
2054
|
this.state.status === "busy" ||
|
|
1731
2055
|
this.state.status === "awaiting_approval")) {
|
|
1732
|
-
throw new Error("codex
|
|
2056
|
+
throw new Error(t("adapter.codex.busy"));
|
|
1733
2057
|
}
|
|
1734
2058
|
const response = await this.sendRpcRequest("thread/resume", {
|
|
1735
2059
|
threadId: trimmedThreadId,
|
|
@@ -1820,7 +2144,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1820
2144
|
this.setStatus("idle", "Codex task interrupted.");
|
|
1821
2145
|
this.emit({
|
|
1822
2146
|
type: "task_complete",
|
|
1823
|
-
summary: "
|
|
2147
|
+
summary: t("task.interrupted"),
|
|
1824
2148
|
timestamp: nowIso(),
|
|
1825
2149
|
});
|
|
1826
2150
|
}, INTERRUPT_SETTLE_DELAY_MS);
|
|
@@ -2079,6 +2403,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2079
2403
|
}
|
|
2080
2404
|
}
|
|
2081
2405
|
trackLocalSharedThread(threadId, options) {
|
|
2406
|
+
if (this.backgroundThreadIds.has(threadId)) {
|
|
2407
|
+
return;
|
|
2408
|
+
}
|
|
2082
2409
|
const weakSignal = options.signal === "status_changed" ||
|
|
2083
2410
|
options.signal === "thread_started" ||
|
|
2084
2411
|
options.signal === "session_fallback";
|
|
@@ -2200,10 +2527,33 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2200
2527
|
this.bridgeResumeReplayUntilMs = 0;
|
|
2201
2528
|
}
|
|
2202
2529
|
clearPendingApprovalState() {
|
|
2530
|
+
this.approvalGeneration++;
|
|
2531
|
+
const hadPending = this.pendingApprovalRequests.length > 0;
|
|
2532
|
+
for (const pending of this.pendingApprovalRequests)
|
|
2533
|
+
this.rememberSettledApproval(pending.requestId);
|
|
2203
2534
|
this.pendingApprovalRequests = [];
|
|
2535
|
+
this.state.pendingApprovals = [];
|
|
2204
2536
|
this.pendingApproval = null;
|
|
2205
2537
|
this.state.pendingApproval = null;
|
|
2206
2538
|
this.state.pendingApprovalOrigin = undefined;
|
|
2539
|
+
if (hadPending)
|
|
2540
|
+
this.emit({ type: "approval_state_changed", requests: [], timestamp: nowIso() });
|
|
2541
|
+
}
|
|
2542
|
+
syncPendingApprovals() {
|
|
2543
|
+
const previousId = this.pendingApproval?.requestId;
|
|
2544
|
+
const requests = this.pendingApprovalRequests.flatMap((pending) => {
|
|
2545
|
+
const request = buildCodexApprovalRequest(pending.method, pending.params);
|
|
2546
|
+
return request ? [{ ...request, requestId: this.rpcRequestKey(pending.requestId) }] : [];
|
|
2547
|
+
});
|
|
2548
|
+
this.pendingApproval = requests[0] ?? null;
|
|
2549
|
+
this.state.pendingApproval = this.pendingApproval;
|
|
2550
|
+
this.state.pendingApprovals = requests;
|
|
2551
|
+
this.state.pendingApprovalOrigin = this.pendingApprovalRequests[0]?.origin;
|
|
2552
|
+
this.setStatus(requests.length ? "awaiting_approval" : this.state.pendingUserInput ? "awaiting_input" : "busy");
|
|
2553
|
+
this.emit({ type: "approval_state_changed", requests, timestamp: nowIso() });
|
|
2554
|
+
if (requests[0] && requests[0].requestId !== previousId) {
|
|
2555
|
+
this.emit({ type: "approval_required", request: requests[0], timestamp: nowIso() });
|
|
2556
|
+
}
|
|
2207
2557
|
}
|
|
2208
2558
|
clearPendingUserInputState() {
|
|
2209
2559
|
if (this.pendingUserInputTimer) {
|
|
@@ -2344,6 +2694,18 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2344
2694
|
if (!isRecord(params)) {
|
|
2345
2695
|
return;
|
|
2346
2696
|
}
|
|
2697
|
+
if (method === "serverRequest/resolved") {
|
|
2698
|
+
const requestId = getCodexRpcRequestId(params.requestId);
|
|
2699
|
+
if (requestId !== null)
|
|
2700
|
+
this.rememberSettledApproval(requestId);
|
|
2701
|
+
const pending = this.pendingApprovalRequests.find((request) => request.requestId === requestId
|
|
2702
|
+
&& (!getNotificationThreadId(params) || request.threadId === getNotificationThreadId(params))
|
|
2703
|
+
&& (!getNotificationTurnId(params) || request.turnId === getNotificationTurnId(params)));
|
|
2704
|
+
if (pending) {
|
|
2705
|
+
this.pendingApprovalRequests = this.pendingApprovalRequests.filter((request) => request !== pending);
|
|
2706
|
+
this.syncPendingApprovals();
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2347
2709
|
if (method === "thread/started") {
|
|
2348
2710
|
this.handleThreadStarted(params);
|
|
2349
2711
|
return;
|
|
@@ -2386,6 +2748,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2386
2748
|
if (!threadId || !turnId) {
|
|
2387
2749
|
return null;
|
|
2388
2750
|
}
|
|
2751
|
+
if (this.backgroundThreadIds.has(threadId)) {
|
|
2752
|
+
return null;
|
|
2753
|
+
}
|
|
2389
2754
|
if (this.hasCompletedTurn(turnId)) {
|
|
2390
2755
|
return null;
|
|
2391
2756
|
}
|
|
@@ -2470,16 +2835,6 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2470
2835
|
}
|
|
2471
2836
|
case "serverRequest/resolved": {
|
|
2472
2837
|
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
2838
|
if (requestId !== null &&
|
|
2484
2839
|
this.pendingUserInputRequest &&
|
|
2485
2840
|
requestId === this.pendingUserInputRequest.requestId &&
|
|
@@ -2553,9 +2908,14 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2553
2908
|
});
|
|
2554
2909
|
return;
|
|
2555
2910
|
}
|
|
2556
|
-
const trackedTurn =
|
|
2911
|
+
const trackedTurn = (method !== "item/tool/requestUserInput" ? this.approvalOwnerForBackgroundThread(params) : null) ??
|
|
2912
|
+
this.identifyTrackedTurn("server/request", params) ??
|
|
2557
2913
|
this.fallbackTrackedTurnForServerRequest(params);
|
|
2558
2914
|
if (!trackedTurn) {
|
|
2915
|
+
if (method !== "item/tool/requestUserInput" && this.activeTurn && this.rpcSocket) {
|
|
2916
|
+
this.discoverBackgroundApprovalOwner(requestId, method, params);
|
|
2917
|
+
return;
|
|
2918
|
+
}
|
|
2559
2919
|
this.sendRpcMessage({
|
|
2560
2920
|
id: requestId,
|
|
2561
2921
|
error: {
|
|
@@ -2582,7 +2942,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2582
2942
|
: { decision: "decline" },
|
|
2583
2943
|
});
|
|
2584
2944
|
this.state.lastOutputAt = nowIso();
|
|
2585
|
-
this.setStatus("busy", `Codex approval auto-denied: ${truncatePreview(denyMessage, 180)}`);
|
|
2945
|
+
this.setStatus(this.pendingApprovalRequests.length ? "awaiting_approval" : "busy", `Codex approval auto-denied: ${truncatePreview(denyMessage, 180)}`);
|
|
2586
2946
|
return;
|
|
2587
2947
|
}
|
|
2588
2948
|
const autoResponse = getCodexApprovalAutoResponse(method, params);
|
|
@@ -2592,7 +2952,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2592
2952
|
result: autoResponse.result,
|
|
2593
2953
|
});
|
|
2594
2954
|
this.state.lastOutputAt = nowIso();
|
|
2595
|
-
this.setStatus("busy", `Codex approval auto-approved: ${truncatePreview(autoResponse.reason, 180)}`);
|
|
2955
|
+
this.setStatus(this.pendingApprovalRequests.length ? "awaiting_approval" : "busy", `Codex approval auto-approved: ${truncatePreview(autoResponse.reason, 180)}`);
|
|
2596
2956
|
return;
|
|
2597
2957
|
}
|
|
2598
2958
|
const request = buildCodexApprovalRequest(method, params);
|
|
@@ -2606,24 +2966,20 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2606
2966
|
});
|
|
2607
2967
|
return;
|
|
2608
2968
|
}
|
|
2969
|
+
if (this.pendingApprovalRequests.some((pending) => pending.requestId === requestId)
|
|
2970
|
+
|| this.settledApprovalIds.has(this.rpcRequestKey(requestId)))
|
|
2971
|
+
return;
|
|
2609
2972
|
this.pendingApprovalRequests.push({
|
|
2973
|
+
ownerTurnId: trackedTurn.turnId,
|
|
2610
2974
|
requestId,
|
|
2611
2975
|
method,
|
|
2612
|
-
threadId: trackedTurn.threadId,
|
|
2613
|
-
turnId: trackedTurn.turnId,
|
|
2976
|
+
threadId: getNotificationThreadId(params) ?? trackedTurn.threadId,
|
|
2977
|
+
turnId: getNotificationTurnId(params) ?? trackedTurn.turnId,
|
|
2614
2978
|
origin: trackedTurn.origin,
|
|
2615
2979
|
params,
|
|
2616
2980
|
});
|
|
2617
|
-
this.pendingApproval = request;
|
|
2618
|
-
this.state.pendingApproval = request;
|
|
2619
|
-
this.state.pendingApprovalOrigin = trackedTurn.origin;
|
|
2620
2981
|
this.state.lastOutputAt = nowIso();
|
|
2621
|
-
this.
|
|
2622
|
-
this.emit({
|
|
2623
|
-
type: "approval_required",
|
|
2624
|
-
request,
|
|
2625
|
-
timestamp: nowIso(),
|
|
2626
|
-
});
|
|
2982
|
+
this.syncPendingApprovals();
|
|
2627
2983
|
}
|
|
2628
2984
|
fallbackTrackedTurnForServerRequest(params) {
|
|
2629
2985
|
if (this.activeTurn) {
|
|
@@ -2631,7 +2987,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2631
2987
|
}
|
|
2632
2988
|
const threadId = getNotificationThreadId(params);
|
|
2633
2989
|
const turnId = getNotificationTurnId(params);
|
|
2634
|
-
if (!threadId || !turnId) {
|
|
2990
|
+
if (!threadId || !turnId || this.backgroundThreadIds.has(threadId)) {
|
|
2635
2991
|
return null;
|
|
2636
2992
|
}
|
|
2637
2993
|
return {
|
|
@@ -2663,7 +3019,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2663
3019
|
this.state.pendingUserInput = request;
|
|
2664
3020
|
this.state.pendingUserInputOrigin = trackedTurn.origin;
|
|
2665
3021
|
this.state.lastOutputAt = nowIso();
|
|
2666
|
-
this.setStatus("awaiting_input", "Codex is waiting for user input.");
|
|
3022
|
+
this.setStatus(this.pendingApprovalRequests.length ? "awaiting_approval" : "awaiting_input", "Codex is waiting for user input.");
|
|
2667
3023
|
this.emit({
|
|
2668
3024
|
type: "user_input_required",
|
|
2669
3025
|
request,
|
|
@@ -2690,11 +3046,83 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2690
3046
|
}, this.userInputAutoResolutionDelayMs);
|
|
2691
3047
|
this.pendingUserInputTimer.unref?.();
|
|
2692
3048
|
}
|
|
3049
|
+
rememberSettledApproval(requestId) {
|
|
3050
|
+
this.settledApprovalIds.add(this.rpcRequestKey(requestId));
|
|
3051
|
+
if (this.settledApprovalIds.size > 512)
|
|
3052
|
+
this.settledApprovalIds.delete(this.settledApprovalIds.values().next().value);
|
|
3053
|
+
}
|
|
3054
|
+
discoverBackgroundApprovalOwner(requestId, method, params) {
|
|
3055
|
+
const id = this.rpcRequestKey(requestId);
|
|
3056
|
+
if (this.discoveringApprovalIds.has(id) || this.settledApprovalIds.has(id))
|
|
3057
|
+
return;
|
|
3058
|
+
this.discoveringApprovalIds.add(id);
|
|
3059
|
+
const owner = this.activeTurn;
|
|
3060
|
+
const generation = this.approvalGeneration;
|
|
3061
|
+
void (async () => {
|
|
3062
|
+
let threadId = getNotificationThreadId(params);
|
|
3063
|
+
const seen = new Set();
|
|
3064
|
+
while (threadId && owner && !seen.has(threadId) && seen.size < 32) {
|
|
3065
|
+
if (this.approvalOwnerForBackgroundThread(params))
|
|
3066
|
+
break;
|
|
3067
|
+
seen.add(threadId);
|
|
3068
|
+
const response = await this.sendRpcRequest("thread/read", { threadId, includeTurns: false });
|
|
3069
|
+
if (!isRecord(response) || !isRecord(response.thread))
|
|
3070
|
+
break;
|
|
3071
|
+
const thread = response.thread;
|
|
3072
|
+
if (thread.id !== threadId || typeof thread.cwd !== "string"
|
|
3073
|
+
|| normalizeComparablePath(thread.cwd) !== normalizeComparablePath(this.options.cwd)
|
|
3074
|
+
|| typeof thread.parentThreadId !== "string")
|
|
3075
|
+
break;
|
|
3076
|
+
this.rememberBackgroundThread(thread);
|
|
3077
|
+
threadId = thread.parentThreadId;
|
|
3078
|
+
}
|
|
3079
|
+
if (generation !== this.approvalGeneration || this.activeTurn !== owner || this.settledApprovalIds.has(id))
|
|
3080
|
+
return;
|
|
3081
|
+
const trackedTurn = this.approvalOwnerForBackgroundThread(params);
|
|
3082
|
+
if (!trackedTurn)
|
|
3083
|
+
throw new Error("Codex approval does not belong to the active parent turn.");
|
|
3084
|
+
this.handleTrackedTurnServerRequest(requestId, method, params, trackedTurn);
|
|
3085
|
+
})().catch((error) => {
|
|
3086
|
+
if (generation === this.approvalGeneration && !this.settledApprovalIds.has(id) && this.rpcSocket) {
|
|
3087
|
+
this.sendRpcMessage({ id: requestId, error: { code: -32602, message: describeUnknownError(error) } });
|
|
3088
|
+
}
|
|
3089
|
+
}).finally(() => this.discoveringApprovalIds.delete(id));
|
|
3090
|
+
}
|
|
3091
|
+
approvalOwnerForBackgroundThread(params) {
|
|
3092
|
+
if (!this.activeTurn)
|
|
3093
|
+
return null;
|
|
3094
|
+
let threadId = getNotificationThreadId(params);
|
|
3095
|
+
const seen = new Set();
|
|
3096
|
+
while (threadId && !seen.has(threadId)) {
|
|
3097
|
+
seen.add(threadId);
|
|
3098
|
+
threadId = this.backgroundThreadParents.get(threadId) ?? null;
|
|
3099
|
+
if (threadId === this.activeTurn.threadId)
|
|
3100
|
+
return this.activeTurn;
|
|
3101
|
+
}
|
|
3102
|
+
return null;
|
|
3103
|
+
}
|
|
3104
|
+
rememberBackgroundThread(thread) {
|
|
3105
|
+
if (!isRecord(thread) || typeof thread.id !== "string" ||
|
|
3106
|
+
(typeof thread.parentThreadId !== "string" && thread.ephemeral !== true)) {
|
|
3107
|
+
return false;
|
|
3108
|
+
}
|
|
3109
|
+
// Later notifications may carry only an id. Keep subagent/ephemeral
|
|
3110
|
+
// threads out of local follow and turn ownership for the server lifetime.
|
|
3111
|
+
this.backgroundThreadIds.add(thread.id);
|
|
3112
|
+
if (typeof thread.parentThreadId === "string"
|
|
3113
|
+
&& (typeof thread.cwd !== "string" || normalizeComparablePath(thread.cwd) === normalizeComparablePath(this.options.cwd))) {
|
|
3114
|
+
this.backgroundThreadParents.set(thread.id, thread.parentThreadId);
|
|
3115
|
+
}
|
|
3116
|
+
return true;
|
|
3117
|
+
}
|
|
2693
3118
|
handleThreadStatusChanged(params) {
|
|
2694
3119
|
const threadId = extractCodexThreadFollowIdFromStatusChanged(params);
|
|
2695
3120
|
if (!threadId) {
|
|
2696
3121
|
return;
|
|
2697
3122
|
}
|
|
3123
|
+
if (this.rememberBackgroundThread(params.thread) || this.backgroundThreadIds.has(threadId)) {
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
2698
3126
|
if (this.pendingVisibleResume || this.visibleThreadProxy) {
|
|
2699
3127
|
return;
|
|
2700
3128
|
}
|
|
@@ -2742,6 +3170,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2742
3170
|
return;
|
|
2743
3171
|
}
|
|
2744
3172
|
const thread = response.thread;
|
|
3173
|
+
if (this.rememberBackgroundThread(thread) || this.backgroundThreadIds.has(threadId)) {
|
|
3174
|
+
return;
|
|
3175
|
+
}
|
|
2745
3176
|
if (thread.id === threadId &&
|
|
2746
3177
|
typeof thread.cwd === "string" &&
|
|
2747
3178
|
normalizeComparablePath(thread.cwd) === normalizeComparablePath(this.options.cwd) &&
|
|
@@ -2774,6 +3205,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2774
3205
|
if (!threadId) {
|
|
2775
3206
|
return;
|
|
2776
3207
|
}
|
|
3208
|
+
if (this.rememberBackgroundThread(params.thread) || this.backgroundThreadIds.has(threadId)) {
|
|
3209
|
+
return;
|
|
3210
|
+
}
|
|
2777
3211
|
if (this.visibleThreadProxy || this.isBridgeResumeReplay(threadId)) {
|
|
2778
3212
|
return;
|
|
2779
3213
|
}
|
|
@@ -2781,13 +3215,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2781
3215
|
return;
|
|
2782
3216
|
}
|
|
2783
3217
|
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
|
-
}
|
|
3218
|
+
if (typeof thread?.cwd === "string" &&
|
|
3219
|
+
normalizeComparablePath(thread.cwd) !== normalizeComparablePath(this.options.cwd)) {
|
|
3220
|
+
return;
|
|
2791
3221
|
}
|
|
2792
3222
|
if (!this.activeTurn ||
|
|
2793
3223
|
this.activeTurn.threadId === threadId ||
|
|
@@ -2876,7 +3306,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2876
3306
|
? "Local terminal turn completed."
|
|
2877
3307
|
: this.currentPreview;
|
|
2878
3308
|
if (this.pendingApprovalRequests.length > 0 &&
|
|
2879
|
-
this.pendingApprovalRequests.some((r) => r.turnId === trackedTurn.turnId)) {
|
|
3309
|
+
this.pendingApprovalRequests.some((r) => (r.ownerTurnId ?? r.turnId) === trackedTurn.turnId)) {
|
|
2880
3310
|
this.clearPendingApprovalState();
|
|
2881
3311
|
}
|
|
2882
3312
|
if (this.pendingUserInputRequest &&
|
|
@@ -3055,6 +3485,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
3055
3485
|
const visibleThreadProxy = this.visibleThreadProxy;
|
|
3056
3486
|
this.visibleThreadProxy = null;
|
|
3057
3487
|
await visibleThreadProxy?.close();
|
|
3488
|
+
this.backgroundThreadIds.clear();
|
|
3489
|
+
this.backgroundThreadParents.clear();
|
|
3490
|
+
this.settledApprovalIds.clear();
|
|
3058
3491
|
if (!this.appServer) {
|
|
3059
3492
|
this.appServerPort = null;
|
|
3060
3493
|
this.appServerShuttingDown = false;
|