remote-codex 0.11.44 → 0.11.49

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 (63) hide show
  1. package/README.md +84 -5
  2. package/apps/relay-server/dist/index.js +18 -2
  3. package/apps/supervisor-api/dist/index.js +10034 -2782
  4. package/apps/supervisor-web/dist/assets/c-BIGW1oBm.js +1 -0
  5. package/apps/supervisor-web/dist/assets/{core-DODVy7wn.js → core-B0prQzhr.js} +1 -1
  6. package/apps/supervisor-web/dist/assets/cpp-DIPi6g--.js +1 -0
  7. package/apps/supervisor-web/dist/assets/csharp-DSvCPggb.js +1 -0
  8. package/apps/supervisor-web/dist/assets/go-C27-OAKa.js +1 -0
  9. package/apps/supervisor-web/dist/assets/index-DZI1aSXo.js +22 -0
  10. package/apps/supervisor-web/dist/assets/index-Dy8PgXgw.css +1 -0
  11. package/apps/supervisor-web/dist/assets/java-CylS5w8V.js +1 -0
  12. package/apps/supervisor-web/dist/assets/{markdown-vendor-RZk8L7-L.js → markdown-vendor-BG6PurxI.js} +33 -33
  13. package/apps/supervisor-web/dist/assets/ruby-B6AkvBWc.js +1 -0
  14. package/apps/supervisor-web/dist/assets/rust-B1yitclQ.js +1 -0
  15. package/apps/supervisor-web/dist/assets/{shellscript-CEILq0vU.js → shellscript-DfDnw5Jg.js} +1 -1
  16. package/apps/supervisor-web/dist/assets/thread-ui-gcslNXur.js +3968 -0
  17. package/apps/supervisor-web/dist/assets/xml-sdJ4AIDG.js +1 -0
  18. package/apps/supervisor-web/dist/index.html +4 -4
  19. package/bin/remote-codex.mjs +426 -35
  20. package/docs/windows-device-manager.zh.md +143 -0
  21. package/docs/windows-device-setup.zh.md +263 -0
  22. package/docs/windows-one-click-installer-research.zh.md +481 -0
  23. package/docs/windows.md +87 -0
  24. package/package.json +20 -4
  25. package/packages/acp/src/agent-catalog.test.ts +57 -0
  26. package/packages/acp/src/agent-catalog.ts +361 -0
  27. package/packages/acp/src/catalog-runtime.test.ts +99 -0
  28. package/packages/acp/src/catalog-runtime.ts +507 -0
  29. package/packages/acp/src/codex-environment.test.ts +58 -0
  30. package/packages/acp/src/codex-environment.ts +33 -0
  31. package/packages/acp/src/index.ts +5 -0
  32. package/packages/acp/src/item-mapper.test.ts +137 -0
  33. package/packages/acp/src/item-mapper.ts +473 -0
  34. package/packages/acp/src/runtimeAdapter.test.ts +79 -0
  35. package/packages/acp/src/runtimeAdapter.ts +1193 -0
  36. package/packages/acp/src/terminal-service.test.ts +31 -0
  37. package/packages/acp/src/terminal-service.ts +137 -0
  38. package/packages/agent-runtime/src/runtime-errors.ts +1 -0
  39. package/packages/agent-runtime/src/types.ts +8 -0
  40. package/packages/claude/src/runtimeAdapter.test.ts +2 -2
  41. package/packages/claude/src/runtimeAdapter.ts +10 -19
  42. package/packages/codex/src/appServerManager.test.ts +47 -0
  43. package/packages/codex/src/appServerManager.ts +6 -2
  44. package/packages/codex/src/runtimeAdapter.test.ts +9 -2
  45. package/packages/db/migrations/0030_thread_agent_id.sql +9 -0
  46. package/packages/db/src/repositories.ts +3 -0
  47. package/packages/db/src/schema.ts +1 -0
  48. package/packages/opencode/src/historyItems.ts +7 -6
  49. package/packages/opencode/src/runtimeAdapter.ts +7 -11
  50. package/packages/process-runtime/src/index.test.ts +132 -0
  51. package/packages/process-runtime/src/index.ts +253 -0
  52. package/packages/shared/src/agent-providers.ts +10 -1
  53. package/packages/shared/src/index.ts +36 -0
  54. package/scripts/service-manager.mjs +112 -4
  55. package/scripts/verify-relay-supervisor-smoke.mjs +262 -0
  56. package/scripts/windows/build-device-manager.ps1 +64 -0
  57. package/scripts/windows/install-relay-supervisor-task.ps1 +44 -0
  58. package/scripts/windows/relay-smoke.ps1 +16 -0
  59. package/scripts/windows/uninstall-relay-supervisor-task.ps1 +27 -0
  60. package/scripts/windows/validate-real-codex.mjs +680 -0
  61. package/apps/supervisor-web/dist/assets/index-BcCLYWAf.css +0 -1
  62. package/apps/supervisor-web/dist/assets/index-CO8cU7a0.js +0 -21
  63. package/apps/supervisor-web/dist/assets/thread-ui-Dmrigdek.js +0 -3714
