codex-relay 1.3.2 → 1.4.1
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 +14 -4
- package/dist/api-schema.js +2 -2
- package/dist/api-schema2.js +76 -1
- package/dist/cli.js +1 -1
- package/dist/paths.js +78 -0
- package/dist/src.js +519 -134
- package/package.json +1 -1
- package/src/api-schema.ts +81 -0
package/dist/src.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as PairRequestSchema, Bt as WorkspaceTerminalOutputResponseSchema, C as ListModelsResponseSchema, Ct as VersionResponseSchema, D as ListWorkspaceDirectoriesResponseSchema, Dt as WorkspaceChangesResponseSchema, E as ListThreadsResponseSchema, Gt as chatMessageDetailsFromPromptContext, H as PushNotificationSettingsResponseSchema, Ht as WorkspaceTerminalSessionResponseSchema, It as WorkspaceTailscaleServeRequestSchema, J as ReasoningEffortSchema, Jt as normalizePromptContext, Kt as createOpenApiDocument, Lt as WorkspaceTailscaleServeResponseSchema, O as ListWorkspaceFilesResponseSchema, Q as RunThreadRequestSchema, Rt as WorkspaceTerminalInputRequestSchema, S as KnownReasoningEffortSchema, St as UpdateWorkspaceFileContentRequestSchema, T as ListSkillsResponseSchema, U as QueuedThreadInputActionResponseSchema, Ut as WorkspaceTerminalStartRequestSchema, Vt as WorkspaceTerminalResizeRequestSchema, Wt as apiPaths, X as ResolveApprovalRequestSchema, Y as RegisterPushNotificationRequestSchema, Yt as promptMarkdownWithSkills, Z as ResolveApprovalResponseSchema, _ as EncryptedPayloadSchema, a as ArchiveThreadResponseSchema, at as StatusResponseSchema, b as InterruptThreadRunResponseSchema, bt as UpdateRuntimePreferencesRequestSchema, ct as SubmitThreadInputResponseSchema, d as CheckoutWorkspaceBranchRequestSchema, dt as ThreadDetailResponseSchema, en as stripPromptSkillMentions, ft as ThreadGoalResponseSchema, gt as ThreadMessageDetailResponseSchema, h as CreateThreadRequestSchema, ht as ThreadMessageDetailFieldSchema, j as PairResponseSchema, jt as WorkspaceGitActionResponseSchema, kt as WorkspaceFileContentResponseSchema, l as ChatMessageSchema, m as ContextWindowUsageSchema, mt as ThreadGoalStatusSchema, nt as RuntimePreferencesResponseSchema, ot as StreamThreadRunEventSchema, p as CommitPushWorkspaceRequestSchema, pt as ThreadGoalSchema, q as RateLimitsResponseSchema, rt as RuntimePreferencesSchema, st as StreamThreadRunRequestSchema, ut as ThreadContextWindowResponseSchema, w as ListQueuedThreadInputsResponseSchema, xt as UpdateThreadGoalRequestSchema, y as ImageAttachmentUploadResponseSchema, yt as ThreadSummarySchema } from "./api-schema2.js";
|
|
2
2
|
import { a as getConnectUrlCandidates, i as createPairingQrPayload, o as getConnectUrlGuidance, r as legacyCodexRelayDataPath, s as createTursoPairingSessionStore, t as codexRelayDataPath } from "./paths.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import qrcode from "qrcode-terminal";
|
|
@@ -19,6 +19,7 @@ import { openRepository } from "es-git";
|
|
|
19
19
|
import { Hono } from "hono";
|
|
20
20
|
import { cors } from "hono/cors";
|
|
21
21
|
import * as pty from "@lydell/node-pty";
|
|
22
|
+
import { connect } from "node:net";
|
|
22
23
|
import { createInterface } from "node:readline";
|
|
23
24
|
import WebSocket from "ws";
|
|
24
25
|
import { Codex } from "@openai/codex-sdk";
|
|
@@ -33,11 +34,7 @@ const stdioAppServerArgs = [
|
|
|
33
34
|
"--listen",
|
|
34
35
|
"stdio://"
|
|
35
36
|
];
|
|
36
|
-
const
|
|
37
|
-
"app-server",
|
|
38
|
-
"--listen",
|
|
39
|
-
"unix://"
|
|
40
|
-
];
|
|
37
|
+
const sharedWindowsServerUrl = "ws://127.0.0.1:8788";
|
|
41
38
|
const windowsShellExtensions = new Set([".bat", ".cmd"]);
|
|
42
39
|
function resolveCodexAppServerMode(env = process.env) {
|
|
43
40
|
const configuredMode = env.CODEX_RELAY_APP_SERVER_MODE?.trim().toLowerCase();
|
|
@@ -46,9 +43,9 @@ function resolveCodexAppServerMode(env = process.env) {
|
|
|
46
43
|
throw new Error(`Unsupported CODEX_RELAY_APP_SERVER_MODE ${JSON.stringify(configuredMode)}. Expected "stdio" or "socket".`);
|
|
47
44
|
}
|
|
48
45
|
function resolveCodexAppServerSpawn(input = {}) {
|
|
49
|
-
const platform$
|
|
46
|
+
const platform$2 = input.platform ?? platform();
|
|
50
47
|
const command = resolveCodexBinary(input.env ?? process.env);
|
|
51
|
-
const isWindows = platform$
|
|
48
|
+
const isWindows = platform$2 === "win32";
|
|
52
49
|
return {
|
|
53
50
|
command,
|
|
54
51
|
args: [...stdioAppServerArgs],
|
|
@@ -57,19 +54,22 @@ function resolveCodexAppServerSpawn(input = {}) {
|
|
|
57
54
|
};
|
|
58
55
|
}
|
|
59
56
|
function resolveCodexSharedAppServerSpawn(input = {}) {
|
|
60
|
-
const platform$
|
|
61
|
-
assertAppServerModeSupported("socket", platform$2);
|
|
57
|
+
const platform$3 = input.platform ?? platform();
|
|
62
58
|
const command = resolveCodexBinary(input.env ?? process.env);
|
|
63
|
-
const isWindows = platform$
|
|
59
|
+
const isWindows = platform$3 === "win32";
|
|
64
60
|
return {
|
|
65
61
|
command,
|
|
66
|
-
args: [
|
|
62
|
+
args: [
|
|
63
|
+
"app-server",
|
|
64
|
+
"--listen",
|
|
65
|
+
resolveCodexSharedAppServerRemoteAddress(platform$3)
|
|
66
|
+
],
|
|
67
67
|
shell: isWindows && shouldUseWindowsShell(command),
|
|
68
68
|
windowsHide: isWindows
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
|
-
function
|
|
72
|
-
|
|
71
|
+
function resolveCodexSharedAppServerRemoteAddress(platform$1 = platform()) {
|
|
72
|
+
return platform$1 === "win32" ? sharedWindowsServerUrl : "unix://";
|
|
73
73
|
}
|
|
74
74
|
function resolveCodexBinary(env) {
|
|
75
75
|
return env.CODEX_BIN?.trim() || "codex";
|
|
@@ -79,17 +79,68 @@ function shouldUseWindowsShell(command) {
|
|
|
79
79
|
return extension === "" || windowsShellExtensions.has(extension);
|
|
80
80
|
}
|
|
81
81
|
//#endregion
|
|
82
|
+
//#region src/debug-log.ts
|
|
83
|
+
function isRelayDebugEnabled() {
|
|
84
|
+
return process.env.CODEX_RELAY_DEBUG === "1" || process.env.CODEX_RELAY_DEBUG_STREAM === "1";
|
|
85
|
+
}
|
|
86
|
+
function relayDebugLog(event, fields = {}) {
|
|
87
|
+
if (!isRelayDebugEnabled()) return;
|
|
88
|
+
const entry = sanitizeDebugValue({
|
|
89
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
90
|
+
event,
|
|
91
|
+
...fields
|
|
92
|
+
});
|
|
93
|
+
const line = `${JSON.stringify(entry)}\n`;
|
|
94
|
+
const path = process.env.CODEX_RELAY_DEBUG_LOG_PATH?.trim();
|
|
95
|
+
if (path) mkdir(dirname(path), { recursive: true }).then(() => appendFile(path, line, { mode: 384 })).catch(() => void 0);
|
|
96
|
+
console.log(`[debug] ${event} ${formatDebugFields(fields)}`.trimEnd());
|
|
97
|
+
}
|
|
98
|
+
function sanitizeDebugValue(value) {
|
|
99
|
+
if (value instanceof Error) return {
|
|
100
|
+
message: value.message,
|
|
101
|
+
name: value.name,
|
|
102
|
+
stack: value.stack
|
|
103
|
+
};
|
|
104
|
+
if (Array.isArray(value)) return value.map(sanitizeDebugValue);
|
|
105
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, sanitizeDebugValue(item)]));
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
function formatDebugFields(fields) {
|
|
109
|
+
return Object.entries(fields).map(([key, value]) => `${key}=${formatDebugValue(value)}`).join(" ");
|
|
110
|
+
}
|
|
111
|
+
function formatDebugValue(value) {
|
|
112
|
+
if (value === void 0) return "undefined";
|
|
113
|
+
if (value === null) return "null";
|
|
114
|
+
if (typeof value === "string") return value.includes(" ") ? JSON.stringify(value) : value;
|
|
115
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
116
|
+
return JSON.stringify(sanitizeDebugValue(value));
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
82
119
|
//#region src/app-server.ts
|
|
120
|
+
const sharedSocketReconnectDelaysMs = [
|
|
121
|
+
50,
|
|
122
|
+
100,
|
|
123
|
+
250,
|
|
124
|
+
500,
|
|
125
|
+
1e3,
|
|
126
|
+
2e3
|
|
127
|
+
];
|
|
83
128
|
var CodexAppServerClient = class {
|
|
84
129
|
child;
|
|
130
|
+
closed = false;
|
|
85
131
|
initialized;
|
|
86
132
|
notificationHandlers = /* @__PURE__ */ new Set();
|
|
87
133
|
nextId = 1;
|
|
88
134
|
pending = /* @__PURE__ */ new Map();
|
|
135
|
+
reconnecting;
|
|
89
136
|
requestHandlers = /* @__PURE__ */ new Set();
|
|
90
137
|
readline;
|
|
91
138
|
sharedServer;
|
|
92
139
|
socket;
|
|
140
|
+
startSharedServer;
|
|
141
|
+
constructor(options = {}) {
|
|
142
|
+
this.startSharedServer = options.startSharedServer ?? startSharedCodexAppServer;
|
|
143
|
+
}
|
|
93
144
|
initialize() {
|
|
94
145
|
return this.ensureInitialized();
|
|
95
146
|
}
|
|
@@ -165,15 +216,18 @@ var CodexAppServerClient = class {
|
|
|
165
216
|
});
|
|
166
217
|
}
|
|
167
218
|
close() {
|
|
219
|
+
if (this.closed) return;
|
|
220
|
+
this.closed = true;
|
|
168
221
|
for (const pending of this.pending.values()) pending.reject(/* @__PURE__ */ new Error("Codex app-server was closed."));
|
|
169
222
|
this.pending.clear();
|
|
170
223
|
this.readline?.close();
|
|
171
224
|
this.child?.kill();
|
|
172
|
-
this.socket
|
|
225
|
+
const socket = this.socket;
|
|
226
|
+
this.socket = void 0;
|
|
227
|
+
socket?.close();
|
|
173
228
|
this.sharedServer?.kill();
|
|
174
229
|
this.readline = void 0;
|
|
175
230
|
this.child = void 0;
|
|
176
|
-
this.socket = void 0;
|
|
177
231
|
this.sharedServer = void 0;
|
|
178
232
|
this.initialized = void 0;
|
|
179
233
|
}
|
|
@@ -197,35 +251,37 @@ var CodexAppServerClient = class {
|
|
|
197
251
|
});
|
|
198
252
|
}
|
|
199
253
|
ensureInitialized() {
|
|
200
|
-
if (
|
|
254
|
+
if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("Codex app-server was closed."));
|
|
255
|
+
if (!this.initialized) {
|
|
256
|
+
const initialized = this.start();
|
|
257
|
+
this.initialized = initialized;
|
|
258
|
+
initialized.catch(() => {
|
|
259
|
+
if (this.initialized === initialized) this.initialized = void 0;
|
|
260
|
+
});
|
|
261
|
+
}
|
|
201
262
|
return this.initialized;
|
|
202
263
|
}
|
|
203
264
|
async start() {
|
|
265
|
+
const mode = resolveCodexAppServerMode();
|
|
204
266
|
try {
|
|
205
|
-
if (
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
} else this.startStdioCodexAppServer();
|
|
209
|
-
await this.requestRaw("initialize", {
|
|
210
|
-
clientInfo: {
|
|
211
|
-
name: "codex-relay",
|
|
212
|
-
title: "Codex Relay Mobile Server",
|
|
213
|
-
version: "1.2.0"
|
|
214
|
-
},
|
|
215
|
-
capabilities: { experimentalApi: true }
|
|
216
|
-
});
|
|
267
|
+
if (mode === "socket") await this.startOrAttachSharedCodexAppServer();
|
|
268
|
+
else this.startStdioCodexAppServer();
|
|
269
|
+
await this.initializeAppServer();
|
|
217
270
|
} catch (error) {
|
|
218
|
-
this.
|
|
219
|
-
this.readline = void 0;
|
|
220
|
-
this.child?.kill();
|
|
221
|
-
this.child = void 0;
|
|
222
|
-
this.socket?.close();
|
|
223
|
-
this.socket = void 0;
|
|
224
|
-
this.sharedServer?.kill();
|
|
225
|
-
this.sharedServer = void 0;
|
|
271
|
+
if (mode === "stdio") this.stopStdioCodexAppServer();
|
|
226
272
|
throw error;
|
|
227
273
|
}
|
|
228
274
|
}
|
|
275
|
+
async initializeAppServer() {
|
|
276
|
+
await this.requestRaw("initialize", {
|
|
277
|
+
clientInfo: {
|
|
278
|
+
name: "codex-relay",
|
|
279
|
+
title: "Codex Relay Mobile Server",
|
|
280
|
+
version: "1.2.0"
|
|
281
|
+
},
|
|
282
|
+
capabilities: { experimentalApi: true }
|
|
283
|
+
});
|
|
284
|
+
}
|
|
229
285
|
startStdioCodexAppServer() {
|
|
230
286
|
const spawnConfig = resolveCodexAppServerSpawn();
|
|
231
287
|
this.child = spawn(spawnConfig.command, spawnConfig.args, {
|
|
@@ -248,31 +304,153 @@ var CodexAppServerClient = class {
|
|
|
248
304
|
this.initialized = void 0;
|
|
249
305
|
});
|
|
250
306
|
}
|
|
307
|
+
stopStdioCodexAppServer() {
|
|
308
|
+
this.readline?.close();
|
|
309
|
+
this.readline = void 0;
|
|
310
|
+
this.child?.kill();
|
|
311
|
+
this.child = void 0;
|
|
312
|
+
}
|
|
313
|
+
async startOrAttachSharedCodexAppServer() {
|
|
314
|
+
try {
|
|
315
|
+
await this.connectSharedCodexAppServer();
|
|
316
|
+
relayDebugLog("app_server.shared_socket.attached", {
|
|
317
|
+
ownership: "attached",
|
|
318
|
+
socketPath: sharedCodexAppServerSocketPath()
|
|
319
|
+
});
|
|
320
|
+
return;
|
|
321
|
+
} catch (error) {
|
|
322
|
+
const attachError = asError(error);
|
|
323
|
+
relayDebugLog("app_server.shared_socket.attach_failed", {
|
|
324
|
+
message: attachError.message,
|
|
325
|
+
socketPath: sharedCodexAppServerSocketPath()
|
|
326
|
+
});
|
|
327
|
+
if (this.sharedServer) throw attachError;
|
|
328
|
+
}
|
|
329
|
+
const sharedServer = await this.startSharedServer();
|
|
330
|
+
this.sharedServer = sharedServer;
|
|
331
|
+
this.observeSharedCodexAppServer(sharedServer);
|
|
332
|
+
relayDebugLog("app_server.shared_process.started", {
|
|
333
|
+
ownership: "relay-owned",
|
|
334
|
+
socketPath: sharedCodexAppServerSocketPath()
|
|
335
|
+
});
|
|
336
|
+
try {
|
|
337
|
+
await this.connectSharedCodexAppServer();
|
|
338
|
+
} catch (error) {
|
|
339
|
+
if (this.sharedServer === sharedServer) {
|
|
340
|
+
sharedServer.kill();
|
|
341
|
+
this.sharedServer = void 0;
|
|
342
|
+
}
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
observeSharedCodexAppServer(sharedServer) {
|
|
347
|
+
sharedServer.on("error", (error) => {
|
|
348
|
+
relayDebugLog("app_server.shared_process.error", {
|
|
349
|
+
message: error.message,
|
|
350
|
+
ownership: "relay-owned"
|
|
351
|
+
});
|
|
352
|
+
if (this.sharedServer === sharedServer) this.sharedServer = void 0;
|
|
353
|
+
});
|
|
354
|
+
sharedServer.once("exit", (code, signal) => {
|
|
355
|
+
if (this.sharedServer !== sharedServer) return;
|
|
356
|
+
this.sharedServer = void 0;
|
|
357
|
+
const error = /* @__PURE__ */ new Error(`Codex shared app-server exited with ${signal ?? code ?? 1}.`);
|
|
358
|
+
relayDebugLog("app_server.shared_process.exited", {
|
|
359
|
+
message: error.message,
|
|
360
|
+
ownership: "relay-owned"
|
|
361
|
+
});
|
|
362
|
+
if (this.socket) this.handleSharedSocketFailure(this.socket, error);
|
|
363
|
+
});
|
|
364
|
+
}
|
|
251
365
|
async connectSharedCodexAppServer() {
|
|
252
|
-
const
|
|
366
|
+
const remoteAddress = resolveCodexSharedAppServerRemoteAddress();
|
|
367
|
+
const socket = new WebSocket(remoteAddress === "unix://" ? `ws+unix://${sharedCodexAppServerSocketPath()}:/` : remoteAddress, { perMessageDeflate: false });
|
|
253
368
|
this.socket = socket;
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
369
|
+
try {
|
|
370
|
+
await new Promise((resolve, reject) => {
|
|
371
|
+
const handleOpen = () => {
|
|
372
|
+
socket.off("error", handleInitialError);
|
|
373
|
+
resolve();
|
|
374
|
+
};
|
|
375
|
+
const handleInitialError = (error) => {
|
|
376
|
+
socket.off("open", handleOpen);
|
|
377
|
+
reject(error);
|
|
378
|
+
};
|
|
379
|
+
socket.once("open", handleOpen);
|
|
380
|
+
socket.once("error", handleInitialError);
|
|
381
|
+
});
|
|
382
|
+
} catch (error) {
|
|
383
|
+
if (this.socket === socket) this.socket = void 0;
|
|
384
|
+
socket.close();
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
266
387
|
socket.on("message", (data) => this.handleLine(String(data)));
|
|
267
|
-
socket.on("error", (error) => this.
|
|
388
|
+
socket.on("error", (error) => this.handleSharedSocketFailure(socket, error));
|
|
268
389
|
socket.once("close", (code, reason) => {
|
|
269
|
-
this.
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
this.
|
|
273
|
-
|
|
390
|
+
this.handleSharedSocketFailure(socket, /* @__PURE__ */ new Error(`Codex app-server socket closed with ${code}${reason.length > 0 ? `: ${String(reason)}` : "."}`));
|
|
391
|
+
});
|
|
392
|
+
relayDebugLog("app_server.shared_socket.connected", {
|
|
393
|
+
ownership: this.sharedAppServerOwnership(),
|
|
394
|
+
socketPath: sharedCodexAppServerSocketPath()
|
|
274
395
|
});
|
|
275
396
|
}
|
|
397
|
+
handleSharedSocketFailure(socket, error) {
|
|
398
|
+
if (this.socket !== socket) return;
|
|
399
|
+
this.socket = void 0;
|
|
400
|
+
this.rejectAll(error);
|
|
401
|
+
this.initialized = void 0;
|
|
402
|
+
relayDebugLog("app_server.shared_socket.disconnected", {
|
|
403
|
+
message: error.message,
|
|
404
|
+
ownership: this.sharedAppServerOwnership()
|
|
405
|
+
});
|
|
406
|
+
this.scheduleSharedSocketReconnect();
|
|
407
|
+
}
|
|
408
|
+
sharedAppServerOwnership() {
|
|
409
|
+
return this.sharedServer ? "relay-owned" : "attached";
|
|
410
|
+
}
|
|
411
|
+
scheduleSharedSocketReconnect() {
|
|
412
|
+
if (this.closed || this.reconnecting) return;
|
|
413
|
+
const reconnecting = this.reconnectSharedCodexAppServer();
|
|
414
|
+
this.reconnecting = reconnecting;
|
|
415
|
+
this.initialized = reconnecting;
|
|
416
|
+
reconnecting.then(() => {
|
|
417
|
+
if (this.reconnecting === reconnecting) this.reconnecting = void 0;
|
|
418
|
+
}, (error) => {
|
|
419
|
+
relayDebugLog("app_server.shared_socket.reconnect_failed", {
|
|
420
|
+
message: asError(error).message,
|
|
421
|
+
ownership: this.sharedAppServerOwnership()
|
|
422
|
+
});
|
|
423
|
+
if (this.initialized === reconnecting) this.initialized = void 0;
|
|
424
|
+
if (this.reconnecting === reconnecting) this.reconnecting = void 0;
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
async reconnectSharedCodexAppServer() {
|
|
428
|
+
let lastError;
|
|
429
|
+
for (const delayMs of sharedSocketReconnectDelaysMs) {
|
|
430
|
+
await setTimeout$1(delayMs);
|
|
431
|
+
if (this.closed) return;
|
|
432
|
+
try {
|
|
433
|
+
await this.connectSharedCodexAppServer();
|
|
434
|
+
await this.initializeAppServer();
|
|
435
|
+
relayDebugLog("app_server.shared_socket.reconnected", {
|
|
436
|
+
ownership: this.sharedAppServerOwnership(),
|
|
437
|
+
socketPath: sharedCodexAppServerSocketPath()
|
|
438
|
+
});
|
|
439
|
+
return;
|
|
440
|
+
} catch (error) {
|
|
441
|
+
lastError = asError(error);
|
|
442
|
+
const socket = this.socket;
|
|
443
|
+
this.socket = void 0;
|
|
444
|
+
socket?.close();
|
|
445
|
+
relayDebugLog("app_server.shared_socket.reconnect_retry", {
|
|
446
|
+
delayMs,
|
|
447
|
+
message: lastError.message,
|
|
448
|
+
ownership: this.sharedAppServerOwnership()
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
throw lastError ?? /* @__PURE__ */ new Error("Unable to reconnect to the shared Codex app-server.");
|
|
453
|
+
}
|
|
276
454
|
requestRaw(method, params) {
|
|
277
455
|
const id = this.nextId++;
|
|
278
456
|
const request = JSON.stringify({
|
|
@@ -376,11 +554,11 @@ async function startSharedCodexAppServer() {
|
|
|
376
554
|
child.once("exit", (code, signal) => {
|
|
377
555
|
exitReason = signal ?? code ?? 1;
|
|
378
556
|
});
|
|
379
|
-
const
|
|
557
|
+
const remoteAddress = resolveCodexSharedAppServerRemoteAddress();
|
|
380
558
|
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
381
559
|
if (spawnError) throw new Error(`Failed to start the shared Codex app-server: ${spawnError.message}`);
|
|
382
560
|
try {
|
|
383
|
-
await
|
|
561
|
+
await waitForSharedCodexAppServer(remoteAddress);
|
|
384
562
|
return child;
|
|
385
563
|
} catch {
|
|
386
564
|
if (exitReason !== void 0) {
|
|
@@ -391,11 +569,34 @@ async function startSharedCodexAppServer() {
|
|
|
391
569
|
}
|
|
392
570
|
}
|
|
393
571
|
child.kill();
|
|
394
|
-
throw new Error(`Timed out waiting for the shared Codex app-server
|
|
572
|
+
throw new Error(`Timed out waiting for the shared Codex app-server at ${remoteAddress}.`);
|
|
573
|
+
}
|
|
574
|
+
async function waitForSharedCodexAppServer(remoteAddress) {
|
|
575
|
+
if (remoteAddress === "unix://") {
|
|
576
|
+
await access(sharedCodexAppServerSocketPath());
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const url = new URL(remoteAddress);
|
|
580
|
+
await new Promise((resolve, reject) => {
|
|
581
|
+
const socket = connect({
|
|
582
|
+
host: url.hostname,
|
|
583
|
+
port: Number(url.port)
|
|
584
|
+
});
|
|
585
|
+
const onError = (error) => reject(error);
|
|
586
|
+
socket.once("error", onError);
|
|
587
|
+
socket.once("connect", () => {
|
|
588
|
+
socket.off("error", onError);
|
|
589
|
+
socket.destroy();
|
|
590
|
+
resolve();
|
|
591
|
+
});
|
|
592
|
+
});
|
|
395
593
|
}
|
|
396
594
|
function sharedCodexAppServerSocketPath() {
|
|
397
595
|
return join(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"), "app-server-control", "app-server-control.sock");
|
|
398
596
|
}
|
|
597
|
+
function asError(error) {
|
|
598
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
599
|
+
}
|
|
399
600
|
function debugAppServer(kind, method, id, detail) {
|
|
400
601
|
if (process.env.CODEX_RELAY_DEBUG_APP_SERVER !== "1") return;
|
|
401
602
|
console.log(`[app-server] ${kind}${id === void 0 ? "" : ` #${id}`}${method ? ` ${method}` : ""}${detail ? ` ${detail}` : ""}`);
|
|
@@ -585,41 +786,79 @@ function sumPositiveIntegers(values) {
|
|
|
585
786
|
return found ? sum : null;
|
|
586
787
|
}
|
|
587
788
|
//#endregion
|
|
588
|
-
//#region src/
|
|
589
|
-
|
|
590
|
-
|
|
789
|
+
//#region src/push-notifications.ts
|
|
790
|
+
const expoPushEndpoint = "https://exp.host/--/api/v2/push/send";
|
|
791
|
+
const expoPushChunkSize = 100;
|
|
792
|
+
function createExpoPushNotificationSender(fetchImpl = fetch) {
|
|
793
|
+
return { async send(notifications) {
|
|
794
|
+
const invalidExpoPushTokens = /* @__PURE__ */ new Set();
|
|
795
|
+
for (const chunk of chunks(notifications, expoPushChunkSize)) {
|
|
796
|
+
const response = await fetchImpl(expoPushEndpoint, {
|
|
797
|
+
body: JSON.stringify(chunk.map((notification) => ({
|
|
798
|
+
body: notification.body,
|
|
799
|
+
channelId: "default",
|
|
800
|
+
data: notification.data,
|
|
801
|
+
priority: "high",
|
|
802
|
+
sound: "default",
|
|
803
|
+
title: notification.title,
|
|
804
|
+
to: notification.to
|
|
805
|
+
}))),
|
|
806
|
+
headers: {
|
|
807
|
+
accept: "application/json",
|
|
808
|
+
"content-type": "application/json"
|
|
809
|
+
},
|
|
810
|
+
method: "POST"
|
|
811
|
+
});
|
|
812
|
+
const body = await response.json().catch(() => void 0);
|
|
813
|
+
if (!response.ok) throw new Error(`Expo push service returned ${response.status}.`);
|
|
814
|
+
const tickets = expoPushTickets(body);
|
|
815
|
+
for (const [index, ticket] of tickets.entries()) {
|
|
816
|
+
const notification = chunk[index];
|
|
817
|
+
if (notification && isDeviceNotRegistered(ticket)) invalidExpoPushTokens.add(notification.to);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return { invalidExpoPushTokens: [...invalidExpoPushTokens] };
|
|
821
|
+
} };
|
|
591
822
|
}
|
|
592
|
-
function
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
...
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
const path = process.env.CODEX_RELAY_DEBUG_LOG_PATH?.trim();
|
|
601
|
-
if (path) mkdir(dirname(path), { recursive: true }).then(() => appendFile(path, line, { mode: 384 })).catch(() => void 0);
|
|
602
|
-
console.log(`[debug] ${event} ${formatDebugFields(fields)}`.trimEnd());
|
|
823
|
+
function createPushNotificationDispatcher(input) {
|
|
824
|
+
return { async dispatch(event) {
|
|
825
|
+
const subscriptions = await input.sessions.listActivePushNotificationSubscriptions(Date.now());
|
|
826
|
+
const selectedTokens = new Set(subscriptions.filter((subscription) => notificationEnabled(subscription, event.intent)).map((subscription) => subscription.expoPushToken));
|
|
827
|
+
if (selectedTokens.size === 0) return;
|
|
828
|
+
const delivery = await input.sender.send([...selectedTokens].map((expoPushToken) => notificationForEvent(expoPushToken, event)));
|
|
829
|
+
await Promise.all(delivery.invalidExpoPushTokens.map((expoPushToken) => input.sessions.deletePushNotificationSubscriptionsByExpoPushToken(expoPushToken)));
|
|
830
|
+
} };
|
|
603
831
|
}
|
|
604
|
-
function
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
832
|
+
function notificationEnabled(subscription, intent) {
|
|
833
|
+
return intent === "action_required" ? subscription.actionRequired : subscription.turnTerminal;
|
|
834
|
+
}
|
|
835
|
+
function notificationForEvent(expoPushToken, event) {
|
|
836
|
+
return {
|
|
837
|
+
body: event.intent === "action_required" ? "Codex needs your attention." : "A Codex turn has finished.",
|
|
838
|
+
data: {
|
|
839
|
+
intent: event.intent,
|
|
840
|
+
threadId: event.threadId,
|
|
841
|
+
...event.turnId ? { turnId: event.turnId } : {}
|
|
842
|
+
},
|
|
843
|
+
title: "Codex Relay",
|
|
844
|
+
to: expoPushToken
|
|
609
845
|
};
|
|
610
|
-
if (Array.isArray(value)) return value.map(sanitizeDebugValue);
|
|
611
|
-
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, sanitizeDebugValue(item)]));
|
|
612
|
-
return value;
|
|
613
846
|
}
|
|
614
|
-
function
|
|
615
|
-
|
|
847
|
+
function expoPushTickets(value) {
|
|
848
|
+
if (!value || typeof value !== "object") return [];
|
|
849
|
+
const data = value.data;
|
|
850
|
+
return Array.isArray(data) ? data : [];
|
|
616
851
|
}
|
|
617
|
-
function
|
|
618
|
-
if (
|
|
619
|
-
|
|
620
|
-
if (
|
|
621
|
-
|
|
622
|
-
|
|
852
|
+
function isDeviceNotRegistered(ticket) {
|
|
853
|
+
if (!ticket || typeof ticket !== "object") return false;
|
|
854
|
+
const record = ticket;
|
|
855
|
+
if (record.status !== "error" || !record.details || typeof record.details !== "object") return false;
|
|
856
|
+
return record.details.error === "DeviceNotRegistered";
|
|
857
|
+
}
|
|
858
|
+
function chunks(items, size) {
|
|
859
|
+
const result = [];
|
|
860
|
+
for (let index = 0; index < items.length; index += size) result.push(items.slice(index, index + size));
|
|
861
|
+
return result;
|
|
623
862
|
}
|
|
624
863
|
//#endregion
|
|
625
864
|
//#region src/preferences-store.ts
|
|
@@ -1179,6 +1418,7 @@ function createApp(options = {}) {
|
|
|
1179
1418
|
const pendingApprovals = /* @__PURE__ */ new Map();
|
|
1180
1419
|
const resolvedApprovals = /* @__PURE__ */ new Map();
|
|
1181
1420
|
const queuedInputsByThreadId = /* @__PURE__ */ new Map();
|
|
1421
|
+
const threadOperationTails = /* @__PURE__ */ new Map();
|
|
1182
1422
|
const workspaceTerminalSessions = /* @__PURE__ */ new Map();
|
|
1183
1423
|
const activeAppServerTurnIdsByThreadId = /* @__PURE__ */ new Map();
|
|
1184
1424
|
const appServerHistoryLoadsByThreadId = /* @__PURE__ */ new Map();
|
|
@@ -1186,6 +1426,20 @@ function createApp(options = {}) {
|
|
|
1186
1426
|
const secureSessionsByTokenHash = /* @__PURE__ */ new Map();
|
|
1187
1427
|
const activeStreamControllers = /* @__PURE__ */ new Set();
|
|
1188
1428
|
const threadOptions = { workingDirectory: workspacePath };
|
|
1429
|
+
function runThreadOperation(threadId, operation) {
|
|
1430
|
+
const result = (threadOperationTails.get(threadId) ?? Promise.resolve()).then(operation, operation);
|
|
1431
|
+
const tail = result.then(() => void 0, () => void 0);
|
|
1432
|
+
threadOperationTails.set(threadId, tail);
|
|
1433
|
+
tail.finally(() => {
|
|
1434
|
+
if (threadOperationTails.get(threadId) === tail) threadOperationTails.delete(threadId);
|
|
1435
|
+
});
|
|
1436
|
+
return result;
|
|
1437
|
+
}
|
|
1438
|
+
const pushNotificationDispatcher = options.pairing ? createPushNotificationDispatcher({
|
|
1439
|
+
sender: options.pushNotificationSender ?? createExpoPushNotificationSender(),
|
|
1440
|
+
sessions: options.pairing.sessions
|
|
1441
|
+
}) : void 0;
|
|
1442
|
+
if (appServer && pushNotificationDispatcher) observeAppServerPushNotifications(appServer, pushNotificationDispatcher);
|
|
1189
1443
|
const scheduleAppServerHistoryLoad = (threadId, cachedMessages) => {
|
|
1190
1444
|
if (!appServer || appServerHistoryLoadsByThreadId.has(threadId)) return;
|
|
1191
1445
|
const startedAt = Date.now();
|
|
@@ -1410,6 +1664,48 @@ function createApp(options = {}) {
|
|
|
1410
1664
|
});
|
|
1411
1665
|
return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
|
|
1412
1666
|
});
|
|
1667
|
+
app.get(apiPaths.pushNotifications, async (c) => {
|
|
1668
|
+
const clientSessionId = await pairedClientSessionIdForAuthorization(c.req.header("authorization"), options.pairing);
|
|
1669
|
+
if (!clientSessionId) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("push_notifications_unavailable", "Pair this device again to manage notifications."), 401);
|
|
1670
|
+
const subscription = await options.pairing.sessions.getPushNotificationSubscription(clientSessionId);
|
|
1671
|
+
const response = PushNotificationSettingsResponseSchema.parse({
|
|
1672
|
+
preferences: subscription ? {
|
|
1673
|
+
actionRequired: subscription.actionRequired,
|
|
1674
|
+
turnTerminal: subscription.turnTerminal
|
|
1675
|
+
} : defaultPushNotificationPreferences(),
|
|
1676
|
+
registered: Boolean(subscription)
|
|
1677
|
+
});
|
|
1678
|
+
return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
|
|
1679
|
+
});
|
|
1680
|
+
app.put(apiPaths.pushNotifications, async (c) => {
|
|
1681
|
+
if (!options.pairing) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("pairing_required", "Pair this device before registering notifications."), 404);
|
|
1682
|
+
const parsed = await parseRequestJson(c, options.pairing, secureSessionsByTokenHash, RegisterPushNotificationRequestSchema);
|
|
1683
|
+
if (!parsed.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, validationError(parsed.error), 400);
|
|
1684
|
+
const clientSessionId = await pairedClientSessionIdForAuthorization(c.req.header("authorization"), options.pairing);
|
|
1685
|
+
if (!clientSessionId) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("push_notifications_unavailable", "Pair this device again to manage notifications."), 401);
|
|
1686
|
+
await options.pairing.sessions.upsertPushNotificationSubscription({
|
|
1687
|
+
actionRequired: parsed.data.preferences.actionRequired,
|
|
1688
|
+
clientSessionId,
|
|
1689
|
+
expoPushToken: parsed.data.expoPushToken,
|
|
1690
|
+
platform: parsed.data.platform,
|
|
1691
|
+
turnTerminal: parsed.data.preferences.turnTerminal
|
|
1692
|
+
});
|
|
1693
|
+
const response = PushNotificationSettingsResponseSchema.parse({
|
|
1694
|
+
preferences: parsed.data.preferences,
|
|
1695
|
+
registered: true
|
|
1696
|
+
});
|
|
1697
|
+
return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
|
|
1698
|
+
});
|
|
1699
|
+
app.delete(apiPaths.pushNotifications, async (c) => {
|
|
1700
|
+
const clientSessionId = await pairedClientSessionIdForAuthorization(c.req.header("authorization"), options.pairing);
|
|
1701
|
+
if (!clientSessionId) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("push_notifications_unavailable", "Pair this device again to manage notifications."), 401);
|
|
1702
|
+
await options.pairing.sessions.deletePushNotificationSubscription(clientSessionId);
|
|
1703
|
+
const response = PushNotificationSettingsResponseSchema.parse({
|
|
1704
|
+
preferences: defaultPushNotificationPreferences(),
|
|
1705
|
+
registered: false
|
|
1706
|
+
});
|
|
1707
|
+
return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
|
|
1708
|
+
});
|
|
1413
1709
|
app.get(apiPaths.workspaceDirectories, async (c) => {
|
|
1414
1710
|
const targetPath = resolve(c.req.query("path") ?? workspacePath);
|
|
1415
1711
|
try {
|
|
@@ -2106,44 +2402,46 @@ function createApp(options = {}) {
|
|
|
2106
2402
|
});
|
|
2107
2403
|
app.post("/v1/threads/:threadId/input", async (c) => {
|
|
2108
2404
|
const threadId = c.req.param("threadId");
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2405
|
+
return runThreadOperation(threadId, async () => {
|
|
2406
|
+
const knownThread = await ensureKnownThread({
|
|
2407
|
+
appServer,
|
|
2408
|
+
threadId,
|
|
2409
|
+
messagesByThreadId,
|
|
2410
|
+
threads
|
|
2411
|
+
});
|
|
2412
|
+
if (!knownThread) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("not_found", `Thread ${threadId} is not known to this server.`), 404);
|
|
2413
|
+
if (!appServer) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("unsupported", "Running-thread input requires the Codex app-server."), 409);
|
|
2414
|
+
if (knownThread.state !== "running") return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("thread_not_running", `Thread ${threadId} is not currently running.`), 409);
|
|
2415
|
+
const parsed = await parseRequestJson(c, options.pairing, secureSessionsByTokenHash, RunThreadRequestSchema);
|
|
2416
|
+
if (!parsed.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, validationError(parsed.error), 400);
|
|
2417
|
+
const runOptions = withRuntimePreferences(await preferences.read(knownThread.cwd ?? workspacePath), parsed.data);
|
|
2418
|
+
const skills = runOptions.skills ?? [];
|
|
2419
|
+
const prompt = runOptions.prompt;
|
|
2420
|
+
const queuedInputs = queuedInputsByThreadId.get(threadId) ?? [];
|
|
2421
|
+
const queuedInput = {
|
|
2422
|
+
attachments: runOptions.attachments ?? [],
|
|
2423
|
+
id: randomUUID(),
|
|
2424
|
+
prompt,
|
|
2425
|
+
runOptions,
|
|
2426
|
+
skills,
|
|
2427
|
+
workspacePath: knownThread.cwd ?? workspacePath
|
|
2428
|
+
};
|
|
2429
|
+
queuedInputs.push(queuedInput);
|
|
2430
|
+
queuedInputsByThreadId.set(threadId, queuedInputs);
|
|
2431
|
+
const thread = updateThread(threads, messagesByThreadId, threadId, {
|
|
2432
|
+
state: "running",
|
|
2433
|
+
lastPrompt: promptWithAttachmentReferences(prompt, runOptions.attachments ?? []),
|
|
2434
|
+
lastError: void 0,
|
|
2435
|
+
...runtimeMetadataFromOptions(runOptions)
|
|
2436
|
+
});
|
|
2437
|
+
const response = SubmitThreadInputResponseSchema.parse({
|
|
2438
|
+
acceptedAs: "queued",
|
|
2439
|
+
input: queuedThreadInputSummary(queuedInput),
|
|
2440
|
+
queueLength: queuedInputsByThreadId.get(threadId)?.length ?? 0,
|
|
2441
|
+
thread
|
|
2442
|
+
});
|
|
2443
|
+
return secureJson(c, options.pairing, secureSessionsByTokenHash, response, 202);
|
|
2145
2444
|
});
|
|
2146
|
-
return secureJson(c, options.pairing, secureSessionsByTokenHash, response, 202);
|
|
2147
2445
|
});
|
|
2148
2446
|
app.delete("/v1/threads/:threadId/input/:inputId", async (c) => {
|
|
2149
2447
|
const threadId = c.req.param("threadId");
|
|
@@ -2364,6 +2662,7 @@ function createApp(options = {}) {
|
|
|
2364
2662
|
messagesByThreadId,
|
|
2365
2663
|
pendingApprovals,
|
|
2366
2664
|
queuedInputsByThreadId,
|
|
2665
|
+
runThreadOperation,
|
|
2367
2666
|
prompt: rawPrompt,
|
|
2368
2667
|
attachments: runOptions.attachments ?? [],
|
|
2369
2668
|
secureSession,
|
|
@@ -2406,6 +2705,18 @@ function createApp(options = {}) {
|
|
|
2406
2705
|
function parseBearerToken(value) {
|
|
2407
2706
|
return (value?.match(/^Bearer\s+(.+)$/i))?.[1];
|
|
2408
2707
|
}
|
|
2708
|
+
async function pairedClientSessionIdForAuthorization(authorization, pairing) {
|
|
2709
|
+
if (!pairing) return;
|
|
2710
|
+
const token = parseBearerToken(authorization);
|
|
2711
|
+
if (!token) return;
|
|
2712
|
+
return (await pairing.sessions.getValidSession(pairing.hashClientToken(token), Date.now()))?.clientSessionId;
|
|
2713
|
+
}
|
|
2714
|
+
function defaultPushNotificationPreferences() {
|
|
2715
|
+
return {
|
|
2716
|
+
actionRequired: false,
|
|
2717
|
+
turnTerminal: false
|
|
2718
|
+
};
|
|
2719
|
+
}
|
|
2409
2720
|
function normalizeApprovalCode(value) {
|
|
2410
2721
|
const normalized = value.toUpperCase().replace(/[^A-Z0-9]/g, "").replaceAll("O", "0").replaceAll("I", "1");
|
|
2411
2722
|
return normalized.length === 8 ? `${normalized.slice(0, 4)}-${normalized.slice(4)}` : normalized;
|
|
@@ -2564,6 +2875,7 @@ async function runPromptStreamed(input) {
|
|
|
2564
2875
|
messagesByThreadId: input.messagesByThreadId,
|
|
2565
2876
|
pendingApprovals: input.pendingApprovals,
|
|
2566
2877
|
queuedInputsByThreadId: input.queuedInputsByThreadId,
|
|
2878
|
+
runThreadOperation: input.runThreadOperation,
|
|
2567
2879
|
prompt: input.prompt,
|
|
2568
2880
|
runOptions: input.runOptions,
|
|
2569
2881
|
secureSession: input.secureSession,
|
|
@@ -3395,11 +3707,16 @@ async function runAppServerPromptStreamed(input) {
|
|
|
3395
3707
|
case "turn/failed": {
|
|
3396
3708
|
const state = terminalTurnState(notification.method, params);
|
|
3397
3709
|
const terminalTurnId = firstString(params, ["turnId"]) ?? turnIdFromParams(params) ?? activeTurnId;
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3710
|
+
input.runThreadOperation(activeThreadId, async () => {
|
|
3711
|
+
finishTerminalTurn({
|
|
3712
|
+
lastError: state === "failed" ? turnErrorMessage(params) : void 0,
|
|
3713
|
+
method: notification.method,
|
|
3714
|
+
state,
|
|
3715
|
+
turnId: terminalTurnId
|
|
3716
|
+
});
|
|
3717
|
+
}).catch((error) => {
|
|
3718
|
+
cleanupNotificationHandler();
|
|
3719
|
+
rejectCompleted(error);
|
|
3403
3720
|
});
|
|
3404
3721
|
return;
|
|
3405
3722
|
}
|
|
@@ -5421,6 +5738,74 @@ function structuredStreamMessage(role, event, fallbackContent) {
|
|
|
5421
5738
|
function isApprovalServerRequest(method) {
|
|
5422
5739
|
return method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval" || method === "item/permissions/requestApproval" || method === "item/tool/requestUserInput" || method === "mcpServer/elicitation/request" || method === "execCommandApproval" || method === "applyPatchApproval";
|
|
5423
5740
|
}
|
|
5741
|
+
function observeAppServerPushNotifications(appServer, dispatcher) {
|
|
5742
|
+
const dispatchedEventIds = /* @__PURE__ */ new Set();
|
|
5743
|
+
const subagentThreadIds = /* @__PURE__ */ new Set();
|
|
5744
|
+
const rememberEventId = (eventId) => {
|
|
5745
|
+
if (dispatchedEventIds.size >= 1e3) dispatchedEventIds.clear();
|
|
5746
|
+
dispatchedEventIds.add(eventId);
|
|
5747
|
+
};
|
|
5748
|
+
const dispatch = (event, eventId) => {
|
|
5749
|
+
if (dispatchedEventIds.has(eventId)) return;
|
|
5750
|
+
rememberEventId(eventId);
|
|
5751
|
+
dispatcher.dispatch(event).catch((error) => {
|
|
5752
|
+
relayDebugLog("push_notification.dispatch_failed", {
|
|
5753
|
+
error: errorMessage(error),
|
|
5754
|
+
intent: event.intent,
|
|
5755
|
+
threadId: event.threadId,
|
|
5756
|
+
turnId: event.turnId
|
|
5757
|
+
});
|
|
5758
|
+
});
|
|
5759
|
+
};
|
|
5760
|
+
appServer.onNotification((notification) => {
|
|
5761
|
+
rememberSubagentThreadIds(notification, subagentThreadIds);
|
|
5762
|
+
const event = pushNotificationEventFromTerminalNotification(notification);
|
|
5763
|
+
if (!event) return;
|
|
5764
|
+
const eventId = `${event.intent}:${event.threadId}:${event.turnId ?? ""}`;
|
|
5765
|
+
if (subagentThreadIds.delete(event.threadId)) {
|
|
5766
|
+
rememberEventId(eventId);
|
|
5767
|
+
return;
|
|
5768
|
+
}
|
|
5769
|
+
dispatch(event, eventId);
|
|
5770
|
+
});
|
|
5771
|
+
appServer.onRequest((request) => {
|
|
5772
|
+
const event = pushNotificationEventFromApprovalRequest(request);
|
|
5773
|
+
if (!event) return;
|
|
5774
|
+
dispatch(event, `${event.intent}:${event.threadId}:${event.turnId ?? ""}:${request.id}`);
|
|
5775
|
+
});
|
|
5776
|
+
}
|
|
5777
|
+
function rememberSubagentThreadIds(notification, subagentThreadIds) {
|
|
5778
|
+
const item = objectRecord(recordParams(notification)?.item);
|
|
5779
|
+
if (item?.type !== "collabAgentToolCall" || ![
|
|
5780
|
+
"spawnAgent",
|
|
5781
|
+
"sendInput",
|
|
5782
|
+
"resumeAgent"
|
|
5783
|
+
].includes(String(item.tool))) return;
|
|
5784
|
+
for (const threadId of stringArray(item.receiverThreadIds)) subagentThreadIds.add(threadId);
|
|
5785
|
+
}
|
|
5786
|
+
function pushNotificationEventFromTerminalNotification(notification) {
|
|
5787
|
+
if (notification.method !== "turn/completed" && notification.method !== "turn/failed") return;
|
|
5788
|
+
const params = recordParams(notification);
|
|
5789
|
+
const status = turnStatus(params);
|
|
5790
|
+
if (status === "aborted" || status === "cancelled" || status === "canceled" || status === "interrupted") return;
|
|
5791
|
+
const threadId = firstString(params, ["threadId", "conversationId"]);
|
|
5792
|
+
if (!threadId) return;
|
|
5793
|
+
return {
|
|
5794
|
+
intent: "turn_terminal",
|
|
5795
|
+
threadId,
|
|
5796
|
+
turnId: firstString(params, ["turnId"]) ?? turnIdFromParams(params)
|
|
5797
|
+
};
|
|
5798
|
+
}
|
|
5799
|
+
function pushNotificationEventFromApprovalRequest(request) {
|
|
5800
|
+
if (!isApprovalServerRequest(request.method)) return;
|
|
5801
|
+
const approval = approvalMessageFromRequest(request);
|
|
5802
|
+
if (!approval) return;
|
|
5803
|
+
return {
|
|
5804
|
+
intent: "action_required",
|
|
5805
|
+
threadId: approval.threadId,
|
|
5806
|
+
turnId: approval.turnId
|
|
5807
|
+
};
|
|
5808
|
+
}
|
|
5424
5809
|
function approvalMessageFromRequest(request) {
|
|
5425
5810
|
const params = recordParams(request);
|
|
5426
5811
|
const threadId = firstString(params, ["threadId", "conversationId"]);
|
|
@@ -6480,7 +6865,7 @@ serve({
|
|
|
6480
6865
|
listenUrl,
|
|
6481
6866
|
pairingPayload,
|
|
6482
6867
|
port: info.port,
|
|
6483
|
-
|
|
6868
|
+
sharedAppServerRemoteAddress: appServerMode === "socket" ? resolveCodexSharedAppServerRemoteAddress() : void 0
|
|
6484
6869
|
}));
|
|
6485
6870
|
});
|
|
6486
6871
|
function stopSharedAppServer(exitCode) {
|
|
@@ -6499,9 +6884,9 @@ function formatStartupInstructions(details) {
|
|
|
6499
6884
|
`${color.prompt("›")} Server: ${color.muted(details.listenUrl)}`,
|
|
6500
6885
|
"",
|
|
6501
6886
|
`${color.prompt("›")} Pairing: ${color.url(details.pairingPayload)}`,
|
|
6502
|
-
...details.
|
|
6887
|
+
...details.sharedAppServerRemoteAddress ? [
|
|
6503
6888
|
"",
|
|
6504
|
-
`${color.prompt("›")} Terminal: ${color.command(
|
|
6889
|
+
`${color.prompt("›")} Terminal: ${color.command(`codex resume --remote ${details.sharedAppServerRemoteAddress}`)}`,
|
|
6505
6890
|
` ${color.muted("Connect through the shared Codex app-server to follow and steer the same live sessions.")}`
|
|
6506
6891
|
] : [],
|
|
6507
6892
|
"",
|