@jacobbd/relay-ai 0.9.5 → 0.9.7

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/dist/cli.js CHANGED
@@ -84,6 +84,7 @@ import {
84
84
  getCodexProxyDebugLogPath,
85
85
  getConfigPath,
86
86
  getGeminiProxyDebugLogPath,
87
+ getLogsPath,
87
88
  getProvidersPath,
88
89
  getProxyDebugLogPath,
89
90
  getReasoningCapabilities,
@@ -195,9 +196,10 @@ import {
195
196
  updateCustomEndpointProvider,
196
197
  upstreamHttpStatus,
197
198
  validateCustomEndpointUrl,
199
+ waitForCodexAppQuit,
198
200
  writeSecureLogLine,
199
201
  zenRegistryStub
200
- } from "./chunk-SCW2TYSG.js";
202
+ } from "./chunk-BJ4AE3PS.js";
201
203
  import {
202
204
  filterTemplates,
203
205
  getTemplateById,
@@ -1295,7 +1297,7 @@ ${pc4.bold("Subcommands:")}
1295
1297
  (none) Provider hub wizard ${pc4.dim("[Phase 1.1]")}
1296
1298
  add Add a provider (Groq, Mistral, Together AI, \u2026) ${pc4.dim("[Phase 1.1]")}
1297
1299
  import Optional one-time import from OpenCode CLI ${pc4.dim("[Phase 1.0]")}
1298
- auth Sign in with OAuth (GitHub Copilot, xAI, OpenAI, ClinePass)
1300
+ auth Sign in with OAuth (Antigravity, GitHub Copilot, xAI, OpenAI, ClinePass)
1299
1301
  list Show configured providers ${pc4.dim("[Phase 1.0]")}
1300
1302
  remove Remove a provider by id ${pc4.dim("[Phase 1.1]")}
1301
1303
  refresh-models Update cached model lists ${pc4.dim("[Phase 1.2]")}`;
@@ -2233,7 +2235,7 @@ async function runProvidersCommand(args) {
2233
2235
  // src/codex.ts
2234
2236
  import pc7 from "picocolors";
2235
2237
  import * as p8 from "@clack/prompts";
2236
- import { join as join5 } from "path";
2238
+ import { join as join6 } from "path";
2237
2239
 
2238
2240
  // src/codex-proxy.ts
2239
2241
  import { createHash as createHash2 } from "crypto";
@@ -3770,6 +3772,53 @@ async function resolveRoutedCollaborationInput(input, context) {
3770
3772
  return normalizePlaintextCollaborationForExternal(out);
3771
3773
  }
3772
3774
 
3775
+ // src/codex/route-audit.ts
3776
+ import { chmodSync, mkdirSync, writeFileSync } from "fs";
3777
+ import { join as join2 } from "path";
3778
+ var DIR_MODE = 448;
3779
+ var FILE_MODE = 384;
3780
+ var CODEX_ROUTE_AUDIT_LOG = "codex-route-audit.jsonl";
3781
+ function safeIdentifier(value) {
3782
+ if (value === void 0) return void 0;
3783
+ return value.replace(/[\u0000-\u001f\u007f]/g, "_").slice(0, 300);
3784
+ }
3785
+ function sanitizeCodexRouteAuditEvent(event) {
3786
+ return {
3787
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3788
+ transport: event.transport,
3789
+ requestedModel: safeIdentifier(event.requestedModel),
3790
+ dispatch: event.dispatch,
3791
+ phase: event.phase,
3792
+ ...event.provider ? { provider: safeIdentifier(event.provider) } : {},
3793
+ ...event.routeModel ? { routeModel: safeIdentifier(event.routeModel) } : {},
3794
+ ...event.upstreamModel ? { upstreamModel: safeIdentifier(event.upstreamModel) } : {},
3795
+ ...event.outcome ? { outcome: event.outcome } : {},
3796
+ ...event.status !== void 0 ? { status: typeof event.status === "string" ? safeIdentifier(event.status) : event.status } : {}
3797
+ };
3798
+ }
3799
+ function getCodexRouteAuditLogPath() {
3800
+ const dir = getLogsPath();
3801
+ mkdirSync(dir, { recursive: true, mode: DIR_MODE });
3802
+ try {
3803
+ chmodSync(dir, DIR_MODE);
3804
+ } catch {
3805
+ }
3806
+ return join2(dir, CODEX_ROUTE_AUDIT_LOG);
3807
+ }
3808
+ function prepareCodexRouteAuditLog(path3 = getCodexRouteAuditLogPath()) {
3809
+ writeFileSync(path3, "", { mode: FILE_MODE });
3810
+ chmodSync(path3, FILE_MODE);
3811
+ return path3;
3812
+ }
3813
+ function appendCodexRouteAudit(path3, event) {
3814
+ try {
3815
+ writeFileSync(path3, `${JSON.stringify(sanitizeCodexRouteAuditEvent(event))}
3816
+ `, { flag: "a", mode: FILE_MODE });
3817
+ chmodSync(path3, FILE_MODE);
3818
+ } catch {
3819
+ }
3820
+ }
3821
+
3773
3822
  // src/codex-proxy.ts
3774
3823
  function captureCompletedResponse(sseText) {
3775
3824
  if (!sseText.includes("response.completed")) return void 0;
@@ -3777,11 +3826,34 @@ function captureCompletedResponse(sseText) {
3777
3826
  if (!dataLine) return void 0;
3778
3827
  try {
3779
3828
  const obj = JSON.parse(dataLine.slice(5).trim());
3780
- if (obj && obj.type === "response.completed") return obj.response;
3829
+ if (obj && obj.type === "response.completed" && obj.response && typeof obj.response === "object") {
3830
+ return obj.response;
3831
+ }
3781
3832
  } catch {
3782
3833
  }
3783
3834
  return void 0;
3784
3835
  }
3836
+ var MAX_EXTERNAL_RESPONSE_STATES = 8;
3837
+ var EXTERNAL_TOOL_OUTPUT_TYPES = /* @__PURE__ */ new Set([
3838
+ "function_call_output",
3839
+ "custom_tool_call_output",
3840
+ "tool_search_output"
3841
+ ]);
3842
+ function responsesInputItems(input) {
3843
+ if (Array.isArray(input)) return input;
3844
+ if (typeof input === "string") {
3845
+ return [{ type: "message", role: "user", content: input }];
3846
+ }
3847
+ return [];
3848
+ }
3849
+ function isExternalToolOutputItem(item) {
3850
+ if (!item || typeof item !== "object" || Array.isArray(item)) return false;
3851
+ const type = item.type;
3852
+ return typeof type === "string" && EXTERNAL_TOOL_OUTPUT_TYPES.has(type);
3853
+ }
3854
+ function isExternalToolContinuation(input) {
3855
+ return Array.isArray(input) && input.length > 0 && input.every(isExternalToolOutputItem);
3856
+ }
3785
3857
  function estimateCodexRequestChars(params) {
3786
3858
  let chars = (params.system ?? "").length;
3787
3859
  for (const msg of params.messages) {
@@ -3980,11 +4052,32 @@ async function prepareExternalCodexBody(body, context) {
3980
4052
  );
3981
4053
  return { ...externalBody, input: resolvedInput };
3982
4054
  }
4055
+ function applyExternalCodexRuntimeIdentity(params, route) {
4056
+ const selectedModel = route.auditUpstreamModelId ?? route.upstreamModelId ?? route.modelId;
4057
+ const provider = route.providerId ?? "relay";
4058
+ const identity = [
4059
+ "<external-model-identity>",
4060
+ `The selected model for this turn is ${JSON.stringify(selectedModel)} through provider ${JSON.stringify(provider)}.`,
4061
+ "Codex is the host application and agent environment, not the model identity.",
4062
+ "Follow Codex host and tool instructions normally, but do not infer that you are an OpenAI or GPT model from host names, tool names, documentation, or conversation context.",
4063
+ "If asked what model you are, report the selected model and provider above; do not use self-identification as evidence of the network route.",
4064
+ "</external-model-identity>"
4065
+ ].join("\n");
4066
+ return {
4067
+ ...params,
4068
+ system: params.system?.trim() ? `${identity}
4069
+
4070
+ ${params.system}` : identity
4071
+ };
4072
+ }
3983
4073
  async function startCodexProxy(routes, options = {}) {
3984
4074
  const opts = typeof options === "boolean" ? { debug: options } : options;
3985
4075
  const debug = opts.debug ?? false;
3986
4076
  const requireAuth = opts.requireAuth ?? true;
3987
4077
  const mixedNative = opts.mixedNative;
4078
+ const audit = (event) => {
4079
+ if (opts.routeAuditPath) appendCodexRouteAudit(opts.routeAuditPath, event);
4080
+ };
3988
4081
  const nativePayloadRelay = mixedNative ? createNativePayloadRelay({}) : void 0;
3989
4082
  silenceSdkWarnings();
3990
4083
  const models = /* @__PURE__ */ new Map();
@@ -4158,6 +4251,7 @@ async function startCodexProxy(routes, options = {}) {
4158
4251
  log14(`subagent dispatch: requested=${modelId} route=${subagentRoute?.modelId ?? "(none)"}`);
4159
4252
  }
4160
4253
  if (mixedNative && markedSubagent && !subagentRoute) {
4254
+ audit({ transport: "http", requestedModel: modelId, dispatch: "relay-subagent", phase: "complete", outcome: "error", status: 503 });
4161
4255
  sendJson(res, 503, {
4162
4256
  error: {
4163
4257
  message: "Codex marked this request as a Sub-agent, but no configured Codex Sub-agent route is available.",
@@ -4170,10 +4264,20 @@ async function startCodexProxy(routes, options = {}) {
4170
4264
  if (!markedSubagent) {
4171
4265
  const dispatch = classifyCodexDispatch(modelId, routes, mixedNative.nativeModelIds);
4172
4266
  if (dispatch.kind === "unknown") {
4267
+ audit({ transport: "http", requestedModel: modelId, dispatch: "unknown", phase: "complete", outcome: "error", status: 404 });
4173
4268
  sendJson(res, 404, { error: { message: `Unknown model: ${modelId}`, type: "invalid_request_error" } });
4174
4269
  return;
4175
4270
  }
4176
4271
  if (dispatch.kind === "native") {
4272
+ audit({
4273
+ transport: "http",
4274
+ requestedModel: modelId,
4275
+ dispatch: "native",
4276
+ phase: "dispatch",
4277
+ provider: "openai-native",
4278
+ routeModel: modelId,
4279
+ upstreamModel: modelId
4280
+ });
4177
4281
  const controller = new AbortController();
4178
4282
  req.once("aborted", () => controller.abort());
4179
4283
  try {
@@ -4187,7 +4291,29 @@ async function startCodexProxy(routes, options = {}) {
4187
4291
  const contentType = nativeResponse.headers.get("content-type");
4188
4292
  res.writeHead(nativeResponse.status, contentType ? { "content-type": contentType } : void 0);
4189
4293
  res.end(Buffer.from(await nativeResponse.arrayBuffer()));
4294
+ audit({
4295
+ transport: "http",
4296
+ requestedModel: modelId,
4297
+ dispatch: "native",
4298
+ phase: "complete",
4299
+ provider: "openai-native",
4300
+ routeModel: modelId,
4301
+ upstreamModel: modelId,
4302
+ outcome: nativeResponse.ok ? "ok" : "error",
4303
+ status: nativeResponse.status
4304
+ });
4190
4305
  } catch (err) {
4306
+ audit({
4307
+ transport: "http",
4308
+ requestedModel: modelId,
4309
+ dispatch: "native",
4310
+ phase: "complete",
4311
+ provider: "openai-native",
4312
+ routeModel: modelId,
4313
+ upstreamModel: modelId,
4314
+ outcome: "error",
4315
+ status: "forward-failed"
4316
+ });
4191
4317
  if (!res.writableEnded) sendJson(res, 502, { error: { message: "Native Codex request failed", type: "upstream_error" } });
4192
4318
  }
4193
4319
  return;
@@ -4212,13 +4338,23 @@ async function startCodexProxy(routes, options = {}) {
4212
4338
  }
4213
4339
  }
4214
4340
  const { route, languageModel } = resolved;
4341
+ const relayDispatch = markedSubagent ? "relay-subagent" : "relay";
4342
+ audit({
4343
+ transport: "http",
4344
+ requestedModel: modelId,
4345
+ dispatch: relayDispatch,
4346
+ phase: "dispatch",
4347
+ provider: route.providerId ?? "relay",
4348
+ routeModel: route.modelId,
4349
+ upstreamModel: route.auditUpstreamModelId ?? route.upstreamModelId
4350
+ });
4215
4351
  try {
4216
4352
  const routedBody = await prepareExternalCodexBody(body, {
4217
4353
  relay: nativePayloadRelay,
4218
4354
  mixedNative,
4219
4355
  headers: req.headers
4220
4356
  });
4221
- let params = applyClaudeCodeOAuthIdentity(route, translateResponsesRequest(
4357
+ let params = applyClaudeCodeOAuthIdentity(route, applyExternalCodexRuntimeIdentity(translateResponsesRequest(
4222
4358
  routedBody,
4223
4359
  route.npm,
4224
4360
  {
@@ -4230,7 +4366,7 @@ async function startCodexProxy(routes, options = {}) {
4230
4366
  upstreamModelId: route.upstreamModelId
4231
4367
  },
4232
4368
  { maxTools: maxToolsForNpm(route.npm) }
4233
- ));
4369
+ ), route));
4234
4370
  if (route.contextWindow && route.contextWindow > 0) {
4235
4371
  const before = params.messages.length;
4236
4372
  const estimatedChars = estimateCodexRequestChars(params);
@@ -4285,9 +4421,31 @@ async function startCodexProxy(routes, options = {}) {
4285
4421
  log14(`response progress: model=${route.modelId} elapsedMs=${progress.elapsedMs} reasoningChars=${progress.reasoningChars} textChars=${progress.textChars} toolCalls=${progress.toolCallCount} reasoningTail=${JSON.stringify(progress.reasoningTail)}`);
4286
4422
  }
4287
4423
  });
4424
+ audit({
4425
+ transport: "http",
4426
+ requestedModel: modelId,
4427
+ dispatch: relayDispatch,
4428
+ phase: "complete",
4429
+ provider: route.providerId ?? "relay",
4430
+ routeModel: route.modelId,
4431
+ upstreamModel: route.auditUpstreamModelId ?? route.upstreamModelId,
4432
+ outcome: "ok",
4433
+ status: 200
4434
+ });
4288
4435
  } catch (err) {
4289
4436
  const msg = formatUpstreamError(err);
4290
4437
  const status = upstreamHttpStatus(err, msg);
4438
+ audit({
4439
+ transport: "http",
4440
+ requestedModel: modelId,
4441
+ dispatch: relayDispatch,
4442
+ phase: "complete",
4443
+ provider: route.providerId ?? "relay",
4444
+ routeModel: route.modelId,
4445
+ upstreamModel: route.auditUpstreamModelId ?? route.upstreamModelId,
4446
+ outcome: "error",
4447
+ status
4448
+ });
4291
4449
  if (debug) log14(`sdk error: ${route.modelId}: ${msg}`);
4292
4450
  if (status === 429) {
4293
4451
  writeResponsesRateLimitStream(modelId, msg, write);
@@ -4309,9 +4467,31 @@ async function startCodexProxy(routes, options = {}) {
4309
4467
  });
4310
4468
  }
4311
4469
  sendJson(res, 200, response);
4470
+ audit({
4471
+ transport: "http",
4472
+ requestedModel: modelId,
4473
+ dispatch: relayDispatch,
4474
+ phase: "complete",
4475
+ provider: route.providerId ?? "relay",
4476
+ routeModel: route.modelId,
4477
+ upstreamModel: route.auditUpstreamModelId ?? route.upstreamModelId,
4478
+ outcome: "ok",
4479
+ status: 200
4480
+ });
4312
4481
  } catch (err) {
4313
4482
  const msg = formatUpstreamError(err);
4314
4483
  const status = upstreamHttpStatus(err, msg);
4484
+ audit({
4485
+ transport: "http",
4486
+ requestedModel: modelId,
4487
+ dispatch: relayDispatch,
4488
+ phase: "complete",
4489
+ provider: route.providerId ?? "relay",
4490
+ routeModel: route.modelId,
4491
+ upstreamModel: route.auditUpstreamModelId ?? route.upstreamModelId,
4492
+ outcome: "error",
4493
+ status
4494
+ });
4315
4495
  if (debug) log14(`sdk error: ${route.modelId}: ${msg}`);
4316
4496
  if (status === 429) {
4317
4497
  sendJson(res, 200, responsesRateLimitBody(modelId, msg));
@@ -4436,21 +4616,53 @@ Sec-WebSocket-Accept: ${wsAcceptKey(clientKey)}\r
4436
4616
  `
4437
4617
  );