@@ -0,0 +1 @@
1
+ import e from"./java-CylS5w8V.js";const n=Object.freeze(JSON.parse(`{"displayName":"XML","name":"xml","patterns":[{"begin":"(<\\\\?)\\\\s*([-0-9A-Z_a-z]+)","captures":{"1":{"name":"punctuation.definition.tag.xml"},"2":{"name":"entity.name.tag.xml"}},"end":"(\\\\?>)","name":"meta.tag.preprocessor.xml","patterns":[{"match":" ([-A-Za-z]+)","name":"entity.other.attribute-name.xml"},{"include":"#doublequotedString"},{"include":"#singlequotedString"}]},{"begin":"(<!)(DOCTYPE)\\\\s+([:A-Z_a-z][-.0-:A-Z_a-z]*)","captures":{"1":{"name":"punctuation.definition.tag.xml"},"2":{"name":"keyword.other.doctype.xml"},"3":{"name":"variable.language.documentroot.xml"}},"end":"\\\\s*(>)","name":"meta.tag.sgml.doctype.xml","patterns":[{"include":"#internalSubset"}]},{"include":"#comments"},{"begin":"(<)((?:([-0-9A-Z_a-z]+)(:))?([-0-:A-Z_a-z]+))(?=(\\\\s[^>]*)?></\\\\2>)","beginCaptures":{"1":{"name":"punctuation.definition.tag.xml"},"2":{"name":"entity.name.tag.xml"},"3":{"name":"entity.name.tag.namespace.xml"},"4":{"name":"punctuation.separator.namespace.xml"},"5":{"name":"entity.name.tag.localname.xml"}},"end":"(>)(</)((?:([-0-9A-Z_a-z]+)(:))?([-0-:A-Z_a-z]+))(>)","endCaptures":{"1":{"name":"punctuation.definition.tag.xml"},"2":{"name":"punctuation.definition.tag.xml"},"3":{"name":"entity.name.tag.xml"},"4":{"name":"entity.name.tag.namespace.xml"},"5":{"name":"punctuation.separator.namespace.xml"},"6":{"name":"entity.name.tag.localname.xml"},"7":{"name":"punctuation.definition.tag.xml"}},"name":"meta.tag.no-content.xml","patterns":[{"include":"#tagStuff"}]},{"begin":"(</?)(?:([-.\\\\w]+)((:)))?([-.:\\\\w]+)","captures":{"1":{"name":"punctuation.definition.tag.xml"},"2":{"name":"entity.name.tag.namespace.xml"},"3":{"name":"entity.name.tag.xml"},"4":{"name":"punctuation.separator.namespace.xml"},"5":{"name":"entity.name.tag.localname.xml"}},"end":"(/?>)","name":"meta.tag.xml","patterns":[{"include":"#tagStuff"}]},{"include":"#entity"},{"include":"#bare-ampersand"},{"begin":"<%@","beginCaptures":{"0":{"name":"punctuation.section.embedded.begin.xml"}},"end":"%>","endCaptures":{"0":{"name":"punctuation.section.embedded.end.xml"}},"name":"source.java-props.embedded.xml","patterns":[{"match":"page|include|taglib","name":"keyword.other.page-props.xml"}]},{"begin":"<%[!=]?(?!--)","beginCaptures":{"0":{"name":"punctuation.section.embedded.begin.xml"}},"end":"(?!--)%>","endCaptures":{"0":{"name":"punctuation.section.embedded.end.xml"}},"name":"source.java.embedded.xml","patterns":[{"include":"source.java"}]},{"begin":"<!\\\\[CDATA\\\\[","beginCaptures":{"0":{"name":"punctuation.definition.string.begin.xml"}},"end":"]]>","endCaptures":{"0":{"name":"punctuation.definition.string.end.xml"}},"name":"string.unquoted.cdata.xml"}],"repository":{"EntityDecl":{"begin":"(<!)(ENTITY)\\\\s+(%\\\\s+)?([:A-Z_a-z][-.0-:A-Z_a-z]*)(\\\\s+(?:SYSTEM|PUBLIC)\\\\s+)?","captures":{"1":{"name":"punctuation.definition.tag.xml"},"2":{"name":"keyword.other.entity.xml"},"3":{"name":"punctuation.definition.entity.xml"},"4":{"name":"variable.language.entity.xml"},"5":{"name":"keyword.other.entitytype.xml"}},"end":"(>)","patterns":[{"include":"#doublequotedString"},{"include":"#singlequotedString"}]},"bare-ampersand":{"match":"&","name":"invalid.illegal.bad-ampersand.xml"},"comments":{"patterns":[{"begin":"<%--","captures":{"0":{"name":"punctuation.definition.comment.xml"},"end":"--%>","name":"comment.block.xml"}},{"begin":"<!--","captures":{"0":{"name":"punctuation.definition.comment.xml"}},"end":"-->","name":"comment.block.xml","patterns":[{"begin":"--(?!>)","captures":{"0":{"name":"invalid.illegal.bad-comments-or-CDATA.xml"}}}]}]},"doublequotedString":{"begin":"\\"","beginCaptures":{"0":{"name":"punctuation.definition.string.begin.xml"}},"end":"\\"","endCaptures":{"0":{"name":"punctuation.definition.string.end.xml"}},"name":"string.quoted.double.xml","patterns":[{"include":"#entity"},{"include":"#bare-ampersand"}]},"entity":{"captures":{"1":{"name":"punctuation.definition.constant.xml"},"3":{"name":"punctuation.definition.constant.xml"}},"match":"(&)([:A-Z_a-z][-.0-:A-Z_a-z]*|#[0-9]+|#x\\\\h+)(;)","name":"constant.character.entity.xml"},"internalSubset":{"begin":"(\\\\[)","captures":{"1":{"name":"punctuation.definition.constant.xml"}},"end":"(])","name":"meta.internalsubset.xml","patterns":[{"include":"#EntityDecl"},{"include":"#parameterEntity"},{"include":"#comments"}]},"parameterEntity":{"captures":{"1":{"name":"punctuation.definition.constant.xml"},"3":{"name":"punctuation.definition.constant.xml"}},"match":"(%)([:A-Z_a-z][-.0-:A-Z_a-z]*)(;)","name":"constant.character.parameter-entity.xml"},"singlequotedString":{"begin":"'","beginCaptures":{"0":{"name":"punctuation.definition.string.begin.xml"}},"end":"'","endCaptures":{"0":{"name":"punctuation.definition.string.end.xml"}},"name":"string.quoted.single.xml","patterns":[{"include":"#entity"},{"include":"#bare-ampersand"}]},"tagStuff":{"patterns":[{"captures":{"1":{"name":"entity.other.attribute-name.namespace.xml"},"2":{"name":"entity.other.attribute-name.xml"},"3":{"name":"punctuation.separator.namespace.xml"},"4":{"name":"entity.other.attribute-name.localname.xml"}},"match":"(?:^|\\\\s+)(?:([-.\\\\w]+)((:)))?([-.:\\\\w]+)\\\\s*="},{"include":"#doublequotedString"},{"include":"#singlequotedString"}]}},"scopeName":"text.xml","embeddedLangs":["java"]}`)),a=[...e,n];export{a as default};
@@ -10,16 +10,16 @@
10
10
  <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
