@nchappell/codex-web-ui 1.0.6 → 1.0.8

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.
Files changed (53) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +3 -3
  3. package/.next/fallback-build-manifest.json +3 -3
  4. package/.next/server/app/_global-error.html +1 -1
  5. package/.next/server/app/_global-error.rsc +1 -1
  6. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/server/app/_not-found.html +1 -1
  12. package/.next/server/app/_not-found.rsc +1 -1
  13. package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  14. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  15. package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  16. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  17. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  18. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  19. package/.next/server/app/index.html +1 -1
  20. package/.next/server/app/index.rsc +1 -1
  21. package/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  22. package/.next/server/app/index.segments/_full.segment.rsc +1 -1
  23. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  24. package/.next/server/app/index.segments/_index.segment.rsc +1 -1
  25. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  26. package/.next/server/app/thread/[threadId]/page_client-reference-manifest.js +1 -1
  27. package/.next/server/app/threads/page_client-reference-manifest.js +1 -1
  28. package/.next/server/app/threads.html +1 -1
  29. package/.next/server/app/threads.rsc +2 -2
  30. package/.next/server/app/threads.segments/_full.segment.rsc +2 -2
  31. package/.next/server/app/threads.segments/_head.segment.rsc +1 -1
  32. package/.next/server/app/threads.segments/_index.segment.rsc +1 -1
  33. package/.next/server/app/threads.segments/_tree.segment.rsc +1 -1
  34. package/.next/server/app/threads.segments/threads/__PAGE__.segment.rsc +2 -2
  35. package/.next/server/app/threads.segments/threads.segment.rsc +1 -1
  36. package/.next/server/chunks/[root-of-the-server]__0pym12l._.js +3 -3
  37. package/.next/server/chunks/[root-of-the-server]__0pym12l._.js.map +1 -1
  38. package/.next/server/chunks/ssr/[root-of-the-server]__0h7ed3q._.js +6 -6
  39. package/.next/server/chunks/ssr/[root-of-the-server]__0h7ed3q._.js.map +1 -1
  40. package/.next/server/middleware-build-manifest.js +3 -3
  41. package/.next/server/pages/404.html +1 -1
  42. package/.next/server/pages/500.html +1 -1
  43. package/.next/static/chunks/{0k4c4eedynfr0.js → 0g7lwnm9wxo3~.js} +7 -7
  44. package/README.md +17 -14
  45. package/bin/codex-web-ui.js +70 -33
  46. package/bin/docker-entrypoint.sh +5 -0
  47. package/client/src/App.tsx +1 -19
  48. package/client/src/api.ts +0 -5
  49. package/package.json +3 -2
  50. package/server/appApi.ts +0 -25
  51. /package/.next/static/{Kq7OCztahzPSRmlZKubC1 → QMdoD-vxQDoRuGNGiCAPJ}/_buildManifest.js +0 -0
  52. /package/.next/static/{Kq7OCztahzPSRmlZKubC1 → QMdoD-vxQDoRuGNGiCAPJ}/_clientMiddlewareManifest.js +0 -0
  53. /package/.next/static/{Kq7OCztahzPSRmlZKubC1 → QMdoD-vxQDoRuGNGiCAPJ}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -23,17 +23,18 @@ The app is a Next.js App Router application with Tailwind, shadcn/ui, and AI Ele
23
23
  ```bash
24
24
  npm install -g @nchappell/codex-web-ui
25
25
  codex-web-ui init
26
+ codex-web-ui app-server start
26
27
  codex-web-ui doctor
27
28
  codex-web-ui
28
29
  ```
29
30
 
30
31
  Open `http://127.0.0.1:4545`.
31
32
 
32
- The normal startup command starts the Next.js server and starts or recovers a
33
- detached `codex app-server` sidecar on a Unix socket. Run `codex-web-ui doctor`
34
- after install or upgrade to check the production build, Codex CLI availability,
35
- login status, auth setup, writable data directories, permission settings, and
36
- app-server socket.
33
+ The normal startup command starts only the Next.js server. The detached
34
+ `codex app-server` sidecar must already be running and websocket-ready on the
35
+ configured Unix socket. Run `codex-web-ui doctor` after install or upgrade to
36
+ check the production build, Codex CLI availability, login status, auth setup,
37
+ writable data directories, permission settings, and app-server socket.
37
38
 
