@jacobbd/relay-ai 0.9.5 → 0.9.6

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,
@@ -197,7 +198,7 @@ import {
197
198
  validateCustomEndpointUrl,
198
199
  writeSecureLogLine,
199
200
  zenRegistryStub
200
- } from "./chunk-SCW2TYSG.js";
201
+ } from "./chunk-PYJQMEJD.js";
201
202
  import {
202
203
  filterTemplates,
203
204
  getTemplateById,
@@ -1295,7 +1296,7 @@ ${pc4.bold("Subcommands:")}
1295
1296
  (none) Provider hub wizard ${pc4.dim("[Phase 1.1]")}
1296
1297
  add Add a provider (Groq, Mistral, Together AI, \u2026) ${pc4.dim("[Phase 1.1]")}
1297
1298
  import Optional one-time import from OpenCode CLI ${pc4.dim("[Phase 1.0]")}
1298
- auth Sign in with OAuth (GitHub Copilot, xAI, OpenAI, ClinePass)
1299
+ auth Sign in with OAuth (Antigravity, GitHub Copilot, xAI, OpenAI, ClinePass)
1299
1300
  list Show configured providers ${pc4.dim("[Phase 1.0]")}
1300
1301
  remove Remove a provider by id ${pc4.dim("[Phase 1.1]")}
1301
1302
  refresh-models Update cached model lists ${pc4.dim("[Phase 1.2]")}`;
@@ -2233,7 +2234,7 @@ async function runProvidersCommand(args) {
2233
2234
  // src/codex.ts
2234
2235
  import pc7 from "picocolors";
2235
2236
  import * as p8 from "@clack/prompts";
2236
- import { join as join5 } from "path";
2237
+ import { join as join6 } from "path";
2237
2238
 
2238
2239
  // src/codex-proxy.ts
2239
2240
  import { createHash as createHash2 } from "crypto";
@@ -3770,6 +3771,53 @@ async function resolveRoutedCollaborationInput(input, context) {
3770
3771
  return normalizePlaintextCollaborationForExternal(out);
3771
3772
  }
3772
3773
 
3774
+ // src/codex/route-audit.ts
3775
+ import { chmodSync, mkdirSync, writeFileSync } from "fs";
3776
+ import { join as join2 } from "path";
3777
+ var DIR_MODE = 448;
3778
+ var FILE_MODE = 384;
3779
+ var CODEX_ROUTE_AUDIT_LOG = "codex-route-audit.jsonl";
3780
+ function safeIdentifier(value) {
3781
+ if (value === void 0) return void 0;
3782
+ return value.replace(/[\u0000-\u001f\u007f]/g, "_").slice(0, 300);
3783
+ }
3784
+ function sanitizeCodexRouteAuditEvent(event) {
3785
+ return {
3786
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3787
+ transport: event.transport,
3788
+ requestedModel: safeIdentifier(event.requestedModel),
3789
+ dispatch: event.dispatch,
3790
+ phase: event.phase,
3791
+ ...event.provider ? { provider: safeIdentifier(event.provider) } : {},
3792
+ ...event.routeModel ? { routeModel: safeIdentifier(event.routeModel) } : {},
3793
+ ...event.upstreamModel ? { upstreamModel: safeIdentifier(event.upstreamModel) } : {},
3794
+ ...event.outcome ? { outcome: event.outcome } : {},
3795
+ ...event.status !== void 0 ? { status: typeof event.status === "string" ? safeIdentifier(event.status) : event.status } : {}
3796
+ };
3797
+ }
3798
+ function getCodexRouteAuditLogPath() {
3799
+ const dir = getLogsPath();
3800
+ mkdirSync(dir, { recursive: true, mode: DIR_MODE });
3801
+ try {
3802
+ chmodSync(dir, DIR_MODE);
3803
+ } catch {
3804
+ }
3805
+ return join2(dir, CODEX_ROUTE_AUDIT_LOG);
3806
+ }
3807
+ function prepareCodexRouteAuditLog(path3 = getCodexRouteAuditLogPath()) {
3808
+ writeFileSync(path3, "", { mode: FILE_MODE });
3809
+ chmodSync(path3, FILE_MODE);
3810
+ return path3;
3811
+ }
3812
+ function appendCodexRouteAudit(path3, event) {
3813
+ try {
3814
+ writeFileSync(path3, `${JSON.stringify(sanitizeCodexRouteAuditEvent(event))}
3815
+ `, { flag: "a", mode: FILE_MODE });
3816
+ chmodSync(path3, FILE_MODE);
3817
+ } catch {
3818
+ }
3819
+ }
3820
+
3773
3821
  // src/codex-proxy.ts
3774
3822
  function captureCompletedResponse(sseText) {
3775
3823
  if (!sseText.includes("response.completed")) return void 0;
@@ -3777,11 +3825,34 @@ function captureCompletedResponse(sseText) {
3777
3825
  if (!dataLine) return void 0;
3778
3826
  try {
3779
3827
  const obj = JSON.parse(dataLine.slice(5).trim());
3780
- if (obj && obj.type === "response.completed") return obj.response;
3828
+ if (obj && obj.type === "response.completed" && obj.response && typeof obj.response === "object") {
3829
+ return obj.response;
3830
+ }
3781
3831
  } catch {
3782
3832
  }
3783
3833
  return void 0;
3784
3834
  }
3835
+ var MAX_EXTERNAL_RESPONSE_STATES = 8;
3836
+ var EXTERNAL_TOOL_OUTPUT_TYPES = /* @__PURE__ */ new Set([
3837
+ "function_call_output",
3838
+ "custom_tool_call_output",
3839
+ "tool_search_output"
3840
+ ]);
3841
+ function responsesInputItems(input) {
3842
+ if (Array.isArray(input)) return input;
3843
+ if (typeof input === "string") {
3844
+ return [{ type: "message", role: "user", content: input }];
3845
+ }
3846
+ return [];
3847
+ }
3848
+ function isExternalToolOutputItem(item) {
3849
+ if (!item || typeof item !== "object" || Array.isArray(item)) return false;
3850
+ const type = item.type;
3851
+ return typeof type === "string" && EXTERNAL_TOOL_OUTPUT_TYPES.has(type);
3852
+ }
3853
+ function isExternalToolContinuation(input) {
3854
+ return Array.isArray(input) && input.length > 0 && input.every(isExternalToolOutputItem);
3855
+ }
3785
3856
  function estimateCodexRequestChars(params) {
3786
3857
  let chars = (params.system ?? "").length;
3787
3858
  for (const msg of params.messages) {
@@ -3980,11 +4051,32 @@ async function prepareExternalCodexBody(body, context) {
3980
4051
  );
3981
4052
  return { ...externalBody, input: resolvedInput };
3982
4053
  }
4054
+ function applyExternalCodexRuntimeIdentity(params, route) {
4055
+ const selectedModel = route.auditUpstreamModelId ?? route.upstreamModelId ?? route.modelId;
4056
+ const provider = route.providerId ?? "relay";
4057
+ const identity = [
4058
+ "<external-model-identity>",
4059
+ `The selected model for this turn is ${JSON.stringify(selectedModel)} through provider ${JSON.stringify(provider)}.`,
4060
+ "Codex is the host application and agent environment, not the model identity.",
4061
+ "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.",
4062
+ "If asked what model you are, report the selected model and provider above; do not use self-identification as evidence of the network route.",
4063
+ "</external-model-identity>"
4064
+ ].join("\n");
4065
+ return {
4066
+ ...params,
4067
+ system: params.system?.trim() ? `${identity}
4068
+
4069
+ ${params.system}` : identity
4070
+ };
4071
+ }
3983
4072
  async function startCodexProxy(routes, options = {}) {
3984
4073
  const opts = typeof options === "boolean" ? { debug: options } : options;
3985
4074
  const debug = opts.debug ?? false;
3986
4075
  const requireAuth = opts.requireAuth ?? true;
3987
4076
  const mixedNative = opts.mixedNative;
4077
+ const audit = (event) => {
4078
+ if (opts.routeAuditPath) appendCodexRouteAudit(opts.routeAuditPath, event);
4079
+ };
3988
4080
  const nativePayloadRelay = mixedNative ? createNativePayloadRelay({}) : void 0;
3989
4081
  silenceSdkWarnings();
3990
4082
  const models = /* @__PURE__ */ new Map();
@@ -4158,6 +4250,7 @@ async function startCodexProxy(routes, options = {}) {
4158
4250
  log14(`subagent dispatch: requested=${modelId} route=${subagentRoute?.modelId ?? "(none)"}`);
4159
4251
  }
4160
4252
  if (mixedNative && markedSubagent && !subagentRoute) {
4253
+ audit({ transport: "http", requestedModel: modelId, dispatch: "relay-subagent", phase: "complete", outcome: "error", status: 503 });
4161
4254
  sendJson(res, 503, {
4162
4255
  error: {
4163
4256
  message: "Codex marked this request as a Sub-agent, but no configured Codex Sub-agent route is available.",
@@ -4170,10 +4263,20 @@ async function startCodexProxy(routes, options = {}) {
4170
4263
  if (!markedSubagent) {
4171
4264
  const dispatch = classifyCodexDispatch(modelId, routes, mixedNative.nativeModelIds);
4172
4265
  if (dispatch.kind === "unknown") {
4266
+ audit({ transport: "http", requestedModel: modelId, dispatch: "unknown", phase: "complete", outcome: "error", status: 404 });
4173
4267
  sendJson(res, 404, { error: { message: `Unknown model: ${modelId}`, type: "invalid_request_error" } });
4174
4268
  return;
4175
4269
  }
4176
4270
  if (dispatch.kind === "native") {
4271
+ audit({
4272
+ transport: "http",
4273
+ requestedModel: modelId,
4274
+ dispatch: "native",
4275
+ phase: "dispatch",
4276
+ provider: "openai-native",
4277
+ routeModel: modelId,
4278
+ upstreamModel: modelId
4279
+ });
4177
4280
  const controller = new AbortController();
4178
4281
  req.once("aborted", () => controller.abort());
4179
4282
  try {
@@ -4187,7 +4290,29 @@ async function startCodexProxy(routes, options = {}) {
4187
4290
  const contentType = nativeResponse.headers.get("content-type");
4188
4291
  res.writeHead(nativeResponse.status, contentType ? { "content-type": contentType } : void 0);
4189
4292
  res.end(Buffer.from(await nativeResponse.arrayBuffer()));
4293
+ audit({
4294
+ transport: "http",
4295
+ requestedModel: modelId,
4296
+ dispatch: "native",
4297
+ phase: "complete",
4298
+ provider: "openai-native",
4299
+ routeModel: modelId,
4300
+ upstreamModel: modelId,
4301
+ outcome: nativeResponse.ok ? "ok" : "error",
4302
+ status: nativeResponse.status
4303
+ });
4190
4304
  } catch (err) {
4305
+ audit({
4306
+ transport: "http",
4307
+ requestedModel: modelId,
4308
+ dispatch: "native",
4309
+ phase: "complete",
4310
+ provider: "openai-native",
4311
+ routeModel: modelId,
4312
+ upstreamModel: modelId,
4313
+ outcome: "error",
4314
+ status: "forward-failed"
4315
+ });
4191
4316
  if (!res.writableEnded) sendJson(res, 502, { error: { message: "Native Codex request failed", type: "upstream_error" } });
4192
4317
  }
4193
4318
  return;
@@ -4212,13 +4337,23 @@ async function startCodexProxy(routes, options = {}) {
4212
4337
  }
4213
4338
  }
4214
4339
  const { route, languageModel } = resolved;
4340
+ const relayDispatch = markedSubagent ? "relay-subagent" : "relay";
4341
+ audit({
4342
+ transport: "http",
4343
+ requestedModel: modelId,
4344
+ dispatch: relayDispatch,
4345
+ phase: "dispatch",
4346
+ provider: route.providerId ?? "relay",
4347
+ routeModel: route.modelId,
4348
+ upstreamModel: route.auditUpstreamModelId ?? route.upstreamModelId
4349
+ });
4215
4350
  try {
4216
4351
  const routedBody = await prepareExternalCodexBody(body, {
4217
4352
  relay: nativePayloadRelay,
4218
4353
  mixedNative,
4219
4354
  headers: req.headers
4220
4355
  });
4221
- let params = applyClaudeCodeOAuthIdentity(route, translateResponsesRequest(
4356
+ let params = applyClaudeCodeOAuthIdentity(route, applyExternalCodexRuntimeIdentity(translateResponsesRequest(
4222
4357
  routedBody,
4223
4358
  route.npm,
4224
4359
  {
@@ -4230,7 +4365,7 @@ async function startCodexProxy(routes, options = {}) {
4230
4365
  upstreamModelId: route.upstreamModelId
4231
4366
  },
4232
4367
  { maxTools: maxToolsForNpm(route.npm) }
4233
- ));
4368
+ ), route));
4234
4369
  if (route.contextWindow && route.contextWindow > 0) {
4235
4370
  const before = params.messages.length;
4236
4371
  const estimatedChars = estimateCodexRequestChars(params);
@@ -4285,9 +4420,31 @@ async function startCodexProxy(routes, options = {}) {
4285
4420
  log14(`response progress: model=${route.modelId} elapsedMs=${progress.elapsedMs} reasoningChars=${progress.reasoningChars} textChars=${progress.textChars} toolCalls=${progress.toolCallCount} reasoningTail=${JSON.stringify(progress.reasoningTail)}`);
4286
4421
  }
4287
4422
  });
4423
+ audit({
4424
+ transport: "http",
4425
+ requestedModel: modelId,
4426
+ dispatch: relayDispatch,
4427
+ phase: "complete",
4428
+ provider: route.providerId ?? "relay",
4429
+ routeModel: route.modelId,
4430
+ upstreamModel: route.auditUpstreamModelId ?? route.upstreamModelId,
4431
+ outcome: "ok",
4432
+ status: 200
4433
+ });
4288
4434
  } catch (err) {
4289
4435
  const msg = formatUpstreamError(err);
4290
4436
  const status = upstreamHttpStatus(err, msg);
4437
+ audit({
4438
+ transport: "http",
4439
+ requestedModel: modelId,
4440
+ dispatch: relayDispatch,
4441
+ phase: "complete",
4442
+ provider: route.providerId ?? "relay",
4443
+ routeModel: route.modelId,
4444
+ upstreamModel: route.auditUpstreamModelId ?? route.upstreamModelId,
4445
+ outcome: "error",
4446
+ status
4447
+ });
4291
4448
  if (debug) log14(`sdk error: ${route.modelId}: ${msg}`);
4292
4449
  if (status === 429) {
4293
4450
  writeResponsesRateLimitStream(modelId, msg, write);
@@ -4309,9 +4466,31 @@ async function startCodexProxy(routes, options = {}) {
4309
4466
  });
4310
4467
  }
4311
4468
  sendJson(res, 200, response);
4469
+ audit({
4470
+ transport: "http",
4471
+ requestedModel: modelId,
4472
+ dispatch: relayDispatch,
4473
+ phase: "complete",
4474
+ provider: route.providerId ?? "relay",
4475
+ routeModel: route.modelId,
4476
+ upstreamModel: route.auditUpstreamModelId ?? route.upstreamModelId,
4477
+ outcome: "ok",
4478
+ status: 200
4479
+ });
4312
4480
  } catch (err) {
4313
4481
  const msg = formatUpstreamError(err);
4314
4482
  const status = upstreamHttpStatus(err, msg);
4483
+ audit({
4484
+ transport: "http",
4485
+ requestedModel: modelId,
4486
+ dispatch: relayDispatch,
4487
+ phase: "complete",
4488
+ provider: route.providerId ?? "relay",
4489
+ routeModel: route.modelId,
4490
+ upstreamModel: route.auditUpstreamModelId ?? route.upstreamModelId,
4491
+ outcome: "error",
4492
+ status
4493
+ });
4315
4494
  if (debug) log14(`sdk error: ${route.modelId}: ${msg}`);
4316
4495
  if (status === 429) {
4317
4496
  sendJson(res, 200, responsesRateLimitBody(modelId, msg));
@@ -4436,21 +4615,53 @@ Sec-WebSocket-Accept: ${wsAcceptKey(clientKey)}\r
4436
4615
  `
4437
4616
  );