11
11
  <link rel="manifest" href="/site.webmanifest" />
12
12
  <title>Remote Codex</title>
13
- <script type="module" crossorigin src="/assets/index-CO8cU7a0.js"></script>
13
+ <script type="module" crossorigin src="/assets/index-DZI1aSXo.js"></script>
14
14
  <link rel="modulepreload" crossorigin href="/assets/react-vendor-Dfg_6BLf.js">
15
15
  <link rel="modulepreload" crossorigin href="/assets/ui-vendor-CuR8GHb0.js">
16
16
  <link rel="modulepreload" crossorigin href="/assets/graph-vendor-DVQUpZ8C.js">
17
17
  <link rel="modulepreload" crossorigin href="/assets/terminal-vendor-C5bTa-Ka.js">
18
- <link rel="modulepreload" crossorigin href="/assets/markdown-vendor-RZk8L7-L.js">
19
- <link rel="modulepreload" crossorigin href="/assets/thread-ui-Dmrigdek.js">
18
+ <link rel="modulepreload" crossorigin href="/assets/markdown-vendor-BG6PurxI.js">
19
+ <link rel="modulepreload" crossorigin href="/assets/thread-ui-gcslNXur.js">
20
20
  <link rel="stylesheet" crossorigin href="/assets/graph-vendor-C5ap-Sga.css">
21
21
  <link rel="stylesheet" crossorigin href="/assets/terminal-vendor-Beg8tuEN.css">
22
- <link rel="stylesheet" crossorigin href="/assets/index-BcCLYWAf.css">
22
+ <link rel="stylesheet" crossorigin href="/assets/index-Dy8PgXgw.css">
23
23
  </head>
24
24
  <body class="bg-stone-950">
25
25
  <div id="root"></div>
@@ -7,6 +7,7 @@ import crypto from 'node:crypto';
7
7
  import readline from 'node:readline/promises';
8
8
  import { spawn, spawnSync } from 'node:child_process';
9
9
  import { fileURLToPath } from 'node:url';
10
+ import net from 'node:net';
10
11
 
11
12
  const binDir = path.dirname(fileURLToPath(import.meta.url));
12
13
  const packageRoot = path.resolve(binDir, '..');