38
39
  For source or git installs, run `codex-web-ui --build` once if the package does
39
40
  not include a `.next` production build.
@@ -81,8 +82,9 @@ npm run test:docker
81
82
  ```
82
83
 
83
84
  The Docker smoke builds `codex-web-ui:smoke`, starts it on port `4555` with a
84
- temporary data directory and `--external-app-server`, verifies `/threads`,
85
- `/api/auth`, and password login, then removes the container and temp volumes.
85
+ temporary data directory, verifies `/threads`, `/api/auth`, and password login,
86
+ then removes the container and temp volumes. The container entrypoint starts the
87
+ sidecar before launching the web process.
86
88
  By default it builds with `@nchappell/codex-web-ui@<package.json version>` from
87
89
  npm. Set `CODEX_WEB_UI_DOCKER_NPM_SPEC=@nchappell/codex-web-ui@latest` or
88
90
  another npm package spec to test a different published package,
@@ -152,10 +154,10 @@ codex-web-ui \
152
154
  --data-dir "$PWD/data"
153
155
  ```
154
156
 
155
- The main command manages the app-server sidecar by default. If you already run
156
- the official app-server yourself, pass `--external-app-server` with
157
- `--app-server-socket`; in that mode startup only connects to the socket and does
158
- not attempt recovery.
157
+ The main command does not manage the app-server sidecar. Start it explicitly
158
+ with `codex-web-ui app-server start`, or run the official app-server yourself
159
+ and point `--app-server-socket` at that Unix socket. Startup fails fast if the
160
+ socket is not websocket-ready.
159
161
 
160
162
  Default permissions are intentionally conservative: `on-request` approval and
161
163
  `workspace-write` sandbox. `danger-full-access`, `on-failure`, and `never`
@@ -345,9 +347,10 @@ npm run app-server:recover
345
347
  npm run app-server:status
346
348
  ```
347
349
 
348
- `app-server:recover` treats a live PID without a connectable socket as
349
- degraded, restarts the sidecar, and waits until the Unix socket accepts
350
- connections. Use it when the UI reports `connect ENOENT ...codex-app-server.sock`.
350
+ `app-server:recover` treats a live PID without a websocket-ready socket as
351
+ degraded, restarts the sidecar, and waits until the Unix socket accepts the
352
+ app-server WebSocket upgrade. Use it when the UI reports `connect ENOENT
353
+ ...codex-app-server.sock` or `Unix socket closed before WebSocket upgrade`.
351
354
 
352
355
  For MCP OAuth from a phone or another machine, Codex Web UI relays OAuth
353
356
  callbacks through the currently used Web UI origin. When an MCP OAuth login
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { spawn } from "node:child_process";
4
4
  import { accessSync, chmodSync, closeSync, constants as fsConstants, existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
- import { randomBytes } from "node:crypto";
5
+ import { createHash, randomBytes } from "node:crypto";
6
6
  import { createConnection } from "node:net";
7
7
  import { homedir } from "node:os";
8
8
  import { dirname, join, resolve } from "node:path";
@@ -123,15 +123,21 @@ if (!existsSync(join(packageRoot, ".next", "BUILD_ID"))) {
123
123
  mkdirSync(dataDir, { recursive: true });
124
124
  mkdirSync(uploadDir, { recursive: true });
125
125
 
126
- if (!externalAppServer) {
127
- await recoverAppServer(appServerControlOptions({ command: codexCommand, socket: appServerSocket }));
126
+ if (!await isSocketWebSocketReady(appServerSocket, 1_000)) {
127
+ fail([
128
+ `Codex app-server socket is not ready: ${appServerSocket}`,
129
+ "Start it before launching the Web UI:",
130
+ ` codex-web-ui app-server start --socket ${appServerSocket}`,
131
+ "",
132
+ "If you run the official app-server yourself, make sure it listens on that Unix socket."
133
+ ].join("\n"));
128
134
  }
129
135
 
130
136
  console.log(`codex-web-ui listening on http://${host}:${port}`);
131
137
  console.log(`codex-web-ui app cwd: ${packageRoot}`);
132
138
  console.log(`codex default cwd: ${env.CODEX_CWD}`);