4438
4618
  let frameBuf = Buffer.alloc(0);
4439
- let handled = false;
4619
+ let externalActive = false;
4440
4620
  let nativeActive = false;
4441
4621
  let nativeUpstream;
4622
+ let nativeSendTurn;
4623
+ let socketClosing = false;
4624
+ const externalResponseStates = /* @__PURE__ */ new Map();
4625
+ let currentExternalCompletedResponse;
4626
+ let currentExternalStateInput;
4627
+ let currentExternalConsumedResponseId;
4442
4628
  let currentRequestModel = "";
4443
- const closeSocket = (code = 1e3) => {
4444
- if (!socket.destroyed) {
4445
- socket.write(wsCloseFrame(code));
4446
- socket.end();
4629
+ const rememberExternalResponse = (response, input) => {
4630
+ const responseId = typeof response.id === "string" ? response.id : void 0;
4631
+ const output = Array.isArray(response.output) ? response.output : void 0;
4632
+ if (!responseId || !output || response.error) return;
4633
+ externalResponseStates.delete(responseId);
4634
+ externalResponseStates.set(responseId, { input: [...input], output: [...output] });
4635
+ while (externalResponseStates.size > MAX_EXTERNAL_RESPONSE_STATES) {
4636
+ const oldest = externalResponseStates.keys().next().value;
4637
+ if (!oldest) break;
4638
+ externalResponseStates.delete(oldest);
4447
4639
  }
4448
4640
  };
4641
+ const resolveExternalContinuation = (body) => {
4642
+ const previousResponseId = typeof body.previous_response_id === "string" ? body.previous_response_id : void 0;
4643
+ if (!previousResponseId || !isExternalToolContinuation(body.input)) return { body };
4644
+ const previous = externalResponseStates.get(previousResponseId);
4645
+ if (!previous) return { body, orphanedResponseId: previousResponseId };
4646
+ return {
4647
+ body: {
4648
+ ...body,
4649
+ input: [...previous.input, ...previous.output, ...body.input]
4650
+ },
4651
+ consumedResponseId: previousResponseId
4652
+ };
4653
+ };
4654
+ const closeSocket = (code = 1e3) => {
4655
+ if (socketClosing || socket.destroyed) return;
4656
+ socketClosing = true;
4657
+ socket.write(wsCloseFrame(code));
4658
+ socket.end();
4659
+ };
4449
4660
  const sendWsEvent = (sseChunk2) => {
4450
- if (socket.destroyed) return;
4451
- if (debug) {
4452
- const completed = captureCompletedResponse(sseChunk2);
4453
- if (completed) {
4661
+ if (socketClosing || socket.destroyed) return;
4662
+ const completed = captureCompletedResponse(sseChunk2);
4663
+ if (completed) {
4664
+ currentExternalCompletedResponse = completed;
4665
+ if (debug) {
4454
4666
  appendCodexBodyDump({
4455
4667
  ts: (/* @__PURE__ */ new Date()).toISOString(),
4456
4668
  transport: "ws",
@@ -4468,7 +4680,6 @@ Sec-WebSocket-Accept: ${wsAcceptKey(clientKey)}\r
4468
4680
  };
4469
4681
  const onData = (chunk) => {
4470
4682
  frameBuf = Buffer.concat([frameBuf, chunk]);
4471
- if (handled && !nativeActive) return;
4472
4683
  const frame = wsDecodeFrame(frameBuf);
4473
4684
  if (!frame) return;
4474
4685
  frameBuf = Buffer.alloc(0);
@@ -4490,7 +4701,10 @@ Sec-WebSocket-Accept: ${wsAcceptKey(clientKey)}\r
4490
4701
  socket.end();
4491
4702
  return;
4492
4703
  }
4493
- handled = true;
4704
+ if (externalActive) {
4705
+ closeSocket(1008);
4706
+ return;
4707
+ }
4494
4708
  void (async () => {
4495
4709
  let body;
4496
4710
  try {
@@ -4531,6 +4745,7 @@ data: ${JSON.stringify({ error: { message: "Invalid JSON", type: "invalid_reques
4531
4745
  log14(`WS subagent dispatch: requested=${modelId} route=${subagentRoute?.modelId ?? "(none)"}`);
4532
4746
  }
4533
4747
  if (mixedNative && markedSubagent && !subagentRoute) {
4748
+ audit({ transport: "ws", requestedModel: modelId, dispatch: "relay-subagent", phase: "complete", outcome: "error", status: 503 });
4534
4749
  sendWsEvent(`event: error
4535
4750
  data: ${JSON.stringify({ error: {
4536
4751
  message: "Codex marked this request as a Sub-agent, but no configured Codex Sub-agent route is available.",
@@ -4545,6 +4760,7 @@ data: ${JSON.stringify({ error: {
4545
4760
  if (!markedSubagent) {
4546
4761
  const dispatch = classifyCodexDispatch(modelId, routes, mixedNative.nativeModelIds);
4547
4762
  if (dispatch.kind === "unknown") {
4763
+ audit({ transport: "ws", requestedModel: modelId, dispatch: "unknown", phase: "complete", outcome: "error", status: 404 });
4548
4764
  sendWsEvent(`event: error
4549
4765
  data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}`, type: "invalid_request_error" } })}
4550
4766
 
@@ -4553,6 +4769,15 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}`, type: "i
4553
4769
  return;
4554
4770
  }
4555
4771
  if (dispatch.kind === "native") {
4772
+ audit({
4773
+ transport: "ws",
4774
+ requestedModel: modelId,
4775
+ dispatch: "native",
4776
+ phase: "dispatch",
4777
+ provider: "openai-native",
4778
+ routeModel: modelId,
4779
+ upstreamModel: modelId
4780
+ });
4556
4781
  const nativeBody = prepareNativeCodexBody(body);
4557
4782
  if (debug && nativeBody !== body) {
4558
4783
  log14(`WS native history normalized: model=${modelId} converted Relay compaction for native verification`);
@@ -4560,7 +4785,7 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}`, type: "i
4560
4785
  if (nativeActive && nativeUpstream) {
4561
4786
  if (nativeUpstream.readyState === WebSocket.OPEN) {
4562
4787
  if (debug) log14(`WS native forwarding next turn: model=${modelId}`);
4563
- nativeUpstream.send(JSON.stringify({ type: "response.create", ...nativeBody }));
4788
+ nativeSendTurn?.(nativeBody, modelId);
4564
4789
  } else if (debug) {
4565
4790
  log14(`WS native cannot forward next turn: upstream_state=${nativeUpstream.readyState}`);
4566
4791
  }
@@ -4571,6 +4796,7 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}`, type: "i
4571
4796
  let upstream;
4572
4797
  let nativeOpened = false;
4573
4798
  let nativeCompleted = false;
4799
+ let nativeTurnModelId = modelId;
4574
4800
  let nativeFrameCount = 0;
4575
4801
  let finished = false;
4576
4802
  let connectTimer;
@@ -4590,10 +4816,24 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}`, type: "i
4590
4816
  if (finished) return;
4591
4817
  finished = true;
4592
4818
  nativeActive = false;
4819
+ nativeSendTurn = void 0;
4593
4820
  if (nativeUpstream === upstream) nativeUpstream = void 0;
4594
4821
  clearTimers();
4595
4822
  if (debug && message) {
4596
- log14(`WS native upstream failed: model=${modelId} opened=${nativeOpened} frames=${nativeFrameCount} message=${message}`);
4823
+ log14(`WS native upstream failed: model=${nativeTurnModelId} opened=${nativeOpened} frames=${nativeFrameCount} message=${message}`);
4824
+ }
4825
+ if (message && !nativeCompleted) {
4826
+ audit({
4827
+ transport: "ws",
4828
+ requestedModel: nativeTurnModelId,
4829
+ dispatch: "native",
4830
+ phase: "complete",
4831
+ provider: "openai-native",
4832
+ routeModel: nativeTurnModelId,
4833
+ upstreamModel: nativeTurnModelId,
4834
+ outcome: "error",
4835
+ status: "upstream-failed"
4836
+ });
4597
4837
  }
4598
4838
  if (message && !nativeCompleted) sendNativeError(message);
4599
4839
  try {
@@ -4602,20 +4842,31 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}`, type: "i
4602
4842
  }
4603
4843
  closeSocket(closeCode);
4604
4844
  };
4845
+ const sendNativeTurn = (turnBody, turnModelId) => {
4846
+ if (!upstream || upstream.readyState !== WebSocket.OPEN) {
4847
+ if (debug) log14(`WS native cannot send turn: model=${turnModelId} upstream_state=${upstream?.readyState ?? "missing"}`);
4848
+ return;
4849
+ }
4850
+ nativeTurnModelId = turnModelId;
4851
+ nativeCompleted = false;
4852
+ if (firstFrameTimer) clearTimeout(firstFrameTimer);
4853
+ upstream.send(JSON.stringify({ type: "response.create", ...turnBody }));
4854
+ firstFrameTimer = setTimeout(() => closeBoth("Native Codex WebSocket response timed out"), 6e4);
4855
+ };
4605
4856
  try {
4606
4857
  if (debug) {
4607
4858
  log14(`WS native connecting: model=${modelId} url=${target.url} headers=[${Object.keys(target.headers).join(",")}]`);
4608
4859
  }
4609
4860
  upstream = new WebSocket(target.url, { headers: target.headers });
4610
4861
  nativeUpstream = upstream;
4862
+ nativeSendTurn = sendNativeTurn;
4611
4863
  nativeActive = true;
4612
4864
  connectTimer = setTimeout(() => closeBoth("Native Codex WebSocket connection timed out"), 15e3);
4613
4865
  upstream.once("open", () => {
4614
4866
  nativeOpened = true;
4615
4867
  if (connectTimer) clearTimeout(connectTimer);
4616
4868
  if (debug) log14(`WS native upstream open: model=${modelId}`);
4617
- upstream?.send(JSON.stringify({ type: "response.create", ...nativeBody }));
4618
- firstFrameTimer = setTimeout(() => closeBoth("Native Codex WebSocket response timed out"), 6e4);
4869
+ sendNativeTurn(nativeBody, modelId);
4619
4870
  });
4620
4871
  upstream.once("unexpected-response", (_request, response) => {
4621
4872
  if (debug) log14(`WS native upstream HTTP rejection: model=${modelId} status=${response.statusCode}`);
@@ -4633,6 +4884,17 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}`, type: "i
4633
4884
  if (typeof parsed.type === "string") eventType = parsed.type;
4634
4885
  if (eventType === "response.completed" || eventType === "response.failed" || eventType === "response.incomplete") {
4635
4886
  nativeCompleted = true;
4887
+ audit({
4888
+ transport: "ws",
4889
+ requestedModel: modelId,
4890
+ dispatch: "native",
4891
+ phase: "complete",
4892
+ provider: "openai-native",
4893
+ routeModel: modelId,
4894
+ upstreamModel: modelId,
4895
+ outcome: eventType === "response.completed" ? "ok" : "error",
4896
+ status: eventType
4897
+ });
4636
4898
  }
4637
4899
  } catch {
4638
4900
  }
@@ -4653,6 +4915,7 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}`, type: "i
4653
4915
  if (debug) log14(`WS native downstream close: model=${modelId} frames=${nativeFrameCount} completed=${nativeCompleted}`);
4654
4916
  finished = true;
4655
4917
  nativeActive = false;
4918
+ nativeSendTurn = void 0;
4656
4919
  if (nativeUpstream === upstream) nativeUpstream = void 0;
4657
4920
  clearTimers();
4658
4921
  try {
@@ -4667,6 +4930,7 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}`, type: "i
4667
4930
  }
4668
4931
  }
4669
4932
  }
4933
+ externalActive = true;
4670
4934
  let resolved = subagentRoute ? resolveModel(routes, models, subagentRoute.modelId) : resolveModel(routes, models, modelId);
4671
4935
  if (!resolved) {
4672
4936
  const fb = routes[0];
@@ -4685,13 +4949,35 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}` } })}
4685
4949
  }
4686
4950
  }
4687
4951
  const { route, languageModel } = resolved;
4952
+ const relayDispatch = markedSubagent ? "relay-subagent" : "relay";
4953
+ audit({
4954
+ transport: "ws",
4955
+ requestedModel: modelId,
4956
+ dispatch: relayDispatch,
4957
+ phase: "dispatch",
4958
+ provider: route.providerId ?? "relay",
4959
+ routeModel: route.modelId,
4960
+ upstreamModel: route.auditUpstreamModelId ?? route.upstreamModelId
4961
+ });
4962
+ currentExternalCompletedResponse = void 0;
4963
+ currentExternalStateInput = void 0;
4964
+ currentExternalConsumedResponseId = void 0;
4965
+ const continuation = resolveExternalContinuation(body);
4966
+ if (continuation.orphanedResponseId) {
4967
+ if (debug) log14(`WS continuation rejected: unknown previous_response_id=${continuation.orphanedResponseId}`);
4968
+ writeResponsesErrorStream(modelId, "Unknown or expired previous_response_id", sendWsEvent, 400);
4969
+ externalActive = false;
4970
+ return;
4971
+ }
4688
4972
  try {
4689
- const routedBody = await prepareExternalCodexBody(body, {
4973
+ const routedBody = await prepareExternalCodexBody(continuation.body, {
4690
4974
  relay: nativePayloadRelay,
4691
4975
  mixedNative,
4692
4976
  headers: req.headers
4693
4977
  });
4694
- let params = applyClaudeCodeOAuthIdentity(route, translateResponsesRequest(
4978
+ currentExternalStateInput = responsesInputItems(routedBody.input);
4979
+ currentExternalConsumedResponseId = continuation.consumedResponseId;
4980
+ let params = applyClaudeCodeOAuthIdentity(route, applyExternalCodexRuntimeIdentity(translateResponsesRequest(
4695
4981
  routedBody,
4696
4982
  route.npm,
4697
4983
  {
@@ -4703,7 +4989,7 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}` } })}
4703
4989
  upstreamModelId: route.upstreamModelId
4704
4990
  },
4705
4991
  { maxTools: maxToolsForNpm(route.npm) }
4706
- ));
4992
+ ), route));
4707
4993
  if (route.contextWindow && route.contextWindow > 0) {
4708
4994
  const before = params.messages.length;
4709
4995
  const estimatedChars = estimateCodexRequestChars(params);
@@ -4736,9 +5022,37 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}` } })}
4736
5022
  log14(`WS response progress: model=${route.modelId} elapsedMs=${progress.elapsedMs} reasoningChars=${progress.reasoningChars} textChars=${progress.textChars} toolCalls=${progress.toolCallCount} reasoningTail=${JSON.stringify(progress.reasoningTail)}`);
4737
5023
  }
4738
5024
  });
5025
+ if (currentExternalCompletedResponse && currentExternalStateInput) {
5026
+ if (currentExternalConsumedResponseId) {
5027
+ externalResponseStates.delete(currentExternalConsumedResponseId);
5028
+ }
5029
+ rememberExternalResponse(currentExternalCompletedResponse, currentExternalStateInput);
5030
+ }
5031
+ audit({
5032
+ transport: "ws",
5033
+ requestedModel: modelId,
5034
+ dispatch: relayDispatch,
5035
+ phase: "complete",
5036
+ provider: route.providerId ?? "relay",
5037
+ routeModel: route.modelId,
5038
+ upstreamModel: route.auditUpstreamModelId ?? route.upstreamModelId,
5039
+ outcome: "ok",
5040
+ status: "response.completed"
5041
+ });
4739
5042
  } catch (err) {
4740
5043
  const msg = formatUpstreamError(err);
4741
5044
  const status = upstreamHttpStatus(err, msg);
5045
+ audit({
5046
+ transport: "ws",
5047
+ requestedModel: modelId,
5048
+ dispatch: relayDispatch,
5049
+ phase: "complete",
5050
+ provider: route.providerId ?? "relay",
5051
+ routeModel: route.modelId,
5052
+ upstreamModel: route.auditUpstreamModelId ?? route.upstreamModelId,
5053
+ outcome: "error",
5054
+ status
5055
+ });
4742
5056
  if (debug) log14(`WS sdk error: ${route.modelId}: ${msg}`);
4743
5057
  if (status === 429) {
4744
5058
  writeResponsesRateLimitStream(modelId, msg, sendWsEvent);
@@ -4746,10 +5060,11 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}` } })}
4746
5060
  writeResponsesErrorStream(modelId, msg, sendWsEvent, status);
4747
5061
  }
4748
5062
  }
4749
- closeSocket();
5063
+ externalActive = false;
4750
5064
  })();
4751
5065
  };
4752
5066
  socket.on("error", () => socket.destroy());
5067
+ socket.once("close", () => externalResponseStates.clear());
4753
5068
  socket.on("data", onData);
4754
5069
  onData(head);
4755
5070
  });
@@ -4774,44 +5089,44 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}` } })}
4774
5089
  }
4775
5090
 
4776
5091
  // src/codex/profile.ts
4777
- import { join as join3 } from "path";
5092
+ import { join as join4 } from "path";
4778
5093
 
4779
5094
  // src/codex/session.ts
4780
5095
  import {
4781
5096
  copyFileSync,
4782
- chmodSync,
5097
+ chmodSync as chmodSync2,
4783
5098
  existsSync as existsSync3,
4784
- mkdirSync,
5099
+ mkdirSync as mkdirSync2,
4785
5100
  readdirSync,
4786
5101
  readFileSync as readFileSync2,
4787
5102
  renameSync,
4788
5103
  rmSync,
4789
5104
  statSync,
4790
5105
  unlinkSync,
4791
- writeFileSync
5106
+ writeFileSync as writeFileSync2
4792
5107
  } from "fs";
4793
5108
  import { homedir as homedir3 } from "os";
4794
- import { basename, dirname, join as join2 } from "path";
5109
+ import { basename, dirname, join as join3 } from "path";
4795
5110
  var CODEX_PROFILE_NAME = "relay-ai-launch";
4796
5111
  var STALE_SESSION_MS = 5 * 60 * 1e3;
4797
5112
  var MAX_BACKUPS = 5;
4798
5113
  function getCodexHome(env = process.env) {
4799
- return env["CODEX_HOME"] || join2(homedir3(), ".codex");
5114
+ return env["CODEX_HOME"] || join3(homedir3(), ".codex");
4800
5115
  }
4801
5116
  function getCodexProfilePath() {
4802
- return join2(getCodexHome(), `${CODEX_PROFILE_NAME}.config.toml`);
5117
+ return join3(getCodexHome(), `${CODEX_PROFILE_NAME}.config.toml`);
4803
5118
  }
4804
5119
  function getRelayAiCodexDir(env = process.env) {
4805
- return join2(getAppHome(env), "codex");
5120
+ return join3(getAppHome(env), "codex");
4806
5121
  }
4807
5122
  function getSessionLockPath(env = process.env) {
4808
- return join2(getRelayAiCodexDir(env), "session.json");
5123
+ return join3(getRelayAiCodexDir(env), "session.json");
4809
5124
  }
4810
5125
  function getBackupsDir(env = process.env) {
4811
- return join2(getRelayAiCodexDir(env), "backups");
5126
+ return join3(getRelayAiCodexDir(env), "backups");
4812
5127
  }
4813
5128
  function getCatalogPath(providerId, env = process.env) {
4814
- return join2(getRelayAiCodexDir(env), `models-${providerId}.json`);
5129
+ return join3(getRelayAiCodexDir(env), `models-${providerId}.json`);
4815
5130
  }
4816
5131
  function ownedOverlayPaths(env = process.env) {
4817
5132
  const paths = [getCodexProfilePath()];
@@ -4819,15 +5134,15 @@ function ownedOverlayPaths(env = process.env) {
4819
5134
  if (existsSync3(codexDir)) {
4820
5135
  for (const name of readdirSync(codexDir)) {
4821
5136
  if (name.startsWith("models-") && name.endsWith(".json")) {
4822
- paths.push(join2(codexDir, name));
5137
+ paths.push(join3(codexDir, name));
4823
5138
  }
4824
5139
  }
4825
5140
  }
4826
- const agentsDir = join2(getCodexHome(env), "agents");
5141
+ const agentsDir = join3(getCodexHome(env), "agents");
4827
5142
  if (existsSync3(agentsDir)) {
4828
5143
  for (const name of readdirSync(agentsDir)) {
4829
5144
  if (/^relay-model-[a-z0-9-]+\.toml$/i.test(name)) {
4830
- paths.push(join2(agentsDir, name));
5145
+ paths.push(join3(agentsDir, name));
4831
5146
  }
4832
5147
  }
4833
5148
  }
@@ -4835,27 +5150,27 @@ function ownedOverlayPaths(env = process.env) {
4835
5150
  return paths;
4836
5151
  }
4837
5152
  function atomicWriteFile(path3, content) {
4838
- mkdirSync(dirname(path3), { recursive: true });
5153
+ mkdirSync2(dirname(path3), { recursive: true });
4839
5154
  const tmp = `${path3}.tmp.${process.pid}`;
4840
- writeFileSync(tmp, content, { encoding: "utf8", mode: 384 });
5155
+ writeFileSync2(tmp, content, { encoding: "utf8", mode: 384 });
4841
5156
  renameSync(tmp, path3);
4842
5157
  try {
4843
- chmodSync(path3, 384);
5158
+ chmodSync2(path3, 384);
4844
5159
  } catch {
4845
5160
  }
4846
5161
  }
4847
5162
  function rotateBackups(filePath, env = process.env) {
4848
5163
  if (!existsSync3(filePath)) return;
4849
5164
  const backupsDir = getBackupsDir(env);
4850
- mkdirSync(backupsDir, { recursive: true });
5165
+ mkdirSync2(backupsDir, { recursive: true });
4851
5166
  const base = basename(filePath);
4852
5167
  const stamp = Date.now();
4853
- const backupPath = join2(backupsDir, `${base}.${stamp}.bak`);
5168
+ const backupPath = join3(backupsDir, `${base}.${stamp}.bak`);
4854
5169
  copyFileSync(filePath, backupPath);
4855
- const backups = readdirSync(backupsDir).filter((n) => n.startsWith(`${base}.`) && n.endsWith(".bak")).map((n) => ({ name: n, mtime: statSync(join2(backupsDir, n)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
5170
+ const backups = readdirSync(backupsDir).filter((n) => n.startsWith(`${base}.`) && n.endsWith(".bak")).map((n) => ({ name: n, mtime: statSync(join3(backupsDir, n)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
4856
5171
  for (const old of backups.slice(MAX_BACKUPS)) {
4857
5172
  try {
4858
- unlinkSync(join2(backupsDir, old.name));
5173
+ unlinkSync(join3(backupsDir, old.name));
4859
5174
  } catch {
4860
5175
  }
4861
5176
  }
@@ -4876,7 +5191,7 @@ function readSessionLock(env = process.env) {
4876
5191
  }
4877
5192
  function writeSessionLock(lock, env = process.env) {
4878
5193
  const path3 = getSessionLockPath(env);
4879
- mkdirSync(getRelayAiCodexDir(env), { recursive: true });
5194
+ mkdirSync2(getRelayAiCodexDir(env), { recursive: true });
4880
5195
  atomicWriteFile(path3, `${JSON.stringify(lock, null, 2)}
4881
5196
  `);
4882
5197
  }
@@ -4999,10 +5314,10 @@ function getCatalogOutputPath(providerId) {
4999
5314
  return getCatalogPath(providerId);
5000
5315
  }
5001
5316
  function getFavoritesCatalogPath() {
5002
- return join3(getRelayAiCodexDir(), "models-favorites.json");
5317
+ return join4(getRelayAiCodexDir(), "models-favorites.json");
5003
5318
  }
5004
5319
  function getFavoritesAppCatalogPath() {
5005
- return join3(getRelayAiCodexDir(), "app-models-favorites.json");
5320
+ return join4(getRelayAiCodexDir(), "app-models-favorites.json");
5006
5321
  }
5007
5322
  function profileName() {
5008
5323
  return CODEX_PROFILE_NAME;
@@ -5013,7 +5328,7 @@ import { execSync as execSync2 } from "child_process";
5013
5328
  import spawn2 from "cross-spawn";
5014
5329
  import { existsSync as existsSync4 } from "fs";
5015
5330
  import { homedir as homedir4 } from "os";
5016
- import { join as join4 } from "path";
5331
+ import { join as join5 } from "path";
5017
5332
  var isWindows2 = process.platform === "win32";
5018
5333
  var CODEX_CI_ENV_VARS = [
5019
5334
  "CI",
@@ -5034,11 +5349,11 @@ function stripCodexInheritedEnv(env) {
5034
5349
  return out;
5035
5350
  }
5036
5351
  var CODEX_FALLBACK_PATHS = isWindows2 ? [
5037
- join4(process.env["APPDATA"] ?? homedir4(), "npm", "codex.cmd"),
5038
- join4(process.env["APPDATA"] ?? homedir4(), "npm", "codex")
5352
+ join5(process.env["APPDATA"] ?? homedir4(), "npm", "codex.cmd"),
5353
+ join5(process.env["APPDATA"] ?? homedir4(), "npm", "codex")
5039
5354
  ] : [
5040
- join4(homedir4(), ".local", "bin", "codex"),
5041
- join4(homedir4(), ".npm", "bin", "codex"),
5355
+ join5(homedir4(), ".local", "bin", "codex"),
5356
+ join5(homedir4(), ".npm", "bin", "codex"),
5042
5357
  "/usr/local/bin/codex",
5043
5358
  "/opt/homebrew/bin/codex"
5044
5359
  ];
@@ -5260,7 +5575,7 @@ function printCodexAppSessionPanel(opts) {
5260
5575
  `${pc6.bold("Provider")} ${fmtProvider(opts.providerName)}`,
5261
5576
  "",
5262
5577
  `${pc6.yellow(pc6.bold("Keep this terminal open"))}${pc6.white(" while you use Codex.")}`,
5263
- `${pc6.white("Press ")}${pc6.bold(pc6.red("Ctrl+C"))}${pc6.white(" to stop the proxy and restore ")}${fmtCommand("~/.codex/config.toml")}${pc6.white(".")}`,
5578
+ `${pc6.white("Press ")}${pc6.bold(pc6.red("Ctrl+C"))}${pc6.white(" to close ChatGPT Desktop, restore ")}${fmtCommand("~/.codex/config.toml")}${pc6.white(", and stop the proxy.")}`,
5264
5579
  `${pc6.dim("Codex may show ")}${pc6.yellow('"Custom"')}${pc6.dim(" if the desktop picker cannot resolve registry models \u2014 check the terminal line above. After restart, pick your model from the picker if it appears.")}`,
5265
5580
  `${pc6.dim("If Codex asks you to sign in after restart: choose API key and enter any character \u2014 that unlocks the model picker for registry providers.")}`,
5266
5581
  `${pc6.dim("Stuck? Run ")}${fmtCommand(opts.restoreCommand)}${pc6.dim(".")}`
@@ -5541,7 +5856,8 @@ async function resolveCodexMixedModels(input) {
5541
5856
  subagents: subagentResult.resolved,
5542
5857
  all,
5543
5858
  providersById: new Map(input.compatible.map((provider) => [provider.id, provider])),
5544
- dropped: [...visibleResult.droppedFavorites, ...subagentResult.droppedFavorites]
5859
+ dropped: [...visibleResult.droppedFavorites, ...subagentResult.droppedFavorites],
5860
+ capacitySkipped: [...visibleResult.capacitySkippedFavorites, ...subagentResult.capacitySkippedFavorites]
5545
5861
  };
5546
5862
  }
5547
5863
 
@@ -5741,6 +6057,7 @@ async function prepareCodexMixedRelayRoutes(models, trace = false) {
5741
6057
  apiKey: backend.token,
5742
6058
  baseURL: `http://127.0.0.1:${backend.port}`,
5743
6059
  upstreamModelId: proxyRoute.aliasId,
6060
+ auditUpstreamModelId: original.model.upstreamModelId || original.model.id,
5744
6061
  providerId: original.providerId,
5745
6062
  authType: "oauth",
5746
6063
  oauthAccountId: original.oauthAccountId,
@@ -6071,7 +6388,7 @@ async function writeFavoritesLaunchArtifacts(resolved, starting, proxyPort) {
6071
6388
  return { profilePath, catalogPath };
6072
6389
  }
6073
6390
  async function writeMixedLaunchArtifacts(plan, proxyPort) {
6074
- const catalogPath = join5(getRelayAiCodexDir(), "models-mixed.json");
6391
+ const catalogPath = join6(getRelayAiCodexDir(), "models-mixed.json");
6075
6392
  writeOverlayFile(catalogPath, serializeCatalog(plan.catalog));
6076
6393
  const profilePath = getProfileOutputPath();
6077
6394
  writeOverlayFile(profilePath, buildCodexMixedProfileToml({
@@ -6656,17 +6973,17 @@ import * as p10 from "@clack/prompts";
6656
6973
 
6657
6974
  // src/gemini/launch.ts
6658
6975
  import { spawn as spawn3 } from "child_process";
6659
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
6976
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
6660
6977
  import { homedir as homedir5, tmpdir } from "os";
6661
- import { join as join6 } from "path";
6978
+ import { join as join7 } from "path";
6662
6979
  var isWindows3 = process.platform === "win32";
6663
6980
  var GEMINI_API_KEY_AUTH_TYPE = "gemini-api-key";
6664
6981
  var GEMINI_FALLBACK_PATHS = isWindows3 ? [
6665
- join6(process.env["APPDATA"] ?? homedir5(), "npm", "gemini.cmd"),
6666
- join6(process.env["APPDATA"] ?? homedir5(), "npm", "gemini")
6982
+ join7(process.env["APPDATA"] ?? homedir5(), "npm", "gemini.cmd"),
6983
+ join7(process.env["APPDATA"] ?? homedir5(), "npm", "gemini")
6667
6984
  ] : [
6668
- join6(homedir5(), ".local", "bin", "gemini"),
6669
- join6(homedir5(), ".npm", "bin", "gemini"),
6985
+ join7(homedir5(), ".local", "bin", "gemini"),
6986
+ join7(homedir5(), ".npm", "bin", "gemini"),
6670
6987
  "/usr/local/bin/gemini",
6671
6988
  "/opt/homebrew/bin/gemini"
6672
6989
  ];
@@ -6687,7 +7004,7 @@ function buildGeminiChildEnv(proxyPort, proxyToken) {
6687
7004
  return env;
6688
7005
  }
6689
7006
  function createGeminiCliHomeOverlay() {
6690
- const cliHome = mkdtempSync(join6(tmpdir(), "relay-ai-gemini-"));
7007
+ const cliHome = mkdtempSync(join7(tmpdir(), "relay-ai-gemini-"));
6691
7008
  const settings = {
6692
7009
  security: {
6693
7010
  auth: {
@@ -6695,9 +7012,9 @@ function createGeminiCliHomeOverlay() {
6695
7012
  }
6696
7013
  }
6697
7014
  };
6698
- const geminiDir = join6(cliHome, ".gemini");
6699
- mkdirSync2(geminiDir);
6700
- writeFileSync2(join6(geminiDir, "settings.json"), `${JSON.stringify(settings, null, 2)}
7015
+ const geminiDir = join7(cliHome, ".gemini");
7016
+ mkdirSync3(geminiDir);
7017
+ writeFileSync3(join7(geminiDir, "settings.json"), `${JSON.stringify(settings, null, 2)}
6701
7018
  `, {
6702
7019
  encoding: "utf8",
6703
7020
  mode: 384
@@ -9939,15 +10256,15 @@ import { execFileSync, execSync as execSync3 } from "child_process";
9939
10256
  import spawn4 from "cross-spawn";
9940
10257
  import { existsSync as existsSync6 } from "fs";
9941
10258
  import { homedir as homedir6 } from "os";
9942
- import { join as join7 } from "path";
10259
+ import { join as join8 } from "path";
9943
10260
  var isWindows4 = process.platform === "win32";
9944
10261
  var FALLBACK_PATHS = isWindows4 ? [
9945
- join7(process.env["APPDATA"] ?? homedir6(), "npm", "agy.cmd"),
9946
- join7(process.env["APPDATA"] ?? homedir6(), "npm", "agy"),
9947
- join7(homedir6(), "AppData", "Roaming", "npm", "agy.cmd")
10262
+ join8(process.env["APPDATA"] ?? homedir6(), "npm", "agy.cmd"),
10263
+ join8(process.env["APPDATA"] ?? homedir6(), "npm", "agy"),
10264
+ join8(homedir6(), "AppData", "Roaming", "npm", "agy.cmd")
9948
10265
  ] : [
9949
- join7(homedir6(), ".local", "bin", "agy"),
9950
- join7(homedir6(), ".npm", "bin", "agy"),
10266
+ join8(homedir6(), ".local", "bin", "agy"),
10267
+ join8(homedir6(), ".npm", "bin", "agy"),
9951
10268
  "/usr/local/bin/agy",
9952
10269
  "/opt/homebrew/bin/agy"
9953
10270
  ];
@@ -10025,7 +10342,7 @@ function launchAntigravityCli(env, extraArgs) {
10025
10342
  import { execFileSync as execFileSync2, execSync as execSync4, spawn as spawn5 } from "child_process";
10026
10343
  import { existsSync as existsSync7 } from "fs";
10027
10344
  import { homedir as homedir7 } from "os";
10028
- import { join as join8 } from "path";
10345
+ import { join as join9 } from "path";
10029
10346
 
10030
10347
  // src/antigravity/ide-profile.ts
10031
10348
  import fs from "fs";
@@ -10059,8 +10376,8 @@ function prepareIdeProfile(profileDir, gatewayUrl) {
10059
10376
  }
10060
10377
 
10061
10378
  // src/antigravity/launch-ide.ts
10062
- var LINUX_APP_PROFILE_DIR = join8(homedir7(), ".relay-ai", "antigravity", "app-profile");
10063
- var LINUX_IDE_PROFILE_DIR = join8(homedir7(), ".relay-ai", "antigravity", "profile");
10379
+ var LINUX_APP_PROFILE_DIR = join9(homedir7(), ".relay-ai", "antigravity", "app-profile");
10380
+ var LINUX_IDE_PROFILE_DIR = join9(homedir7(), ".relay-ai", "antigravity", "profile");
10064
10381
  function sleep(ms) {
10065
10382
  return new Promise((resolve2) => setTimeout(resolve2, ms));
10066
10383
  }
@@ -10068,7 +10385,7 @@ function linuxAntigravityBinary() {
10068
10385
  const candidates = [
10069
10386
  "/usr/share/antigravity/antigravity",
10070
10387
  "/opt/antigravity/antigravity",
10071
- join8(homedir7(), ".local", "share", "antigravity", "antigravity")
10388
+ join9(homedir7(), ".local", "share", "antigravity", "antigravity")
10072
10389
  ];
10073
10390
  for (const candidate of candidates) {
10074
10391
  if (existsSync7(candidate)) return candidate;
@@ -10223,15 +10540,15 @@ function findAntigravityAppBinary() {
10223
10540
  const override = getAppPathOverride("antigravity");
10224
10541
  if (override) return existsSync7(override) ? override : null;
10225
10542
  if (process.platform === "win32") {
10226
- const localAppData = process.env["LOCALAPPDATA"] ?? join8(homedir7(), "AppData", "Local");
10227
- const winPath = join8(localAppData, "Programs", "Antigravity", "Antigravity.exe");
10543
+ const localAppData = process.env["LOCALAPPDATA"] ?? join9(homedir7(), "AppData", "Local");
10544
+ const winPath = join9(localAppData, "Programs", "Antigravity", "Antigravity.exe");
10228
10545
  return existsSync7(winPath) ? winPath : null;
10229
10546
  }
10230
10547
  if (process.platform === "linux") return linuxAntigravityBinary();
10231
10548
  if (process.platform !== "darwin") return null;
10232
10549
  const defaultPath = "/Applications/Antigravity.app/Contents/MacOS/Antigravity";
10233
10550
  if (existsSync7(defaultPath)) return defaultPath;
10234
- const homePath = join8(homedir7(), "Applications", "Antigravity.app", "Contents", "MacOS", "Antigravity");
10551
+ const homePath = join9(homedir7(), "Applications", "Antigravity.app", "Contents", "MacOS", "Antigravity");
10235
10552
  if (existsSync7(homePath)) return homePath;
10236
10553
  return null;
10237
10554
  }
@@ -10239,15 +10556,15 @@ function findAntigravityIdeBinary() {
10239
10556
  const override = getAppPathOverride("antigravity-ide");
10240
10557
  if (override) return existsSync7(override) ? override : null;
10241
10558
  if (process.platform === "win32") {
10242
- const localAppData = process.env["LOCALAPPDATA"] ?? join8(homedir7(), "AppData", "Local");
10243
- const winPath = join8(localAppData, "Programs", "Antigravity IDE", "Antigravity IDE.exe");
10559
+ const localAppData = process.env["LOCALAPPDATA"] ?? join9(homedir7(), "AppData", "Local");
10560
+ const winPath = join9(localAppData, "Programs", "Antigravity IDE", "Antigravity IDE.exe");
10244
10561
  return existsSync7(winPath) ? winPath : null;
10245
10562
  }
10246
10563
  if (process.platform === "linux") return linuxAntigravityBinary();
10247
10564
  if (process.platform !== "darwin") return null;
10248
10565
  const defaultPath = "/Applications/Antigravity IDE.app/Contents/Resources/app/bin/antigravity-ide";
10249
10566
  if (existsSync7(defaultPath)) return defaultPath;
10250
- const homePath = join8(homedir7(), "Applications", "Antigravity IDE.app", "Contents", "Resources", "app", "bin", "antigravity-ide");
10567
+ const homePath = join9(homedir7(), "Applications", "Antigravity IDE.app", "Contents", "Resources", "app", "bin", "antigravity-ide");
10251
10568
  if (existsSync7(homePath)) return homePath;
10252
10569
  return null;
10253
10570
  }
@@ -10308,7 +10625,7 @@ function launchAntigravityIde(env, profileDir, gatewayUrl, extraArgs) {
10308
10625
  return;
10309
10626
  }
10310
10627
  prepareIdeProfile(profileDir, gatewayUrl);
10311
- const relayExtensionsDir = join8(homedir7(), ".relay-ai", "antigravity", "extensions");
10628
+ const relayExtensionsDir = join9(homedir7(), ".relay-ai", "antigravity", "extensions");
10312
10629
  const args = [
10313
10630
  `--user-data-dir=${profileDir}`,
10314
10631
  `--extensions-dir=${relayExtensionsDir}`,
@@ -10338,7 +10655,7 @@ function launchAntigravityIde(env, profileDir, gatewayUrl, extraArgs) {
10338
10655
 
10339
10656
  // src/antigravity.ts
10340
10657
  import { homedir as homedir8 } from "os";
10341
- import { join as join9 } from "path";
10658
+ import { join as join10 } from "path";
10342
10659
  var SHUTDOWN_DRAIN_MS = 500;
10343
10660
  var AGY_FAVORITES_PROVIDER_ID = "__relay_agy_favorites__";
10344
10661
  var AGY_FAVORITES_PROVIDER_LABEL = "\u2605 Antigravity CLI Favorites";
@@ -10632,7 +10949,7 @@ async function runAntigravityAppCommand(childArgs, trace = false, boot) {
10632
10949
  trace,
10633
10950
  boot,
10634
10951
  async (env, _routes, gatewayHandle) => {
10635
- const profileDir = join9(homedir8(), ".relay-ai", "antigravity", "app-profile");
10952
+ const profileDir = join10(homedir8(), ".relay-ai", "antigravity", "app-profile");
10636
10953
  if (isAntigravityAppRunning(profileDir)) {
10637
10954
  const restart = await p11.confirm({
10638
10955
  message: "Restart Antigravity to apply this Relay gateway?",
@@ -10680,7 +10997,7 @@ async function runAntigravityIdeCommand(childArgs, trace = false, boot) {
10680
10997
  trace,
10681
10998
  boot,
10682
10999
  async (env, _routes, gatewayHandle) => {
10683
- const profileDir = join9(homedir8(), ".relay-ai", "antigravity", "profile");
11000
+ const profileDir = join10(homedir8(), ".relay-ai", "antigravity", "profile");
10684
11001
  if (isAntigravityIdeRunning(profileDir)) {
10685
11002
  const restart = await p11.confirm({
10686
11003
  message: "Restart Antigravity IDE to apply this Relay gateway?",
@@ -10725,7 +11042,7 @@ async function runAntigravityIdeCommand(childArgs, trace = false, boot) {
10725
11042
  // src/codex-app.ts
10726
11043
  import pc10 from "picocolors";
10727
11044
  import * as p12 from "@clack/prompts";
10728
- import { join as join12 } from "path";
11045
+ import { join as join13 } from "path";
10729
11046
 
10730
11047
  // src/codex/app-provider-routes.ts
10731
11048
  function codexRouteToProxyRoute(provider, model, apiKey) {
@@ -10814,14 +11131,14 @@ async function buildCodexAppProviderCatalogRoutes(provider, apiKey, selectedMode
10814
11131
  }
10815
11132
 
10816
11133
  // src/codex/app-config.ts
10817
- import { existsSync as existsSync8, readFileSync as readFileSync3, rmSync as rmSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
10818
- import { dirname as dirname2, join as join10 } from "path";
11134
+ import { existsSync as existsSync8, readFileSync as readFileSync3, rmSync as rmSync3, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
11135
+ import { dirname as dirname2, join as join11 } from "path";
10819
11136
  import { parse, stringify } from "smol-toml";
10820
11137
  function getCodexConfigPath() {
10821
- return join10(getCodexHome(), "config.toml");
11138
+ return join11(getCodexHome(), "config.toml");
10822
11139
  }
10823
11140
  function getCodexAppSidecarProfilePath() {
10824
- return join10(getCodexHome(), `${CODEX_APP_PROVIDER_ID}.config.toml`);
11141
+ return join11(getCodexHome(), `${CODEX_APP_PROVIDER_ID}.config.toml`);
10825
11142
  }
10826
11143
  function asRecord(value) {
10827
11144
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
@@ -10996,8 +11313,13 @@ function applyAppConfigPatch(spec, configPath = getCodexConfigPath()) {
10996
11313
  const text5 = `${stringify(merged)}
10997
11314
  `;
10998
11315
  validateAppConfigText(text5, spec);
10999
- mkdirSync3(dirname2(configPath), { recursive: true });
11000
- writeFileSync3(configPath, text5, "utf8");
11316
+ mkdirSync4(dirname2(configPath), { recursive: true });
11317
+ atomicWriteFile(configPath, text5);
11318
+ const written = readCodexConfigText(configPath);
11319
+ if (written !== text5) {
11320
+ throw new Error(`Codex config readback mismatch at ${configPath}`);
11321
+ }
11322
+ validateAppConfigText(written, spec);
11001
11323
  return text5;
11002
11324
  }
11003
11325
  function applyRestoreKey(config, key, had, value) {
@@ -11053,7 +11375,7 @@ function restoreConfigFromState(state, configPath = getCodexConfigPath()) {
11053
11375
  rmSync3(configPath, { force: true });
11054
11376
  return true;
11055
11377
  }
11056
- writeFileSync3(configPath, `${stringify(config)}
11378
+ writeFileSync4(configPath, `${stringify(config)}
11057
11379
  `, "utf8");
11058
11380
  return true;
11059
11381
  }
@@ -11064,31 +11386,69 @@ function previewAppConfigToml(spec) {
11064
11386
  return text5;
11065
11387
  }
11066
11388
 
11389
+ // src/codex/app-readiness.ts
11390
+ import { readFileSync as readFileSync4 } from "fs";
11391
+ function proxyRoot(spec) {
11392
+ const base = spec.proxyBaseUrl ?? `http://127.0.0.1:${spec.proxyPort}/v1`;
11393
+ if (!base.endsWith("/v1")) throw new Error("Codex App proxy base URL must end in /v1");
11394
+ return base.slice(0, -3);
11395
+ }
11396
+ async function checkedJson(url, fetchImpl) {
11397
+ const response = await fetchImpl(url);
11398
+ if (!response.ok) throw new Error(`Relay readiness check failed: GET ${url} returned HTTP ${response.status}`);
11399
+ return response.json();
11400
+ }
11401
+ async function verifyCodexAppReadiness(spec, options = {}) {
11402
+ const fetchImpl = options.fetchImpl ?? fetch;
11403
+ const root = proxyRoot(spec);
11404
+ const health = await checkedJson(`${root}/health`, fetchImpl);
11405
+ if (health.ok !== true) throw new Error("Relay proxy health check did not report ready");
11406
+ const catalog = JSON.parse(readFileSync4(spec.catalogPath, "utf8"));
11407
+ if (!Array.isArray(catalog.models) || catalog.models.length === 0) {
11408
+ throw new Error("Relay Codex model catalog is empty or invalid");
11409
+ }
11410
+ const catalogIds = catalog.models.map((model) => model?.slug).filter((id) => typeof id === "string" && id.length > 0);
11411
+ if (catalogIds.length !== catalog.models.length) throw new Error("Relay Codex model catalog contains an invalid model slug");
11412
+ if (!catalogIds.includes(spec.route.modelId)) {
11413
+ throw new Error(`Relay Codex model catalog is missing selected model ${spec.route.modelId}`);
11414
+ }
11415
+ const advertised = await checkedJson(`${root}/v1/models`, fetchImpl);
11416
+ const advertisedIds = new Set((advertised.data ?? []).map((model) => model.id).filter((id) => typeof id === "string"));
11417
+ for (const id of catalogIds) {
11418
+ if (!advertisedIds.has(id)) throw new Error(`Relay proxy does not advertise catalog model ${id}`);
11419
+ }
11420
+ validateAppConfigText(readCodexConfigText(options.configPath), spec);
11421
+ }
11422
+
11067
11423
  // src/codex/app-session.ts
11068
11424
  import {
11069
11425
  copyFileSync as copyFileSync2,
11070
11426
  existsSync as existsSync9,
11071
- mkdirSync as mkdirSync4,
11427
+ mkdirSync as mkdirSync5,
11072
11428
  readdirSync as readdirSync2,
11073
- readFileSync as readFileSync4,
11429
+ readFileSync as readFileSync5,
11074
11430
  rmSync as rmSync4,
11075
11431
  statSync as statSync2
11076
11432
  } from "fs";
11077
- import { basename as basename2, join as join11 } from "path";
11433
+ import { basename as basename2, join as join12 } from "path";
11434
+ import { createHash as createHash3 } from "crypto";
11078
11435
  function getAppSessionLockPath(env = process.env) {
11079
- return join11(getRelayAiCodexDir(env), "session-app.json");
11436
+ return join12(getRelayAiCodexDir(env), "session-app.json");
11080
11437
  }
11081
11438
  function getAppRestoreStatePath(env = process.env) {
11082
- return join11(getRelayAiCodexDir(env), "app-restore-state.json");
11439
+ return join12(getRelayAiCodexDir(env), "app-restore-state.json");
11083
11440
  }
11084
11441
  function getAppCatalogPath(providerId, env = process.env) {
11085
- return join11(getRelayAiCodexDir(env), `app-models-${providerId}.json`);
11442
+ return join12(getRelayAiCodexDir(env), `app-models-${providerId}.json`);
11443
+ }
11444
+ function fileSha256(path3) {
11445
+ return createHash3("sha256").update(readFileSync5(path3)).digest("hex");
11086
11446
  }
11087
11447
  function readAppSessionLock(env = process.env) {
11088
11448
  const path3 = getAppSessionLockPath(env);
11089
11449
  if (!existsSync9(path3)) return null;
11090
11450
  try {
11091
- const parsed = JSON.parse(readFileSync4(path3, "utf8"));
11451
+ const parsed = JSON.parse(readFileSync5(path3, "utf8"));
11092
11452
  if (typeof parsed.pid === "number" && typeof parsed.startedAt === "string") return parsed;
11093
11453
  } catch {
11094
11454
  }
@@ -11106,7 +11466,7 @@ function readAppRestoreState(env = process.env) {
11106
11466
  const path3 = getAppRestoreStatePath(env);
11107
11467
  if (!existsSync9(path3)) return null;
11108
11468
  try {
11109
- return JSON.parse(readFileSync4(path3, "utf8"));
11469
+ return JSON.parse(readFileSync5(path3, "utf8"));
11110
11470
  } catch {
11111
11471
  return null;
11112
11472
  }
@@ -11125,9 +11485,9 @@ function backupConfigToml(env = process.env) {
11125
11485
  if (!existsSync9(configPath)) return void 0;
11126
11486
  rotateBackups(configPath, env);
11127
11487
  const backupsDir = getBackupsDir(env);
11128
- mkdirSync4(backupsDir, { recursive: true });
11488
+ mkdirSync5(backupsDir, { recursive: true });
11129
11489
  const base = basename2(configPath);
11130
- const backupPath = join11(backupsDir, `${base}.${Date.now()}.bak`);
11490
+ const backupPath = join12(backupsDir, `${base}.${Date.now()}.bak`);
11131
11491
  copyFileSync2(configPath, backupPath);
11132
11492
  return backupPath;
11133
11493
  }
@@ -11144,7 +11504,7 @@ function saveAppRestoreStateBeforePatch(env = process.env) {
11144
11504
  function ownedAppCatalogPaths(env = process.env) {
11145
11505
  const codexDir = getRelayAiCodexDir(env);
11146
11506
  if (!existsSync9(codexDir)) return [];
11147
- return readdirSync2(codexDir).filter((n) => n.startsWith("app-models-") && n.endsWith(".json")).map((n) => join11(codexDir, n));
11507
+ return readdirSync2(codexDir).filter((n) => n.startsWith("app-models-") && n.endsWith(".json")).map((n) => join12(codexDir, n));
11148
11508
  }
11149
11509
  function removeAppCatalogs(env = process.env) {
11150
11510
  const removed = [];
@@ -11162,7 +11522,7 @@ function newestConfigBackup(env = process.env) {
11162
11522
  if (!existsSync9(backupDir)) return null;
11163
11523
  const configBase = basename2(getCodexConfigPath());
11164
11524
  const candidates = readdirSync2(backupDir).filter((name) => name.startsWith(`${configBase}.`) && name.endsWith(".bak")).map((name) => {
11165
- const path3 = join11(backupDir, name);
11525
+ const path3 = join12(backupDir, name);
11166
11526
  try {
11167
11527
  return { path: path3, mtimeMs: statSync2(path3).mtimeMs };
11168
11528
  } catch {
@@ -11188,7 +11548,12 @@ function restoreCodexAppOverlay(env = process.env) {
11188
11548
  clearAppSessionLock(env);
11189
11549
  return { restored: false, message: "Nothing to restore." };
11190
11550
  }
11191
- if (restoreState) {
11551
+ const exactBackupIsSafe = Boolean(
11552
+ managed && lock?.backupPath && existsSync9(lock.backupPath) && lock.patchedConfigSha256 && lock.originalConfigSha256 && fileSha256(getCodexConfigPath()) === lock.patchedConfigSha256 && fileSha256(lock.backupPath) === lock.originalConfigSha256
11553
+ );
11554
+ if (exactBackupIsSafe) {
11555
+ copyFileSync2(lock.backupPath, getCodexConfigPath());
11556
+ } else if (restoreState) {
11192
11557
  restoreConfigFromState(restoreState);
11193
11558
  } else if (lock?.backupPath && existsSync9(lock.backupPath)) {
11194
11559
  copyFileSync2(lock.backupPath, getCodexConfigPath());
@@ -11244,6 +11609,23 @@ function waitForShutdown2() {
11244
11609
  });
11245
11610
  }
11246
11611
 
11612
+ // src/codex/app-shutdown.ts
11613
+ async function shutdownCodexAppSession(dependencies) {
11614
+ if (dependencies.isAppRunning()) {
11615
+ dependencies.quitApp();
11616
+ const exited = await dependencies.waitForAppExit();
11617
+ if (!exited) {
11618
+ throw new Error(
11619
+ "ChatGPT Desktop did not exit after graceful shutdown; refusing to restore config until Desktop exits. Close Desktop, then run relay-ai codex-app --restore."
11620
+ );
11621
+ }
11622
+ }
11623
+ const result = dependencies.restoreOverlay();
11624
+ if (result.liveSession) throw new Error(result.message);
11625
+ dependencies.closeResources();
11626
+ return result;
11627
+ }
11628
+
11247
11629
  // src/codex-app.ts
11248
11630
  function codexProxyRouteToCodexRoute(route, fallbackProviderId) {
11249
11631
  return {
@@ -11264,11 +11646,15 @@ function codexProxyRouteToCodexRoute(route, fallbackProviderId) {
11264
11646
  refreshToken: route.refreshToken
11265
11647
  };
11266
11648
  }
11649
+ function codexAppUsesExplicitSelection(configOnly, launchProvider, launchModel) {
11650
+ void configOnly;
11651
+ return Boolean(launchProvider && launchModel);
11652
+ }
11267
11653
  async function waitForShutdownWithConfirm(assumeYes = false) {
11268
11654
  while (true) {
11269
11655
  const signal = await waitForShutdown2();
11270
- if (signal !== "sigint") break;
11271
- if (assumeYes) break;
11656
+ if (signal !== "sigint") return signal;
11657
+ if (assumeYes) return signal;
11272
11658
  console.log("");
11273
11659
  const choice = await p12.select({
11274
11660
  message: "Close ChatGPT Desktop and restore your Codex config?",
@@ -11277,20 +11663,7 @@ async function waitForShutdownWithConfirm(assumeYes = false) {
11277
11663
  { value: "no", label: "No, keep session running" }
11278
11664
  ]
11279
11665
  });
11280
- if (p12.isCancel(choice) || choice === "yes") break;
11281
- }
11282
- }
11283
- async function maybeCloseRunningCodexApp(assumeYes = false) {
11284
- if (!isCodexAppRunning()) return;
11285
- if (assumeYes) {
11286
- p12.log.step("Stopping ChatGPT Desktop...");
11287
- quitCodexAppGracefully();
11288
- return;
11289
- }
11290
- const shouldClose = await p12.confirm({ message: "ChatGPT Desktop is still running. Close it?" });
11291
- if (shouldClose && !p12.isCancel(shouldClose)) {
11292
- p12.log.step("Stopping ChatGPT Desktop...");
11293
- quitCodexAppGracefully();
11666
+ if (p12.isCancel(choice) || choice === "yes") return signal;
11294
11667
  }
11295
11668
  }
11296
11669
  function codexAppHelpText() {
@@ -11310,7 +11683,7 @@ ${pc10.bold("Options:")}
11310
11683
  --vertex Use Claude models through Google Vertex AI
11311
11684
  --with-native Load native Codex models beside Relay models for this launch
11312
11685
  --relay-only Keep the current Relay-only launch behavior
11313
- --yes, -y Run a fully specified launch unattended (no launch/stop prompts)
11686
+ --yes, -y Approve a fully specified launch/restart without prompting
11314
11687
  --restore Restore Codex config after an interrupted app session
11315
11688
  --config Preview the generated Codex app configuration without launching
11316
11689
  --trace Write proxy debug logs to ~/.relay-ai/logs/ and show errors on exit
@@ -11326,7 +11699,7 @@ ${pc10.bold("Platforms:")}
11326
11699
  macOS, Windows, and Linux (ChatGPT desktop app preview).
11327
11700
 
11328
11701
  ${pc10.bold("Cleanup:")}
11329
- Ctrl+C stops the proxy and restores your previous Codex config.
11702
+ Ctrl+C closes ChatGPT Desktop, restores your previous Codex config, and stops the proxy.
11330
11703
  After crash: relay-ai codex-app --restore
11331
11704
 
11332
11705
  ${pc10.bold("Preview (no writes):")}
@@ -11424,6 +11797,25 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
11424
11797
  }
11425
11798
  let proxyHandle = null;
11426
11799
  let sessionActive = false;
11800
+ let shutdownFailed = false;
11801
+ let resourcesClosed = false;
11802
+ const closeResources = () => {
11803
+ if (resourcesClosed) return;
11804
+ resourcesClosed = true;
11805
+ proxyHandle?.close();
11806
+ };
11807
+ const restoreOverlay = () => {
11808
+ const result = restoreCodexAppOverlay();
11809
+ if (!result.liveSession) sessionActive = false;
11810
+ return result;
11811
+ };
11812
+ const restoreOverlaySafely = () => {
11813
+ try {
11814
+ restoreOverlay();
11815
+ } catch (err) {
11816
+ p12.log.error(String(err instanceof Error ? err.message : err));
11817
+ }
11818
+ };
11427
11819
  try {
11428
11820
  proxyHandle = await startCodexProxy(
11429
11821
  vertexModels.map((m) => ({
@@ -11446,8 +11838,10 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
11446
11838
  catalogPath
11447
11839
  };
11448
11840
  saveAppRestoreStateBeforePatch();
11841
+ sessionActive = true;
11449
11842
  const backupPath = backupConfigToml();
11450
11843
  applyAppConfigPatch(spec);
11844
+ await verifyCodexAppReadiness(spec);
11451
11845
  writeAppSessionLock({
11452
11846
  pid: process.pid,
11453
11847
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -11455,9 +11849,10 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
11455
11849
  catalogPaths: [catalogPath],
11456
11850
  restoreStatePath: getAppRestoreStatePath(),
11457
11851
  backupPath,
11458
- proxyPort
11852
+ proxyPort,
11853
+ patchedConfigSha256: fileSha256(getCodexConfigPath()),
11854
+ ...backupPath ? { originalConfigSha256: fileSha256(backupPath) } : {}
11459
11855
  });
11460
- sessionActive = true;
11461
11856
  p12.log.info(`Vertex AI \xB7 ${selectedEntry.display_name} \u2014 project: ${config.project} / location: ${config.location}`);
11462
11857
  logProxy(proxyPort);
11463
11858
  logActiveModel(selectedEntry.display_name, selectedEntry.id);
@@ -11466,6 +11861,7 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
11466
11861
  } catch (err) {
11467
11862
  p12.log.warn(String(err instanceof Error ? err.message : err));
11468
11863
  p12.log.info(codexAppInstallHint());
11864
+ throw err;
11469
11865
  }
11470
11866
  printCodexAppSessionPanel({
11471
11867
  modelLabel: selectedEntry.display_name,
@@ -11476,15 +11872,27 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
11476
11872
  codexAppOutro(selectedEntry.display_name);
11477
11873
  await waitForShutdownWithConfirm();
11478
11874
  console.log("");
11479
- if (sessionActive) {
11480
- restoreCodexAppOverlay();
11481
- sessionActive = false;
11875
+ try {
11876
+ const result = await shutdownCodexAppSession({
11877
+ isAppRunning: isCodexAppRunning,
11878
+ quitApp: quitCodexAppGracefully,
11879
+ waitForAppExit: () => waitForCodexAppQuit(),
11880
+ restoreOverlay,
11881
+ closeResources
11882
+ });
11883
+ p12.log.success(result.message);
11884
+ return 0;
11885
+ } catch (err) {
11886
+ shutdownFailed = true;
11887
+ p12.log.error(String(err instanceof Error ? err.message : err));
11888
+ return 1;
11482
11889
  }
11483
- await maybeCloseRunningCodexApp();
11484
- return 0;
11485
11890
  } finally {
11486
- proxyHandle?.close();
11487
- if (sessionActive) restoreCodexAppOverlay();
11891
+ if (sessionActive && !isCodexAppRunning()) restoreOverlaySafely();
11892
+ closeResources();
11893
+ if (sessionActive && !shutdownFailed) {
11894
+ p12.log.error("ChatGPT Desktop is still running; config restoration was skipped. Close Desktop, then run relay-ai codex-app --restore.");
11895
+ }
11488
11896
  }
11489
11897
  }
11490
11898
  async function runCodexAppCommand(args, opts = {}) {
@@ -11576,7 +11984,7 @@ async function runCodexAppCommand(args, opts = {}) {
11576
11984
  compatible.find((lp) => lp.id === prefs.lastCodexProvider) ?? compatible[0]
11577
11985
  );
11578
11986
  let selectedModel = activeProvider.models.find((m) => m.id === prefs.lastCodexModel) ?? activeProvider.models[0];
11579
- if (!configOnly && opts.launchProvider && opts.launchModel) {
11987
+ if (codexAppUsesExplicitSelection(configOnly, opts.launchProvider, opts.launchModel)) {
11580
11988
  const bootSelection = resolveBootSelection(
11581
11989
  compatible,
11582
11990
  opts.launchProvider,
@@ -11655,6 +12063,11 @@ async function runCodexAppCommand(args, opts = {}) {
11655
12063
  generalFavorites: favorites,
11656
12064
  subagentFavorites: prefs.codexSubagentModels ?? []
11657
12065
  });
12066
+ if (mixedModels.capacitySkipped.length > 0) {
12067
+ p12.log.warn(
12068
+ `Skipped ${mixedModels.capacitySkipped.length} favorite(s) because the mixed catalog is full: ` + mixedModels.capacitySkipped.map((f) => `${f.providerId}:${f.modelId}`).join(", ")
12069
+ );
12070
+ }
11658
12071
  assertConfiguredCodexSubagentsResolved(prefs.codexSubagentModels ?? [], mixedModels);
11659
12072
  const multiAgentV2Supported = mixedModels.subagents.length === 0 || supportsMultiAgentV2(embeddedBinary);
11660
12073
  if (!multiAgentV2Supported) {
@@ -11693,8 +12106,29 @@ Mixed Codex App mode is unavailable: ${err instanceof Error ? err.message : err}
11693
12106
  }
11694
12107
  let proxyHandle = null;
11695
12108
  let sessionActive = false;
12109
+ let shutdownFailed = false;
12110
+ let resourcesClosed = false;
12111
+ const closeResources = () => {
12112
+ if (resourcesClosed) return;
12113
+ resourcesClosed = true;
12114
+ proxyHandle?.close();
12115
+ cloudCodeBackend?.handle.close();
12116
+ cloudCodeBackendFav?.handle.close();
12117
+ };
12118
+ const restoreOverlay = () => {
12119
+ const result = restoreCodexAppOverlay();
12120
+ if (!result.liveSession) sessionActive = false;
12121
+ return result;
12122
+ };
12123
+ const restoreOverlaySafely = () => {
12124
+ try {
12125
+ restoreOverlay();
12126
+ } catch (err) {
12127
+ p12.log.error(String(err instanceof Error ? err.message : err));
12128
+ }
12129
+ };
11696
12130
  try {
11697
- const catalogPath = mixedPlan ? join12(getRelayAiCodexDir(), "app-models-mixed.json") : favoritesActive && resolvedFavorites.length > 0 ? getFavoritesAppCatalogPath() : getAppCatalogPath(route.providerId);
12131
+ const catalogPath = mixedPlan ? join13(getRelayAiCodexDir(), "app-models-mixed.json") : favoritesActive && resolvedFavorites.length > 0 ? getFavoritesAppCatalogPath() : getAppCatalogPath(route.providerId);
11698
12132
  const activeRoute = mixedPlan ? {
11699
12133
  tier: "proxy",
11700
12134
  modelId: mixedPlan.selectedSlug,
@@ -11757,10 +12191,12 @@ Mixed Codex App mode is unavailable: ${err instanceof Error ? err.message : err}
11757
12191
  return 0;
11758
12192
  }
11759
12193
  let proxyPort;
12194
+ const routeAuditPath = mixedPlan ? prepareCodexRouteAuditLog() : void 0;
11760
12195
  if (mixedPlan) {
11761
12196
  proxyHandle = await startCodexProxy(mixedPlan.relayRoutes, {
11762
12197
  requireAuth: false,
11763
12198
  debug: trace,
12199
+ routeAuditPath,
11764
12200
  mixedNative: {
11765
12201
  nativeModelIds: mixedPlan.nativeModelIds,
11766
12202
  subagentRouteModelId: mixedPlan.subagentRouteModelId,
@@ -11769,6 +12205,7 @@ Mixed Codex App mode is unavailable: ${err instanceof Error ? err.message : err}
11769
12205
  }
11770
12206
  });
11771
12207
  proxyPort = proxyHandle.port;
12208
+ p12.log.info(`Route audit (metadata only): ${routeAuditPath}`);
11772
12209
  } else if (favoritesActive && resolvedFavorites.length > 0) {
11773
12210
  const needsBackend = (r) => {
11774
12211
  const m = r.model;
@@ -11828,8 +12265,10 @@ Mixed Codex App mode is unavailable: ${err instanceof Error ? err.message : err}
11828
12265
  ...mixedPlan ? { proxyBaseUrl: `${mixedProxyBaseUrl(proxyPort, mixedPlan.capability)}/v1` } : {}
11829
12266
  };
11830
12267
  saveAppRestoreStateBeforePatch();
12268
+ sessionActive = true;
11831
12269
  const backupPath = backupConfigToml();
11832
12270
  applyAppConfigPatch(spec);
12271
+ await verifyCodexAppReadiness(spec);
11833
12272
  writeAppSessionLock({
11834
12273
  pid: process.pid,
11835
12274
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -11837,9 +12276,10 @@ Mixed Codex App mode is unavailable: ${err instanceof Error ? err.message : err}
11837
12276
  catalogPaths: [catalogPath],
11838
12277
  restoreStatePath: getAppRestoreStatePath(),
11839
12278
  backupPath,
11840
- proxyPort
12279
+ proxyPort,
12280
+ patchedConfigSha256: fileSha256(getCodexConfigPath()),
12281
+ ...backupPath ? { originalConfigSha256: fileSha256(backupPath) } : {}
11841
12282
  });
11842
- sessionActive = true;
11843
12283
  const prevRecent = prefs.recentModelsByProvider?.[activeProvider.id] ?? [];
11844
12284
  const updatedRecent = [selectedModel.id, ...prevRecent.filter((id) => id !== selectedModel.id)].slice(0, 3);
11845
12285
  savePreferences({
@@ -11854,6 +12294,7 @@ Mixed Codex App mode is unavailable: ${err instanceof Error ? err.message : err}
11854
12294
  } catch (err) {
11855
12295
  p12.log.warn(String(err instanceof Error ? err.message : err));
11856
12296
  p12.log.info(codexAppInstallHint());
12297
+ throw err;
11857
12298
  }
11858
12299
  printCodexAppSessionPanel({
11859
12300
  modelLabel,
@@ -11865,21 +12306,27 @@ Mixed Codex App mode is unavailable: ${err instanceof Error ? err.message : err}
11865
12306
  await waitForShutdownWithConfirm(opts.assumeYes);
11866
12307
  if (trace) printTraceLog(debugLogPath);
11867
12308
  console.log("");
11868
- if (sessionActive) {
11869
- restoreCodexAppOverlay();
11870
- sessionActive = false;
12309
+ try {
12310
+ const result = await shutdownCodexAppSession({
12311
+ isAppRunning: isCodexAppRunning,
12312
+ quitApp: quitCodexAppGracefully,
12313
+ waitForAppExit: () => waitForCodexAppQuit(),
12314
+ restoreOverlay,
12315
+ closeResources
12316
+ });
12317
+ p12.log.success(result.message);
12318
+ return 0;
12319
+ } catch (err) {
12320
+ shutdownFailed = true;
12321
+ p12.log.error(String(err instanceof Error ? err.message : err));
12322
+ return 1;
11871
12323
  }
11872
- await maybeCloseRunningCodexApp(opts.assumeYes);
11873
- return 0;
11874
12324
  } finally {
11875
- proxyHandle?.close();
11876
- if (cloudCodeBackend) {
11877
- cloudCodeBackend.handle.close();
12325
+ if (sessionActive && !isCodexAppRunning()) restoreOverlaySafely();
12326
+ closeResources();
12327
+ if (sessionActive && !shutdownFailed) {
12328
+ p12.log.error("ChatGPT Desktop is still running; config restoration was skipped. Close Desktop, then run relay-ai codex-app --restore.");
11878
12329
  }
11879
- if (cloudCodeBackendFav) {
11880
- cloudCodeBackendFav.handle.close();
11881
- }
11882
- if (sessionActive) restoreCodexAppOverlay();
11883
12330
  }
11884
12331
  }
11885
12332
 
@@ -11888,38 +12335,38 @@ import pc11 from "picocolors";
11888
12335
  import * as p13 from "@clack/prompts";
11889
12336
 
11890
12337
  // src/claude-desktop/app-config.ts
11891
- import { existsSync as existsSync10, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5 } from "fs";
12338
+ import { existsSync as existsSync10, readFileSync as readFileSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
11892
12339
  import { homedir as homedir9 } from "os";
11893
- import { join as join13, dirname as dirname3 } from "path";
12340
+ import { join as join14, dirname as dirname3 } from "path";
11894
12341
  import { randomUUID as randomUUID3 } from "crypto";
11895
12342
  function getClaudeDesktopHome() {
11896
12343
  if (process.platform === "win32") {
11897
- return join13(process.env.LOCALAPPDATA || join13(homedir9(), "AppData", "Local"), "Claude-3p");
12344
+ return join14(process.env.LOCALAPPDATA || join14(homedir9(), "AppData", "Local"), "Claude-3p");
11898
12345
  }
11899
12346
  if (process.platform === "linux") {
11900
- return join13(process.env.XDG_CONFIG_HOME || join13(homedir9(), ".config"), "Claude-3p");
12347
+ return join14(process.env.XDG_CONFIG_HOME || join14(homedir9(), ".config"), "Claude-3p");
11901
12348
  }
11902
- return join13(homedir9(), "Library", "Application Support", "Claude-3p");
12349
+ return join14(homedir9(), "Library", "Application Support", "Claude-3p");
11903
12350
  }
11904
12351
  function getConfigLibraryPath() {
11905
- return join13(getClaudeDesktopHome(), "configLibrary");
12352
+ return join14(getClaudeDesktopHome(), "configLibrary");
11906
12353
  }
11907
12354
  function getMetaJsonPath() {
11908
- return join13(getConfigLibraryPath(), "_meta.json");
12355
+ return join14(getConfigLibraryPath(), "_meta.json");
11909
12356
  }
11910
12357
  function readMetaJson() {
11911
12358
  const metaPath = getMetaJsonPath();
11912
12359
  if (!existsSync10(metaPath)) return null;
11913
12360
  try {
11914
- return JSON.parse(readFileSync5(metaPath, "utf8"));
12361
+ return JSON.parse(readFileSync6(metaPath, "utf8"));
11915
12362
  } catch {
11916
12363
  return null;
11917
12364
  }
11918
12365
  }
11919
12366
  function writeMetaJson(meta) {
11920
12367
  const metaPath = getMetaJsonPath();
11921
- mkdirSync5(dirname3(metaPath), { recursive: true });
11922
- writeFileSync4(metaPath, `${JSON.stringify(meta, null, 2)}
12368
+ mkdirSync6(dirname3(metaPath), { recursive: true });
12369
+ writeFileSync5(metaPath, `${JSON.stringify(meta, null, 2)}
11923
12370
  `, "utf8");
11924
12371
  }
11925
12372
  function buildRelayAiConfig(proxyPort) {
@@ -11933,10 +12380,10 @@ function buildRelayAiConfig(proxyPort) {
11933
12380
  }
11934
12381
  function writeRelayAiConfig(proxyPort) {
11935
12382
  const uuid = randomUUID3();
11936
- const configPath = join13(getConfigLibraryPath(), `${uuid}.json`);
12383
+ const configPath = join14(getConfigLibraryPath(), `${uuid}.json`);
11937
12384
  const config = buildRelayAiConfig(proxyPort);
11938
- mkdirSync5(dirname3(configPath), { recursive: true });
11939
- writeFileSync4(configPath, `${JSON.stringify(config, null, 2)}
12385
+ mkdirSync6(dirname3(configPath), { recursive: true });
12386
+ writeFileSync5(configPath, `${JSON.stringify(config, null, 2)}
11940
12387
  `, "utf8");
11941
12388
  const meta = readMetaJson() || { appliedId: "", entries: [] };
11942
12389
  meta.appliedId = uuid;
@@ -12091,22 +12538,22 @@ async function buildClaudeAppServerCatalog(entries, providersById, trace) {
12091
12538
  import {
12092
12539
  copyFileSync as copyFileSync3,
12093
12540
  existsSync as existsSync11,
12094
- mkdirSync as mkdirSync6,
12095
- readFileSync as readFileSync6,
12541
+ mkdirSync as mkdirSync7,
12542
+ readFileSync as readFileSync7,
12096
12543
  renameSync as renameSync2,
12097
12544
  rmSync as rmSync5,
12098
12545
  unlinkSync as unlinkSync2,
12099
- writeFileSync as writeFileSync5
12546
+ writeFileSync as writeFileSync6
12100
12547
  } from "fs";
12101
- import { dirname as dirname4, join as join14 } from "path";
12548
+ import { dirname as dirname4, join as join15 } from "path";
12102
12549
  function getSessionLockPath2() {
12103
- return join14(getClaudeDesktopHome(), ".relay-ai.lock");
12550
+ return join15(getClaudeDesktopHome(), ".relay-ai.lock");
12104
12551
  }
12105
12552
  function inspectSessionLock() {
12106
12553
  const path3 = getSessionLockPath2();
12107
12554
  if (!existsSync11(path3)) return { status: "missing" };
12108
12555
  try {
12109
- const parsed = JSON.parse(readFileSync6(path3, "utf8"));
12556
+ const parsed = JSON.parse(readFileSync7(path3, "utf8"));
12110
12557
  if (typeof parsed.pid === "number" && typeof parsed.startedAt === "string" && typeof parsed.uuid === "string" && typeof parsed.proxyPort === "number") {
12111
12558
  return { status: "valid", lock: parsed };
12112
12559
  }
@@ -12117,9 +12564,9 @@ function inspectSessionLock() {
12117
12564
  function writeSessionLock2(lock) {
12118
12565
  const path3 = getSessionLockPath2();
12119
12566
  const tempPath = `${path3}.tmp.${process.pid}`;
12120
- mkdirSync6(dirname4(path3), { recursive: true });
12567
+ mkdirSync7(dirname4(path3), { recursive: true });
12121
12568
  try {
12122
- writeFileSync5(tempPath, `${JSON.stringify(lock, null, 2)}
12569
+ writeFileSync6(tempPath, `${JSON.stringify(lock, null, 2)}
12123
12570
  `, "utf8");
12124
12571
  renameSync2(tempPath, path3);
12125
12572
  } finally {
@@ -12154,7 +12601,7 @@ function restoreMetaJson() {
12154
12601
  }
12155
12602
  }
12156
12603
  function removeRelayAiConfig(uuid) {
12157
- const configPath = join14(getConfigLibraryPath(), `${uuid}.json`);
12604
+ const configPath = join15(getConfigLibraryPath(), `${uuid}.json`);
12158
12605
  if (existsSync11(configPath)) {
12159
12606
  try {
12160
12607
  rmSync5(configPath, { force: true });
@@ -12472,17 +12919,17 @@ ${pc11.bold("Claude Desktop 3P Mode Active")}`);
12472
12919
  }
12473
12920
 
12474
12921
  // src/ai-doc.ts
12475
- import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
12922
+ import { existsSync as existsSync12, mkdirSync as mkdirSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
12476
12923
  import { homedir as homedir10 } from "os";
12477
- import { join as join15 } from "path";
12924
+ import { join as join16 } from "path";
12478
12925
  var SKILL_DIR_NAME = "relay-ai-cli";
12479
12926
  var SKILL_INSTALL_DIRS = [
12480
- join15(getAppHome(), "skills"),
12481
- join15(homedir10(), ".claude", "skills"),
12482
- join15(homedir10(), ".agents", "skills"),
12483
- join15(homedir10(), ".codex", "skills"),
12484
- join15(homedir10(), ".cursor", "skills"),
12485
- join15(homedir10(), ".cursor", "skills-cursor")
12927
+ join16(getAppHome(), "skills"),
12928
+ join16(homedir10(), ".claude", "skills"),
12929
+ join16(homedir10(), ".agents", "skills"),
12930
+ join16(homedir10(), ".codex", "skills"),
12931
+ join16(homedir10(), ".cursor", "skills"),
12932
+ join16(homedir10(), ".cursor", "skills-cursor")
12486
12933
  ];
12487
12934
  function parseSkillVersion(content) {
12488
12935
  const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
@@ -12496,10 +12943,10 @@ function parseSkillVersion(content) {
12496
12943
  return null;
12497
12944
  }
12498
12945
  function readInstalledSkillVersion(skillDir) {
12499
- const skillPath = join15(skillDir, "SKILL.md");
12946
+ const skillPath = join16(skillDir, "SKILL.md");
12500
12947
  if (!existsSync12(skillPath)) return null;
12501
12948
  try {
12502
- const head = readFileSync7(skillPath, "utf-8").slice(0, 1024);
12949
+ const head = readFileSync8(skillPath, "utf-8").slice(0, 1024);
12503
12950
  return parseSkillVersion(head.includes("---", 4) ? head : `${head}
12504
12951
  ---
12505
12952
  `);
@@ -12509,8 +12956,8 @@ function readInstalledSkillVersion(skillDir) {
12509
12956
  }
12510
12957
  function skillInstallTargets() {
12511
12958
  return SKILL_INSTALL_DIRS.map((dir) => {
12512
- const skillDir = join15(dir, SKILL_DIR_NAME);
12513
- return { skillDir, skillPath: join15(skillDir, "SKILL.md") };
12959
+ const skillDir = join16(dir, SKILL_DIR_NAME);
12960
+ return { skillDir, skillPath: join16(skillDir, "SKILL.md") };
12514
12961
  });
12515
12962
  }
12516
12963
  function formatProviderModels(provider) {
@@ -13014,8 +13461,8 @@ function installAiDoc(opts = {}) {
13014
13461
  result.skipped.push(skillPath);
13015
13462
  continue;
13016
13463
  }
13017
- mkdirSync7(skillDir, { recursive: true });
13018
- writeFileSync6(skillPath, doc, "utf-8");
13464
+ mkdirSync8(skillDir, { recursive: true });
13465
+ writeFileSync7(skillPath, doc, "utf-8");
13019
13466
  if (previous) {
13020
13467
  result.updated.push({ path: skillPath, fromVersion: previous });
13021
13468
  } else {
@@ -13175,16 +13622,16 @@ function buildHttpProxyChildEnv(baseEnv, proxyUrl, caCertPath) {
13175
13622
  // src/http-proxy/ca.ts
13176
13623
  import { randomBytes as randomBytes2, randomUUID as randomUUID4 } from "crypto";
13177
13624
  import {
13178
- chmodSync as chmodSync2,
13625
+ chmodSync as chmodSync3,
13179
13626
  existsSync as existsSync13,
13180
- mkdirSync as mkdirSync9,
13181
- readFileSync as readFileSync8,
13627
+ mkdirSync as mkdirSync10,
13628
+ readFileSync as readFileSync9,
13182
13629
  readdirSync as readdirSync3,
13183
13630
  rmSync as rmSync6,
13184
13631
  statSync as statSync3,
13185
- writeFileSync as writeFileSync8
13632
+ writeFileSync as writeFileSync9
13186
13633
  } from "fs";
13187
- import { dirname as dirname6, join as join17, resolve } from "path";
13634
+ import { dirname as dirname6, join as join18, resolve } from "path";
13188
13635
  import forge from "node-forge";
13189
13636
  var SESSION_ROOT = "http-proxy-sessions";
13190
13637
  var OWNER_FILE = "owner.pid";
@@ -13204,22 +13651,22 @@ function processIsRunning(pid) {
13204
13651
  }
13205
13652
  }
13206
13653
  function cleanupStaleHttpProxySessions(appHome = getAppHome()) {
13207
- const root = join17(appHome, SESSION_ROOT);
13654
+ const root = join18(appHome, SESSION_ROOT);
13208
13655
  if (!existsSync13(root)) return;
13209
13656
  const now = Date.now();
13210
13657
  for (const name of readdirSync3(root)) {
13211
- const sessionDir = join17(root, name);
13658
+ const sessionDir = join18(root, name);
13212
13659
  try {
13213
13660
  const stat = statSync3(sessionDir);
13214
13661
  if (!stat.isDirectory()) continue;
13215
- const ownerPath = join17(sessionDir, OWNER_FILE);
13662
+ const ownerPath = join18(sessionDir, OWNER_FILE);
13216
13663
  if (!existsSync13(ownerPath)) {
13217
13664
  if (now - stat.mtimeMs > MID_CREATION_GRACE_MS) {
13218
13665
  rmSync6(sessionDir, { recursive: true, force: true });
13219
13666
  }
13220
13667
  continue;
13221
13668
  }
13222
- const pid = Number(readFileSync8(ownerPath, "utf8").trim());
13669
+ const pid = Number(readFileSync9(ownerPath, "utf8").trim());
13223
13670
  if (!Number.isSafeInteger(pid) || pid <= 0) {
13224
13671
  const ownerStat = statSync3(ownerPath);
13225
13672
  const newestMtimeMs = Math.max(stat.mtimeMs, ownerStat.mtimeMs);
@@ -13235,13 +13682,13 @@ function cleanupStaleHttpProxySessions(appHome = getAppHome()) {
13235
13682
  }
13236
13683
  function createHttpProxyCertificates(appHome = getAppHome()) {
13237
13684
  cleanupStaleHttpProxySessions(appHome);
13238
- const root = join17(appHome, SESSION_ROOT);
13239
- mkdirSync9(root, { recursive: true, mode: 448 });
13240
- chmodSync2(root, 448);
13241
- const sessionDir = join17(root, randomUUID4());
13242
- mkdirSync9(sessionDir, { mode: 448 });
13243
- chmodSync2(sessionDir, 448);
13244
- writeFileSync8(join17(sessionDir, OWNER_FILE), `${process.pid}
13685
+ const root = join18(appHome, SESSION_ROOT);
13686
+ mkdirSync10(root, { recursive: true, mode: 448 });
13687
+ chmodSync3(root, 448);
13688
+ const sessionDir = join18(root, randomUUID4());
13689
+ mkdirSync10(sessionDir, { mode: 448 });
13690
+ chmodSync3(sessionDir, 448);
13691
+ writeFileSync9(join18(sessionDir, OWNER_FILE), `${process.pid}
13245
13692
  `, { mode: 384 });
13246
13693
  try {
13247
13694
  const caKeys = forge.pki.rsa.generateKeyPair(2048);
@@ -13276,9 +13723,9 @@ function createHttpProxyCertificates(appHome = getAppHome()) {
13276
13723
  ]);
13277
13724
  server.sign(caKeys.privateKey, forge.md.sha256.create());
13278
13725
  const caCert = forge.pki.certificateToPem(ca);
13279
- const caCertPath = join17(sessionDir, "relay-ai-ca.pem");
13280
- writeFileSync8(caCertPath, caCert, { encoding: "utf8", mode: 384 });
13281
- chmodSync2(caCertPath, 384);
13726
+ const caCertPath = join18(sessionDir, "relay-ai-ca.pem");
13727
+ writeFileSync9(caCertPath, caCert, { encoding: "utf8", mode: 384 });
13728
+ chmodSync3(caCertPath, 384);
13282
13729
  let cleaned = false;
13283
13730
  const cleanupOnExit = () => {
13284
13731
  if (cleaned) return;
@@ -13319,18 +13766,18 @@ function createHttpProxyCaBundle(relayCaCertPath, additionalCaCertPath) {
13319
13766
  if (resolve(additionalCaCertPath) === resolve(relayCaCertPath)) {
13320
13767
  return relayCaCertPath;
13321
13768
  }
13322
- const relayCa = readFileSync8(relayCaCertPath, "utf8").trimEnd();
13323
- const additionalCa = readFileSync8(additionalCaCertPath, "utf8").trim();
13769
+ const relayCa = readFileSync9(relayCaCertPath, "utf8").trimEnd();
13770
+ const additionalCa = readFileSync9(additionalCaCertPath, "utf8").trim();
13324
13771
  if (!additionalCa) return relayCaCertPath;
13325
- const combinedPath = join17(dirname6(relayCaCertPath), "combined-ca.pem");
13326
- writeFileSync8(
13772
+ const combinedPath = join18(dirname6(relayCaCertPath), "combined-ca.pem");
13773
+ writeFileSync9(
13327
13774
  combinedPath,
13328
13775
  `${relayCa}
13329
13776
  ${additionalCa}
13330
13777
  `,
13331
13778
  { encoding: "utf8", mode: 384 }
13332
13779
  );
13333
- chmodSync2(combinedPath, 384);
13780
+ chmodSync3(combinedPath, 384);
13334
13781
  return combinedPath;
13335
13782
  }
13336
13783
 
@@ -15345,7 +15792,7 @@ Options:
15345
15792
  --trace Write debug logs under ~/.relay-ai/logs/`);
15346
15793
  return 0;
15347
15794
  }
15348
- const { runUiCommand } = await import("./ui-command-Q6LBKVM3.js");
15795
+ const { runUiCommand } = await import("./ui-command-NM4MNZLO.js");
15349
15796
  return runUiCommand({ trace: parsed.trace, serverMode: parsed.uiServerMode });
15350
15797
  }
15351
15798
  if (parsed.command === "models") {