@@ -19,6 +20,13 @@ const supervisorSourceEntry = path.join(packageRoot, 'apps', 'supervisor-api', '
19
20
  const relaySupervisorConfigPath = process.env.REMOTE_CODEX_RELAY_SUPERVISOR_CONFIG
20
21
  ? path.resolve(process.env.REMOTE_CODEX_RELAY_SUPERVISOR_CONFIG)
21
22
  : path.join(os.homedir(), '.remote-codex', 'relay-supervisor.json');
23
+ const relaySupervisorStatePath = process.env.REMOTE_CODEX_RELAY_SUPERVISOR_STATE
24
+ ? path.resolve(process.env.REMOTE_CODEX_RELAY_SUPERVISOR_STATE)
25
+ : path.join(os.homedir(), '.remote-codex', 'relay-supervisor-state.json');
26
+ const relaySupervisorStartLockPath = `${relaySupervisorStatePath}.start.lock`;
27
+ const relaySupervisorLogPath = process.env.REMOTE_CODEX_RELAY_SUPERVISOR_LOG
28
+ ? path.resolve(process.env.REMOTE_CODEX_RELAY_SUPERVISOR_LOG)
29
+ : path.join(os.homedir(), '.remote-codex', 'logs', 'relay-supervisor.log');
22
30
  const relaySupervisorTmuxSession = process.env.REMOTE_CODEX_RELAY_SUPERVISOR_TMUX_SESSION?.trim()
23
31
  || 'remote-codex-relay-supervisor';
24
32
  const relaySupervisorConfigKeys = [
@@ -204,11 +212,19 @@ async function runRelaySupervisor() {
204
212
  return;
205
213
  }
206
214
  if (action === 'status') {
207
- relaySupervisorStatus();
215
+ if (process.platform === 'win32') {
216
+ await relaySupervisorWindowsStatus();
217
+ } else {
218
+ relaySupervisorStatus();
219
+ }
208
220
  return;
209
221
  }
210
222
  if (action === 'stop') {
211
- stopRelaySupervisorTmux();
223
+ if (process.platform === 'win32') {
224
+ await stopRelaySupervisorWindows();
225
+ } else {
226
+ stopRelaySupervisorTmux();
227
+ }
212
228
  return;
213
229
  }
214
230
  if (action !== 'start' && action !== 'run') {
@@ -217,6 +233,10 @@ async function runRelaySupervisor() {
217
233
  process.exit(1);
218
234
  }
219
235
  await ensureRelaySupervisorConfig();
236
+ if (action === 'start' && process.platform === 'win32') {
237
+ await startRelaySupervisorWindows();
238
+ return;
239
+ }
220
240
  if (action === 'start' && shouldStartRelaySupervisorInTmux()) {
221
241
  startRelaySupervisorTmux();
222
242
  return;
@@ -274,10 +294,36 @@ function runRelaySupervisorForeground() {
274
294
  validateRequiredEnv(guidance);
275
295
  printEnvSummary(guidance);
276
296
 
297
+ const { commandToRun, args } = resolveSupervisorLaunch();
298
+
299
+ const supervisor = spawn(commandToRun, args, {
300
+ cwd: packageRoot,
301
+ env: relaySupervisorProcessEnv(),
302
+ stdio: 'inherit',
303
+ });
304
+
305
+ supervisor.on('exit', (code, signal) => {
306
+ if (signal) {
307
+ process.kill(process.pid, signal);
308
+ return;
309
+ }
310
+
311
+ if (code && code !== 0) {
312
+ console.error(`remote-codex relay-supervisor exited with code ${code}. Check the relay server URL, device token, local REMOTE_CODEX_RELAY_SUPERVISOR_PORT, and environment values above.`);
313
+ }
314
+ process.exit(code ?? 1);
315
+ });
316
+
317
+ supervisor.on('error', (error) => {
318
+ console.error(`Failed to run remote-codex relay-supervisor: ${error.message}`);
319
+ process.exit(1);
320
+ });
321
+ }
322
+
323
+ function resolveSupervisorLaunch() {
277
324
  const supervisorEntry = fs.existsSync(supervisorDistEntry)
278
325
  ? supervisorDistEntry
279
326
  : supervisorSourceEntry;
280
- let commandToRun = process.execPath;
281
327
  let args = [supervisorEntry];
282
328
 
283
329
  if (!fs.existsSync(supervisorEntry)) {
@@ -295,37 +341,330 @@ function runRelaySupervisorForeground() {
295
341
  }
296
342
  args = [tsxEntry, supervisorSourceEntry];
297
343
  }
344
+ return { commandToRun: process.execPath, args };
345
+ }
298
346
 
299
- const supervisor = spawn(commandToRun, args, {
347
+ function relaySupervisorProcessEnv(extra = {}) {
348
+ return {
349
+ ...process.env,
350
+ REMOTE_CODEX_MODE: 'relay',
351
+ REMOTE_CODEX_PACKAGE_ROOT: process.env.REMOTE_CODEX_PACKAGE_ROOT ?? packageRoot,
352
+ REMOTE_CODEX_DISABLE_BUILD_RESTART:
353
+ process.env.REMOTE_CODEX_DISABLE_BUILD_RESTART ?? (sourceCheckout ? 'false' : 'true'),
354
+ ...extra,
355
+ };
356
+ }
357
+
358
+ async function startRelaySupervisorWindows() {
359
+ const releaseStartLock = acquireRelaySupervisorStartLock();
360
+ if (!releaseStartLock) {
361
+ process.exitCode = 1;
362
+ return;
363
+ }
364
+ try {
365
+ await startRelaySupervisorWindowsUnlocked();
366
+ } finally {
367
+ releaseStartLock();
368
+ }
369
+ }
370
+
371
+ async function startRelaySupervisorWindowsUnlocked() {
372
+ persistRelaySupervisorRuntimeConfig();
373
+ const existing = readRelaySupervisorState();
374
+ if (existing) {
375
+ const response = await requestRelaySupervisorLifecycle(existing, 'status').catch(() => null);
376
+ if (response?.ok === true && response.instanceId === existing.instanceId) {
377
+ console.log(`remote-codex relay-supervisor is already running (pid ${existing.pid}).`);
378
+ console.log(`Logs: ${existing.logPath ?? relaySupervisorLogPath}`);
379
+ return;
380
+ }
381
+ if (isPidAlive(existing.pid)) {
382
+ console.error('A relay-supervisor state file exists for a live process, but its identity could not be verified.');
383
+ console.error(`Refusing to replace or terminate pid ${existing.pid}. Inspect ${relaySupervisorStatePath}.`);
384
+ process.exitCode = 1;
385
+ return;
386
+ }
387
+ removeRelaySupervisorState();
388
+ }
389
+
390
+ const { commandToRun, args } = resolveSupervisorLaunch();
391
+ const instanceId = crypto.randomUUID();
392
+ const controlToken = randomSecret(32);
393
+ const identityHash = crypto
394
+ .createHash('sha256')
395
+ .update(`${relaySupervisorConfigPath}\0${os.userInfo().username}`)
396
+ .digest('hex')
397
+ .slice(0, 24);
398
+ const controlEndpoint = `\\\\.\\pipe\\remote-codex-relay-supervisor-${identityHash}`;
399
+ fs.mkdirSync(path.dirname(relaySupervisorLogPath), { recursive: true });
400
+ rotateRelaySupervisorLog();
401
+ const logFd = fs.openSync(relaySupervisorLogPath, 'a');
402
+ const child = spawn(commandToRun, args, {
300
403
  cwd: packageRoot,
301
- env: {
302
- ...process.env,
303
- REMOTE_CODEX_MODE: 'relay',
304
- REMOTE_CODEX_PACKAGE_ROOT: process.env.REMOTE_CODEX_PACKAGE_ROOT ?? packageRoot,
305
- REMOTE_CODEX_DISABLE_BUILD_RESTART:
306
- process.env.REMOTE_CODEX_DISABLE_BUILD_RESTART ?? (sourceCheckout ? 'false' : 'true'),
307
- },
308
- stdio: 'inherit',
404
+ detached: true,
405
+ windowsHide: true,
406
+ env: relaySupervisorProcessEnv({
407
+ REMOTE_CODEX_LIFECYCLE_CONTROL_ENDPOINT: controlEndpoint,
408
+ REMOTE_CODEX_LIFECYCLE_CONTROL_TOKEN: controlToken,
409
+ REMOTE_CODEX_LIFECYCLE_INSTANCE_ID: instanceId,
410
+ REMOTE_CODEX_ENABLED_AGENT_PROVIDERS:
411
+ process.env.REMOTE_CODEX_ENABLED_AGENT_PROVIDERS ?? 'codex',
412
+ }),
413
+ stdio: ['ignore', logFd, logFd],
309
414
  });
415
+ child.unref();
416
+ fs.closeSync(logFd);
417
+ if (!child.pid) {
418
+ console.error('Failed to start relay-supervisor in the background.');
419
+ process.exitCode = 1;
420
+ return;
421
+ }
310
422
 
311
- supervisor.on('exit', (code, signal) => {
312
- if (signal) {
313
- process.kill(process.pid, signal);
423
+ const state = {
424
+ schemaVersion: 1,
425
+ pid: child.pid,
426
+ instanceId,
427
+ controlToken,
428
+ controlEndpoint,
429
+ startedAt: new Date().toISOString(),
430
+ host: envValue(['REMOTE_CODEX_RELAY_SUPERVISOR_HOST', 'HOST'], '127.0.0.1'),
431
+ port: Number(envValue(['REMOTE_CODEX_RELAY_SUPERVISOR_PORT', 'PORT'], '8787')),
432
+ logPath: relaySupervisorLogPath,
433
+ version: readPackageVersion(),
434
+ };
435
+ writeRelaySupervisorState(state);
436
+
437
+ const deadline = Date.now() + 15_000;
438
+ while (Date.now() < deadline) {
439
+ if (!isPidAlive(child.pid)) {
440
+ console.error(`relay-supervisor exited before becoming ready. Logs: ${relaySupervisorLogPath}`);
441
+ removeRelaySupervisorState();
442
+ process.exitCode = 1;
314
443
  return;
315
444
  }
445
+ const response = await requestRelaySupervisorLifecycle(state, 'status').catch(() => null);
446
+ if (response?.ok === true && response.instanceId === instanceId) {
447
+ console.log(`Started remote-codex relay-supervisor (pid ${child.pid}).`);
448
+ console.log(`Logs: ${relaySupervisorLogPath}`);
449
+ console.log('Status: remote-codex relay-supervisor status');
450
+ console.log('Stop: remote-codex relay-supervisor stop');
451
+ return;
452
+ }
453
+ await sleep(250);
454
+ }
455
+ console.error(`Timed out waiting for relay-supervisor lifecycle control. Logs: ${relaySupervisorLogPath}`);
456
+ if (isPidAlive(child.pid)) {
457
+ spawnSync('taskkill.exe', ['/PID', String(child.pid), '/T', '/F'], {
458
+ windowsHide: true,
459
+ stdio: 'ignore',
460
+ });
461
+ }
462
+ removeRelaySupervisorState();
463
+ process.exitCode = 1;
464
+ }
316
465
 
317
- if (code && code !== 0) {
318
- console.error(`remote-codex relay-supervisor exited with code ${code}. Check the relay server URL, device token, local REMOTE_CODEX_RELAY_SUPERVISOR_PORT, and environment values above.`);
466
+ function acquireRelaySupervisorStartLock() {
467
+ fs.mkdirSync(path.dirname(relaySupervisorStartLockPath), { recursive: true });
468
+ for (let attempt = 0; attempt < 2; attempt += 1) {
469
+ try {
470
+ const descriptor = fs.openSync(relaySupervisorStartLockPath, 'wx', 0o600);
471
+ fs.writeFileSync(descriptor, `${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() })}\n`);
472
+ fs.fsyncSync(descriptor);
473
+ fs.closeSync(descriptor);
474
+ restrictWindowsPathAcl(relaySupervisorStartLockPath, false);
475
+ return () => {
476
+ try { fs.unlinkSync(relaySupervisorStartLockPath); } catch { /* Best-effort lock cleanup. */ }
477
+ };
478
+ } catch (error) {
479
+ if (error?.code !== 'EEXIST') throw error;
480
+ const owner = readStartLockOwner();
481
+ if (attempt === 0 && isRelaySupervisorStartLockStale(owner)) {
482
+ try { fs.unlinkSync(relaySupervisorStartLockPath); } catch { /* Retry reports contention. */ }
483
+ continue;
484
+ }
485
+ console.error(`Another relay-supervisor start operation is active${owner?.pid ? ` (pid ${owner.pid})` : ''}.`);
486
+ return null;
319
487
  }
320
- process.exit(code ?? 1);
321
- });
488
+ }
489
+ return null;
490
+ }
322
491
 
323
- supervisor.on('error', (error) => {
324
- console.error(`Failed to run remote-codex relay-supervisor: ${error.message}`);
325
- process.exit(1);
492
+ function isRelaySupervisorStartLockStale(owner) {
493
+ if (owner) return !isPidAlive(owner.pid);
494
+ try {
495
+ return Date.now() - fs.statSync(relaySupervisorStartLockPath).mtimeMs > 30_000;
496
+ } catch {
497
+ return true;
498
+ }
499
+ }
500
+
501
+ function readStartLockOwner() {
502
+ try {
503
+ const value = JSON.parse(fs.readFileSync(relaySupervisorStartLockPath, 'utf8'));
504
+ return Number.isInteger(value?.pid) ? value : null;
505
+ } catch {
506
+ return null;
507
+ }
508
+ }
509
+
510
+ async function relaySupervisorWindowsStatus() {
511
+ const state = readRelaySupervisorState();
512
+ if (!state) {
513
+ console.log('remote-codex relay-supervisor is not running.');
514
+ process.exitCode = 1;
515
+ return;
516
+ }
517
+ const response = await requestRelaySupervisorLifecycle(state, 'status').catch(() => null);
518
+ if (response?.ok !== true || response.instanceId !== state.instanceId) {
519
+ if (!isPidAlive(state.pid)) {
520
+ removeRelaySupervisorState();
521
+ console.log('remote-codex relay-supervisor is not running; stale state was removed.');
522
+ } else {
523
+ console.log(`remote-codex relay-supervisor state is unverified for live pid ${state.pid}.`);
524
+ }
525
+ process.exitCode = 1;
526
+ return;
527
+ }
528
+ console.log('State: running');
529
+ console.log(`PID: ${state.pid}`);
530
+ console.log(`Started: ${state.startedAt}`);
531
+ console.log(`API: http://${state.host}:${state.port}`);
532
+ console.log(`Logs: ${state.logPath}`);
533
+ }
534
+
535
+ async function stopRelaySupervisorWindows() {
536
+ const state = readRelaySupervisorState();
537
+ if (!state) {
538
+ console.log('remote-codex relay-supervisor is not running.');
539
+ return;
540
+ }
541
+ const status = await requestRelaySupervisorLifecycle(state, 'status').catch(() => null);
542
+ if (status?.ok !== true || status.instanceId !== state.instanceId) {
543
+ if (!isPidAlive(state.pid)) {
544
+ removeRelaySupervisorState();
545
+ console.log('remote-codex relay-supervisor was not running; stale state was removed.');
546
+ return;
547
+ }
548
+ console.error(`Refusing to stop unverified live pid ${state.pid}.`);
549
+ process.exitCode = 1;
550
+ return;
551
+ }
552
+
553
+ const shutdown = await requestRelaySupervisorLifecycle(state, 'shutdown').catch(() => null);
554
+ if (shutdown?.ok !== true || shutdown.instanceId !== state.instanceId) {
555
+ console.error('relay-supervisor rejected the authenticated shutdown request.');
556
+ process.exitCode = 1;
557
+ return;
558
+ }
559
+ const deadline = Date.now() + 10_000;
560
+ while (Date.now() < deadline && isPidAlive(state.pid)) {
561
+ await sleep(250);
562
+ }
563
+ if (isPidAlive(state.pid)) {
564
+ const finalIdentity = await requestRelaySupervisorLifecycle(state, 'status').catch(() => null);
565
+ if (finalIdentity?.instanceId === state.instanceId) {
566
+ spawnSync('taskkill.exe', ['/PID', String(state.pid), '/T', '/F'], {
567
+ windowsHide: true,
568
+ stdio: 'ignore',
569
+ });
570
+ }
571
+ }
572
+ if (isPidAlive(state.pid)) {
573
+ console.error(`relay-supervisor pid ${state.pid} did not stop. State was preserved.`);
574
+ process.exitCode = 1;
575
+ return;
576
+ }
577
+ removeRelaySupervisorState();
578
+ console.log('Stopped remote-codex relay-supervisor.');
579
+ }
580
+
581
+ function requestRelaySupervisorLifecycle(state, action, timeoutMs = 2_000) {
582
+ return new Promise((resolve, reject) => {
583
+ const socket = net.createConnection(state.controlEndpoint);
584
+ const timer = setTimeout(() => {
585
+ socket.destroy();
586
+ reject(new Error('Lifecycle control request timed out.'));
587
+ }, timeoutMs);
588
+ let output = '';
589
+ socket.setEncoding('utf8');
590
+ socket.once('connect', () => {
591
+ socket.write(`${JSON.stringify({
592
+ action,
593
+ token: state.controlToken,
594
+ instanceId: state.instanceId,
595
+ })}\n`);
596
+ });
597
+ socket.on('data', (chunk) => {
598
+ output += chunk;
599
+ const newline = output.indexOf('\n');
600
+ if (newline < 0) return;
601
+ clearTimeout(timer);
602
+ socket.end();
603
+ try {
604
+ resolve(JSON.parse(output.slice(0, newline)));
605
+ } catch (error) {
606
+ reject(error);
607
+ }
608
+ });
609
+ socket.once('error', (error) => {
610
+ clearTimeout(timer);
611
+ reject(error);
612
+ });
326
613
  });
327
614
  }
328
615
 
616
+ function readRelaySupervisorState() {
617
+ try {
618
+ const state = JSON.parse(fs.readFileSync(relaySupervisorStatePath, 'utf8'));
619
+ if (
620
+ state?.schemaVersion !== 1 ||
621
+ !Number.isInteger(state.pid) ||
622
+ typeof state.instanceId !== 'string' ||
623
+ typeof state.controlToken !== 'string' ||
624
+ typeof state.controlEndpoint !== 'string'
625
+ ) {
626
+ return null;
627
+ }
628
+ return state;
629
+ } catch {
630
+ return null;
631
+ }
632
+ }
633
+
634
+ function writeRelaySupervisorState(state) {
635
+ writePrivateJsonFile(relaySupervisorStatePath, state);
636
+ }
637
+
638
+ function removeRelaySupervisorState() {
639
+ try {
640
+ fs.unlinkSync(relaySupervisorStatePath);
641
+ } catch (error) {
642
+ if (error?.code !== 'ENOENT') {
643
+ console.error(`Failed to remove relay-supervisor state: ${error.message}`);
644
+ }
645
+ }
646
+ }
647
+
648
+ function rotateRelaySupervisorLog() {
649
+ const rotatedPath = `${relaySupervisorLogPath}.1`;
650
+ if (fs.existsSync(rotatedPath)) fs.rmSync(rotatedPath, { force: true });
651
+ if (fs.existsSync(relaySupervisorLogPath)) fs.renameSync(relaySupervisorLogPath, rotatedPath);
652
+ }
653
+
654
+ function isPidAlive(pid) {
655
+ if (!Number.isInteger(pid) || pid <= 1) return false;
656
+ try {
657
+ process.kill(pid, 0);
658
+ return true;
659
+ } catch {
660
+ return false;
661
+ }
662
+ }
663
+
664
+ function sleep(milliseconds) {
665
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
666
+ }
667
+
329
668
  function shouldStartRelaySupervisorInTmux() {
330
669
  const setting = process.env.REMOTE_CODEX_RELAY_SUPERVISOR_TMUX?.trim().toLowerCase();
331
670
  if (['0', 'false', 'no', 'off'].includes(setting ?? '')) {
@@ -520,12 +859,59 @@ function readRelaySupervisorConfig() {
520
859
  }
521
860
 
522
861
  function writeRelaySupervisorConfig(config) {
523
- fs.mkdirSync(path.dirname(relaySupervisorConfigPath), { recursive: true });
524
- fs.writeFileSync(relaySupervisorConfigPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
862
+ writePrivateJsonFile(relaySupervisorConfigPath, config);
863
+ }
864
+
865
+ function writePrivateJsonFile(filePath, value) {
866
+ const directory = path.dirname(filePath);
867
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
868
+ const temporaryPath = `${filePath}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
869
+ let descriptor = null;
525
870
  try {
526
- fs.chmodSync(relaySupervisorConfigPath, 0o600);
527
- } catch {
528
- // Best-effort on filesystems that do not support chmod.
871
+ descriptor = fs.openSync(temporaryPath, 'wx', 0o600);
872
+ fs.writeFileSync(descriptor, `${JSON.stringify(value, null, 2)}\n`);
873
+ fs.fsyncSync(descriptor);
874
+ fs.closeSync(descriptor);
875
+ descriptor = null;
876
+ try {
877
+ fs.chmodSync(temporaryPath, 0o600);
878
+ fs.chmodSync(directory, 0o700);
879
+ } catch {
880
+ // ACLs are applied separately on Windows.
881
+ }
882
+ if (process.platform === 'win32') {
883
+ restrictWindowsPathAcl(directory, true);
884
+ restrictWindowsPathAcl(temporaryPath, false);
885
+ }
886
+ fs.renameSync(temporaryPath, filePath);
887
+ if (process.platform === 'win32') {
888
+ restrictWindowsPathAcl(filePath, false);
889
+ }
890
+ } catch (error) {
891
+ if (descriptor !== null) {
892
+ try { fs.closeSync(descriptor); } catch { /* Preserve the original write failure. */ }
893
+ }
894
+ try {
895
+ fs.rmSync(temporaryPath, { force: true });
896
+ } catch {
897
+ // Preserve the original write failure.
898
+ }
899
+ throw error;
900
+ }
901
+ }
902
+
903
+ function restrictWindowsPathAcl(targetPath, directory) {
904
+ const username = process.env.USERDOMAIN && process.env.USERNAME
905
+ ? `${process.env.USERDOMAIN}\\${process.env.USERNAME}`
906
+ : process.env.USERNAME ?? os.userInfo().username;
907
+ const permission = directory ? '(OI)(CI)F' : 'F';
908
+ const result = spawnSync(
909
+ 'icacls.exe',
910
+ [targetPath, '/inheritance:r', '/grant:r', `${username}:${permission}`, `SYSTEM:${permission}`],
911
+ { windowsHide: true, encoding: 'utf8' },
912
+ );
913
+ if (result.status !== 0) {
914
+ console.error(`Warning: unable to restrict Windows ACL for ${targetPath}.`);
529
915
  }
530
916
  }
531
917
 
@@ -980,6 +1366,14 @@ Typical flow:
980
1366
  }
981
1367
 
982
1368
  function printRelaySupervisorHelp() {
1369
+ const processManagement = process.platform === 'win32'
1370
+ ? `On Windows, "start" launches a detached supervisor managed through an
1371
+ authenticated local named pipe. "status" verifies the instance identity and
1372
+ "stop" requests a clean shutdown before any process-tree fallback.`
1373
+ : `By default, this command tries to start the supervisor in a detached tmux
1374
+ session named "${relaySupervisorTmuxSession}" so the device stays online
1375
+ after the launching terminal closes. If tmux is not installed, or if
1376
+ REMOTE_CODEX_RELAY_SUPERVISOR_TMUX=0 is set, it runs in the foreground.`;
983
1377
  console.log(`remote-codex relay-supervisor
984
1378
 
985
1379
  Run the private-machine supervisor backend in relay mode. This is the process
@@ -997,14 +1391,11 @@ This command automatically sets for the child supervisor:
997
1391
  REMOTE_CODEX_MODE=relay
998
1392
 
999
1393
  Default process management:
1000
- By default, this command tries to start the supervisor in a detached tmux
1001
- session named "${relaySupervisorTmuxSession}" so the device stays online
1002
- after the launching terminal closes. If tmux is not installed, or if
1003
- REMOTE_CODEX_RELAY_SUPERVISOR_TMUX=0 is set, it runs in the foreground.
1394
+ ${processManagement}
1004
1395
 
1005
1396
  Use "remote-codex relay-supervisor run" for explicit foreground/debug mode.
1006
- Use "remote-codex relay-supervisor status" to inspect the tmux session.
1007
- Use "remote-codex relay-supervisor stop" to stop the tmux session.
1397
+ Use "remote-codex relay-supervisor status" to inspect the managed instance.
1398
+ Use "remote-codex relay-supervisor stop" to stop the managed instance.
1008
1399
 
1009
1400
  Interactive setup:
1010
1401
  When REMOTE_CODEX_RELAY_SERVER_URL or REMOTE_CODEX_RELAY_AGENT_TOKEN is
@@ -1013,8 +1404,8 @@ Interactive setup:
1013
1404
  ${relaySupervisorConfigPath}
1014
1405
 
1015
1406
  The saved config also includes generated local supervisor auth/session values
1016
- and copied setup values such as REMOTE_CODEX_RELAY_SUPERVISOR_PORT, so the
1017
- tmux child process can start with the same effective configuration.
1407
+ and copied setup values such as REMOTE_CODEX_RELAY_SUPERVISOR_PORT, so a
1408
+ managed child process can start with the same effective configuration.
1018
1409
 
1019
1410
  Use "remote-codex relay-supervisor reset" to delete the saved config.
1020
1411