133
139
  console.log(`runtime data dir: ${env.CODEX_WEB_UI_DATA_DIR}`);
134
- console.log(`codex app-server socket: ${env.CODEX_APP_SERVER_SOCKET}${externalAppServer ? " (external)" : " (managed)"}`);
140
+ console.log(`codex app-server socket: ${env.CODEX_APP_SERVER_SOCKET}`);
135
141
  console.log(`permissions: approval=${env.CODEX_WEB_UI_APPROVAL_POLICY}, sandbox=${env.CODEX_WEB_UI_SANDBOX}${env.CODEX_WEB_UI_LOCK_PERMISSIONS === "1" ? ", locked" : ""}${unsafePermissions ? ", unsafe enabled" : ""}`);
136
142
  if (configPath) {
137
143
  console.log(`config file: ${configPath}`);
@@ -373,11 +379,11 @@ async function runDoctorCommand(options) {
373
379
  }
374
380
 
375
381
  const socketPresent = existsSync(options.appServerSocket);
376
- const socketConnectable = socketPresent ? await isSocketConnectable(options.appServerSocket, 500) : false;
382
+ const socketReady = socketPresent ? await isSocketWebSocketReady(options.appServerSocket, 500) : false;
377
383
  checks.push([
378
384
  "app-server socket",
379
- `${options.appServerSocket}${socketConnectable ? " (connectable)" : socketPresent ? " (present, not connectable)" : " (missing)"}`,
380
- options.externalAppServer ? socketConnectable : true
385
+ `${options.appServerSocket}${socketReady ? " (websocket ready)" : socketPresent ? " (present, not websocket-ready)" : " (missing)"}`,
386
+ socketReady
381
387
  ]);
382
388
 
383
389
  console.log("Codex Web UI doctor");
@@ -385,8 +391,8 @@ async function runDoctorCommand(options) {
385
391
  console.log(`${ok ? "ok " : "fail"} ${name}: ${detail}`);
386
392
  }
387
393
  console.log("");
388
- if (!socketConnectable && !options.externalAppServer) {
389
- console.log("The main `codex-web-ui` command will try to start or recover the managed app-server sidecar.");
394
+ if (!socketReady) {
395
+ console.log("Start the sidecar first with `codex-web-ui app-server start`, then run `codex-web-ui`.");
390
396
  }
391
397
  if (!options.password && isLoopbackHost(options.host)) {
392
398
  console.log("No password is configured; `codex-web-ui` will print a temporary local password on each start.");
@@ -410,12 +416,12 @@ function appServerControlOptions({ command, logFile, pidFile, socket }) {
410
416
  async function startAppServer(options) {
411
417
  const existingPid = readPid(options.pidFile);
412
418
  if (existingPid && isPidRunning(existingPid)) {
413
- if (await isSocketConnectable(options.socket, 750)) {
419
+ if (await isSocketWebSocketReady(options.socket, 750)) {
414
420
  console.log(`codex app-server already running: pid=${existingPid}`);
415
- console.log(`socket: ${options.socket} (connectable)`);
421
+ console.log(`socket: ${options.socket} (websocket ready)`);
416
422
  return;
417
423
  }
418
- console.warn(`codex app-server pid ${existingPid} is present, but the socket is not connectable; restarting`);
424
+ console.warn(`codex app-server pid ${existingPid} is present, but the socket is not websocket-ready; restarting`);
419
425
  await stopAppServer(options, { quiet: true });
420
426
  }
421
427
  mkdirSync(dirname(options.socket), { recursive: true });
@@ -442,7 +448,7 @@ async function startAppServer(options) {
442
448
  fail(`Timed out waiting for codex app-server socket: ${options.socket}. Check ${options.logFile}`);
443
449
  }
444
450
  console.log(`codex app-server started: pid=${child.pid}`);
445
- console.log(`socket: ${options.socket} (connectable)`);
451
+ console.log(`socket: ${options.socket} (websocket ready)`);
446
452
  console.log(`log: ${options.logFile}`);
447
453
  }
448
454
 
@@ -473,9 +479,9 @@ async function stopAppServer(options, { quiet = false } = {}) {
473
479
 
474
480
  async function recoverAppServer(options) {
475
481
  const pid = readPid(options.pidFile);
476
- if (pid && isPidRunning(pid) && await isSocketConnectable(options.socket, 750)) {
482
+ if (pid && isPidRunning(pid) && await isSocketWebSocketReady(options.socket, 750)) {
477
483
  console.log(`codex app-server healthy: pid=${pid}`);
478
- console.log(`socket: ${options.socket} (connectable)`);
484
+ console.log(`socket: ${options.socket} (websocket ready)`);
479
485
  return;
480
486
  }
481
487
  await stopAppServer(options, { quiet: true });
@@ -487,8 +493,8 @@ async function printAppServerStatus(options) {
487
493
  const pid = readPid(options.pidFile);
488
494
  const running = Boolean(pid && isPidRunning(pid));
489
495
  const socketPresent = existsSync(options.socket);
490
- const socketConnectable = socketPresent ? await isSocketConnectable(options.socket, 500) : false;
491
- const state = running && socketConnectable
496
+ const socketReady = socketPresent ? await isSocketWebSocketReady(options.socket, 500) : false;
497
+ const state = running && socketReady
492
498
  ? "running"
493
499
  : running
494
500
  ? "degraded"
@@ -496,7 +502,7 @@ async function printAppServerStatus(options) {
496
502
  ? "stopped with stale socket"
497
503
  : "stopped";
498
504
  console.log(`codex app-server ${state}${pid ? `: pid=${pid}` : ""}`);
499
- console.log(`socket: ${options.socket}${socketConnectable ? " (connectable)" : socketPresent ? " (present, not connectable)" : " (missing)"}`);
505
+ console.log(`socket: ${options.socket}${socketReady ? " (websocket ready)" : socketPresent ? " (present, not websocket-ready)" : " (missing)"}`);
500
506
  console.log(`pid file: ${options.pidFile}`);
501
507
  console.log(`log: ${options.logFile}`);
502
508
  }
@@ -546,7 +552,7 @@ function waitForSocket(socket, timeoutMs) {
546
552
  const startedAt = Date.now();
547
553
  return new Promise((resolve) => {
548
554
  const tick = async () => {
549
- if (await isSocketConnectable(socket, 250)) {
555
+ if (await isSocketWebSocketReady(socket, 250)) {
550
556
  resolve(true);
551
557
  return;
552
558
  }
@@ -560,25 +566,56 @@ function waitForSocket(socket, timeoutMs) {
560
566
  });
561
567
  }
562
568
 
563
- function isSocketConnectable(socket, timeoutMs) {
569
+ function isSocketWebSocketReady(socket, timeoutMs) {
564
570
  if (!existsSync(socket)) {
565
571
  return Promise.resolve(false);
566
572
  }
567
573
  return new Promise((resolve) => {
568
574
  const client = createConnection(socket);
569
- const timer = setTimeout(() => {
575
+ const key = randomBytes(16).toString("base64");
576
+ const expectedAccept = createHash("sha1")
577
+ .update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`)
578
+ .digest("base64");
579
+ let buffer = Buffer.alloc(0);
580
+ let settled = false;
581
+ const settle = (ready) => {
582
+ if (settled) {
583
+ return;
584
+ }
585
+ settled = true;
586
+ clearTimeout(timer);
570
587
  client.destroy();
571
- resolve(false);
572
- }, timeoutMs);
588
+ resolve(ready);
589
+ };
590
+ const timer = setTimeout(() => settle(false), timeoutMs);
573
591
  client.once("connect", () => {
574
- clearTimeout(timer);
575
- client.end();
576
- resolve(true);
592
+ client.write([
593
+ "GET / HTTP/1.1",
594
+ "Host: localhost",
595
+ "Upgrade: websocket",
596
+ "Connection: Upgrade",
597
+ `Sec-WebSocket-Key: ${key}`,
598
+ "Sec-WebSocket-Version: 13",
599
+ "",
600
+ ""
601
+ ].join("\r\n"));
577
602
  });
578
- client.once("error", () => {
579
- clearTimeout(timer);
580
- resolve(false);
603
+ client.on("data", (chunk) => {
604
+ buffer = Buffer.concat([buffer, chunk]);
605
+ const headerEnd = buffer.indexOf("\r\n\r\n");
606
+ if (headerEnd === -1) {
607
+ return;
608
+ }
609
+ const headerText = buffer.subarray(0, headerEnd).toString("utf8");
610
+ const lines = headerText.split("\r\n");
611
+ const headers = Object.fromEntries(lines.slice(1).map((line) => {
612
+ const index = line.indexOf(":");
613
+ return index === -1 ? ["", ""] : [line.slice(0, index).trim().toLowerCase(), line.slice(index + 1).trim()];
614
+ }).filter(([name]) => name));
615
+ settle(lines[0]?.startsWith("HTTP/1.1 101") && headers["sec-websocket-accept"] === expectedAccept);
581
616
  });
617
+ client.once("error", () => settle(false));
618
+ client.once("close", () => settle(false));
582
619
  });
583
620
  }
584
621
 
@@ -803,8 +840,8 @@ function printHelp() {
803
840
  codex-web-ui doctor [options]
804
841
  codex-web-ui app-server <start|stop|restart|recover|status> [options]
805
842
 
806
- Starts the Codex Web UI using Next.js production mode. By default this also
807
- starts or recovers a detached codex app-server sidecar.
843
+ Starts the Codex Web UI using Next.js production mode. The codex app-server
844
+ sidecar must already be running.
808
845
 
809
846
  Options:
810
847
  -c, --config <path> JSON config file. Default search:
@@ -818,8 +855,8 @@ Options:
818
855
  --allowed-origins <csv> Set CODEX_WEB_UI_ALLOWED_ORIGINS
819
856
  --app-server-socket <path> Codex app-server Unix socket. Default:
820
857
  ~/.codex-webgui/codex-app-server.sock
821
- --external-app-server Use the socket but do not start/recover the
822
- app-server sidecar
858
+ --external-app-server Deprecated no-op; app-server is always external
859
+ to the web process
823
860
  --codex-command <command> Codex command. Default: codex
824
861
  --cwd <path> Default Codex working directory
825
862
  --model <model> Default Codex model. Default: gpt-5.5
@@ -0,0 +1,5 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ codex-web-ui app-server start --socket "${CODEX_APP_SERVER_SOCKET:-/home/node/.codex-webgui/codex-app-server.sock}"
5
+ exec codex-web-ui "$@"
@@ -85,7 +85,6 @@ import {
85
85
  logout,
86
86
  openEventStream,
87
87
  readReferencedFile,
88
- recoverAppServer,
89
88
  reloadMcpServers,
90
89
  respondClientRequest,
91
90
  restartServer,
@@ -896,7 +895,6 @@ export default function App({ initialThreadId = null }: AppProps) {
896
895
  rateLimits={rateLimits}
897
896
  status={serverStatus}
898
897
  onClose={() => setStatusOpen(false)}
899
- onRecover={recoverAppServerFromUi}
900
898
  onRefreshMcp={reloadMcpServerStatus}
901
899
  onLoginMcpServer={handleLoginMcpServer}
902
900
  statusRefreshing={statusRefreshing}
@@ -1002,17 +1000,6 @@ export default function App({ initialThreadId = null }: AppProps) {
1002
1000
  }
1003
1001
  }
1004
1002
 
1005
- async function recoverAppServerFromUi() {
1006
- try {
1007
- const result = await recoverAppServer();
1008
- setServerStatus(result.status);
1009
- showToast(result.output || "App server recovered");
1010
- await loadSessions();
1011
- } catch (error) {
1012
- showToast(error);
1013
- }
1014
- }
1015
-
1016
1003
  async function refreshStatus() {
1017
1004
  setStatusRefreshing(true);
1018
1005
  try {
@@ -2563,7 +2550,6 @@ function StatusModal({
2563
2550
  statusRefreshing,
2564
2551
  onClose,
2565
2552
  onLoginMcpServer,
2566
- onRecover,
2567
2553
  onRefresh,
2568
2554
  onRefreshMcp,
2569
2555
  onSaveMcpServer
@@ -2576,7 +2562,6 @@ function StatusModal({
2576
2562
  statusRefreshing: boolean;
2577
2563
  onClose: () => void;
2578
2564
  onLoginMcpServer: (name: string) => Promise<string>;
2579
- onRecover: () => Promise<void>;
2580
2565
  onRefresh: () => Promise<void>;
2581
2566
  onRefreshMcp: () => Promise<void>;
2582
2567
  onSaveMcpServer: (input: { name: string; url: string }) => Promise<void>;
@@ -2703,11 +2688,8 @@ function StatusModal({
2703
2688
  </div>
2704
2689
  </section>
2705
2690
  )}
2706
- <p className="muted">Rate limits use the app-server account quota snapshot. Recover checks the sidecar PID and socket, then reconnects this UI.</p>
2691
+ <p className="muted">Rate limits use the app-server account quota snapshot. Start or recover the sidecar from the CLI, then reconnect this UI.</p>
2707
2692
  <footer className="modal-actions">
2708
- <button className="secondary-button" type="button" onClick={() => void onRecover()}>
2709
- <RefreshCw size={16} /> Recover app-server
2710
- </button>
2711
2693
  <button className="primary-button" type="button" onClick={() => void onRefresh()} disabled={statusRefreshing}>
2712
2694
  <RefreshCw size={16} className={statusRefreshing ? "spin-icon" : ""} /> Refresh
2713
2695
  </button>
package/client/src/api.ts CHANGED
@@ -29,11 +29,6 @@ export async function restartServer(): Promise<ServerStatus> {
29
29
  return body.status;
30
30
  }
31
31
 
32
- export async function recoverAppServer(): Promise<{ output: string; status: ServerStatus }> {
33
- const body = await postJson<{ output: string; status: ServerStatus }>("/api/app-server/recover", {});
34
- return body;
35
- }
36
-
37
32
  export async function rpc<T = unknown>(method: string, params: JsonValue = {}): Promise<T> {
38
33
  const body = await postJson<{ result: T }>("/api/rpc", { method, params });
39
34
  return body.result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nchappell/codex-web-ui",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "private": false,
5
5
  "description": "Mobile-friendly web UI client for the official Codex app-server.",
6
6
  "license": "MIT",
@@ -22,7 +22,8 @@
22
22
  ],
23
23
  "type": "module",
24
24
  "bin": {
25
- "codex-web-ui": "bin/codex-web-ui.js"
25
+ "codex-web-ui": "bin/codex-web-ui.js",
26
+ "codex-web-ui-docker-entrypoint": "bin/docker-entrypoint.sh"
26
27
  },
27
28
  "publishConfig": {
28
29
  "access": "public"
package/server/appApi.ts CHANGED
@@ -231,12 +231,6 @@ async function dispatchApiRequest(request: Request, url: URL, cors: Headers): Pr
231
231
  return json({ ok: true, status: bridge.summary() }, 200, cors);
232
232
  }
233
233
 
234
- if (pathname === "/api/app-server/recover" && request.method === "POST") {
235
- const output = await recoverAppServerSidecar();
236
- await bridge.restart();
237
- return json({ ok: true, output, status: bridge.summary() }, 200, cors);
238
- }
239
-
240
234
  if (pathname === "/api/server/stop" && request.method === "POST") {
241
235
  await bridge.stop();
242
236
  return json({ ok: true, status: bridge.summary() }, 200, cors);
@@ -828,25 +822,6 @@ function json(payload: unknown, status = 200, cors?: Headers): Response {
828
822
  );
829
823
  }
830
824
 
831
- async function recoverAppServerSidecar(): Promise<string> {
832
- const socket = process.env.CODEX_APP_SERVER_SOCKET;
833
- if (!socket) {
834
- throw new Error("CODEX_APP_SERVER_SOCKET is not configured; this server owns its app-server connection.");
835
- }
836
- const binPath = path.join(projectRoot, "bin", "codex-web-ui.js");
837
- return new Promise((resolve, reject) => {
838
- execFile(process.execPath, [binPath, "app-server", "recover", "--socket", socket], { cwd: projectRoot, env: process.env, timeout: 12_000 }, (error, stdout, stderr) => {
839
- const output = [stdout, stderr].filter(Boolean).join("\n").trim();
840
- if (error) {
841
- const message = output || error.message;
842
- reject(new Error(message));
843
- return;
844
- }
845
- resolve(output);
846
- });
847
- });
848
- }
849
-
850
825
  function noStoreHeaders(): Headers {
851
826
  return new Headers({
852
827
  "Cache-Control": "no-store"