@unlinearity/cli-wechat-bridge 1.0.4 → 1.0.6
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 +136 -25
- package/bin/wechat-daemon.mjs +5 -0
- package/dist/bridge/bridge-adapters.claude.js +33 -3
- package/dist/bridge/bridge-adapters.codex.js +216 -6
- package/dist/bridge/bridge-adapters.core.js +46 -6
- package/dist/bridge/bridge-adapters.opencode.js +79 -13
- package/dist/bridge/bridge-adapters.shared.js +215 -1
- package/dist/bridge/bridge-controller.js +6 -2
- package/dist/bridge/bridge-utils.js +92 -9
- package/dist/bridge/claude-hooks.js +64 -3
- package/dist/bridge/wechat-bridge.js +102 -8
- package/dist/companion/codex-remote-client.js +8 -4
- package/dist/companion/local-companion-link.js +56 -26
- package/dist/companion/local-companion-start.js +48 -5
- package/dist/companion/local-companion.js +3 -1
- package/dist/daemon/daemon-link.js +175 -0
- package/dist/daemon/wechat-daemon.js +1202 -0
- package/dist/wechat/channel-config.js +126 -33
- package/dist/wechat/standalone-bot.js +5 -6
- package/dist/wechat/wechat-channel.js +21 -10
- package/dist/wechat/wechat-transport.js +291 -9
- package/package.json +62 -98
|
@@ -7,7 +7,7 @@ import { AbstractPtyAdapter } from "./bridge-adapters.core.js";
|
|
|
7
7
|
import * as shared from "./bridge-adapters.shared.js";
|
|
8
8
|
import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
|
|
9
9
|
import { CODEX_REMOTE_AUTH_TOKEN_ENV, LOCAL_CLIENT_PROTOCOL_VERSION, } from "../runtime/runtime-types.js";
|
|
10
|
-
const { CODEX_APP_SERVER_HOST, CODEX_APP_SERVER_LOG_LIMIT, 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, buildCliEnvironment, buildCodexApprovalRequest, buildCodexCliArgs, buildCodexUserInputRequest, coerceWebSocketMessageData, delay, describeUnknownError, extractCodexFinalTextFromItem, extractCodexThreadFollowIdFromStatusChanged, extractCodexThreadStartedThreadId, extractCodexUserMessageText, findCodexSessionFile, findRecentCodexSessionFileForCwd, getCodexRpcRequestId, getNotificationThreadId, getNotificationTurnId, isRecord, isRecentIsoTimestamp, listCodexResumeSessions, normalizeComparablePath, normalizeCodexRpcError, reserveLocalPort, resolveSpawnTarget, shouldAutoCompleteCodexWechatTurnAfterFinalReply, shouldIgnoreCodexSessionReplayEntry, shouldRecoverCodexStaleBusyState, waitForTcpPort, } = shared;
|
|
10
|
+
const { CODEX_APP_SERVER_HOST, CODEX_APP_SERVER_LOG_LIMIT, 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, buildCliEnvironment, 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;
|
|
11
11
|
const CODEX_LOCAL_THREAD_ANNOUNCE_SETTLE_MS = 150;
|
|
12
12
|
export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
13
13
|
runtimeKind = "codex_runtime_host";
|
|
@@ -24,6 +24,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
24
24
|
cleanPanelExitInProgress = false;
|
|
25
25
|
rpcRequestCounter = 0;
|
|
26
26
|
pendingRpcRequests = new Map();
|
|
27
|
+
subscribedThreadIds = new Set();
|
|
27
28
|
sharedThreadId = null;
|
|
28
29
|
announcedThreadId = null;
|
|
29
30
|
pendingThreadAnnouncement = null;
|
|
@@ -194,6 +195,27 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
194
195
|
}
|
|
195
196
|
return await super.resolveApproval(action);
|
|
196
197
|
}
|
|
198
|
+
async submitUserInput(answers) {
|
|
199
|
+
if (!this.pendingUserInputRequest) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
const request = this.pendingUserInputRequest;
|
|
203
|
+
const responseAnswers = {};
|
|
204
|
+
for (const [questionId, values] of Object.entries(answers)) {
|
|
205
|
+
responseAnswers[questionId] = {
|
|
206
|
+
answers: values,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
this.sendRpcMessage({
|
|
210
|
+
id: request.requestId,
|
|
211
|
+
result: {
|
|
212
|
+
answers: responseAnswers,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
this.clearPendingUserInputState();
|
|
216
|
+
this.setStatus("busy", "Codex user input submitted.");
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
197
219
|
async dispose() {
|
|
198
220
|
this.resetTurnTracking({ preserveThread: false });
|
|
199
221
|
if (this.isEmbeddedCliMode()) {
|
|
@@ -752,6 +774,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
752
774
|
if (this.pendingApproval) {
|
|
753
775
|
throw new Error("A Codex approval request is pending. Reply with /confirm <code> or /deny.");
|
|
754
776
|
}
|
|
777
|
+
if (this.pendingUserInputRequest || this.state.pendingUserInput) {
|
|
778
|
+
throw new Error("Codex is waiting for user input. Reply with /answer and your response, or use /stop.");
|
|
779
|
+
}
|
|
755
780
|
if (this.pendingTurnStart || this.activeTurn || this.state.status === "busy") {
|
|
756
781
|
const origin = this.state.activeTurnOrigin;
|
|
757
782
|
if (origin === "local") {
|
|
@@ -766,6 +791,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
766
791
|
this.rememberInjectedInput(text);
|
|
767
792
|
this.clearPendingApprovalState();
|
|
768
793
|
const threadId = await this.ensureThreadStarted();
|
|
794
|
+
const subscribedBeforeTurnStart = await this.tryEnsureSharedThreadSubscribed(threadId);
|
|
769
795
|
this.pendingTurnStart = true;
|
|
770
796
|
this.pendingTurnThreadId = threadId;
|
|
771
797
|
this.interruptPendingTurnStart = false;
|
|
@@ -793,6 +819,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
793
819
|
turnId,
|
|
794
820
|
origin: "wechat",
|
|
795
821
|
});
|
|
822
|
+
if (!subscribedBeforeTurnStart) {
|
|
823
|
+
await this.tryEnsureSharedThreadSubscribed(threadId);
|
|
824
|
+
}
|
|
796
825
|
if (this.interruptPendingTurnStart) {
|
|
797
826
|
await this.requestActiveTurnInterrupt();
|
|
798
827
|
this.armInterruptFallback();
|
|
@@ -813,11 +842,15 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
813
842
|
if (this.isNativePanelMode() && !this.nativeProcess) {
|
|
814
843
|
return false;
|
|
815
844
|
}
|
|
816
|
-
const turnPending = this.pendingTurnStart ||
|
|
845
|
+
const turnPending = this.pendingTurnStart ||
|
|
846
|
+
this.state.status === "busy" ||
|
|
847
|
+
this.state.status === "awaiting_approval" ||
|
|
848
|
+
this.state.status === "awaiting_input";
|
|
817
849
|
if (!turnPending) {
|
|
818
850
|
return false;
|
|
819
851
|
}
|
|
820
852
|
this.clearPendingApprovalState();
|
|
853
|
+
this.clearPendingUserInputState();
|
|
821
854
|
if (this.pendingTurnStart && !this.activeTurn) {
|
|
822
855
|
this.interruptPendingTurnStart = true;
|
|
823
856
|
this.armInterruptFallback();
|
|
@@ -980,6 +1013,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
980
1013
|
attachRpcSocket(socket) {
|
|
981
1014
|
this.rpcSocket = socket;
|
|
982
1015
|
this.rpcShuttingDown = false;
|
|
1016
|
+
this.subscribedThreadIds.clear();
|
|
983
1017
|
socket.addEventListener("message", (event) => {
|
|
984
1018
|
this.handleRpcMessageData(event.data);
|
|
985
1019
|
});
|
|
@@ -991,6 +1025,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
991
1025
|
const socket = this.rpcSocket;
|
|
992
1026
|
this.rpcSocket = null;
|
|
993
1027
|
this.rpcShuttingDown = true;
|
|
1028
|
+
this.subscribedThreadIds.clear();
|
|
994
1029
|
this.rejectPendingRpcRequests("Codex websocket connection closed.");
|
|
995
1030
|
if (!socket) {
|
|
996
1031
|
this.rpcShuttingDown = false;
|
|
@@ -1028,6 +1063,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1028
1063
|
cleanPanelExitInProgress: this.cleanPanelExitInProgress,
|
|
1029
1064
|
});
|
|
1030
1065
|
this.rpcSocket = null;
|
|
1066
|
+
this.subscribedThreadIds.clear();
|
|
1031
1067
|
this.rejectPendingRpcRequests("Codex websocket connection closed.");
|
|
1032
1068
|
this.rpcShuttingDown = false;
|
|
1033
1069
|
if (expectedShutdown) {
|
|
@@ -1156,9 +1192,44 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1156
1192
|
throw new Error("Codex did not return a thread id for the bridge session.");
|
|
1157
1193
|
}
|
|
1158
1194
|
this.rememberBridgeOwnedThreadSignal(threadId);
|
|
1195
|
+
this.subscribedThreadIds.add(threadId);
|
|
1159
1196
|
this.updateSharedThread(threadId);
|
|
1160
1197
|
return threadId;
|
|
1161
1198
|
}
|
|
1199
|
+
async tryEnsureSharedThreadSubscribed(threadId) {
|
|
1200
|
+
if (this.subscribedThreadIds.has(threadId)) {
|
|
1201
|
+
return true;
|
|
1202
|
+
}
|
|
1203
|
+
try {
|
|
1204
|
+
await this.ensureSharedThreadSubscribed(threadId);
|
|
1205
|
+
return true;
|
|
1206
|
+
}
|
|
1207
|
+
catch {
|
|
1208
|
+
return false;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
async ensureSharedThreadSubscribed(threadId) {
|
|
1212
|
+
if (this.subscribedThreadIds.has(threadId)) {
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
const response = await this.sendRpcRequest("thread/resume", {
|
|
1216
|
+
threadId,
|
|
1217
|
+
cwd: this.options.cwd,
|
|
1218
|
+
approvalPolicy: "on-request",
|
|
1219
|
+
approvalsReviewer: "user",
|
|
1220
|
+
sandbox: "workspace-write",
|
|
1221
|
+
excludeTurns: true,
|
|
1222
|
+
});
|
|
1223
|
+
const resumedThreadId = this.extractThreadIdFromResponse(response);
|
|
1224
|
+
if (!resumedThreadId) {
|
|
1225
|
+
throw new Error("Codex did not return a thread id while subscribing the bridge client.");
|
|
1226
|
+
}
|
|
1227
|
+
this.rememberBridgeOwnedThreadSignal(resumedThreadId);
|
|
1228
|
+
this.subscribedThreadIds.add(resumedThreadId);
|
|
1229
|
+
if (resumedThreadId !== this.sharedThreadId) {
|
|
1230
|
+
this.updateSharedThread(resumedThreadId);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1162
1233
|
async resumeSharedThread(threadId, options = {}) {
|
|
1163
1234
|
const trimmedThreadId = threadId.trim();
|
|
1164
1235
|
if (!trimmedThreadId) {
|
|
@@ -1180,12 +1251,14 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1180
1251
|
approvalPolicy: "on-request",
|
|
1181
1252
|
approvalsReviewer: "user",
|
|
1182
1253
|
sandbox: "workspace-write",
|
|
1254
|
+
excludeTurns: true,
|
|
1183
1255
|
});
|
|
1184
1256
|
const resumedThreadId = this.extractThreadIdFromResponse(response);
|
|
1185
1257
|
if (!resumedThreadId) {
|
|
1186
1258
|
throw new Error("Codex did not return a thread id while resuming the saved thread.");
|
|
1187
1259
|
}
|
|
1188
1260
|
this.rememberBridgeOwnedThreadSignal(resumedThreadId);
|
|
1261
|
+
this.subscribedThreadIds.add(resumedThreadId);
|
|
1189
1262
|
this.sessionFilePath = null;
|
|
1190
1263
|
this.sessionReadOffset = 0;
|
|
1191
1264
|
this.sessionPartialLine = "";
|
|
@@ -1238,7 +1311,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1238
1311
|
this.clearInterruptTimer();
|
|
1239
1312
|
this.interruptTimer = setTimeout(() => {
|
|
1240
1313
|
this.interruptTimer = null;
|
|
1241
|
-
if (this.state.status !== "busy" &&
|
|
1314
|
+
if (this.state.status !== "busy" &&
|
|
1315
|
+
this.state.status !== "awaiting_approval" &&
|
|
1316
|
+
this.state.status !== "awaiting_input") {
|
|
1242
1317
|
return;
|
|
1243
1318
|
}
|
|
1244
1319
|
this.resetTurnTracking({ preserveThread: true });
|
|
@@ -1263,6 +1338,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1263
1338
|
pendingTurnStart: this.pendingTurnStart,
|
|
1264
1339
|
hasActiveTurn: Boolean(this.activeTurn),
|
|
1265
1340
|
hasPendingApproval: Boolean(this.pendingApproval || this.pendingApprovalRequest),
|
|
1341
|
+
hasPendingUserInput: Boolean(this.pendingUserInputRequest || this.state.pendingUserInput),
|
|
1266
1342
|
activeTurnId: this.state.activeTurnId,
|
|
1267
1343
|
})) {
|
|
1268
1344
|
return;
|
|
@@ -1301,6 +1377,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1301
1377
|
this.interruptPendingTurnStart = false;
|
|
1302
1378
|
this.pendingThreadFollowId = null;
|
|
1303
1379
|
this.clearPendingApprovalState();
|
|
1380
|
+
this.clearPendingUserInputState();
|
|
1304
1381
|
this.queuedTurnNotifications = [];
|
|
1305
1382
|
this.queuedTurnServerRequests = [];
|
|
1306
1383
|
this.turnFinalMessages.clear();
|
|
@@ -1503,6 +1580,11 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1503
1580
|
this.state.pendingApproval = null;
|
|
1504
1581
|
this.state.pendingApprovalOrigin = undefined;
|
|
1505
1582
|
}
|
|
1583
|
+
clearPendingUserInputState() {
|
|
1584
|
+
this.pendingUserInputRequest = null;
|
|
1585
|
+
this.state.pendingUserInput = null;
|
|
1586
|
+
this.state.pendingUserInputOrigin = undefined;
|
|
1587
|
+
}
|
|
1506
1588
|
cleanupTurnArtifacts(turnId) {
|
|
1507
1589
|
this.clearFinalReplyCompletionTimerForTurn(turnId);
|
|
1508
1590
|
this.turnFinalMessages.delete(turnId);
|
|
@@ -1570,6 +1652,13 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1570
1652
|
socket.send(JSON.stringify(payload));
|
|
1571
1653
|
}
|
|
1572
1654
|
async respondToApprovalRequest(request, action) {
|
|
1655
|
+
if (request.method === "item/permissions/requestApproval") {
|
|
1656
|
+
this.sendRpcMessage({
|
|
1657
|
+
id: request.requestId,
|
|
1658
|
+
result: buildCodexPermissionsRequestApprovalResponse(request.params, action),
|
|
1659
|
+
});
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1573
1662
|
const decision = action === "confirm" ? "accept" : "decline";
|
|
1574
1663
|
this.sendRpcMessage({
|
|
1575
1664
|
id: request.requestId,
|
|
@@ -1754,6 +1843,15 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1754
1843
|
this.setStatus("busy", "Codex approval resolved.");
|
|
1755
1844
|
}
|
|
1756
1845
|
}
|
|
1846
|
+
if (requestId !== null &&
|
|
1847
|
+
this.pendingUserInputRequest &&
|
|
1848
|
+
requestId === this.pendingUserInputRequest.requestId &&
|
|
1849
|
+
trackedTurn.turnId === this.pendingUserInputRequest.turnId) {
|
|
1850
|
+
this.clearPendingUserInputState();
|
|
1851
|
+
if (this.state.status === "awaiting_input") {
|
|
1852
|
+
this.setStatus("busy", "Codex user input resolved.");
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1757
1855
|
return;
|
|
1758
1856
|
}
|
|
1759
1857
|
case "turn/completed": {
|
|
@@ -1764,8 +1862,24 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1764
1862
|
}
|
|
1765
1863
|
}
|
|
1766
1864
|
handleRpcServerRequest(requestId, method, params) {
|
|
1865
|
+
if (method === "mcpServer/elicitation/request") {
|
|
1866
|
+
this.sendRpcMessage({
|
|
1867
|
+
id: requestId,
|
|
1868
|
+
result: buildCodexMcpServerElicitationDeclineResponse(),
|
|
1869
|
+
});
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
if (method === "item/tool/call") {
|
|
1873
|
+
this.sendRpcMessage({
|
|
1874
|
+
id: requestId,
|
|
1875
|
+
result: buildCodexDynamicToolCallFailureResponse(),
|
|
1876
|
+
});
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1767
1879
|
if (method !== "item/commandExecution/requestApproval" &&
|
|
1768
|
-
method !== "item/fileChange/requestApproval"
|
|
1880
|
+
method !== "item/fileChange/requestApproval" &&
|
|
1881
|
+
method !== "item/permissions/requestApproval" &&
|
|
1882
|
+
method !== "item/tool/requestUserInput") {
|
|
1769
1883
|
this.sendRpcMessage({
|
|
1770
1884
|
id: requestId,
|
|
1771
1885
|
error: {
|
|
@@ -1793,16 +1907,57 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1793
1907
|
});
|
|
1794
1908
|
return;
|
|
1795
1909
|
}
|
|
1796
|
-
const trackedTurn = this.identifyTrackedTurn("server/request", params)
|
|
1910
|
+
const trackedTurn = this.identifyTrackedTurn("server/request", params) ??
|
|
1911
|
+
this.fallbackTrackedTurnForServerRequest(params);
|
|
1797
1912
|
if (!trackedTurn) {
|
|
1913
|
+
this.sendRpcMessage({
|
|
1914
|
+
id: requestId,
|
|
1915
|
+
error: {
|
|
1916
|
+
code: -32602,
|
|
1917
|
+
message: "Invalid Codex server request payload: missing threadId or turnId.",
|
|
1918
|
+
},
|
|
1919
|
+
});
|
|
1798
1920
|
return;
|
|
1799
1921
|
}
|
|
1800
1922
|
this.handleTrackedTurnStarted(trackedTurn);
|
|
1801
1923
|
this.handleTrackedTurnServerRequest(requestId, method, params, trackedTurn);
|
|
1802
1924
|
}
|
|
1803
1925
|
handleTrackedTurnServerRequest(requestId, method, params, trackedTurn) {
|
|
1926
|
+
if (method === "item/tool/requestUserInput") {
|
|
1927
|
+
this.handleTrackedTurnUserInputRequest(requestId, params, trackedTurn);
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
const denyMessage = getCodexWechatOutboundAttachmentDenyMessage(method, params);
|
|
1931
|
+
if (denyMessage) {
|
|
1932
|
+
this.sendRpcMessage({
|
|
1933
|
+
id: requestId,
|
|
1934
|
+
result: method === "item/permissions/requestApproval"
|
|
1935
|
+
? buildCodexPermissionsRequestApprovalResponse(params, "deny")
|
|
1936
|
+
: { decision: "decline" },
|
|
1937
|
+
});
|
|
1938
|
+
this.state.lastOutputAt = nowIso();
|
|
1939
|
+
this.setStatus("busy", `Codex approval auto-denied: ${truncatePreview(denyMessage, 180)}`);
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
const autoResponse = getCodexApprovalAutoResponse(method, params);
|
|
1943
|
+
if (autoResponse) {
|
|
1944
|
+
this.sendRpcMessage({
|
|
1945
|
+
id: requestId,
|
|
1946
|
+
result: autoResponse.result,
|
|
1947
|
+
});
|
|
1948
|
+
this.state.lastOutputAt = nowIso();
|
|
1949
|
+
this.setStatus("busy", `Codex approval auto-approved: ${truncatePreview(autoResponse.reason, 180)}`);
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1804
1952
|
const request = buildCodexApprovalRequest(method, params);
|
|
1805
1953
|
if (!request) {
|
|
1954
|
+
this.sendRpcMessage({
|
|
1955
|
+
id: requestId,
|
|
1956
|
+
error: {
|
|
1957
|
+
code: -32602,
|
|
1958
|
+
message: "Invalid Codex approval request payload.",
|
|
1959
|
+
},
|
|
1960
|
+
});
|
|
1806
1961
|
return;
|
|
1807
1962
|
}
|
|
1808
1963
|
this.pendingApprovalRequest = {
|
|
@@ -1811,18 +1966,63 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1811
1966
|
threadId: trackedTurn.threadId,
|
|
1812
1967
|
turnId: trackedTurn.turnId,
|
|
1813
1968
|
origin: trackedTurn.origin,
|
|
1969
|
+
params,
|
|
1814
1970
|
};
|
|
1815
1971
|
this.pendingApproval = request;
|
|
1816
1972
|
this.state.pendingApproval = request;
|
|
1817
1973
|
this.state.pendingApprovalOrigin = trackedTurn.origin;
|
|
1818
1974
|
this.state.lastOutputAt = nowIso();
|
|
1819
|
-
this.setStatus("awaiting_approval",
|
|
1975
|
+
this.setStatus("awaiting_approval", `Codex approval is required: ${truncatePreview(request.commandPreview, 180)}`);
|
|
1820
1976
|
this.emit({
|
|
1821
1977
|
type: "approval_required",
|
|
1822
1978
|
request,
|
|
1823
1979
|
timestamp: nowIso(),
|
|
1824
1980
|
});
|
|
1825
1981
|
}
|
|
1982
|
+
fallbackTrackedTurnForServerRequest(params) {
|
|
1983
|
+
if (this.activeTurn) {
|
|
1984
|
+
return null;
|
|
1985
|
+
}
|
|
1986
|
+
const threadId = getNotificationThreadId(params);
|
|
1987
|
+
const turnId = getNotificationTurnId(params);
|
|
1988
|
+
if (!threadId || !turnId) {
|
|
1989
|
+
return null;
|
|
1990
|
+
}
|
|
1991
|
+
return {
|
|
1992
|
+
threadId,
|
|
1993
|
+
turnId,
|
|
1994
|
+
origin: this.bridgeOwnedTurnIds.has(turnId) ? "wechat" : "local",
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
handleTrackedTurnUserInputRequest(requestId, params, trackedTurn) {
|
|
1998
|
+
const request = buildCodexUserInputRequest(params);
|
|
1999
|
+
if (!request) {
|
|
2000
|
+
this.sendRpcMessage({
|
|
2001
|
+
id: requestId,
|
|
2002
|
+
error: {
|
|
2003
|
+
code: -32602,
|
|
2004
|
+
message: "Invalid Codex user input request payload.",
|
|
2005
|
+
},
|
|
2006
|
+
});
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
this.pendingUserInputRequest = {
|
|
2010
|
+
requestId,
|
|
2011
|
+
method: "item/tool/requestUserInput",
|
|
2012
|
+
threadId: trackedTurn.threadId,
|
|
2013
|
+
turnId: trackedTurn.turnId,
|
|
2014
|
+
origin: trackedTurn.origin,
|
|
2015
|
+
};
|
|
2016
|
+
this.state.pendingUserInput = request;
|
|
2017
|
+
this.state.pendingUserInputOrigin = trackedTurn.origin;
|
|
2018
|
+
this.state.lastOutputAt = nowIso();
|
|
2019
|
+
this.setStatus("awaiting_input", "Codex is waiting for user input.");
|
|
2020
|
+
this.emit({
|
|
2021
|
+
type: "user_input_required",
|
|
2022
|
+
request,
|
|
2023
|
+
timestamp: nowIso(),
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
1826
2026
|
handleThreadStatusChanged(params) {
|
|
1827
2027
|
const threadId = extractCodexThreadFollowIdFromStatusChanged(params);
|
|
1828
2028
|
if (!threadId) {
|
|
@@ -1935,6 +2135,10 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1935
2135
|
this.pendingApprovalRequest.turnId === trackedTurn.turnId) {
|
|
1936
2136
|
this.clearPendingApprovalState();
|
|
1937
2137
|
}
|
|
2138
|
+
if (this.pendingUserInputRequest &&
|
|
2139
|
+
this.pendingUserInputRequest.turnId === trackedTurn.turnId) {
|
|
2140
|
+
this.clearPendingUserInputState();
|
|
2141
|
+
}
|
|
1938
2142
|
if (this.activeTurn?.turnId === trackedTurn.turnId) {
|
|
1939
2143
|
this.setActiveTurn(null);
|
|
1940
2144
|
}
|
|
@@ -2023,6 +2227,8 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2023
2227
|
this.pendingTurnStart ||
|
|
2024
2228
|
this.pendingApproval ||
|
|
2025
2229
|
this.pendingApprovalRequest ||
|
|
2230
|
+
this.pendingUserInputRequest ||
|
|
2231
|
+
this.state.pendingUserInput ||
|
|
2026
2232
|
!this.collectTurnOutput(turnId)) {
|
|
2027
2233
|
return;
|
|
2028
2234
|
}
|
|
@@ -2039,6 +2245,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2039
2245
|
const finalText = this.collectTurnOutput(turnId);
|
|
2040
2246
|
const lastActivityAtMs = this.turnLastActivityAtMs.get(turnId) ?? null;
|
|
2041
2247
|
const pendingApproval = Boolean(this.pendingApproval || this.pendingApprovalRequest);
|
|
2248
|
+
const pendingUserInput = Boolean(this.pendingUserInputRequest || this.state.pendingUserInput);
|
|
2042
2249
|
const nowMs = Date.now();
|
|
2043
2250
|
if (!shouldAutoCompleteCodexWechatTurnAfterFinalReply({
|
|
2044
2251
|
candidateTurnId: turnId,
|
|
@@ -2046,6 +2253,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2046
2253
|
activeTurnOrigin: activeTurn?.origin,
|
|
2047
2254
|
pendingTurnStart: this.pendingTurnStart,
|
|
2048
2255
|
hasPendingApproval: pendingApproval,
|
|
2256
|
+
hasPendingUserInput: pendingUserInput,
|
|
2049
2257
|
hasFinalOutput: Boolean(finalText),
|
|
2050
2258
|
hasCompletedTurn: this.hasCompletedTurn(turnId),
|
|
2051
2259
|
lastActivityAtMs,
|
|
@@ -2056,6 +2264,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2056
2264
|
activeTurn.origin === "wechat" &&
|
|
2057
2265
|
!this.pendingTurnStart &&
|
|
2058
2266
|
!pendingApproval &&
|
|
2267
|
+
!pendingUserInput &&
|
|
2059
2268
|
finalText &&
|
|
2060
2269
|
typeof lastActivityAtMs === "number") {
|
|
2061
2270
|
const remainingMs = CODEX_FINAL_REPLY_SETTLE_DELAY_MS - (nowMs - lastActivityAtMs);
|
|
@@ -2073,6 +2282,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2073
2282
|
return;
|
|
2074
2283
|
}
|
|
2075
2284
|
this.clearPendingApprovalState();
|
|
2285
|
+
this.clearPendingUserInputState();
|
|
2076
2286
|
this.setActiveTurn(null);
|
|
2077
2287
|
this.cleanupTurnArtifacts(turnId);
|
|
2078
2288
|
this.state.lastOutputAt = nowIso();
|
|
@@ -47,7 +47,10 @@ export class LocalCompanionProxyAdapter {
|
|
|
47
47
|
this.shuttingDown = false;
|
|
48
48
|
this.expectedCloseReason = null;
|
|
49
49
|
this.clearReconnectShutdownTimer();
|
|
50
|
-
this.setStatus("starting",
|
|
50
|
+
this.setStatus("starting", formatLocalCompanionStartupMessage({
|
|
51
|
+
kind: this.options.kind,
|
|
52
|
+
launchMode: this.options.companionLaunchMode,
|
|
53
|
+
}));
|
|
51
54
|
await new Promise((resolve, reject) => {
|
|
52
55
|
const server = net.createServer((socket) => {
|
|
53
56
|
this.handlePanelSocket(socket);
|
|
@@ -133,7 +136,9 @@ export class LocalCompanionProxyAdapter {
|
|
|
133
136
|
this.expectedCloseReason = null;
|
|
134
137
|
this.clearReconnectShutdownTimer();
|
|
135
138
|
this.rejectPendingRequests(`${this.options.kind} companion proxy is shutting down.`);
|
|
136
|
-
clearLocalCompanionEndpoint(this.options.cwd, this.endpoint?.instanceId
|
|
139
|
+
clearLocalCompanionEndpoint(this.options.cwd, this.endpoint?.instanceId, {
|
|
140
|
+
adapter: this.options.kind,
|
|
141
|
+
});
|
|
137
142
|
if (this.socket) {
|
|
138
143
|
try {
|
|
139
144
|
sendLocalCompanionMessage(this.socket, {
|
|
@@ -185,10 +190,15 @@ export class LocalCompanionProxyAdapter {
|
|
|
185
190
|
this.expectedCloseReason = null;
|
|
186
191
|
this.socket = socket;
|
|
187
192
|
this.detachMessageListener = detachListener;
|
|
193
|
+
const companionConnectedAt = nowIso();
|
|
194
|
+
if (this.endpoint) {
|
|
195
|
+
this.endpoint.companionPid = message.companionPid;
|
|
196
|
+
this.endpoint.companionConnectedAt = companionConnectedAt;
|
|
197
|
+
}
|
|
188
198
|
updateLocalCompanionOccupancy(this.options.cwd, {
|
|
189
199
|
companionPid: message.companionPid,
|
|
190
|
-
companionConnectedAt
|
|
191
|
-
}, this.endpoint?.instanceId);
|
|
200
|
+
companionConnectedAt,
|
|
201
|
+
}, this.endpoint?.instanceId, { adapter: this.options.kind });
|
|
192
202
|
sendLocalCompanionMessage(socket, { type: "hello_ack" });
|
|
193
203
|
return;
|
|
194
204
|
}
|
|
@@ -198,7 +208,9 @@ export class LocalCompanionProxyAdapter {
|
|
|
198
208
|
if (this.socket === socket) {
|
|
199
209
|
const expectedCloseReason = this.expectedCloseReason;
|
|
200
210
|
this.expectedCloseReason = null;
|
|
201
|
-
clearLocalCompanionOccupancy(this.options.cwd, this.endpoint?.instanceId
|
|
211
|
+
clearLocalCompanionOccupancy(this.options.cwd, this.endpoint?.instanceId, {
|
|
212
|
+
adapter: this.options.kind,
|
|
213
|
+
});
|
|
202
214
|
this.detachPanelSocket();
|
|
203
215
|
if (!this.shuttingDown) {
|
|
204
216
|
this.handleCompanionDisconnect(expectedCloseReason);
|
|
@@ -296,6 +308,7 @@ export class LocalCompanionProxyAdapter {
|
|
|
296
308
|
lifecycle: this.options.lifecycle,
|
|
297
309
|
expectedClose: isExpectedLocalCompanionClose(expectedCloseReason),
|
|
298
310
|
reconnectGraceMs: LOCAL_COMPANION_RECONNECT_GRACE_MS,
|
|
311
|
+
launchMode: this.options.companionLaunchMode,
|
|
299
312
|
});
|
|
300
313
|
if (disposition.action === "shutdown") {
|
|
301
314
|
this.clearReconnectShutdownTimer();
|
|
@@ -362,7 +375,10 @@ export class LocalCompanionProxyAdapter {
|
|
|
362
375
|
async sendRequest(payload) {
|
|
363
376
|
const socket = this.socket;
|
|
364
377
|
if (!socket) {
|
|
365
|
-
throw new Error(
|
|
378
|
+
throw new Error(formatCompanionNotConnectedMessage({
|
|
379
|
+
kind: this.options.kind,
|
|
380
|
+
launchMode: this.options.companionLaunchMode,
|
|
381
|
+
}));
|
|
366
382
|
}
|
|
367
383
|
if (!this.state.pid && payload.command !== "dispose") {
|
|
368
384
|
throw new Error(`${this.options.kind} companion is connected but not ready yet. Wait for it to finish starting.`);
|
|
@@ -389,6 +405,18 @@ export function buildCompanionHealthPatch(state, timestamp) {
|
|
|
389
405
|
companionWorkerPid: state.pid,
|
|
390
406
|
};
|
|
391
407
|
}
|
|
408
|
+
export function formatLocalCompanionStartupMessage(params) {
|
|
409
|
+
if (params.launchMode === "daemon_auto") {
|
|
410
|
+
return `Waiting for daemon-managed ${params.kind} companion connection. The daemon will open or reuse the visible CLI automatically.`;
|
|
411
|
+
}
|
|
412
|
+
return `Waiting for manual ${params.kind} companion connection. Run "${getLocalCompanionCommandName(params.kind)}" in a second terminal for this directory.`;
|
|
413
|
+
}
|
|
414
|
+
export function formatCompanionNotConnectedMessage(params) {
|
|
415
|
+
if (params.launchMode === "daemon_auto") {
|
|
416
|
+
return `${params.kind} companion is not connected yet. Send /${params.kind} in WeChat to open or reconnect the visible CLI automatically.`;
|
|
417
|
+
}
|
|
418
|
+
return `${params.kind} companion is not connected. Run "${getLocalCompanionCommandName(params.kind)}" in a second terminal for this directory.`;
|
|
419
|
+
}
|
|
392
420
|
export function isExpectedLocalCompanionClose(reason) {
|
|
393
421
|
return typeof reason === "string" && reason.length > 0;
|
|
394
422
|
}
|
|
@@ -411,11 +439,23 @@ export function getCompanionDisconnectDisposition(params) {
|
|
|
411
439
|
};
|
|
412
440
|
}
|
|
413
441
|
if (params.expectedClose) {
|
|
442
|
+
if (params.launchMode === "daemon_auto") {
|
|
443
|
+
return {
|
|
444
|
+
action: "await_manual_reconnect",
|
|
445
|
+
message: `${params.kind} companion closed. Send /${params.kind} in WeChat to reopen the visible CLI automatically.`,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
414
448
|
return {
|
|
415
449
|
action: "await_manual_reconnect",
|
|
416
450
|
message: `${params.kind} companion closed. Run "${commandName}" again in a second terminal for this directory.`,
|
|
417
451
|
};
|
|
418
452
|
}
|
|
453
|
+
if (params.launchMode === "daemon_auto") {
|
|
454
|
+
return {
|
|
455
|
+
action: "await_manual_reconnect",
|
|
456
|
+
message: `${params.kind} companion disconnected unexpectedly. Send /${params.kind} in WeChat to reopen the visible CLI automatically.`,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
419
459
|
return {
|
|
420
460
|
action: "await_manual_reconnect",
|
|
421
461
|
message: `${params.kind} companion disconnected unexpectedly. Run "${commandName}" again in a second terminal for this directory to reconnect.`,
|