4438
4617
  let frameBuf = Buffer.alloc(0);
4439
- let handled = false;
4618
+ let externalActive = false;
4440
4619
  let nativeActive = false;
4441
4620
  let nativeUpstream;
4621
+ let nativeSendTurn;
4622
+ let socketClosing = false;
4623
+ const externalResponseStates = /* @__PURE__ */ new Map();
4624
+ let currentExternalCompletedResponse;
4625
+ let currentExternalStateInput;
4626
+ let currentExternalConsumedResponseId;
4442
4627
  let currentRequestModel = "";
4443
- const closeSocket = (code = 1e3) => {
4444
- if (!socket.destroyed) {
4445
- socket.write(wsCloseFrame(code));
4446
- socket.end();
4628
+ const rememberExternalResponse = (response, input) => {
4629
+ const responseId = typeof response.id === "string" ? response.id : void 0;
4630
+ const output = Array.isArray(response.output) ? response.output : void 0;
4631
+ if (!responseId || !output || response.error) return;
4632
+ externalResponseStates.delete(responseId);
4633
+ externalResponseStates.set(responseId, { input: [...input], output: [...output] });
4634
+ while (externalResponseStates.size > MAX_EXTERNAL_RESPONSE_STATES) {
4635
+ const oldest = externalResponseStates.keys().next().value;
4636
+ if (!oldest) break;
4637
+ externalResponseStates.delete(oldest);
4447
4638
  }
4448
4639
  };
4640
+ const resolveExternalContinuation = (body) => {
4641
+ const previousResponseId = typeof body.previous_response_id === "string" ? body.previous_response_id : void 0;
4642
+ if (!previousResponseId || !isExternalToolContinuation(body.input)) return { body };
4643
+ const previous = externalResponseStates.get(previousResponseId);
4644
+ if (!previous) return { body, orphanedResponseId: previousResponseId };
4645
+ return {
4646
+ body: {
4647
+ ...body,
4648
+ input: [...previous.input, ...previous.output, ...body.input]
4649
+ },
4650
+ consumedResponseId: previousResponseId
4651
+ };
4652
+ };
4653
+ const closeSocket = (code = 1e3) => {
4654
+ if (socketClosing || socket.destroyed) return;
4655
+ socketClosing = true;
4656
+ socket.write(wsCloseFrame(code));
4657
+ socket.end();
4658
+ };
4449
4659
  const sendWsEvent = (sseChunk2) => {
4450
- if (socket.destroyed) return;
4451
- if (debug) {
4452
- const completed = captureCompletedResponse(sseChunk2);
4453
- if (completed) {
4660
+ if (socketClosing || socket.destroyed) return;
4661
+ const completed = captureCompletedResponse(sseChunk2);
4662
+ if (completed) {
4663
+ currentExternalCompletedResponse = completed;
4664
+ if (debug) {
4454
4665
  appendCodexBodyDump({
4455
4666
  ts: (/* @__PURE__ */ new Date()).toISOString(),
4456
4667
  transport: "ws",
@@ -4468,7 +4679,6 @@ Sec-WebSocket-Accept: ${wsAcceptKey(clientKey)}\r
4468
4679
  };
4469
4680
  const onData = (chunk) => {
4470
4681
  frameBuf = Buffer.concat([frameBuf, chunk]);
4471
- if (handled && !nativeActive) return;
4472
4682
  const frame = wsDecodeFrame(frameBuf);
4473
4683
  if (!frame) return;
4474
4684
  frameBuf = Buffer.alloc(0);
@@ -4490,7 +4700,10 @@ Sec-WebSocket-Accept: ${wsAcceptKey(clientKey)}\r
4490
4700
  socket.end();
4491
4701
  return;
4492
4702
  }
4493
- handled = true;
4703
+ if (externalActive) {
4704
+ closeSocket(1008);
4705
+ return;
4706
+ }
4494
4707
  void (async () => {
4495
4708
  let body;
4496
4709
  try {
@@ -4531,6 +4744,7 @@ data: ${JSON.stringify({ error: { message: "Invalid JSON", type: "invalid_reques
4531
4744
  log14(`WS subagent dispatch: requested=${modelId} route=${subagentRoute?.modelId ?? "(none)"}`);
4532
4745
  }
4533
4746
  if (mixedNative && markedSubagent && !subagentRoute) {
4747
+ audit({ transport: "ws", requestedModel: modelId, dispatch: "relay-subagent", phase: "complete", outcome: "error", status: 503 });
4534
4748
  sendWsEvent(`event: error
4535
4749
  data: ${JSON.stringify({ error: {
4536
4750
  message: "Codex marked this request as a Sub-agent, but no configured Codex Sub-agent route is available.",
@@ -4545,6 +4759,7 @@ data: ${JSON.stringify({ error: {
4545
4759
  if (!markedSubagent) {
4546
4760
  const dispatch = classifyCodexDispatch(modelId, routes, mixedNative.nativeModelIds);
4547
4761
  if (dispatch.kind === "unknown") {
4762
+ audit({ transport: "ws", requestedModel: modelId, dispatch: "unknown", phase: "complete", outcome: "error", status: 404 });
4548
4763
  sendWsEvent(`event: error
4549
4764
  data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}`, type: "invalid_request_error" } })}
4550
4765
 
@@ -4553,6 +4768,15 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}`, type: "i
4553
4768
  return;
4554
4769
  }
4555
4770
  if (dispatch.kind === "native") {
4771
+ audit({
4772
+ transport: "ws",
4773
+ requestedModel: modelId,
4774
+ dispatch: "native",
4775
+ phase: "dispatch",
4776
+ provider: "openai-native",
4777
+ routeModel: modelId,
4778
+ upstreamModel: modelId
4779
+ });
4556
4780
  const nativeBody = prepareNativeCodexBody(body);
4557
4781
  if (debug && nativeBody !== body) {
4558
4782
  log14(`WS native history normalized: model=${modelId} converted Relay compaction for native verification`);
@@ -4560,7 +4784,7 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}`, type: "i
4560
4784
  if (nativeActive && nativeUpstream) {
4561
4785
  if (nativeUpstream.readyState === WebSocket.OPEN) {
4562
4786
  if (debug) log14(`WS native forwarding next turn: model=${modelId}`);
4563
- nativeUpstream.send(JSON.stringify({ type: "response.create", ...nativeBody }));
4787
+ nativeSendTurn?.(nativeBody, modelId);
4564
4788
  } else if (debug) {
4565
4789
  log14(`WS native cannot forward next turn: upstream_state=${nativeUpstream.readyState}`);
4566
4790
  }
@@ -4571,6 +4795,7 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}`, type: "i
4571
4795
  let upstream;
4572
4796
  let nativeOpened = false;
4573
4797
  let nativeCompleted = false;
4798
+ let nativeTurnModelId = modelId;
4574
4799
  let nativeFrameCount = 0;
4575
4800
  let finished = false;
4576
4801
  let connectTimer;
@@ -4590,10 +4815,24 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}`, type: "i
4590
4815
  if (finished) return;
4591
4816
  finished = true;
4592
4817
  nativeActive = false;
4818
+ nativeSendTurn = void 0;
4593
4819
  if (nativeUpstream === upstream) nativeUpstream = void 0;
4594
4820
  clearTimers();
4595
4821
  if (debug && message) {
4596
- log14(`WS native upstream failed: model=${modelId} opened=${nativeOpened} frames=${nativeFrameCount} message=${message}`);
4822
+ log14(`WS native upstream failed: model=${nativeTurnModelId} opened=${nativeOpened} frames=${nativeFrameCount} message=${message}`);
4823
+ }
4824
+ if (message && !nativeCompleted) {
4825
+ audit({
4826
+ transport: "ws",
4827
+ requestedModel: nativeTurnModelId,
4828
+ dispatch: "native",
4829
+ phase: "complete",
4830
+ provider: "openai-native",
4831
+ routeModel: nativeTurnModelId,
4832
+ upstreamModel: nativeTurnModelId,
4833
+ outcome: "error",
4834
+ status: "upstream-failed"
4835
+ });
4597
4836
  }
4598
4837
  if (message && !nativeCompleted) sendNativeError(message);
4599
4838
  try {
@@ -4602,20 +4841,31 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}`, type: "i
4602
4841
  }
4603
4842
  closeSocket(closeCode);
4604
4843
  };
4844
+ const sendNativeTurn = (turnBody, turnModelId) => {
4845
+ if (!upstream || upstream.readyState !== WebSocket.OPEN) {
4846
+ if (debug) log14(`WS native cannot send turn: model=${turnModelId} upstream_state=${upstream?.readyState ?? "missing"}`);
4847
+ return;
4848
+ }
4849
+ nativeTurnModelId = turnModelId;
4850
+ nativeCompleted = false;
4851
+ if (firstFrameTimer) clearTimeout(firstFrameTimer);
4852
+ upstream.send(JSON.stringify({ type: "response.create", ...turnBody }));
4853
+ firstFrameTimer = setTimeout(() => closeBoth("Native Codex WebSocket response timed out"), 6e4);
4854
+ };
4605
4855
  try {
4606
4856
  if (debug) {
4607
4857
  log14(`WS native connecting: model=${modelId} url=${target.url} headers=[${Object.keys(target.headers).join(",")}]`);
4608
4858
  }
4609
4859
  upstream = new WebSocket(target.url, { headers: target.headers });
4610
4860
  nativeUpstream = upstream;
4861
+ nativeSendTurn = sendNativeTurn;
4611
4862
  nativeActive = true;
4612
4863
  connectTimer = setTimeout(() => closeBoth("Native Codex WebSocket connection timed out"), 15e3);
4613
4864
  upstream.once("open", () => {
4614
4865
  nativeOpened = true;
4615
4866
  if (connectTimer) clearTimeout(connectTimer);
4616
4867
  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);
4868
+ sendNativeTurn(nativeBody, modelId);
4619
4869
  });
4620
4870
  upstream.once("unexpected-response", (_request, response) => {
4621
4871
  if (debug) log14(`WS native upstream HTTP rejection: model=${modelId} status=${response.statusCode}`);
@@ -4633,6 +4883,17 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}`, type: "i
4633
4883
  if (typeof parsed.type === "string") eventType = parsed.type;
4634
4884
  if (eventType === "response.completed" || eventType === "response.failed" || eventType === "response.incomplete") {
4635
4885
  nativeCompleted = true;
4886
+ audit({
4887
+ transport: "ws",
4888
+ requestedModel: modelId,
4889
+ dispatch: "native",
4890
+ phase: "complete",
4891
+ provider: "openai-native",
4892
+ routeModel: modelId,
4893
+ upstreamModel: modelId,
4894
+ outcome: eventType === "response.completed" ? "ok" : "error",
4895
+ status: eventType
4896
+ });
4636
4897
  }
4637
4898
  } catch {
4638
4899
  }
@@ -4653,6 +4914,7 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}`, type: "i
4653
4914
  if (debug) log14(`WS native downstream close: model=${modelId} frames=${nativeFrameCount} completed=${nativeCompleted}`);
4654
4915
  finished = true;
4655
4916
  nativeActive = false;
4917
+ nativeSendTurn = void 0;
4656
4918
  if (nativeUpstream === upstream) nativeUpstream = void 0;
4657
4919
  clearTimers();
4658
4920
  try {
@@ -4667,6 +4929,7 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}`, type: "i
4667
4929
  }
4668
4930
  }
4669
4931
  }
4932
+ externalActive = true;
4670
4933
  let resolved = subagentRoute ? resolveModel(routes, models, subagentRoute.modelId) : resolveModel(routes, models, modelId);
4671
4934
  if (!resolved) {
4672
4935
  const fb = routes[0];
@@ -4685,13 +4948,35 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}` } })}
4685
4948
  }
4686
4949
  }
4687
4950
  const { route, languageModel } = resolved;
4951
+ const relayDispatch = markedSubagent ? "relay-subagent" : "relay";
4952
+ audit({
4953
+ transport: "ws",
4954
+ requestedModel: modelId,
4955
+ dispatch: relayDispatch,
4956
+ phase: "dispatch",
4957
+ provider: route.providerId ?? "relay",
4958
+ routeModel: route.modelId,
4959
+ upstreamModel: route.auditUpstreamModelId ?? route.upstreamModelId
4960
+ });
4961
+ currentExternalCompletedResponse = void 0;
4962
+ currentExternalStateInput = void 0;
4963
+ currentExternalConsumedResponseId = void 0;
4964
+ const continuation = resolveExternalContinuation(body);
4965
+ if (continuation.orphanedResponseId) {
4966
+ if (debug) log14(`WS continuation rejected: unknown previous_response_id=${continuation.orphanedResponseId}`);
4967
+ writeResponsesErrorStream(modelId, "Unknown or expired previous_response_id", sendWsEvent, 400);
4968
+ externalActive = false;
4969
+ return;
4970
+ }
4688
4971
  try {
4689
- const routedBody = await prepareExternalCodexBody(body, {
4972
+ const routedBody = await prepareExternalCodexBody(continuation.body, {
4690
4973
  relay: nativePayloadRelay,
4691
4974
  mixedNative,
4692
4975
  headers: req.headers
4693
4976
  });
4694
- let params = applyClaudeCodeOAuthIdentity(route, translateResponsesRequest(
4977
+ currentExternalStateInput = responsesInputItems(routedBody.input);
4978
+ currentExternalConsumedResponseId = continuation.consumedResponseId;
4979
+ let params = applyClaudeCodeOAuthIdentity(route, applyExternalCodexRuntimeIdentity(translateResponsesRequest(
4695
4980
  routedBody,
4696
4981
  route.npm,
4697
4982
  {
@@ -4703,7 +4988,7 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}` } })}
4703
4988
  upstreamModelId: route.upstreamModelId
4704
4989
  },
4705
4990
  { maxTools: maxToolsForNpm(route.npm) }
4706
- ));
4991
+ ), route));
4707
4992
  if (route.contextWindow && route.contextWindow > 0) {
4708
4993
  const before = params.messages.length;
4709
4994
  const estimatedChars = estimateCodexRequestChars(params);
@@ -4736,9 +5021,37 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}` } })}
4736
5021
  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
5022
  }
4738
5023
  });
5024
+ if (currentExternalCompletedResponse && currentExternalStateInput) {
5025
+ if (currentExternalConsumedResponseId) {
5026
+ externalResponseStates.delete(currentExternalConsumedResponseId);
5027
+ }
5028
+ rememberExternalResponse(currentExternalCompletedResponse, currentExternalStateInput);
5029
+ }
5030
+ audit({
5031
+ transport: "ws",
5032
+ requestedModel: modelId,
5033
+ dispatch: relayDispatch,
5034
+ phase: "complete",
5035
+ provider: route.providerId ?? "relay",
5036
+ routeModel: route.modelId,
5037
+ upstreamModel: route.auditUpstreamModelId ?? route.upstreamModelId,
5038
+ outcome: "ok",
5039
+ status: "response.completed"
5040
+ });
4739
5041
  } catch (err) {
4740
5042
  const msg = formatUpstreamError(err);
4741
5043
  const status = upstreamHttpStatus(err, msg);
5044
+ audit({
5045
+ transport: "ws",
5046
+ requestedModel: modelId,
5047
+ dispatch: relayDispatch,
5048
+ phase: "complete",
5049
+ provider: route.providerId ?? "relay",
5050
+ routeModel: route.modelId,
5051
+ upstreamModel: route.auditUpstreamModelId ?? route.upstreamModelId,
5052
+ outcome: "error",
5053
+ status
5054
+ });
4742
5055
  if (debug) log14(`WS sdk error: ${route.modelId}: ${msg}`);
4743
5056
  if (status === 429) {
4744
5057
  writeResponsesRateLimitStream(modelId, msg, sendWsEvent);
@@ -4746,10 +5059,11 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}` } })}
4746
5059
  writeResponsesErrorStream(modelId, msg, sendWsEvent, status);
4747
5060
  }
4748
5061
  }
4749
- closeSocket();
5062
+ externalActive = false;
4750
5063
  })();
4751
5064
  };
4752
5065
  socket.on("error", () => socket.destroy());
5066
+ socket.once("close", () => externalResponseStates.clear());
4753
5067
  socket.on("data", onData);
4754
5068
  onData(head);
4755
5069
  });
@@ -4774,44 +5088,44 @@ data: ${JSON.stringify({ error: { message: `Unknown model: ${modelId}` } })}
4774
5088
  }
4775
5089
 
4776
5090
  // src/codex/profile.ts
4777
- import { join as join3 } from "path";
5091
+ import { join as join4 } from "path";
4778
5092
 
4779
5093
  // src/codex/session.ts
4780
5094
  import {
4781
5095
  copyFileSync,
4782
- chmodSync,
5096
+ chmodSync as chmodSync2,
4783
5097
  existsSync as existsSync3,
4784
- mkdirSync,
5098
+ mkdirSync as mkdirSync2,
4785
5099
  readdirSync,
4786
5100
  readFileSync as readFileSync2,
4787
5101
  renameSync,
4788
5102
  rmSync,
4789
5103
  statSync,
4790
5104
  unlinkSync,
4791
- writeFileSync
5105
+ writeFileSync as writeFileSync2
4792
5106
  } from "fs";
4793
5107
  import { homedir as homedir3 } from "os";
4794
- import { basename, dirname, join as join2 } from "path";
5108
+ import { basename, dirname, join as join3 } from "path";
4795
5109
  var CODEX_PROFILE_NAME = "relay-ai-launch";
4796
5110
  var STALE_SESSION_MS = 5 * 60 * 1e3;
4797
5111
  var MAX_BACKUPS = 5;
4798
5112
  function getCodexHome(env = process.env) {
4799
- return env["CODEX_HOME"] || join2(homedir3(), ".codex");
5113
+ return env["CODEX_HOME"] || join3(homedir3(), ".codex");
4800
5114
  }
4801
5115
  function getCodexProfilePath() {
4802
- return join2(getCodexHome(), `${CODEX_PROFILE_NAME}.config.toml`);
5116
+ return join3(getCodexHome(), `${CODEX_PROFILE_NAME}.config.toml`);
4803
5117
  }
4804
5118
  function getRelayAiCodexDir(env = process.env) {
4805
- return join2(getAppHome(env), "codex");
5119
+ return join3(getAppHome(env), "codex");
4806
5120
  }
4807
5121
  function getSessionLockPath(env = process.env) {
4808
- return join2(getRelayAiCodexDir(env), "session.json");
5122
+ return join3(getRelayAiCodexDir(env), "session.json");
4809
5123
  }
4810
5124
  function getBackupsDir(env = process.env) {
4811
- return join2(getRelayAiCodexDir(env), "backups");
5125
+ return join3(getRelayAiCodexDir(env), "backups");
4812
5126
  }
4813
5127
  function getCatalogPath(providerId, env = process.env) {
4814
- return join2(getRelayAiCodexDir(env), `models-${providerId}.json`);
5128
+ return join3(getRelayAiCodexDir(env), `models-${providerId}.json`);
4815
5129
  }
4816
5130
  function ownedOverlayPaths(env = process.env) {
4817
5131
  const paths = [getCodexProfilePath()];
@@ -4819,15 +5133,15 @@ function ownedOverlayPaths(env = process.env) {
4819
5133
  if (existsSync3(codexDir)) {
4820
5134
  for (const name of readdirSync(codexDir)) {
4821
5135
  if (name.startsWith("models-") && name.endsWith(".json")) {
4822
- paths.push(join2(codexDir, name));
5136
+ paths.push(join3(codexDir, name));
4823
5137
  }
4824
5138
  }
4825
5139
  }
4826
- const agentsDir = join2(getCodexHome(env), "agents");
5140
+ const agentsDir = join3(getCodexHome(env), "agents");
4827
5141
  if (existsSync3(agentsDir)) {
4828
5142
  for (const name of readdirSync(agentsDir)) {
4829
5143
  if (/^relay-model-[a-z0-9-]+\.toml$/i.test(name)) {
4830
- paths.push(join2(agentsDir, name));
5144
+ paths.push(join3(agentsDir, name));
4831
5145
  }
4832
5146
  }
4833
5147
  }
@@ -4835,27 +5149,27 @@ function ownedOverlayPaths(env = process.env) {
4835
5149
  return paths;
4836
5150
  }
4837
5151
  function atomicWriteFile(path3, content) {
4838
- mkdirSync(dirname(path3), { recursive: true });
5152
+ mkdirSync2(dirname(path3), { recursive: true });
4839
5153
  const tmp = `${path3}.tmp.${process.pid}`;
4840
- writeFileSync(tmp, content, { encoding: "utf8", mode: 384 });
5154
+ writeFileSync2(tmp, content, { encoding: "utf8", mode: 384 });
4841
5155
  renameSync(tmp, path3);
4842
5156
  try {
4843
- chmodSync(path3, 384);
5157
+ chmodSync2(path3, 384);
4844
5158
  } catch {
4845
5159
  }
4846
5160
  }
4847
5161
  function rotateBackups(filePath, env = process.env) {
4848
5162
  if (!existsSync3(filePath)) return;
4849
5163
  const backupsDir = getBackupsDir(env);
4850
- mkdirSync(backupsDir, { recursive: true });
5164
+ mkdirSync2(backupsDir, { recursive: true });
4851
5165
  const base = basename(filePath);
4852
5166
  const stamp = Date.now();
4853
- const backupPath = join2(backupsDir, `${base}.${stamp}.bak`);
5167
+ const backupPath = join3(backupsDir, `${base}.${stamp}.bak`);
4854
5168
  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);
5169
+ 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
5170
  for (const old of backups.slice(MAX_BACKUPS)) {
4857
5171
  try {
4858
- unlinkSync(join2(backupsDir, old.name));
5172
+ unlinkSync(join3(backupsDir, old.name));
4859
5173
  } catch {
4860
5174
  }
4861
5175
  }
@@ -4876,7 +5190,7 @@ function readSessionLock(env = process.env) {
4876
5190
  }
4877
5191
  function writeSessionLock(lock, env = process.env) {
4878
5192
  const path3 = getSessionLockPath(env);
4879
- mkdirSync(getRelayAiCodexDir(env), { recursive: true });
5193
+ mkdirSync2(getRelayAiCodexDir(env), { recursive: true });
4880
5194
  atomicWriteFile(path3, `${JSON.stringify(lock, null, 2)}
4881
5195
  `);
4882
5196
  }
@@ -4999,10 +5313,10 @@ function getCatalogOutputPath(providerId) {
4999
5313
  return getCatalogPath(providerId);
5000
5314
  }
5001
5315
  function getFavoritesCatalogPath() {
5002
- return join3(getRelayAiCodexDir(), "models-favorites.json");
5316
+ return join4(getRelayAiCodexDir(), "models-favorites.json");
5003
5317
  }
5004
5318
  function getFavoritesAppCatalogPath() {
5005
- return join3(getRelayAiCodexDir(), "app-models-favorites.json");
5319
+ return join4(getRelayAiCodexDir(), "app-models-favorites.json");
5006
5320
  }
5007
5321
  function profileName() {
5008
5322
  return CODEX_PROFILE_NAME;
@@ -5013,7 +5327,7 @@ import { execSync as execSync2 } from "child_process";
5013
5327
  import spawn2 from "cross-spawn";
5014
5328
  import { existsSync as existsSync4 } from "fs";
5015
5329
  import { homedir as homedir4 } from "os";
5016
- import { join as join4 } from "path";
5330
+ import { join as join5 } from "path";
5017
5331
  var isWindows2 = process.platform === "win32";
5018
5332
  var CODEX_CI_ENV_VARS = [
5019
5333
  "CI",
@@ -5034,11 +5348,11 @@ function stripCodexInheritedEnv(env) {
5034
5348
  return out;
5035
5349
  }
5036
5350
  var CODEX_FALLBACK_PATHS = isWindows2 ? [
5037
- join4(process.env["APPDATA"] ?? homedir4(), "npm", "codex.cmd"),
5038
- join4(process.env["APPDATA"] ?? homedir4(), "npm", "codex")
5351
+ join5(process.env["APPDATA"] ?? homedir4(), "npm", "codex.cmd"),
5352
+ join5(process.env["APPDATA"] ?? homedir4(), "npm", "codex")
5039
5353
  ] : [
5040
- join4(homedir4(), ".local", "bin", "codex"),
5041
- join4(homedir4(), ".npm", "bin", "codex"),
5354
+ join5(homedir4(), ".local", "bin", "codex"),
5355
+ join5(homedir4(), ".npm", "bin", "codex"),
5042
5356
  "/usr/local/bin/codex",
5043
5357
  "/opt/homebrew/bin/codex"
5044
5358
  ];
@@ -5541,7 +5855,8 @@ async function resolveCodexMixedModels(input) {
5541
5855
  subagents: subagentResult.resolved,
5542
5856
  all,
5543
5857
  providersById: new Map(input.compatible.map((provider) => [provider.id, provider])),
5544
- dropped: [...visibleResult.droppedFavorites, ...subagentResult.droppedFavorites]
5858
+ dropped: [...visibleResult.droppedFavorites, ...subagentResult.droppedFavorites],
5859
+ capacitySkipped: [...visibleResult.capacitySkippedFavorites, ...subagentResult.capacitySkippedFavorites]
5545
5860
  };
5546
5861
  }
5547
5862
 
@@ -5741,6 +6056,7 @@ async function prepareCodexMixedRelayRoutes(models, trace = false) {
5741
6056
  apiKey: backend.token,
5742
6057
  baseURL: `http://127.0.0.1:${backend.port}`,
5743
6058
  upstreamModelId: proxyRoute.aliasId,
6059
+ auditUpstreamModelId: original.model.upstreamModelId || original.model.id,
5744
6060
  providerId: original.providerId,
5745
6061
  authType: "oauth",
5746
6062
  oauthAccountId: original.oauthAccountId,
@@ -6071,7 +6387,7 @@ async function writeFavoritesLaunchArtifacts(resolved, starting, proxyPort) {
6071
6387
  return { profilePath, catalogPath };
6072
6388
  }
6073
6389
  async function writeMixedLaunchArtifacts(plan, proxyPort) {
6074
- const catalogPath = join5(getRelayAiCodexDir(), "models-mixed.json");
6390
+ const catalogPath = join6(getRelayAiCodexDir(), "models-mixed.json");
6075
6391
  writeOverlayFile(catalogPath, serializeCatalog(plan.catalog));
6076
6392
  const profilePath = getProfileOutputPath();
6077
6393
  writeOverlayFile(profilePath, buildCodexMixedProfileToml({
@@ -6656,17 +6972,17 @@ import * as p10 from "@clack/prompts";
6656
6972
 
6657
6973
  // src/gemini/launch.ts
6658
6974
  import { spawn as spawn3 } from "child_process";
6659
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
6975
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
6660
6976
  import { homedir as homedir5, tmpdir } from "os";
6661
- import { join as join6 } from "path";
6977
+ import { join as join7 } from "path";
6662
6978
  var isWindows3 = process.platform === "win32";
6663
6979
  var GEMINI_API_KEY_AUTH_TYPE = "gemini-api-key";
6664
6980
  var GEMINI_FALLBACK_PATHS = isWindows3 ? [
6665
- join6(process.env["APPDATA"] ?? homedir5(), "npm", "gemini.cmd"),
6666
- join6(process.env["APPDATA"] ?? homedir5(), "npm", "gemini")
6981
+ join7(process.env["APPDATA"] ?? homedir5(), "npm", "gemini.cmd"),
6982
+ join7(process.env["APPDATA"] ?? homedir5(), "npm", "gemini")
6667
6983
  ] : [
6668
- join6(homedir5(), ".local", "bin", "gemini"),
6669
- join6(homedir5(), ".npm", "bin", "gemini"),
6984
+ join7(homedir5(), ".local", "bin", "gemini"),
6985
+ join7(homedir5(), ".npm", "bin", "gemini"),
6670
6986
  "/usr/local/bin/gemini",
6671
6987
  "/opt/homebrew/bin/gemini"
6672
6988
  ];
@@ -6687,7 +7003,7 @@ function buildGeminiChildEnv(proxyPort, proxyToken) {
6687
7003
  return env;
6688
7004
  }
6689
7005
  function createGeminiCliHomeOverlay() {
6690
- const cliHome = mkdtempSync(join6(tmpdir(), "relay-ai-gemini-"));
7006
+ const cliHome = mkdtempSync(join7(tmpdir(), "relay-ai-gemini-"));
6691
7007
  const settings = {
6692
7008
  security: {
6693
7009
  auth: {
@@ -6695,9 +7011,9 @@ function createGeminiCliHomeOverlay() {
6695
7011
  }
6696
7012
  }
6697
7013
  };
6698
- const geminiDir = join6(cliHome, ".gemini");
6699
- mkdirSync2(geminiDir);
6700
- writeFileSync2(join6(geminiDir, "settings.json"), `${JSON.stringify(settings, null, 2)}
7014
+ const geminiDir = join7(cliHome, ".gemini");
7015
+ mkdirSync3(geminiDir);
7016
+ writeFileSync3(join7(geminiDir, "settings.json"), `${JSON.stringify(settings, null, 2)}
6701
7017
  `, {
6702
7018
  encoding: "utf8",
6703
7019
  mode: 384
@@ -9939,15 +10255,15 @@ import { execFileSync, execSync as execSync3 } from "child_process";
9939
10255
  import spawn4 from "cross-spawn";
9940
10256
  import { existsSync as existsSync6 } from "fs";
9941
10257
  import { homedir as homedir6 } from "os";
9942
- import { join as join7 } from "path";
10258
+ import { join as join8 } from "path";
9943
10259
  var isWindows4 = process.platform === "win32";
9944
10260
  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")
10261
+ join8(process.env["APPDATA"] ?? homedir6(), "npm", "agy.cmd"),
10262
+ join8(process.env["APPDATA"] ?? homedir6(), "npm", "agy"),
10263
+ join8(homedir6(), "AppData", "Roaming", "npm", "agy.cmd")
9948
10264
  ] : [
9949
- join7(homedir6(), ".local", "bin", "agy"),
9950
- join7(homedir6(), ".npm", "bin", "agy"),
10265
+ join8(homedir6(), ".local", "bin", "agy"),
10266
+ join8(homedir6(), ".npm", "bin", "agy"),
9951
10267
  "/usr/local/bin/agy",
9952
10268
  "/opt/homebrew/bin/agy"
9953
10269
  ];
@@ -10025,7 +10341,7 @@ function launchAntigravityCli(env, extraArgs) {
10025
10341
  import { execFileSync as execFileSync2, execSync as execSync4, spawn as spawn5 } from "child_process";
10026
10342
  import { existsSync as existsSync7 } from "fs";
10027
10343
  import { homedir as homedir7 } from "os";
10028
- import { join as join8 } from "path";
10344
+ import { join as join9 } from "path";
10029
10345
 
10030
10346
  // src/antigravity/ide-profile.ts
10031
10347
  import fs from "fs";
@@ -10059,8 +10375,8 @@ function prepareIdeProfile(profileDir, gatewayUrl) {
10059
10375
  }
10060
10376
 
10061
10377
  // 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");
10378
+ var LINUX_APP_PROFILE_DIR = join9(homedir7(), ".relay-ai", "antigravity", "app-profile");
10379
+ var LINUX_IDE_PROFILE_DIR = join9(homedir7(), ".relay-ai", "antigravity", "profile");
10064
10380
  function sleep(ms) {
10065
10381
  return new Promise((resolve2) => setTimeout(resolve2, ms));
10066
10382
  }
@@ -10068,7 +10384,7 @@ function linuxAntigravityBinary() {
10068
10384
  const candidates = [
10069
10385
  "/usr/share/antigravity/antigravity",
10070
10386
  "/opt/antigravity/antigravity",
10071
- join8(homedir7(), ".local", "share", "antigravity", "antigravity")
10387
+ join9(homedir7(), ".local", "share", "antigravity", "antigravity")
10072
10388
  ];
10073
10389
  for (const candidate of candidates) {
10074
10390
  if (existsSync7(candidate)) return candidate;
@@ -10223,15 +10539,15 @@ function findAntigravityAppBinary() {
10223
10539
  const override = getAppPathOverride("antigravity");
10224
10540
  if (override) return existsSync7(override) ? override : null;
10225
10541
  if (process.platform === "win32") {
10226
- const localAppData = process.env["LOCALAPPDATA"] ?? join8(homedir7(), "AppData", "Local");
10227
- const winPath = join8(localAppData, "Programs", "Antigravity", "Antigravity.exe");
10542
+ const localAppData = process.env["LOCALAPPDATA"] ?? join9(homedir7(), "AppData", "Local");
10543
+ const winPath = join9(localAppData, "Programs", "Antigravity", "Antigravity.exe");
10228
10544
  return existsSync7(winPath) ? winPath : null;
10229
10545
  }
10230
10546
  if (process.platform === "linux") return linuxAntigravityBinary();
10231
10547
  if (process.platform !== "darwin") return null;
10232
10548
  const defaultPath = "/Applications/Antigravity.app/Contents/MacOS/Antigravity";
10233
10549
  if (existsSync7(defaultPath)) return defaultPath;
10234
- const homePath = join8(homedir7(), "Applications", "Antigravity.app", "Contents", "MacOS", "Antigravity");
10550
+ const homePath = join9(homedir7(), "Applications", "Antigravity.app", "Contents", "MacOS", "Antigravity");
10235
10551
  if (existsSync7(homePath)) return homePath;
10236
10552
  return null;
10237
10553
  }
@@ -10239,15 +10555,15 @@ function findAntigravityIdeBinary() {
10239
10555
  const override = getAppPathOverride("antigravity-ide");
10240
10556
  if (override) return existsSync7(override) ? override : null;
10241
10557
  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");
10558
+ const localAppData = process.env["LOCALAPPDATA"] ?? join9(homedir7(), "AppData", "Local");
10559
+ const winPath = join9(localAppData, "Programs", "Antigravity IDE", "Antigravity IDE.exe");
10244
10560
  return existsSync7(winPath) ? winPath : null;
10245
10561
  }
10246
10562
  if (process.platform === "linux") return linuxAntigravityBinary();
10247
10563
  if (process.platform !== "darwin") return null;
10248
10564
  const defaultPath = "/Applications/Antigravity IDE.app/Contents/Resources/app/bin/antigravity-ide";
10249
10565
  if (existsSync7(defaultPath)) return defaultPath;
10250
- const homePath = join8(homedir7(), "Applications", "Antigravity IDE.app", "Contents", "Resources", "app", "bin", "antigravity-ide");
10566
+ const homePath = join9(homedir7(), "Applications", "Antigravity IDE.app", "Contents", "Resources", "app", "bin", "antigravity-ide");
10251
10567
  if (existsSync7(homePath)) return homePath;
10252
10568
  return null;
10253
10569
  }
@@ -10308,7 +10624,7 @@ function launchAntigravityIde(env, profileDir, gatewayUrl, extraArgs) {
10308
10624
  return;
10309
10625
  }
10310
10626
  prepareIdeProfile(profileDir, gatewayUrl);
10311
- const relayExtensionsDir = join8(homedir7(), ".relay-ai", "antigravity", "extensions");
10627
+ const relayExtensionsDir = join9(homedir7(), ".relay-ai", "antigravity", "extensions");
10312
10628
  const args = [
10313
10629
  `--user-data-dir=${profileDir}`,
10314
10630
  `--extensions-dir=${relayExtensionsDir}`,
@@ -10338,7 +10654,7 @@ function launchAntigravityIde(env, profileDir, gatewayUrl, extraArgs) {
10338
10654
 
10339
10655
  // src/antigravity.ts
10340
10656
  import { homedir as homedir8 } from "os";
10341
- import { join as join9 } from "path";
10657
+ import { join as join10 } from "path";
10342
10658
  var SHUTDOWN_DRAIN_MS = 500;
10343
10659
  var AGY_FAVORITES_PROVIDER_ID = "__relay_agy_favorites__";
10344
10660
  var AGY_FAVORITES_PROVIDER_LABEL = "\u2605 Antigravity CLI Favorites";
@@ -10632,7 +10948,7 @@ async function runAntigravityAppCommand(childArgs, trace = false, boot) {
10632
10948
  trace,
10633
10949
  boot,
10634
10950
  async (env, _routes, gatewayHandle) => {
10635
- const profileDir = join9(homedir8(), ".relay-ai", "antigravity", "app-profile");
10951
+ const profileDir = join10(homedir8(), ".relay-ai", "antigravity", "app-profile");
10636
10952
  if (isAntigravityAppRunning(profileDir)) {
10637
10953
  const restart = await p11.confirm({
10638
10954
  message: "Restart Antigravity to apply this Relay gateway?",
@@ -10680,7 +10996,7 @@ async function runAntigravityIdeCommand(childArgs, trace = false, boot) {
10680
10996
  trace,
10681
10997
  boot,
10682
10998
  async (env, _routes, gatewayHandle) => {
10683
- const profileDir = join9(homedir8(), ".relay-ai", "antigravity", "profile");
10999
+ const profileDir = join10(homedir8(), ".relay-ai", "antigravity", "profile");
10684
11000
  if (isAntigravityIdeRunning(profileDir)) {
10685
11001
  const restart = await p11.confirm({
10686
11002
  message: "Restart Antigravity IDE to apply this Relay gateway?",
@@ -10725,7 +11041,7 @@ async function runAntigravityIdeCommand(childArgs, trace = false, boot) {
10725
11041
  // src/codex-app.ts
10726
11042
  import pc10 from "picocolors";
10727
11043
  import * as p12 from "@clack/prompts";
10728
- import { join as join12 } from "path";
11044
+ import { join as join13 } from "path";
10729
11045
 
10730
11046
  // src/codex/app-provider-routes.ts
10731
11047
  function codexRouteToProxyRoute(provider, model, apiKey) {
@@ -10814,14 +11130,14 @@ async function buildCodexAppProviderCatalogRoutes(provider, apiKey, selectedMode
10814
11130
  }
10815
11131
 
10816
11132
  // 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";
11133
+ import { existsSync as existsSync8, readFileSync as readFileSync3, rmSync as rmSync3, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
11134
+ import { dirname as dirname2, join as join11 } from "path";
10819
11135
  import { parse, stringify } from "smol-toml";
10820
11136
  function getCodexConfigPath() {
10821
- return join10(getCodexHome(), "config.toml");
11137
+ return join11(getCodexHome(), "config.toml");
10822
11138
  }
10823
11139
  function getCodexAppSidecarProfilePath() {
10824
- return join10(getCodexHome(), `${CODEX_APP_PROVIDER_ID}.config.toml`);
11140
+ return join11(getCodexHome(), `${CODEX_APP_PROVIDER_ID}.config.toml`);
10825
11141
  }
10826
11142
  function asRecord(value) {
10827
11143
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
@@ -10996,8 +11312,13 @@ function applyAppConfigPatch(spec, configPath = getCodexConfigPath()) {
10996
11312
  const text5 = `${stringify(merged)}
10997
11313
  `;
10998
11314
  validateAppConfigText(text5, spec);
10999
- mkdirSync3(dirname2(configPath), { recursive: true });
11000
- writeFileSync3(configPath, text5, "utf8");
11315
+ mkdirSync4(dirname2(configPath), { recursive: true });
11316
+ atomicWriteFile(configPath, text5);
11317
+ const written = readCodexConfigText(configPath);
11318
+ if (written !== text5) {
11319
+ throw new Error(`Codex config readback mismatch at ${configPath}`);
11320
+ }
11321
+ validateAppConfigText(written, spec);
11001
11322
  return text5;
11002
11323
  }
11003
11324
  function applyRestoreKey(config, key, had, value) {
@@ -11053,7 +11374,7 @@ function restoreConfigFromState(state, configPath = getCodexConfigPath()) {
11053
11374
  rmSync3(configPath, { force: true });
11054
11375
  return true;
11055
11376
  }
11056
- writeFileSync3(configPath, `${stringify(config)}
11377
+ writeFileSync4(configPath, `${stringify(config)}
11057
11378
  `, "utf8");
11058
11379
  return true;
11059
11380
  }
@@ -11064,31 +11385,69 @@ function previewAppConfigToml(spec) {
11064
11385
  return text5;
11065
11386
  }
11066
11387
 
11388
+ // src/codex/app-readiness.ts
11389
+ import { readFileSync as readFileSync4 } from "fs";
11390
+ function proxyRoot(spec) {
11391
+ const base = spec.proxyBaseUrl ?? `http://127.0.0.1:${spec.proxyPort}/v1`;
11392
+ if (!base.endsWith("/v1")) throw new Error("Codex App proxy base URL must end in /v1");
11393
+ return base.slice(0, -3);
11394
+ }
11395
+ async function checkedJson(url, fetchImpl) {
11396
+ const response = await fetchImpl(url);
11397
+ if (!response.ok) throw new Error(`Relay readiness check failed: GET ${url} returned HTTP ${response.status}`);
11398
+ return response.json();
11399
+ }
11400
+ async function verifyCodexAppReadiness(spec, options = {}) {
11401
+ const fetchImpl = options.fetchImpl ?? fetch;
11402
+ const root = proxyRoot(spec);
11403
+ const health = await checkedJson(`${root}/health`, fetchImpl);
11404
+ if (health.ok !== true) throw new Error("Relay proxy health check did not report ready");
11405
+ const catalog = JSON.parse(readFileSync4(spec.catalogPath, "utf8"));
11406
+ if (!Array.isArray(catalog.models) || catalog.models.length === 0) {
11407
+ throw new Error("Relay Codex model catalog is empty or invalid");
11408
+ }
11409
+ const catalogIds = catalog.models.map((model) => model?.slug).filter((id) => typeof id === "string" && id.length > 0);
11410
+ if (catalogIds.length !== catalog.models.length) throw new Error("Relay Codex model catalog contains an invalid model slug");
11411
+ if (!catalogIds.includes(spec.route.modelId)) {
11412
+ throw new Error(`Relay Codex model catalog is missing selected model ${spec.route.modelId}`);
11413
+ }
11414
+ const advertised = await checkedJson(`${root}/v1/models`, fetchImpl);
11415
+ const advertisedIds = new Set((advertised.data ?? []).map((model) => model.id).filter((id) => typeof id === "string"));
11416
+ for (const id of catalogIds) {
11417
+ if (!advertisedIds.has(id)) throw new Error(`Relay proxy does not advertise catalog model ${id}`);
11418
+ }
11419
+ validateAppConfigText(readCodexConfigText(options.configPath), spec);
11420
+ }
11421
+
11067
11422
  // src/codex/app-session.ts
11068
11423
  import {
11069
11424
  copyFileSync as copyFileSync2,
11070
11425
  existsSync as existsSync9,
11071
- mkdirSync as mkdirSync4,
11426
+ mkdirSync as mkdirSync5,
11072
11427
  readdirSync as readdirSync2,
11073
- readFileSync as readFileSync4,
11428
+ readFileSync as readFileSync5,
11074
11429
  rmSync as rmSync4,
11075
11430
  statSync as statSync2
11076
11431
  } from "fs";
11077
- import { basename as basename2, join as join11 } from "path";
11432
+ import { basename as basename2, join as join12 } from "path";
11433
+ import { createHash as createHash3 } from "crypto";
11078
11434
  function getAppSessionLockPath(env = process.env) {
11079
- return join11(getRelayAiCodexDir(env), "session-app.json");
11435
+ return join12(getRelayAiCodexDir(env), "session-app.json");
11080
11436
  }
11081
11437
  function getAppRestoreStatePath(env = process.env) {
11082
- return join11(getRelayAiCodexDir(env), "app-restore-state.json");
11438
+ return join12(getRelayAiCodexDir(env), "app-restore-state.json");
11083
11439
  }
11084
11440
  function getAppCatalogPath(providerId, env = process.env) {
11085
- return join11(getRelayAiCodexDir(env), `app-models-${providerId}.json`);
11441
+ return join12(getRelayAiCodexDir(env), `app-models-${providerId}.json`);
11442
+ }
11443
+ function fileSha256(path3) {
11444
+ return createHash3("sha256").update(readFileSync5(path3)).digest("hex");
11086
11445
  }
11087
11446
  function readAppSessionLock(env = process.env) {
11088
11447
  const path3 = getAppSessionLockPath(env);
11089
11448
  if (!existsSync9(path3)) return null;
11090
11449
  try {
11091
- const parsed = JSON.parse(readFileSync4(path3, "utf8"));
11450
+ const parsed = JSON.parse(readFileSync5(path3, "utf8"));
11092
11451
  if (typeof parsed.pid === "number" && typeof parsed.startedAt === "string") return parsed;
11093
11452
  } catch {
11094
11453
  }
@@ -11106,7 +11465,7 @@ function readAppRestoreState(env = process.env) {
11106
11465
  const path3 = getAppRestoreStatePath(env);
11107
11466
  if (!existsSync9(path3)) return null;
11108
11467
  try {
11109
- return JSON.parse(readFileSync4(path3, "utf8"));
11468
+ return JSON.parse(readFileSync5(path3, "utf8"));
11110
11469
  } catch {
11111
11470
  return null;
11112
11471
  }
@@ -11125,9 +11484,9 @@ function backupConfigToml(env = process.env) {
11125
11484
  if (!existsSync9(configPath)) return void 0;
11126
11485
  rotateBackups(configPath, env);
11127
11486
  const backupsDir = getBackupsDir(env);
11128
- mkdirSync4(backupsDir, { recursive: true });
11487
+ mkdirSync5(backupsDir, { recursive: true });
11129
11488
  const base = basename2(configPath);
11130
- const backupPath = join11(backupsDir, `${base}.${Date.now()}.bak`);
11489
+ const backupPath = join12(backupsDir, `${base}.${Date.now()}.bak`);
11131
11490
  copyFileSync2(configPath, backupPath);
11132
11491
  return backupPath;
11133
11492
  }
@@ -11144,7 +11503,7 @@ function saveAppRestoreStateBeforePatch(env = process.env) {
11144
11503
  function ownedAppCatalogPaths(env = process.env) {
11145
11504
  const codexDir = getRelayAiCodexDir(env);
11146
11505
  if (!existsSync9(codexDir)) return [];
11147
- return readdirSync2(codexDir).filter((n) => n.startsWith("app-models-") && n.endsWith(".json")).map((n) => join11(codexDir, n));
11506
+ return readdirSync2(codexDir).filter((n) => n.startsWith("app-models-") && n.endsWith(".json")).map((n) => join12(codexDir, n));
11148
11507
  }
11149
11508
  function removeAppCatalogs(env = process.env) {
11150
11509
  const removed = [];
@@ -11162,7 +11521,7 @@ function newestConfigBackup(env = process.env) {
11162
11521
  if (!existsSync9(backupDir)) return null;
11163
11522
  const configBase = basename2(getCodexConfigPath());
11164
11523
  const candidates = readdirSync2(backupDir).filter((name) => name.startsWith(`${configBase}.`) && name.endsWith(".bak")).map((name) => {
11165
- const path3 = join11(backupDir, name);
11524
+ const path3 = join12(backupDir, name);
11166
11525
  try {
11167
11526
  return { path: path3, mtimeMs: statSync2(path3).mtimeMs };
11168
11527
  } catch {
@@ -11188,7 +11547,12 @@ function restoreCodexAppOverlay(env = process.env) {
11188
11547
  clearAppSessionLock(env);
11189
11548
  return { restored: false, message: "Nothing to restore." };
11190
11549
  }
11191
- if (restoreState) {
11550
+ const exactBackupIsSafe = Boolean(
11551
+ managed && lock?.backupPath && existsSync9(lock.backupPath) && lock.patchedConfigSha256 && lock.originalConfigSha256 && fileSha256(getCodexConfigPath()) === lock.patchedConfigSha256 && fileSha256(lock.backupPath) === lock.originalConfigSha256
11552
+ );
11553
+ if (exactBackupIsSafe) {
11554
+ copyFileSync2(lock.backupPath, getCodexConfigPath());
11555
+ } else if (restoreState) {
11192
11556
  restoreConfigFromState(restoreState);
11193
11557
  } else if (lock?.backupPath && existsSync9(lock.backupPath)) {
11194
11558
  copyFileSync2(lock.backupPath, getCodexConfigPath());
@@ -11264,11 +11628,15 @@ function codexProxyRouteToCodexRoute(route, fallbackProviderId) {
11264
11628
  refreshToken: route.refreshToken
11265
11629
  };
11266
11630
  }
11631
+ function codexAppUsesExplicitSelection(configOnly, launchProvider, launchModel) {
11632
+ void configOnly;
11633
+ return Boolean(launchProvider && launchModel);
11634
+ }
11267
11635
  async function waitForShutdownWithConfirm(assumeYes = false) {
11268
11636
  while (true) {
11269
11637
  const signal = await waitForShutdown2();
11270
- if (signal !== "sigint") break;
11271
- if (assumeYes) break;
11638
+ if (signal !== "sigint") return signal;
11639
+ if (assumeYes) return signal;
11272
11640
  console.log("");
11273
11641
  const choice = await p12.select({
11274
11642
  message: "Close ChatGPT Desktop and restore your Codex config?",
@@ -11277,9 +11645,12 @@ async function waitForShutdownWithConfirm(assumeYes = false) {
11277
11645
  { value: "no", label: "No, keep session running" }
11278
11646
  ]
11279
11647
  });
11280
- if (p12.isCancel(choice) || choice === "yes") break;
11648
+ if (p12.isCancel(choice) || choice === "yes") return signal;
11281
11649
  }
11282
11650
  }
11651
+ function unattendedShutdownClosesApp(assumeYes, signal) {
11652
+ return assumeYes && signal !== "sigint";
11653
+ }
11283
11654
  async function maybeCloseRunningCodexApp(assumeYes = false) {
11284
11655
  if (!isCodexAppRunning()) return;
11285
11656
  if (assumeYes) {
@@ -11310,7 +11681,7 @@ ${pc10.bold("Options:")}
11310
11681
  --vertex Use Claude models through Google Vertex AI
11311
11682
  --with-native Load native Codex models beside Relay models for this launch
11312
11683
  --relay-only Keep the current Relay-only launch behavior
11313
- --yes, -y Run a fully specified launch unattended (no launch/stop prompts)
11684
+ --yes, -y Approve a fully specified launch/restart without prompting
11314
11685
  --restore Restore Codex config after an interrupted app session
11315
11686
  --config Preview the generated Codex app configuration without launching
11316
11687
  --trace Write proxy debug logs to ~/.relay-ai/logs/ and show errors on exit
@@ -11446,8 +11817,10 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
11446
11817
  catalogPath
11447
11818
  };
11448
11819
  saveAppRestoreStateBeforePatch();
11820
+ sessionActive = true;
11449
11821
  const backupPath = backupConfigToml();
11450
11822
  applyAppConfigPatch(spec);
11823
+ await verifyCodexAppReadiness(spec);
11451
11824
  writeAppSessionLock({
11452
11825
  pid: process.pid,
11453
11826
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -11455,9 +11828,10 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
11455
11828
  catalogPaths: [catalogPath],
11456
11829
  restoreStatePath: getAppRestoreStatePath(),
11457
11830
  backupPath,
11458
- proxyPort
11831
+ proxyPort,
11832
+ patchedConfigSha256: fileSha256(getCodexConfigPath()),
11833
+ ...backupPath ? { originalConfigSha256: fileSha256(backupPath) } : {}
11459
11834
  });
11460
- sessionActive = true;
11461
11835
  p12.log.info(`Vertex AI \xB7 ${selectedEntry.display_name} \u2014 project: ${config.project} / location: ${config.location}`);
11462
11836
  logProxy(proxyPort);
11463
11837
  logActiveModel(selectedEntry.display_name, selectedEntry.id);
@@ -11466,6 +11840,7 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
11466
11840
  } catch (err) {
11467
11841
  p12.log.warn(String(err instanceof Error ? err.message : err));
11468
11842
  p12.log.info(codexAppInstallHint());
11843
+ throw err;
11469
11844
  }
11470
11845
  printCodexAppSessionPanel({
11471
11846
  modelLabel: selectedEntry.display_name,
@@ -11576,7 +11951,7 @@ async function runCodexAppCommand(args, opts = {}) {
11576
11951
  compatible.find((lp) => lp.id === prefs.lastCodexProvider) ?? compatible[0]
11577
11952
  );
11578
11953
  let selectedModel = activeProvider.models.find((m) => m.id === prefs.lastCodexModel) ?? activeProvider.models[0];
11579
- if (!configOnly && opts.launchProvider && opts.launchModel) {
11954
+ if (codexAppUsesExplicitSelection(configOnly, opts.launchProvider, opts.launchModel)) {
11580
11955
  const bootSelection = resolveBootSelection(
11581
11956
  compatible,
11582
11957
  opts.launchProvider,
@@ -11655,6 +12030,11 @@ async function runCodexAppCommand(args, opts = {}) {
11655
12030
  generalFavorites: favorites,
11656
12031
  subagentFavorites: prefs.codexSubagentModels ?? []
11657
12032
  });
12033
+ if (mixedModels.capacitySkipped.length > 0) {
12034
+ p12.log.warn(
12035
+ `Skipped ${mixedModels.capacitySkipped.length} favorite(s) because the mixed catalog is full: ` + mixedModels.capacitySkipped.map((f) => `${f.providerId}:${f.modelId}`).join(", ")
12036
+ );
12037
+ }
11658
12038
  assertConfiguredCodexSubagentsResolved(prefs.codexSubagentModels ?? [], mixedModels);
11659
12039
  const multiAgentV2Supported = mixedModels.subagents.length === 0 || supportsMultiAgentV2(embeddedBinary);
11660
12040
  if (!multiAgentV2Supported) {
@@ -11694,7 +12074,7 @@ Mixed Codex App mode is unavailable: ${err instanceof Error ? err.message : err}
11694
12074
  let proxyHandle = null;
11695
12075
  let sessionActive = false;
11696
12076
  try {
11697
- const catalogPath = mixedPlan ? join12(getRelayAiCodexDir(), "app-models-mixed.json") : favoritesActive && resolvedFavorites.length > 0 ? getFavoritesAppCatalogPath() : getAppCatalogPath(route.providerId);
12077
+ const catalogPath = mixedPlan ? join13(getRelayAiCodexDir(), "app-models-mixed.json") : favoritesActive && resolvedFavorites.length > 0 ? getFavoritesAppCatalogPath() : getAppCatalogPath(route.providerId);
11698
12078
  const activeRoute = mixedPlan ? {
11699
12079
  tier: "proxy",
11700
12080
  modelId: mixedPlan.selectedSlug,
@@ -11757,10 +12137,12 @@ Mixed Codex App mode is unavailable: ${err instanceof Error ? err.message : err}
11757
12137
  return 0;
11758
12138
  }
11759
12139
  let proxyPort;
12140
+ const routeAuditPath = mixedPlan ? prepareCodexRouteAuditLog() : void 0;
11760
12141
  if (mixedPlan) {
11761
12142
  proxyHandle = await startCodexProxy(mixedPlan.relayRoutes, {
11762
12143
  requireAuth: false,
11763
12144
  debug: trace,
12145
+ routeAuditPath,
11764
12146
  mixedNative: {
11765
12147
  nativeModelIds: mixedPlan.nativeModelIds,
11766
12148
  subagentRouteModelId: mixedPlan.subagentRouteModelId,
@@ -11769,6 +12151,7 @@ Mixed Codex App mode is unavailable: ${err instanceof Error ? err.message : err}
11769
12151
  }
11770
12152
  });
11771
12153
  proxyPort = proxyHandle.port;
12154
+ p12.log.info(`Route audit (metadata only): ${routeAuditPath}`);
11772
12155
  } else if (favoritesActive && resolvedFavorites.length > 0) {
11773
12156
  const needsBackend = (r) => {
11774
12157
  const m = r.model;
@@ -11828,8 +12211,10 @@ Mixed Codex App mode is unavailable: ${err instanceof Error ? err.message : err}
11828
12211
  ...mixedPlan ? { proxyBaseUrl: `${mixedProxyBaseUrl(proxyPort, mixedPlan.capability)}/v1` } : {}
11829
12212
  };
11830
12213
  saveAppRestoreStateBeforePatch();
12214
+ sessionActive = true;
11831
12215
  const backupPath = backupConfigToml();
11832
12216
  applyAppConfigPatch(spec);
12217
+ await verifyCodexAppReadiness(spec);
11833
12218
  writeAppSessionLock({
11834
12219
  pid: process.pid,
11835
12220
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -11837,9 +12222,10 @@ Mixed Codex App mode is unavailable: ${err instanceof Error ? err.message : err}
11837
12222
  catalogPaths: [catalogPath],
11838
12223
  restoreStatePath: getAppRestoreStatePath(),
11839
12224
  backupPath,
11840
- proxyPort
12225
+ proxyPort,
12226
+ patchedConfigSha256: fileSha256(getCodexConfigPath()),
12227
+ ...backupPath ? { originalConfigSha256: fileSha256(backupPath) } : {}
11841
12228
  });
11842
- sessionActive = true;
11843
12229
  const prevRecent = prefs.recentModelsByProvider?.[activeProvider.id] ?? [];
11844
12230
  const updatedRecent = [selectedModel.id, ...prevRecent.filter((id) => id !== selectedModel.id)].slice(0, 3);
11845
12231
  savePreferences({
@@ -11854,6 +12240,7 @@ Mixed Codex App mode is unavailable: ${err instanceof Error ? err.message : err}
11854
12240
  } catch (err) {
11855
12241
  p12.log.warn(String(err instanceof Error ? err.message : err));
11856
12242
  p12.log.info(codexAppInstallHint());
12243
+ throw err;
11857
12244
  }
11858
12245
  printCodexAppSessionPanel({
11859
12246
  modelLabel,
@@ -11862,14 +12249,21 @@ Mixed Codex App mode is unavailable: ${err instanceof Error ? err.message : err}
11862
12249
  restoreCommand: "relay-ai codex-app --restore"
11863
12250
  });
11864
12251
  codexAppOutro(modelLabel);
11865
- await waitForShutdownWithConfirm(opts.assumeYes);
12252
+ const shutdownSignal = await waitForShutdownWithConfirm(opts.assumeYes);
11866
12253
  if (trace) printTraceLog(debugLogPath);
11867
12254
  console.log("");
11868
12255
  if (sessionActive) {
11869
12256
  restoreCodexAppOverlay();
11870
12257
  sessionActive = false;
11871
12258
  }
11872
- await maybeCloseRunningCodexApp(opts.assumeYes);
12259
+ if (unattendedShutdownClosesApp(Boolean(opts.assumeYes), shutdownSignal)) {
12260
+ if (isCodexAppRunning()) {
12261
+ p12.log.step("Stopping ChatGPT Desktop after unattended Relay shutdown...");
12262
+ quitCodexAppGracefully();
12263
+ }
12264
+ } else {
12265
+ await maybeCloseRunningCodexApp(opts.assumeYes);
12266
+ }
11873
12267
  return 0;
11874
12268
  } finally {
11875
12269
  proxyHandle?.close();
@@ -11888,38 +12282,38 @@ import pc11 from "picocolors";
11888
12282
  import * as p13 from "@clack/prompts";
11889
12283
 
11890
12284
  // src/claude-desktop/app-config.ts
11891
- import { existsSync as existsSync10, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5 } from "fs";
12285
+ import { existsSync as existsSync10, readFileSync as readFileSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
11892
12286
  import { homedir as homedir9 } from "os";
11893
- import { join as join13, dirname as dirname3 } from "path";
12287
+ import { join as join14, dirname as dirname3 } from "path";
11894
12288
  import { randomUUID as randomUUID3 } from "crypto";
11895
12289
  function getClaudeDesktopHome() {
11896
12290
  if (process.platform === "win32") {
11897
- return join13(process.env.LOCALAPPDATA || join13(homedir9(), "AppData", "Local"), "Claude-3p");
12291
+ return join14(process.env.LOCALAPPDATA || join14(homedir9(), "AppData", "Local"), "Claude-3p");
11898
12292
  }
11899
12293
  if (process.platform === "linux") {
11900
- return join13(process.env.XDG_CONFIG_HOME || join13(homedir9(), ".config"), "Claude-3p");
12294
+ return join14(process.env.XDG_CONFIG_HOME || join14(homedir9(), ".config"), "Claude-3p");
11901
12295
  }
11902
- return join13(homedir9(), "Library", "Application Support", "Claude-3p");
12296
+ return join14(homedir9(), "Library", "Application Support", "Claude-3p");
11903
12297
  }
11904
12298
  function getConfigLibraryPath() {
11905
- return join13(getClaudeDesktopHome(), "configLibrary");
12299
+ return join14(getClaudeDesktopHome(), "configLibrary");
11906
12300
  }
11907
12301
  function getMetaJsonPath() {
11908
- return join13(getConfigLibraryPath(), "_meta.json");
12302
+ return join14(getConfigLibraryPath(), "_meta.json");
11909
12303
  }
11910
12304
  function readMetaJson() {
11911
12305
  const metaPath = getMetaJsonPath();
11912
12306
  if (!existsSync10(metaPath)) return null;
11913
12307
  try {
11914
- return JSON.parse(readFileSync5(metaPath, "utf8"));
12308
+ return JSON.parse(readFileSync6(metaPath, "utf8"));
11915
12309
  } catch {
11916
12310
  return null;
11917
12311
  }
11918
12312
  }
11919
12313
  function writeMetaJson(meta) {
11920
12314
  const metaPath = getMetaJsonPath();
11921
- mkdirSync5(dirname3(metaPath), { recursive: true });
11922
- writeFileSync4(metaPath, `${JSON.stringify(meta, null, 2)}
12315
+ mkdirSync6(dirname3(metaPath), { recursive: true });
12316
+ writeFileSync5(metaPath, `${JSON.stringify(meta, null, 2)}
11923
12317
  `, "utf8");
11924
12318
  }
11925
12319
  function buildRelayAiConfig(proxyPort) {
@@ -11933,10 +12327,10 @@ function buildRelayAiConfig(proxyPort) {
11933
12327
  }
11934
12328
  function writeRelayAiConfig(proxyPort) {
11935
12329
  const uuid = randomUUID3();
11936
- const configPath = join13(getConfigLibraryPath(), `${uuid}.json`);
12330
+ const configPath = join14(getConfigLibraryPath(), `${uuid}.json`);
11937
12331
  const config = buildRelayAiConfig(proxyPort);
11938
- mkdirSync5(dirname3(configPath), { recursive: true });
11939
- writeFileSync4(configPath, `${JSON.stringify(config, null, 2)}
12332
+ mkdirSync6(dirname3(configPath), { recursive: true });
12333
+ writeFileSync5(configPath, `${JSON.stringify(config, null, 2)}
11940
12334
  `, "utf8");
11941
12335
  const meta = readMetaJson() || { appliedId: "", entries: [] };
11942
12336
  meta.appliedId = uuid;
@@ -12091,22 +12485,22 @@ async function buildClaudeAppServerCatalog(entries, providersById, trace) {
12091
12485
  import {
12092
12486
  copyFileSync as copyFileSync3,
12093
12487
  existsSync as existsSync11,
12094
- mkdirSync as mkdirSync6,
12095
- readFileSync as readFileSync6,
12488
+ mkdirSync as mkdirSync7,
12489
+ readFileSync as readFileSync7,
12096
12490
  renameSync as renameSync2,
12097
12491
  rmSync as rmSync5,
12098
12492
  unlinkSync as unlinkSync2,
12099
- writeFileSync as writeFileSync5
12493
+ writeFileSync as writeFileSync6
12100
12494
  } from "fs";
12101
- import { dirname as dirname4, join as join14 } from "path";
12495
+ import { dirname as dirname4, join as join15 } from "path";
12102
12496
  function getSessionLockPath2() {
12103
- return join14(getClaudeDesktopHome(), ".relay-ai.lock");
12497
+ return join15(getClaudeDesktopHome(), ".relay-ai.lock");
12104
12498
  }
12105
12499
  function inspectSessionLock() {
12106
12500
  const path3 = getSessionLockPath2();
12107
12501
  if (!existsSync11(path3)) return { status: "missing" };
12108
12502
  try {
12109
- const parsed = JSON.parse(readFileSync6(path3, "utf8"));
12503
+ const parsed = JSON.parse(readFileSync7(path3, "utf8"));
12110
12504
  if (typeof parsed.pid === "number" && typeof parsed.startedAt === "string" && typeof parsed.uuid === "string" && typeof parsed.proxyPort === "number") {
12111
12505
  return { status: "valid", lock: parsed };
12112
12506
  }
@@ -12117,9 +12511,9 @@ function inspectSessionLock() {
12117
12511
  function writeSessionLock2(lock) {
12118
12512
  const path3 = getSessionLockPath2();
12119
12513
  const tempPath = `${path3}.tmp.${process.pid}`;
12120
- mkdirSync6(dirname4(path3), { recursive: true });
12514
+ mkdirSync7(dirname4(path3), { recursive: true });
12121
12515
  try {
12122
- writeFileSync5(tempPath, `${JSON.stringify(lock, null, 2)}
12516
+ writeFileSync6(tempPath, `${JSON.stringify(lock, null, 2)}
12123
12517
  `, "utf8");
12124
12518
  renameSync2(tempPath, path3);
12125
12519
  } finally {
@@ -12154,7 +12548,7 @@ function restoreMetaJson() {
12154
12548
  }
12155
12549
  }
12156
12550
  function removeRelayAiConfig(uuid) {
12157
- const configPath = join14(getConfigLibraryPath(), `${uuid}.json`);
12551
+ const configPath = join15(getConfigLibraryPath(), `${uuid}.json`);
12158
12552
  if (existsSync11(configPath)) {
12159
12553
  try {
12160
12554
  rmSync5(configPath, { force: true });
@@ -12472,17 +12866,17 @@ ${pc11.bold("Claude Desktop 3P Mode Active")}`);
12472
12866
  }
12473
12867
 
12474
12868
  // src/ai-doc.ts
12475
- import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
12869
+ import { existsSync as existsSync12, mkdirSync as mkdirSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
12476
12870
  import { homedir as homedir10 } from "os";
12477
- import { join as join15 } from "path";
12871
+ import { join as join16 } from "path";
12478
12872
  var SKILL_DIR_NAME = "relay-ai-cli";
12479
12873
  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")
12874
+ join16(getAppHome(), "skills"),
12875
+ join16(homedir10(), ".claude", "skills"),
12876
+ join16(homedir10(), ".agents", "skills"),
12877
+ join16(homedir10(), ".codex", "skills"),
12878
+ join16(homedir10(), ".cursor", "skills"),
12879
+ join16(homedir10(), ".cursor", "skills-cursor")
12486
12880
  ];
12487
12881
  function parseSkillVersion(content) {
12488
12882
  const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
@@ -12496,10 +12890,10 @@ function parseSkillVersion(content) {
12496
12890
  return null;
12497
12891
  }
12498
12892
  function readInstalledSkillVersion(skillDir) {
12499
- const skillPath = join15(skillDir, "SKILL.md");
12893
+ const skillPath = join16(skillDir, "SKILL.md");
12500
12894
  if (!existsSync12(skillPath)) return null;
12501
12895
  try {
12502
- const head = readFileSync7(skillPath, "utf-8").slice(0, 1024);
12896
+ const head = readFileSync8(skillPath, "utf-8").slice(0, 1024);
12503
12897
  return parseSkillVersion(head.includes("---", 4) ? head : `${head}
12504
12898
  ---
12505
12899
  `);
@@ -12509,8 +12903,8 @@ function readInstalledSkillVersion(skillDir) {
12509
12903
  }
12510
12904
  function skillInstallTargets() {
12511
12905
  return SKILL_INSTALL_DIRS.map((dir) => {
12512
- const skillDir = join15(dir, SKILL_DIR_NAME);
12513
- return { skillDir, skillPath: join15(skillDir, "SKILL.md") };
12906
+ const skillDir = join16(dir, SKILL_DIR_NAME);
12907
+ return { skillDir, skillPath: join16(skillDir, "SKILL.md") };
12514
12908
  });
12515
12909
  }
12516
12910
  function formatProviderModels(provider) {
@@ -13014,8 +13408,8 @@ function installAiDoc(opts = {}) {
13014
13408
  result.skipped.push(skillPath);
13015
13409
  continue;
13016
13410
  }
13017
- mkdirSync7(skillDir, { recursive: true });
13018
- writeFileSync6(skillPath, doc, "utf-8");
13411
+ mkdirSync8(skillDir, { recursive: true });
13412
+ writeFileSync7(skillPath, doc, "utf-8");
13019
13413
  if (previous) {
13020
13414
  result.updated.push({ path: skillPath, fromVersion: previous });
13021
13415
  } else {
@@ -13175,16 +13569,16 @@ function buildHttpProxyChildEnv(baseEnv, proxyUrl, caCertPath) {
13175
13569
  // src/http-proxy/ca.ts
13176
13570
  import { randomBytes as randomBytes2, randomUUID as randomUUID4 } from "crypto";
13177
13571
  import {
13178
- chmodSync as chmodSync2,
13572
+ chmodSync as chmodSync3,
13179
13573
  existsSync as existsSync13,
13180
- mkdirSync as mkdirSync9,
13181
- readFileSync as readFileSync8,
13574
+ mkdirSync as mkdirSync10,
13575
+ readFileSync as readFileSync9,
13182
13576
  readdirSync as readdirSync3,
13183
13577
  rmSync as rmSync6,
13184
13578
  statSync as statSync3,
13185
- writeFileSync as writeFileSync8
13579
+ writeFileSync as writeFileSync9
13186
13580
  } from "fs";
13187
- import { dirname as dirname6, join as join17, resolve } from "path";
13581
+ import { dirname as dirname6, join as join18, resolve } from "path";
13188
13582
  import forge from "node-forge";
13189
13583
  var SESSION_ROOT = "http-proxy-sessions";
13190
13584
  var OWNER_FILE = "owner.pid";
@@ -13204,22 +13598,22 @@ function processIsRunning(pid) {
13204
13598
  }
13205
13599
  }
13206
13600
  function cleanupStaleHttpProxySessions(appHome = getAppHome()) {
13207
- const root = join17(appHome, SESSION_ROOT);
13601
+ const root = join18(appHome, SESSION_ROOT);
13208
13602
  if (!existsSync13(root)) return;
13209
13603
  const now = Date.now();
13210
13604
  for (const name of readdirSync3(root)) {
13211
- const sessionDir = join17(root, name);
13605
+ const sessionDir = join18(root, name);
13212
13606
  try {
13213
13607
  const stat = statSync3(sessionDir);
13214
13608
  if (!stat.isDirectory()) continue;
13215
- const ownerPath = join17(sessionDir, OWNER_FILE);
13609
+ const ownerPath = join18(sessionDir, OWNER_FILE);
13216
13610
  if (!existsSync13(ownerPath)) {
13217
13611
  if (now - stat.mtimeMs > MID_CREATION_GRACE_MS) {
13218
13612
  rmSync6(sessionDir, { recursive: true, force: true });
13219
13613
  }
13220
13614
  continue;
13221
13615
  }
13222
- const pid = Number(readFileSync8(ownerPath, "utf8").trim());
13616
+ const pid = Number(readFileSync9(ownerPath, "utf8").trim());
13223
13617
  if (!Number.isSafeInteger(pid) || pid <= 0) {
13224
13618
  const ownerStat = statSync3(ownerPath);
13225
13619
  const newestMtimeMs = Math.max(stat.mtimeMs, ownerStat.mtimeMs);
@@ -13235,13 +13629,13 @@ function cleanupStaleHttpProxySessions(appHome = getAppHome()) {
13235
13629
  }
13236
13630
  function createHttpProxyCertificates(appHome = getAppHome()) {
13237
13631
  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}
13632
+ const root = join18(appHome, SESSION_ROOT);
13633
+ mkdirSync10(root, { recursive: true, mode: 448 });
13634
+ chmodSync3(root, 448);
13635
+ const sessionDir = join18(root, randomUUID4());
13636
+ mkdirSync10(sessionDir, { mode: 448 });
13637
+ chmodSync3(sessionDir, 448);
13638
+ writeFileSync9(join18(sessionDir, OWNER_FILE), `${process.pid}
13245
13639
  `, { mode: 384 });
13246
13640
  try {
13247
13641
  const caKeys = forge.pki.rsa.generateKeyPair(2048);
@@ -13276,9 +13670,9 @@ function createHttpProxyCertificates(appHome = getAppHome()) {
13276
13670
  ]);
13277
13671
  server.sign(caKeys.privateKey, forge.md.sha256.create());
13278
13672
  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);
13673
+ const caCertPath = join18(sessionDir, "relay-ai-ca.pem");
13674
+ writeFileSync9(caCertPath, caCert, { encoding: "utf8", mode: 384 });
13675
+ chmodSync3(caCertPath, 384);
13282
13676
  let cleaned = false;
13283
13677
  const cleanupOnExit = () => {
13284
13678
  if (cleaned) return;
@@ -13319,18 +13713,18 @@ function createHttpProxyCaBundle(relayCaCertPath, additionalCaCertPath) {
13319
13713
  if (resolve(additionalCaCertPath) === resolve(relayCaCertPath)) {
13320
13714
  return relayCaCertPath;
13321
13715
  }
13322
- const relayCa = readFileSync8(relayCaCertPath, "utf8").trimEnd();
13323
- const additionalCa = readFileSync8(additionalCaCertPath, "utf8").trim();
13716
+ const relayCa = readFileSync9(relayCaCertPath, "utf8").trimEnd();
13717
+ const additionalCa = readFileSync9(additionalCaCertPath, "utf8").trim();
13324
13718
  if (!additionalCa) return relayCaCertPath;
13325
- const combinedPath = join17(dirname6(relayCaCertPath), "combined-ca.pem");
13326
- writeFileSync8(
13719
+ const combinedPath = join18(dirname6(relayCaCertPath), "combined-ca.pem");
13720
+ writeFileSync9(
13327
13721
  combinedPath,
13328
13722
  `${relayCa}
13329
13723
  ${additionalCa}
13330
13724
  `,
13331
13725
  { encoding: "utf8", mode: 384 }
13332
13726
  );
13333
- chmodSync2(combinedPath, 384);
13727
+ chmodSync3(combinedPath, 384);
13334
13728
  return combinedPath;
13335
13729
  }
13336
13730
 
@@ -15345,7 +15739,7 @@ Options:
15345
15739
  --trace Write debug logs under ~/.relay-ai/logs/`);
15346
15740
  return 0;
15347
15741
  }
15348
- const { runUiCommand } = await import("./ui-command-Q6LBKVM3.js");
15742
+ const { runUiCommand } = await import("./ui-command-NYYLGTCD.js");
15349
15743
  return runUiCommand({ trace: parsed.trace, serverMode: parsed.uiServerMode });
15350
15744
  }
15351
15745
  if (parsed.command === "models") {