codex-relay 1.4.2 → 1.4.4
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 +28 -18
- package/dist/api-schema2.js +1 -0
- package/dist/cli.js +60 -2
- package/dist/src.js +216 -109
- package/package.json +2 -2
- package/src/api-schema.ts +1 -0
package/README.md
CHANGED
|
@@ -30,14 +30,18 @@ After approval, the phone can list Codex threads, start new work, stream message
|
|
|
30
30
|
|
|
31
31
|
## Shared Terminal and Mobile Sessions
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
On macOS, Codex Relay prefers Codex's shared Unix socket so terminal and mobile clients can follow the same live sessions. If the shared app-server cannot start or initialize, the relay prints a warning and continues with a private app-server.
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
Linux, WSL, and native Windows keep using a private app-server by default. A terminal TUI that was started separately can resume the same saved thread, but it does not receive the relay process's live events.
|
|
36
|
+
|
|
37
|
+
Require the shared app-server on any platform:
|
|
36
38
|
|
|
37
39
|
```sh
|
|
38
40
|
npx codex-relay@latest --shared-app-server
|
|
39
41
|
```
|
|
40
42
|
|
|
43
|
+
This explicit mode does not fall back to a private app-server when shared startup fails.
|
|
44
|
+
|
|
41
45
|
When a shared app-server is already running, the relay attaches to it instead of starting another one. If the relay's own socket connection resets, it reconnects without deliberately stopping the shared app-server.
|
|
42
46
|
|
|
43
47
|
Then connect a new terminal TUI to the shared app-server. On macOS, Linux, or WSL:
|
|
@@ -56,7 +60,7 @@ Pass a thread ID after the remote endpoint to open a specific thread. The relay
|
|
|
56
60
|
|
|
57
61
|
Shared mode uses Codex's experimental app-server transport. A directly connected terminal TUI has its own WebSocket connection, which the relay cannot observe or reconnect. If that terminal reports a socket reset while the thread continues on mobile, reconnect it with the matching remote endpoint above and append the thread ID if needed.
|
|
58
62
|
|
|
59
|
-
Shared mode requires a recent Codex CLI with app-server and `resume --remote` support. It uses a Unix socket on macOS, Linux, and WSL, or a loopback-only WebSocket on Windows. If
|
|
63
|
+
Shared mode requires a recent Codex CLI with app-server and `resume --remote` support. It uses a Unix socket on macOS, Linux, and WSL, or a loopback-only WebSocket on Windows. If explicit shared mode is unavailable, update Codex or omit `--shared-app-server`. On macOS, set `CODEX_RELAY_APP_SERVER_MODE=stdio` to force private mode instead of using the shared-first default.
|
|
60
64
|
|
|
61
65
|
## Background Mode
|
|
62
66
|
|
|
@@ -79,10 +83,10 @@ Print the current pairing QR again:
|
|
|
79
83
|
npx codex-relay@latest qr
|
|
80
84
|
```
|
|
81
85
|
|
|
82
|
-
Stop a background server
|
|
86
|
+
Stop a background server:
|
|
83
87
|
|
|
84
88
|
```sh
|
|
85
|
-
|
|
89
|
+
npx codex-relay@latest stop
|
|
86
90
|
```
|
|
87
91
|
|
|
88
92
|
## Commands
|
|
@@ -99,11 +103,17 @@ npx codex-relay@latest --bg
|
|
|
99
103
|
|
|
100
104
|
Start the relay in the background.
|
|
101
105
|
|
|
106
|
+
```sh
|
|
107
|
+
npx codex-relay@latest stop
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Stop the background relay. Repeating this command is safe when no background server is running.
|
|
111
|
+
|
|
102
112
|
```sh
|
|
103
113
|
npx codex-relay@latest --shared-app-server
|
|
104
114
|
```
|
|
105
115
|
|
|
106
|
-
|
|
116
|
+
Require the relay to start through Codex's shared app-server socket.
|
|
107
117
|
|
|
108
118
|
```sh
|
|
109
119
|
npx codex-relay@latest qr
|
|
@@ -127,17 +137,17 @@ Start the relay and automatically approve mobile pairing requests. Use this only
|
|
|
127
137
|
|
|
128
138
|
The relay listens on `0.0.0.0:8787` by default. Configure it with environment variables:
|
|
129
139
|
|
|
130
|
-
| Variable | Purpose
|
|
131
|
-
| -------------------------------------- |
|
|
132
|
-
| `PORT` | Server port. Defaults to `8787`.
|
|
133
|
-
| `HOST` | Listen host. Defaults to `0.0.0.0`.
|
|
134
|
-
| `CODEX_RELAY_WORKSPACE_PATH` | Workspace path Codex should use. Defaults to the directory where you run `npx codex-relay@latest`.
|
|
135
|
-
| `CODEX_RELAY_AUTH_DB_PATH` | Pairing and session database path. Defaults to `.codex-relay/auth.db`.
|
|
136
|
-
| `CODEX_RELAY_APPROVAL_SECRET` | Secret used by the local approve command. Usually generated automatically.
|
|
137
|
-
| `CODEX_RELAY_DANGEROUSLY_AUTO_APPROVE` | Set to `1` to auto-approve mobile pairing requests. Prefer the CLI flag for local use.
|
|
138
|
-
| `CODEX_RELAY_APP_SERVER_MODE` | Set to `socket`
|
|
139
|
-
| `CODEX_HOME` | Codex home directory, used when reading Codex session metadata.
|
|
140
|
-
| `CODEX_BIN` | Codex CLI executable path.
|
|
140
|
+
| Variable | Purpose |
|
|
141
|
+
| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
142
|
+
| `PORT` | Server port. Defaults to `8787`. |
|
|
143
|
+
| `HOST` | Listen host. Defaults to `0.0.0.0`. |
|
|
144
|
+
| `CODEX_RELAY_WORKSPACE_PATH` | Workspace path Codex should use. Defaults to the directory where you run `npx codex-relay@latest`. |
|
|
145
|
+
| `CODEX_RELAY_AUTH_DB_PATH` | Pairing and session database path. Defaults to `.codex-relay/auth.db`. |
|
|
146
|
+
| `CODEX_RELAY_APPROVAL_SECRET` | Secret used by the local approve command. Usually generated automatically. |
|
|
147
|
+
| `CODEX_RELAY_DANGEROUSLY_AUTO_APPROVE` | Set to `1` to auto-approve mobile pairing requests. Prefer the CLI flag for local use. |
|
|
148
|
+
| `CODEX_RELAY_APP_SERVER_MODE` | Set to `socket` to require shared mode or `stdio` to require private mode. Unset prefers shared mode with startup fallback on macOS and private mode elsewhere. |
|
|
149
|
+
| `CODEX_HOME` | Codex home directory, used when reading Codex session metadata. |
|
|
150
|
+
| `CODEX_BIN` | Codex CLI executable path. |
|
|
141
151
|
|
|
142
152
|
Examples:
|
|
143
153
|
|
|
@@ -174,7 +184,7 @@ npx codex-relay@latest qr
|
|
|
174
184
|
Or stop the background process shown by the CLI:
|
|
175
185
|
|
|
176
186
|
```sh
|
|
177
|
-
|
|
187
|
+
npx codex-relay@latest stop
|
|
178
188
|
```
|
|
179
189
|
|
|
180
190
|
If the mobile app cannot connect, confirm that the phone can reach the printed `Mobile:` URL and that the chosen port is not blocked by a firewall.
|
package/dist/api-schema2.js
CHANGED
|
@@ -268,6 +268,7 @@ const ChatMessageSchema = z.object({
|
|
|
268
268
|
});
|
|
269
269
|
const ThreadSummarySchema = z.object({
|
|
270
270
|
id: z.string().min(1),
|
|
271
|
+
parentThreadId: z.string().min(1).optional(),
|
|
271
272
|
title: z.string().min(1),
|
|
272
273
|
createdAt: IsoDateTimeSchema,
|
|
273
274
|
updatedAt: IsoDateTimeSchema,
|
package/dist/cli.js
CHANGED
|
@@ -20,6 +20,23 @@ async function readRunningRelayPid(pidPath, options = {}) {
|
|
|
20
20
|
const command = await (options.commandReader ?? readProcessCommand)(pid);
|
|
21
21
|
return command && isRelayProcessCommand(command) ? pid : void 0;
|
|
22
22
|
}
|
|
23
|
+
async function stopRunningRelay(pidPath, options = {}) {
|
|
24
|
+
const pid = await readRunningRelayPid(pidPath, options);
|
|
25
|
+
if (!pid) {
|
|
26
|
+
await rm(pidPath, { force: true });
|
|
27
|
+
return { kind: "not-running" };
|
|
28
|
+
}
|
|
29
|
+
(options.signalProcess ?? signalRelayProcess)(pid);
|
|
30
|
+
if (!await (options.waitForProcessExit ?? waitForRelayProcessExit)(pid)) return {
|
|
31
|
+
kind: "timed-out",
|
|
32
|
+
pid
|
|
33
|
+
};
|
|
34
|
+
await rm(pidPath, { force: true });
|
|
35
|
+
return {
|
|
36
|
+
kind: "stopped",
|
|
37
|
+
pid
|
|
38
|
+
};
|
|
39
|
+
}
|
|
23
40
|
function defaultIsProcessAlive(pid) {
|
|
24
41
|
try {
|
|
25
42
|
process.kill(pid, 0);
|
|
@@ -28,6 +45,21 @@ function defaultIsProcessAlive(pid) {
|
|
|
28
45
|
return false;
|
|
29
46
|
}
|
|
30
47
|
}
|
|
48
|
+
function signalRelayProcess(pid) {
|
|
49
|
+
try {
|
|
50
|
+
process.kill(pid, "SIGTERM");
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error instanceof Error && "code" in error && error.code === "ESRCH") return;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function waitForRelayProcessExit(pid) {
|
|
57
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
58
|
+
if (!defaultIsProcessAlive(pid)) return true;
|
|
59
|
+
await setTimeout(50);
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
31
63
|
async function readProcessCommand(pid) {
|
|
32
64
|
try {
|
|
33
65
|
const { stdout } = await execFileAsync("ps", [
|
|
@@ -48,12 +80,13 @@ function isRelayProcessCommand(command) {
|
|
|
48
80
|
//#endregion
|
|
49
81
|
//#region src/cli.ts
|
|
50
82
|
const npxCommand = "npx codex-relay@latest";
|
|
51
|
-
const program = new Command().name("codex-relay").description("Run and approve the codex-relay local CLI bridge.").option("--bg", "run the Codex Relay server in the background").option("--debug", "write verbose relay diagnostics to debug.log").option("--shared-app-server", "
|
|
83
|
+
const program = new Command().name("codex-relay").description("Run and approve the codex-relay local CLI bridge.").option("--bg", "run the Codex Relay server in the background").option("--debug", "write verbose relay diagnostics to debug.log").option("--shared-app-server", "require a shared Codex app-server so terminal and mobile can share live sessions").option("--dangerously-auto-approve", "automatically approve mobile pairing requests without a local approval command").addHelpText("after", `
|
|
52
84
|
|
|
53
85
|
Examples:
|
|
54
86
|
${npxCommand} Start the relay and print a pairing QR
|
|
55
87
|
${npxCommand} --shared-app-server Share live sessions with a connected terminal
|
|
56
88
|
${npxCommand} --bg Start the relay in the background
|
|
89
|
+
${npxCommand} stop Stop the background relay
|
|
57
90
|
${npxCommand} qr Print the current pairing QR
|
|
58
91
|
${npxCommand} clear Sign out every paired mobile app
|
|
59
92
|
${npxCommand} approve CODE Approve a pending mobile pairing request`).action(async (options) => {
|
|
@@ -66,6 +99,9 @@ Examples:
|
|
|
66
99
|
}
|
|
67
100
|
await import("./src.js").catch(handleServerStartError);
|
|
68
101
|
});
|
|
102
|
+
program.command("stop").description("Stop the background Codex Relay server.").action(async () => {
|
|
103
|
+
await stopBackgroundServer();
|
|
104
|
+
});
|
|
69
105
|
program.command("qr").description("Print the current pairing QR for an already running server.").action(async () => {
|
|
70
106
|
await printPairingQr();
|
|
71
107
|
});
|
|
@@ -124,6 +160,7 @@ async function startBackgroundServer() {
|
|
|
124
160
|
console.log(`Logs: ${logPath}`);
|
|
125
161
|
if (process.env.CODEX_RELAY_DEBUG === "1") console.log(`Debug logs: ${debugLogPath}`);
|
|
126
162
|
console.log(`Print the pairing QR later with: ${npxCommand} qr`);
|
|
163
|
+
console.log(`Stop the background relay with: ${npxCommand} stop`);
|
|
127
164
|
}
|
|
128
165
|
function backgroundArgs() {
|
|
129
166
|
return process.argv.slice(2).filter((arg) => arg !== "--bg");
|
|
@@ -140,6 +177,27 @@ async function waitForBackgroundPid(child, pidPath) {
|
|
|
140
177
|
await setTimeout(100);
|
|
141
178
|
}
|
|
142
179
|
}
|
|
180
|
+
async function stopBackgroundServer() {
|
|
181
|
+
printStopResult(await stopRunningRelay(codexRelayDataPath("server.pid")));
|
|
182
|
+
}
|
|
183
|
+
function printStopResult(result) {
|
|
184
|
+
switch (result.kind) {
|
|
185
|
+
case "not-running":
|
|
186
|
+
console.log("No background Codex Relay server is running.");
|
|
187
|
+
return;
|
|
188
|
+
case "stopped":
|
|
189
|
+
console.log(`Stopped codex-relay background server (pid ${result.pid}).`);
|
|
190
|
+
return;
|
|
191
|
+
case "timed-out":
|
|
192
|
+
console.error(`Timed out stopping codex-relay background server (pid ${result.pid}).`);
|
|
193
|
+
process.exitCode = 1;
|
|
194
|
+
return;
|
|
195
|
+
default: return assertNever(result);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function assertNever(value) {
|
|
199
|
+
throw new TypeError(`Unexpected background stop result: ${JSON.stringify(value)}`);
|
|
200
|
+
}
|
|
143
201
|
async function approvePairing(rawCode) {
|
|
144
202
|
const approvalCode = normalizeApprovalCode(rawCode ?? "");
|
|
145
203
|
if (!approvalCode) {
|
|
@@ -277,7 +335,7 @@ async function handleServerStartError(error) {
|
|
|
277
335
|
console.error(` ${npxCommand} qr`);
|
|
278
336
|
console.error("");
|
|
279
337
|
console.error("To stop the background server:");
|
|
280
|
-
console.error(`
|
|
338
|
+
console.error(` ${npxCommand} stop`);
|
|
281
339
|
console.error("");
|
|
282
340
|
console.error(`Logs: ${logPath}`);
|
|
283
341
|
} else {
|
package/dist/src.js
CHANGED
|
@@ -36,16 +36,31 @@ const stdioAppServerArgs = [
|
|
|
36
36
|
];
|
|
37
37
|
const sharedWindowsServerUrl = "ws://127.0.0.1:8788";
|
|
38
38
|
const windowsShellExtensions = new Set([".bat", ".cmd"]);
|
|
39
|
-
function resolveCodexAppServerMode(
|
|
39
|
+
function resolveCodexAppServerMode(input = {}) {
|
|
40
|
+
const env = input.env ?? process.env;
|
|
41
|
+
const platform$2 = input.platform ?? platform();
|
|
40
42
|
const configuredMode = env.CODEX_RELAY_APP_SERVER_MODE?.trim().toLowerCase();
|
|
41
|
-
if (!configuredMode
|
|
42
|
-
|
|
43
|
+
if (!configuredMode) return platform$2 === "darwin" ? {
|
|
44
|
+
fallbackToStdio: true,
|
|
45
|
+
mode: "socket"
|
|
46
|
+
} : {
|
|
47
|
+
fallbackToStdio: false,
|
|
48
|
+
mode: "stdio"
|
|
49
|
+
};
|
|
50
|
+
if (configuredMode === "stdio") return {
|
|
51
|
+
fallbackToStdio: false,
|
|
52
|
+
mode: "stdio"
|
|
53
|
+
};
|
|
54
|
+
if (configuredMode === "socket") return {
|
|
55
|
+
fallbackToStdio: false,
|
|
56
|
+
mode: "socket"
|
|
57
|
+
};
|
|
43
58
|
throw new Error(`Unsupported CODEX_RELAY_APP_SERVER_MODE ${JSON.stringify(configuredMode)}. Expected "stdio" or "socket".`);
|
|
44
59
|
}
|
|
45
60
|
function resolveCodexAppServerSpawn(input = {}) {
|
|
46
|
-
const platform$
|
|
61
|
+
const platform$3 = input.platform ?? platform();
|
|
47
62
|
const command = resolveCodexBinary(input.env ?? process.env);
|
|
48
|
-
const isWindows = platform$
|
|
63
|
+
const isWindows = platform$3 === "win32";
|
|
49
64
|
return {
|
|
50
65
|
command,
|
|
51
66
|
args: [...stdioAppServerArgs],
|
|
@@ -54,15 +69,15 @@ function resolveCodexAppServerSpawn(input = {}) {
|
|
|
54
69
|
};
|
|
55
70
|
}
|
|
56
71
|
function resolveCodexSharedAppServerSpawn(input = {}) {
|
|
57
|
-
const platform$
|
|
72
|
+
const platform$4 = input.platform ?? platform();
|
|
58
73
|
const command = resolveCodexBinary(input.env ?? process.env);
|
|
59
|
-
const isWindows = platform$
|
|
74
|
+
const isWindows = platform$4 === "win32";
|
|
60
75
|
return {
|
|
61
76
|
command,
|
|
62
77
|
args: [
|
|
63
78
|
"app-server",
|
|
64
79
|
"--listen",
|
|
65
|
-
resolveCodexSharedAppServerRemoteAddress(platform$
|
|
80
|
+
resolveCodexSharedAppServerRemoteAddress(platform$4)
|
|
66
81
|
],
|
|
67
82
|
shell: isWindows && shouldUseWindowsShell(command),
|
|
68
83
|
windowsHide: isWindows
|
|
@@ -126,8 +141,10 @@ const sharedSocketReconnectDelaysMs = [
|
|
|
126
141
|
2e3
|
|
127
142
|
];
|
|
128
143
|
var CodexAppServerClient = class {
|
|
144
|
+
activeMode;
|
|
129
145
|
child;
|
|
130
146
|
closed = false;
|
|
147
|
+
fallbackToStdio;
|
|
131
148
|
initialized;
|
|
132
149
|
notificationHandlers = /* @__PURE__ */ new Set();
|
|
133
150
|
nextId = 1;
|
|
@@ -135,12 +152,21 @@ var CodexAppServerClient = class {
|
|
|
135
152
|
reconnecting;
|
|
136
153
|
requestHandlers = /* @__PURE__ */ new Set();
|
|
137
154
|
readline;
|
|
155
|
+
sharedReconnectEnabled = false;
|
|
138
156
|
sharedServer;
|
|
139
157
|
socket;
|
|
158
|
+
onStartupFallback;
|
|
140
159
|
startSharedServer;
|
|
141
160
|
constructor(options = {}) {
|
|
161
|
+
const mode = options.mode ?? resolveCodexAppServerMode();
|
|
162
|
+
this.activeMode = mode.mode;
|
|
163
|
+
this.fallbackToStdio = mode.fallbackToStdio;
|
|
164
|
+
this.onStartupFallback = options.onStartupFallback;
|
|
142
165
|
this.startSharedServer = options.startSharedServer ?? startSharedCodexAppServer;
|
|
143
166
|
}
|
|
167
|
+
get appServerMode() {
|
|
168
|
+
return this.activeMode;
|
|
169
|
+
}
|
|
144
170
|
initialize() {
|
|
145
171
|
return this.ensureInitialized();
|
|
146
172
|
}
|
|
@@ -220,15 +246,8 @@ var CodexAppServerClient = class {
|
|
|
220
246
|
this.closed = true;
|
|
221
247
|
for (const pending of this.pending.values()) pending.reject(/* @__PURE__ */ new Error("Codex app-server was closed."));
|
|
222
248
|
this.pending.clear();
|
|
223
|
-
this.
|
|
224
|
-
this.
|
|
225
|
-
const socket = this.socket;
|
|
226
|
-
this.socket = void 0;
|
|
227
|
-
socket?.close();
|
|
228
|
-
this.sharedServer?.kill();
|
|
229
|
-
this.readline = void 0;
|
|
230
|
-
this.child = void 0;
|
|
231
|
-
this.sharedServer = void 0;
|
|
249
|
+
this.stopStdioCodexAppServer();
|
|
250
|
+
this.stopSharedCodexAppServer();
|
|
232
251
|
this.initialized = void 0;
|
|
233
252
|
}
|
|
234
253
|
async request(method, params) {
|
|
@@ -262,13 +281,31 @@ var CodexAppServerClient = class {
|
|
|
262
281
|
return this.initialized;
|
|
263
282
|
}
|
|
264
283
|
async start() {
|
|
265
|
-
|
|
284
|
+
if (this.activeMode === "stdio") {
|
|
285
|
+
await this.startAndInitializeStdioCodexAppServer();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
await this.startOrAttachSharedCodexAppServer();
|
|
290
|
+
await this.initializeAppServer();
|
|
291
|
+
this.fallbackToStdio = false;
|
|
292
|
+
this.sharedReconnectEnabled = true;
|
|
293
|
+
} catch (error) {
|
|
294
|
+
const sharedError = error instanceof Error ? error : new Error(String(error));
|
|
295
|
+
this.stopSharedCodexAppServer();
|
|
296
|
+
if (!this.fallbackToStdio) throw sharedError;
|
|
297
|
+
this.activeMode = "stdio";
|
|
298
|
+
relayDebugLog("app_server.shared_socket.startup_fallback", { message: sharedError.message });
|
|
299
|
+
this.onStartupFallback?.(sharedError);
|
|
300
|
+
await this.startAndInitializeStdioCodexAppServer();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
async startAndInitializeStdioCodexAppServer() {
|
|
266
304
|
try {
|
|
267
|
-
|
|
268
|
-
else this.startStdioCodexAppServer();
|
|
305
|
+
this.startStdioCodexAppServer();
|
|
269
306
|
await this.initializeAppServer();
|
|
270
307
|
} catch (error) {
|
|
271
|
-
|
|
308
|
+
this.stopStdioCodexAppServer();
|
|
272
309
|
throw error;
|
|
273
310
|
}
|
|
274
311
|
}
|
|
@@ -310,6 +347,14 @@ var CodexAppServerClient = class {
|
|
|
310
347
|
this.child?.kill();
|
|
311
348
|
this.child = void 0;
|
|
312
349
|
}
|
|
350
|
+
stopSharedCodexAppServer() {
|
|
351
|
+
this.sharedReconnectEnabled = false;
|
|
352
|
+
const socket = this.socket;
|
|
353
|
+
this.socket = void 0;
|
|
354
|
+
socket?.close();
|
|
355
|
+
this.sharedServer?.kill();
|
|
356
|
+
this.sharedServer = void 0;
|
|
357
|
+
}
|
|
313
358
|
async startOrAttachSharedCodexAppServer() {
|
|
314
359
|
try {
|
|
315
360
|
await this.connectSharedCodexAppServer();
|
|
@@ -319,7 +364,7 @@ var CodexAppServerClient = class {
|
|
|
319
364
|
});
|
|
320
365
|
return;
|
|
321
366
|
} catch (error) {
|
|
322
|
-
const attachError =
|
|
367
|
+
const attachError = error instanceof Error ? error : new Error(String(error));
|
|
323
368
|
relayDebugLog("app_server.shared_socket.attach_failed", {
|
|
324
369
|
message: attachError.message,
|
|
325
370
|
socketPath: sharedCodexAppServerSocketPath()
|
|
@@ -409,7 +454,7 @@ var CodexAppServerClient = class {
|
|
|
409
454
|
return this.sharedServer ? "relay-owned" : "attached";
|
|
410
455
|
}
|
|
411
456
|
scheduleSharedSocketReconnect() {
|
|
412
|
-
if (this.closed || this.reconnecting) return;
|
|
457
|
+
if (this.closed || !this.sharedReconnectEnabled || this.reconnecting) return;
|
|
413
458
|
const reconnecting = this.reconnectSharedCodexAppServer();
|
|
414
459
|
this.reconnecting = reconnecting;
|
|
415
460
|
this.initialized = reconnecting;
|
|
@@ -428,7 +473,7 @@ var CodexAppServerClient = class {
|
|
|
428
473
|
let lastError;
|
|
429
474
|
for (const delayMs of sharedSocketReconnectDelaysMs) {
|
|
430
475
|
await setTimeout$1(delayMs);
|
|
431
|
-
if (this.closed) return;
|
|
476
|
+
if (this.closed || !this.sharedReconnectEnabled) return;
|
|
432
477
|
try {
|
|
433
478
|
await this.connectSharedCodexAppServer();
|
|
434
479
|
await this.initializeAppServer();
|
|
@@ -438,7 +483,7 @@ var CodexAppServerClient = class {
|
|
|
438
483
|
});
|
|
439
484
|
return;
|
|
440
485
|
} catch (error) {
|
|
441
|
-
lastError =
|
|
486
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
442
487
|
const socket = this.socket;
|
|
443
488
|
this.socket = void 0;
|
|
444
489
|
socket?.close();
|
|
@@ -572,16 +617,8 @@ async function startSharedCodexAppServer() {
|
|
|
572
617
|
throw new Error(`Timed out waiting for the shared Codex app-server at ${remoteAddress}.`);
|
|
573
618
|
}
|
|
574
619
|
async function waitForSharedCodexAppServer(remoteAddress) {
|
|
575
|
-
if (remoteAddress === "unix://") {
|
|
576
|
-
await access(sharedCodexAppServerSocketPath());
|
|
577
|
-
return;
|
|
578
|
-
}
|
|
579
|
-
const url = new URL(remoteAddress);
|
|
580
620
|
await new Promise((resolve, reject) => {
|
|
581
|
-
const socket = connect({
|
|
582
|
-
host: url.hostname,
|
|
583
|
-
port: Number(url.port)
|
|
584
|
-
});
|
|
621
|
+
const socket = remoteAddress === "unix://" ? connect({ path: sharedCodexAppServerSocketPath() }) : connectRemoteAddress(remoteAddress);
|
|
585
622
|
const onError = (error) => reject(error);
|
|
586
623
|
socket.once("error", onError);
|
|
587
624
|
socket.once("connect", () => {
|
|
@@ -591,6 +628,13 @@ async function waitForSharedCodexAppServer(remoteAddress) {
|
|
|
591
628
|
});
|
|
592
629
|
});
|
|
593
630
|
}
|
|
631
|
+
function connectRemoteAddress(remoteAddress) {
|
|
632
|
+
const url = new URL(remoteAddress);
|
|
633
|
+
return connect({
|
|
634
|
+
host: url.hostname,
|
|
635
|
+
port: Number(url.port)
|
|
636
|
+
});
|
|
637
|
+
}
|
|
594
638
|
function sharedCodexAppServerSocketPath() {
|
|
595
639
|
return join(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"), "app-server-control", "app-server-control.sock");
|
|
596
640
|
}
|
|
@@ -1424,7 +1468,7 @@ function createApp(options = {}) {
|
|
|
1424
1468
|
const appServerHistoryLoadsByThreadId = /* @__PURE__ */ new Map();
|
|
1425
1469
|
const steeringThreads = /* @__PURE__ */ new Set();
|
|
1426
1470
|
const secureSessionsByTokenHash = /* @__PURE__ */ new Map();
|
|
1427
|
-
const activeStreamControllers = /* @__PURE__ */ new
|
|
1471
|
+
const activeStreamControllers = /* @__PURE__ */ new Map();
|
|
1428
1472
|
const threadOptions = { workingDirectory: workspacePath };
|
|
1429
1473
|
function runThreadOperation(threadId, operation) {
|
|
1430
1474
|
const result = (threadOperationTails.get(threadId) ?? Promise.resolve()).then(operation, operation);
|
|
@@ -1871,7 +1915,7 @@ function createApp(options = {}) {
|
|
|
1871
1915
|
const stream = new ReadableStream({
|
|
1872
1916
|
start(controller) {
|
|
1873
1917
|
streamController = controller;
|
|
1874
|
-
activeStreamControllers.
|
|
1918
|
+
activeStreamControllers.set(controller, closeStream);
|
|
1875
1919
|
send(workspaceTerminalOutputResponse(session, since));
|
|
1876
1920
|
if (session.exitedAt) return;
|
|
1877
1921
|
unsubscribe = subscribeWorkspaceTerminalOutput(session, send);
|
|
@@ -2062,7 +2106,7 @@ function createApp(options = {}) {
|
|
|
2062
2106
|
if (appServer) try {
|
|
2063
2107
|
const appServerThreads = await appServer.listThreads();
|
|
2064
2108
|
const response = ListThreadsResponseSchema.parse({
|
|
2065
|
-
threads: appServerThreads.map((thread) => rememberAppServerThread(threads, thread)),
|
|
2109
|
+
threads: appServerThreads.filter((thread) => !isSubagentThread(thread)).map((thread) => rememberAppServerThread(threads, thread)),
|
|
2066
2110
|
source: "app-server"
|
|
2067
2111
|
});
|
|
2068
2112
|
return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
|
|
@@ -2085,7 +2129,7 @@ function createApp(options = {}) {
|
|
|
2085
2129
|
const appServerThreads = await appServer.listThreads();
|
|
2086
2130
|
const response = ArchiveThreadResponseSchema.parse({
|
|
2087
2131
|
archivedThreadId: threadId,
|
|
2088
|
-
threads: appServerThreads.map((thread) => rememberAppServerThread(threads, thread)),
|
|
2132
|
+
threads: appServerThreads.filter((thread) => !isSubagentThread(thread)).map((thread) => rememberAppServerThread(threads, thread)),
|
|
2089
2133
|
source: "app-server"
|
|
2090
2134
|
});
|
|
2091
2135
|
return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
|
|
@@ -2112,9 +2156,12 @@ function createApp(options = {}) {
|
|
|
2112
2156
|
const detailStartedAt = Date.now();
|
|
2113
2157
|
relayDebugLog("thread.detail.requested", { threadId });
|
|
2114
2158
|
const knownThread = threads.get(threadId);
|
|
2159
|
+
if (knownThread && isSubagentThread(knownThread)) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("not_found", `Thread ${threadId} is not known to this server.`), 404);
|
|
2115
2160
|
const wasKnownRunning = knownThread?.state === "running" || activeAppServerTurnIdsByThreadId.has(threadId);
|
|
2116
2161
|
if (appServer) try {
|
|
2117
|
-
const
|
|
2162
|
+
const thread = await appServer.readThread(threadId, { includeTurns: false });
|
|
2163
|
+
if (isSubagentThread(thread)) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("not_found", `Thread ${threadId} is not known to this server.`), 404);
|
|
2164
|
+
const mappedThread = rememberAppServerThread(threads, thread);
|
|
2118
2165
|
const cachedMessages = messagesByThreadId.get(threadId) ?? [];
|
|
2119
2166
|
let loadedMessages = false;
|
|
2120
2167
|
let messages = cachedMessages;
|
|
@@ -2593,20 +2640,30 @@ function createApp(options = {}) {
|
|
|
2593
2640
|
});
|
|
2594
2641
|
const encoder = new TextEncoder();
|
|
2595
2642
|
const secureSession = getSecureSessionForRequest(c, options.pairing, secureSessionsByTokenHash);
|
|
2596
|
-
let streamController;
|
|
2597
2643
|
let streamSettled = false;
|
|
2644
|
+
let closeStream = () => {};
|
|
2598
2645
|
const stream = new ReadableStream({
|
|
2599
2646
|
start(controller) {
|
|
2600
|
-
|
|
2601
|
-
|
|
2647
|
+
const attachmentAbortController = new AbortController();
|
|
2648
|
+
let closed = false;
|
|
2649
|
+
let stopPreviewMonitor = () => {};
|
|
2650
|
+
closeStream = () => {
|
|
2651
|
+
if (closed) return;
|
|
2652
|
+
closed = true;
|
|
2653
|
+
stopPreviewMonitor();
|
|
2654
|
+
attachmentAbortController.abort();
|
|
2655
|
+
activeStreamControllers.delete(controller);
|
|
2656
|
+
closeSseController(controller);
|
|
2657
|
+
};
|
|
2658
|
+
activeStreamControllers.set(controller, closeStream);
|
|
2602
2659
|
relayDebugLog("thread.stream.started", {
|
|
2603
2660
|
mode: !runOptions.prompt && appServer ? "attach" : "run",
|
|
2604
2661
|
threadId
|
|
2605
2662
|
});
|
|
2606
|
-
|
|
2663
|
+
stopPreviewMonitor = startWebPreviewTargetMonitor({
|
|
2607
2664
|
bridgeUrl: c.req.url,
|
|
2608
2665
|
send(target) {
|
|
2609
|
-
sendSse(controller, encoder, secureSession, {
|
|
2666
|
+
return sendSse(controller, encoder, secureSession, {
|
|
2610
2667
|
type: "thread.preview_target.detected",
|
|
2611
2668
|
threadId,
|
|
2612
2669
|
target
|
|
@@ -2621,6 +2678,7 @@ function createApp(options = {}) {
|
|
|
2621
2678
|
messagesByThreadId,
|
|
2622
2679
|
pendingApprovals,
|
|
2623
2680
|
secureSession,
|
|
2681
|
+
signal: attachmentAbortController.signal,
|
|
2624
2682
|
threadId,
|
|
2625
2683
|
threads
|
|
2626
2684
|
}).finally(() => {
|
|
@@ -2629,8 +2687,7 @@ function createApp(options = {}) {
|
|
|
2629
2687
|
mode: "attach",
|
|
2630
2688
|
threadId
|
|
2631
2689
|
});
|
|
2632
|
-
|
|
2633
|
-
stopPreviewMonitor();
|
|
2690
|
+
closeStream();
|
|
2634
2691
|
});
|
|
2635
2692
|
return;
|
|
2636
2693
|
}
|
|
@@ -2641,14 +2698,12 @@ function createApp(options = {}) {
|
|
|
2641
2698
|
thread: knownThread,
|
|
2642
2699
|
error: apiError("unsupported", "Running-thread stream attachment requires the Codex app-server.").error
|
|
2643
2700
|
});
|
|
2644
|
-
closeSseController(controller);
|
|
2645
2701
|
streamSettled = true;
|
|
2646
2702
|
relayDebugLog("thread.stream.finished", {
|
|
2647
2703
|
mode: "unsupported",
|
|
2648
2704
|
threadId
|
|
2649
2705
|
});
|
|
2650
|
-
|
|
2651
|
-
stopPreviewMonitor();
|
|
2706
|
+
closeStream();
|
|
2652
2707
|
return;
|
|
2653
2708
|
}
|
|
2654
2709
|
const skills = runOptions.skills ?? [];
|
|
@@ -2681,12 +2736,11 @@ function createApp(options = {}) {
|
|
|
2681
2736
|
mode: "run",
|
|
2682
2737
|
threadId
|
|
2683
2738
|
});
|
|
2684
|
-
|
|
2685
|
-
stopPreviewMonitor();
|
|
2739
|
+
closeStream();
|
|
2686
2740
|
});
|
|
2687
2741
|
},
|
|
2688
2742
|
cancel(reason) {
|
|
2689
|
-
|
|
2743
|
+
closeStream();
|
|
2690
2744
|
relayDebugLog("thread.stream.cancelled_by_client", {
|
|
2691
2745
|
reason: debugReason(reason),
|
|
2692
2746
|
settled: streamSettled,
|
|
@@ -3030,8 +3084,6 @@ async function runPromptStreamed(input) {
|
|
|
3030
3084
|
thread: threadSummary,
|
|
3031
3085
|
error: errorBody.error
|
|
3032
3086
|
});
|
|
3033
|
-
} finally {
|
|
3034
|
-
closeSseController(input.controller);
|
|
3035
3087
|
}
|
|
3036
3088
|
}
|
|
3037
3089
|
async function startAppServerTurn(appServer, threadId, input) {
|
|
@@ -3122,7 +3174,16 @@ async function streamRunningAppServerThread(input) {
|
|
|
3122
3174
|
let observedTurnActivity = false;
|
|
3123
3175
|
let producedTurnOutput = false;
|
|
3124
3176
|
let threadSummary = input.threads.get(input.threadId);
|
|
3125
|
-
|
|
3177
|
+
let cleanupNotificationHandler = () => void 0;
|
|
3178
|
+
let cleanupRequestHandler = () => void 0;
|
|
3179
|
+
let handlersCleaned = false;
|
|
3180
|
+
const cleanupHandlers = () => {
|
|
3181
|
+
if (handlersCleaned) return;
|
|
3182
|
+
handlersCleaned = true;
|
|
3183
|
+
cleanupRequestHandler();
|
|
3184
|
+
cleanupNotificationHandler();
|
|
3185
|
+
};
|
|
3186
|
+
cleanupRequestHandler = input.appServer.onRequest((request) => {
|
|
3126
3187
|
if (!isApprovalServerRequest(request.method)) {
|
|
3127
3188
|
input.appServer.rejectRequest(request.id, -32601, `${request.method} is not supported by Codex Relay mobile yet.`);
|
|
3128
3189
|
return;
|
|
@@ -3175,8 +3236,15 @@ async function streamRunningAppServerThread(input) {
|
|
|
3175
3236
|
message
|
|
3176
3237
|
});
|
|
3177
3238
|
});
|
|
3178
|
-
let cleanupNotificationHandler = () => void 0;
|
|
3179
3239
|
const completed = new Promise((resolve, reject) => {
|
|
3240
|
+
let settled = false;
|
|
3241
|
+
const finish = (error) => {
|
|
3242
|
+
if (settled) return;
|
|
3243
|
+
settled = true;
|
|
3244
|
+
cleanupHandlers();
|
|
3245
|
+
if (error === void 0) resolve();
|
|
3246
|
+
else reject(error);
|
|
3247
|
+
};
|
|
3180
3248
|
cleanupNotificationHandler = input.appServer.onNotification((notification) => {
|
|
3181
3249
|
const params = recordParams(notification);
|
|
3182
3250
|
const notificationThreadId = firstString(params, ["threadId"]);
|
|
@@ -3191,10 +3259,7 @@ async function streamRunningAppServerThread(input) {
|
|
|
3191
3259
|
type: "thread.state.changed",
|
|
3192
3260
|
thread: threadSummary
|
|
3193
3261
|
});
|
|
3194
|
-
if (state !== "running")
|
|
3195
|
-
cleanupNotificationHandler();
|
|
3196
|
-
resolve();
|
|
3197
|
-
}
|
|
3262
|
+
if (state !== "running") finish();
|
|
3198
3263
|
return;
|
|
3199
3264
|
}
|
|
3200
3265
|
case "thread/goal/updated":
|
|
@@ -3303,8 +3368,7 @@ async function streamRunningAppServerThread(input) {
|
|
|
3303
3368
|
threadId: input.threadId,
|
|
3304
3369
|
threads: input.threads
|
|
3305
3370
|
});
|
|
3306
|
-
|
|
3307
|
-
resolve();
|
|
3371
|
+
finish();
|
|
3308
3372
|
return;
|
|
3309
3373
|
}
|
|
3310
3374
|
if (assistantMessageId) {
|
|
@@ -3342,27 +3406,40 @@ async function streamRunningAppServerThread(input) {
|
|
|
3342
3406
|
error: errorBody.error
|
|
3343
3407
|
});
|
|
3344
3408
|
}
|
|
3345
|
-
|
|
3346
|
-
resolve();
|
|
3409
|
+
finish();
|
|
3347
3410
|
return;
|
|
3348
3411
|
}
|
|
3349
3412
|
}
|
|
3350
3413
|
} catch (error) {
|
|
3351
|
-
|
|
3352
|
-
reject(error);
|
|
3414
|
+
finish(error);
|
|
3353
3415
|
}
|
|
3354
3416
|
});
|
|
3355
3417
|
});
|
|
3418
|
+
let removeAbortListener = () => {};
|
|
3419
|
+
const aborted = new Promise((resolve) => {
|
|
3420
|
+
const onAbort = () => {
|
|
3421
|
+
cleanupHandlers();
|
|
3422
|
+
resolve();
|
|
3423
|
+
};
|
|
3424
|
+
if (input.signal.aborted) {
|
|
3425
|
+
onAbort();
|
|
3426
|
+
return;
|
|
3427
|
+
}
|
|
3428
|
+
input.signal.addEventListener("abort", onAbort, { once: true });
|
|
3429
|
+
removeAbortListener = () => input.signal.removeEventListener("abort", onAbort);
|
|
3430
|
+
});
|
|
3356
3431
|
try {
|
|
3357
|
-
const appServerThread = await input.appServer.readThread(input.threadId, { includeTurns: false });
|
|
3432
|
+
const appServerThread = await Promise.race([input.appServer.readThread(input.threadId, { includeTurns: false }), aborted.then(() => void 0)]);
|
|
3433
|
+
if (!appServerThread || input.signal.aborted) return;
|
|
3358
3434
|
threadSummary = rememberAppServerThread(input.threads, appServerThread);
|
|
3359
3435
|
sendSse(input.controller, input.encoder, input.secureSession, {
|
|
3360
3436
|
type: "thread.state.changed",
|
|
3361
3437
|
thread: threadSummary
|
|
3362
3438
|
});
|
|
3363
3439
|
if (threadSummary.state !== "running") return;
|
|
3364
|
-
await completed;
|
|
3440
|
+
await Promise.race([completed, aborted]);
|
|
3365
3441
|
} catch (error) {
|
|
3442
|
+
if (input.signal.aborted) return;
|
|
3366
3443
|
threadSummary = updateThread(input.threads, input.messagesByThreadId, input.threadId, {
|
|
3367
3444
|
state: "failed",
|
|
3368
3445
|
lastError: errorMessage(error)
|
|
@@ -3373,9 +3450,8 @@ async function streamRunningAppServerThread(input) {
|
|
|
3373
3450
|
error: apiError("codex_run_failed", threadSummary.lastError ?? "Codex run failed.").error
|
|
3374
3451
|
});
|
|
3375
3452
|
} finally {
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
closeSseController(input.controller);
|
|
3453
|
+
removeAbortListener();
|
|
3454
|
+
cleanupHandlers();
|
|
3379
3455
|
}
|
|
3380
3456
|
}
|
|
3381
3457
|
async function runAppServerPromptStreamed(input) {
|
|
@@ -3843,7 +3919,6 @@ async function runAppServerPromptStreamed(input) {
|
|
|
3843
3919
|
input.activeAppServerTurnIdsByThreadId.delete(activeThreadId);
|
|
3844
3920
|
input.steeringThreads.delete(activeThreadId);
|
|
3845
3921
|
}
|
|
3846
|
-
closeSseController(input.controller);
|
|
3847
3922
|
}
|
|
3848
3923
|
}
|
|
3849
3924
|
function sendEmptyAppServerTurnError(input) {
|
|
@@ -4027,14 +4102,15 @@ function createSecureSessionHandle(pairing, tokenHash, session) {
|
|
|
4027
4102
|
};
|
|
4028
4103
|
}
|
|
4029
4104
|
function sortedThreads(threads) {
|
|
4030
|
-
return [...threads.values()].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
4105
|
+
return [...threads.values()].filter((thread) => !isSubagentThread(thread)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
4031
4106
|
}
|
|
4032
4107
|
async function ensureKnownThread(input) {
|
|
4033
4108
|
const knownThread = input.threads.get(input.threadId);
|
|
4034
|
-
if (knownThread) return knownThread;
|
|
4109
|
+
if (knownThread) return isSubagentThread(knownThread) ? void 0 : knownThread;
|
|
4035
4110
|
if (!input.appServer) return;
|
|
4036
4111
|
try {
|
|
4037
4112
|
const appServerThread = await input.appServer.readThread(input.threadId, { includeTurns: false });
|
|
4113
|
+
if (isSubagentThread(appServerThread)) return;
|
|
4038
4114
|
return rememberAppServerThread(input.threads, appServerThread);
|
|
4039
4115
|
} catch {
|
|
4040
4116
|
return;
|
|
@@ -4464,13 +4540,17 @@ function sendSse(controller, encoder, secureSession, event) {
|
|
|
4464
4540
|
stage: "event",
|
|
4465
4541
|
threadId
|
|
4466
4542
|
});
|
|
4467
|
-
return;
|
|
4543
|
+
return false;
|
|
4468
4544
|
}
|
|
4469
|
-
if (!enqueueSseChunk(controller, encoder.encode(`data: ${JSON.stringify(data)}\n\n`)))
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4545
|
+
if (!enqueueSseChunk(controller, encoder.encode(`data: ${JSON.stringify(data)}\n\n`))) {
|
|
4546
|
+
relayDebugLog("thread.stream.sse.enqueue_failed", {
|
|
4547
|
+
eventType: parsed.type,
|
|
4548
|
+
stage: "data",
|
|
4549
|
+
threadId
|
|
4550
|
+
});
|
|
4551
|
+
return false;
|
|
4552
|
+
}
|
|
4553
|
+
return true;
|
|
4474
4554
|
}
|
|
4475
4555
|
function sendTerminalOutputSse(controller, encoder, secureSession, response) {
|
|
4476
4556
|
const parsed = WorkspaceTerminalOutputResponseSchema.parse(response);
|
|
@@ -4502,7 +4582,7 @@ function closeSseController(controller) {
|
|
|
4502
4582
|
}
|
|
4503
4583
|
}
|
|
4504
4584
|
function closeActiveStreamControllers(controllers) {
|
|
4505
|
-
for (const
|
|
4585
|
+
for (const closeStream of controllers.values()) closeStream();
|
|
4506
4586
|
controllers.clear();
|
|
4507
4587
|
}
|
|
4508
4588
|
function isClosedStreamControllerError(error) {
|
|
@@ -4510,28 +4590,36 @@ function isClosedStreamControllerError(error) {
|
|
|
4510
4590
|
}
|
|
4511
4591
|
function startWebPreviewTargetMonitor({ bridgeUrl, send }) {
|
|
4512
4592
|
const urls = webPreviewCandidateUrls(bridgeUrl);
|
|
4513
|
-
const
|
|
4593
|
+
const abortController = new AbortController();
|
|
4514
4594
|
let stopped = false;
|
|
4595
|
+
let nextScan;
|
|
4596
|
+
const stop = () => {
|
|
4597
|
+
if (stopped) return;
|
|
4598
|
+
stopped = true;
|
|
4599
|
+
abortController.abort();
|
|
4600
|
+
if (nextScan) {
|
|
4601
|
+
clearTimeout(nextScan);
|
|
4602
|
+
nextScan = void 0;
|
|
4603
|
+
}
|
|
4604
|
+
};
|
|
4515
4605
|
async function scan() {
|
|
4516
4606
|
if (stopped) return;
|
|
4517
|
-
const
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4607
|
+
const target = await detectWebPreviewTarget(urls, abortController.signal);
|
|
4608
|
+
if (stopped) return;
|
|
4609
|
+
if (target) {
|
|
4610
|
+
stop();
|
|
4521
4611
|
try {
|
|
4522
4612
|
send(target);
|
|
4523
|
-
} catch {
|
|
4524
|
-
|
|
4525
|
-
return;
|
|
4526
|
-
}
|
|
4613
|
+
} catch {}
|
|
4614
|
+
return;
|
|
4527
4615
|
}
|
|
4616
|
+
nextScan = setTimeout(() => {
|
|
4617
|
+
nextScan = void 0;
|
|
4618
|
+
scan();
|
|
4619
|
+
}, 1500);
|
|
4528
4620
|
}
|
|
4529
4621
|
scan();
|
|
4530
|
-
|
|
4531
|
-
return () => {
|
|
4532
|
-
stopped = true;
|
|
4533
|
-
clearInterval(interval);
|
|
4534
|
-
};
|
|
4622
|
+
return stop;
|
|
4535
4623
|
}
|
|
4536
4624
|
function webPreviewCandidateUrls(bridgeUrl) {
|
|
4537
4625
|
const bridge = new URL(bridgeUrl);
|
|
@@ -4554,11 +4642,14 @@ function webPreviewCandidatePorts() {
|
|
|
4554
4642
|
function readCollaborationModeTemplate(name) {
|
|
4555
4643
|
return readFileSync(new URL(`./collaboration-mode-templates/${name}.md`, import.meta.url), "utf8").replaceAll("{{KNOWN_MODE_NAMES}}", knownCollaborationModeNames).trim();
|
|
4556
4644
|
}
|
|
4557
|
-
async function
|
|
4558
|
-
return (await Promise.all(urls.map((url) => probeWebPreviewTarget(url)))).
|
|
4645
|
+
async function detectWebPreviewTarget(urls, signal) {
|
|
4646
|
+
return (await Promise.all(urls.map((url) => probeWebPreviewTarget(url, signal)))).find((target) => Boolean(target));
|
|
4559
4647
|
}
|
|
4560
|
-
async function probeWebPreviewTarget(url) {
|
|
4648
|
+
async function probeWebPreviewTarget(url, signal) {
|
|
4561
4649
|
const controller = new AbortController();
|
|
4650
|
+
const abort = () => controller.abort();
|
|
4651
|
+
if (signal.aborted) abort();
|
|
4652
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
4562
4653
|
const timeout = setTimeout(() => controller.abort(), 700);
|
|
4563
4654
|
try {
|
|
4564
4655
|
const response = await fetch(url, { signal: controller.signal });
|
|
@@ -4579,6 +4670,7 @@ async function probeWebPreviewTarget(url) {
|
|
|
4579
4670
|
return;
|
|
4580
4671
|
} finally {
|
|
4581
4672
|
clearTimeout(timeout);
|
|
4673
|
+
signal.removeEventListener("abort", abort);
|
|
4582
4674
|
}
|
|
4583
4675
|
}
|
|
4584
4676
|
function looksLikeHtml(value) {
|
|
@@ -4758,6 +4850,7 @@ function mapAppServerThread(thread, fallbackMessageCount) {
|
|
|
4758
4850
|
const mappedState = mapAppServerThreadState(thread.status, thread.turns);
|
|
4759
4851
|
return ThreadSummarySchema.parse({
|
|
4760
4852
|
id: thread.id,
|
|
4853
|
+
parentThreadId: thread.parentThreadId ?? void 0,
|
|
4761
4854
|
title: thread.name ?? preview(thread.preview || "Untitled thread"),
|
|
4762
4855
|
createdAt,
|
|
4763
4856
|
updatedAt,
|
|
@@ -4781,6 +4874,9 @@ function rememberAppServerThread(threads, thread) {
|
|
|
4781
4874
|
threads.set(threadWithLocalRuntime.id, threadWithLocalRuntime);
|
|
4782
4875
|
return threadWithLocalRuntime;
|
|
4783
4876
|
}
|
|
4877
|
+
function isSubagentThread(thread) {
|
|
4878
|
+
return Boolean(thread.parentThreadId);
|
|
4879
|
+
}
|
|
4784
4880
|
function mapAppServerThreadGoal(goal) {
|
|
4785
4881
|
if (!goal) return null;
|
|
4786
4882
|
return ThreadGoalSchema.parse({
|
|
@@ -5771,7 +5867,12 @@ function observeAppServerPushNotifications(appServer, dispatcher) {
|
|
|
5771
5867
|
appServer.onRequest((request) => {
|
|
5772
5868
|
const event = pushNotificationEventFromApprovalRequest(request);
|
|
5773
5869
|
if (!event) return;
|
|
5774
|
-
|
|
5870
|
+
const eventId = `${event.intent}:${event.threadId}:${event.turnId ?? ""}:${request.id}`;
|
|
5871
|
+
if (subagentThreadIds.has(event.threadId)) {
|
|
5872
|
+
rememberEventId(eventId);
|
|
5873
|
+
return;
|
|
5874
|
+
}
|
|
5875
|
+
dispatch(event, eventId);
|
|
5775
5876
|
});
|
|
5776
5877
|
}
|
|
5777
5878
|
function rememberSubagentThreadIds(notification, subagentThreadIds) {
|
|
@@ -6784,16 +6885,21 @@ const npxCommand = "npx codex-relay@latest";
|
|
|
6784
6885
|
const sessionStore = await createTursoPairingSessionStore(process.env.CODEX_RELAY_AUTH_DB_PATH ?? await prepareCodexRelayDataPath("auth.db", ["auth.db-shm", "auth.db-wal"]));
|
|
6785
6886
|
const preferencesStore = createFileRuntimePreferencesStore(process.env.CODEX_RELAY_PREFERENCES_PATH ?? await prepareCodexRelayDataPath("preferences.json"));
|
|
6786
6887
|
const appServerMode = resolveCodexAppServerMode();
|
|
6787
|
-
const
|
|
6788
|
-
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
|
|
6792
|
-
|
|
6888
|
+
const relayAppServer = appServerMode.mode === "socket" ? new CodexAppServerClient({
|
|
6889
|
+
mode: appServerMode,
|
|
6890
|
+
onStartupFallback: (error) => {
|
|
6891
|
+
logRuntimeEvent("Fallback", `Shared app-server unavailable; continuing with a private app-server (${error.message}).`);
|
|
6892
|
+
}
|
|
6893
|
+
}) : void 0;
|
|
6894
|
+
if (relayAppServer) {
|
|
6895
|
+
await relayAppServer.initialize();
|
|
6896
|
+
process.once("SIGINT", () => stopRelayAppServer(130));
|
|
6897
|
+
process.once("SIGTERM", () => stopRelayAppServer(143));
|
|
6898
|
+
process.once("exit", () => relayAppServer.close());
|
|
6793
6899
|
}
|
|
6794
6900
|
serve({
|
|
6795
6901
|
fetch: createApp({
|
|
6796
|
-
appServer:
|
|
6902
|
+
appServer: relayAppServer,
|
|
6797
6903
|
pairing: {
|
|
6798
6904
|
approvalSecret,
|
|
6799
6905
|
dangerouslyAutoApprove,
|
|
@@ -6865,11 +6971,11 @@ serve({
|
|
|
6865
6971
|
listenUrl,
|
|
6866
6972
|
pairingPayload,
|
|
6867
6973
|
port: info.port,
|
|
6868
|
-
sharedAppServerRemoteAddress: appServerMode === "socket" ? resolveCodexSharedAppServerRemoteAddress() : void 0
|
|
6974
|
+
sharedAppServerRemoteAddress: relayAppServer?.appServerMode === "socket" ? resolveCodexSharedAppServerRemoteAddress() : void 0
|
|
6869
6975
|
}));
|
|
6870
6976
|
});
|
|
6871
|
-
function
|
|
6872
|
-
|
|
6977
|
+
function stopRelayAppServer(exitCode) {
|
|
6978
|
+
relayAppServer?.close();
|
|
6873
6979
|
process.exit(exitCode);
|
|
6874
6980
|
}
|
|
6875
6981
|
function formatStartupInstructions(details) {
|
|
@@ -6893,6 +6999,7 @@ function formatStartupInstructions(details) {
|
|
|
6893
6999
|
`${color.prompt("›")} Commands`,
|
|
6894
7000
|
` ${color.command(npxCommand)} Start and print a pairing QR`,
|
|
6895
7001
|
` ${color.command(`${npxCommand} --bg`)} Start in the background`,
|
|
7002
|
+
` ${color.command(`${npxCommand} stop`)} Stop the background relay`,
|
|
6896
7003
|
` ${color.command(`${npxCommand} qr`)} Print this QR again`,
|
|
6897
7004
|
` ${color.command(`${npxCommand} approve <code>`)} Approve a device`,
|
|
6898
7005
|
"",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-relay",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.4",
|
|
4
4
|
"description": "Local Codex Relay CLI bridge for the Codex Relay mobile app.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"@noble/ciphers": "2.2.0",
|
|
42
42
|
"@noble/curves": "2.2.0",
|
|
43
43
|
"@noble/hashes": "2.2.0",
|
|
44
|
-
"@openai/codex-sdk": "0.
|
|
44
|
+
"@openai/codex-sdk": "0.145.0",
|
|
45
45
|
"base64-js": "1.5.1",
|
|
46
46
|
"commander": "^14.0.3",
|
|
47
47
|
"es-git": "^0.6.0",
|
package/src/api-schema.ts
CHANGED
|
@@ -304,6 +304,7 @@ export const ChatMessageSchema = z.object({
|
|
|
304
304
|
|
|
305
305
|
export const ThreadSummarySchema = z.object({
|
|
306
306
|
id: z.string().min(1),
|
|
307
|
+
parentThreadId: z.string().min(1).optional(),
|
|
307
308
|
title: z.string().min(1),
|
|
308
309
|
createdAt: IsoDateTimeSchema,
|
|
309
310
|
updatedAt: IsoDateTimeSchema,
|