@bitkyc08/opencodex 2.48.0 → 2.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +8 -5
  2. package/SPONSORS.md +1 -1
  3. package/assets/sponsors/orcarouter.png +0 -0
  4. package/assets/sponsors/packycode.png +0 -0
  5. package/gui/dist/assets/index-BoBRSehJ.css +1 -0
  6. package/gui/dist/assets/index-BtyONQrZ.js +115 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/packycode.svg +19 -0
  9. package/gui/dist/provider-icons/qoder.svg +5 -0
  10. package/package.json +5 -3
  11. package/src/adapters/anthropic.ts +31 -16
  12. package/src/adapters/codebuddy/adapter.ts +85 -0
  13. package/src/adapters/codebuddy/profiles.ts +52 -0
  14. package/src/adapters/coding-agent/profile.ts +100 -0
  15. package/src/adapters/coding-agent/protocol.ts +463 -0
  16. package/src/adapters/coding-agent/turn.ts +353 -0
  17. package/src/adapters/google.ts +15 -11
  18. package/src/adapters/mimo-free.ts +3 -0
  19. package/src/adapters/openai-chat.ts +2 -2
  20. package/src/adapters/openai-responses.ts +18 -11
  21. package/src/adapters/qoder/adapter.ts +70 -0
  22. package/src/adapters/qoder/live-models.ts +89 -0
  23. package/src/adapters/qoder/profiles.ts +36 -0
  24. package/src/adapters/registry.ts +12 -0
  25. package/src/adapters/responses-tool-schema.ts +113 -8
  26. package/src/cli/dispatch.ts +2 -0
  27. package/src/cli/doctor.ts +40 -0
  28. package/src/cli/effort.ts +24 -8
  29. package/src/cli/help.ts +2 -0
  30. package/src/cli/index.ts +24 -1
  31. package/src/cli/provider-runtime.ts +2 -1
  32. package/src/cli/registry.ts +2 -2
  33. package/src/cli/system-command.ts +10 -3
  34. package/src/cli/usage-report.ts +9 -5
  35. package/src/codex/account-lifecycle.ts +35 -2
  36. package/src/codex/auth-api.ts +6 -3
  37. package/src/codex/auth-collision.ts +12 -2
  38. package/src/codex/auth-context.ts +75 -7
  39. package/src/codex/catalog/provider-fetch.ts +73 -9
  40. package/src/codex/inject.ts +127 -30
  41. package/src/codex/internal/catalog-writer.ts +3 -0
  42. package/src/codex/journal.ts +61 -12
  43. package/src/codex/model-cache.ts +11 -4
  44. package/src/codex/native-profile-startup.ts +72 -5
  45. package/src/codex/native-profile-store.ts +2 -2
  46. package/src/codex/ocx-compaction-history.ts +226 -0
  47. package/src/codex/project-config-warnings.ts +3 -1
  48. package/src/codex/quota.ts +18 -8
  49. package/src/codex/reserve-availability.ts +21 -5
  50. package/src/codex/runtime.ts +45 -1
  51. package/src/codex/sync.ts +5 -0
  52. package/src/config.ts +1 -0
  53. package/src/generated/compatibility-version.json +126 -74
  54. package/src/images/loop.ts +1 -0
  55. package/src/images/xai-video-client.ts +2 -0
  56. package/src/integrations/registry.ts +1 -0
  57. package/src/lib/process-control.ts +52 -8
  58. package/src/lib/upstream-retry.ts +1 -0
  59. package/src/oauth/chatgpt.ts +83 -0
  60. package/src/oauth/xai.ts +151 -8
  61. package/src/providers/api-key-selection-capture.ts +10 -0
  62. package/src/providers/api-key-selection.ts +2 -7
  63. package/src/providers/caller-authorization.ts +36 -0
  64. package/src/providers/codebuddy-models.ts +184 -0
  65. package/src/providers/derive.ts +5 -0
  66. package/src/providers/free-directory.ts +26 -2
  67. package/src/providers/openai-sidecar.ts +35 -11
  68. package/src/providers/qoder-models.ts +25 -0
  69. package/src/providers/registry.ts +139 -0
  70. package/src/responses/compaction.ts +4 -0
  71. package/src/responses/task-input.ts +21 -1
  72. package/src/router.ts +1 -1
  73. package/src/server/chat-completions.ts +26 -9
  74. package/src/server/claude-messages.ts +12 -2
  75. package/src/server/images.ts +1 -0
  76. package/src/server/management/account-selection-stream.ts +13 -4
  77. package/src/server/management/config-routes.ts +24 -5
  78. package/src/server/management/logs-usage-routes.ts +1 -0
  79. package/src/server/management/native-integration-routes.ts +2 -1
  80. package/src/server/management/provider-routes.ts +24 -0
  81. package/src/server/management-api.ts +14 -0
  82. package/src/server/port-reclaim.ts +11 -26
  83. package/src/server/request-log.ts +6 -0
  84. package/src/server/responses/collaboration.ts +4 -9
  85. package/src/server/responses/core.ts +141 -19
  86. package/src/server/responses/fetch-helpers.ts +18 -7
  87. package/src/server/responses/policy-fallback.ts +12 -0
  88. package/src/service.ts +36 -2
  89. package/src/storage/cleanup.ts +77 -45
  90. package/src/types/config.ts +7 -1
  91. package/src/usage/log.ts +24 -0
  92. package/src/vision/anthropic-describe.ts +1 -0
  93. package/src/web-search/anthropic-executor.ts +1 -0
  94. package/src/web-search/loop.ts +1 -0
  95. package/src/web-search/progress-stream.ts +4 -0
  96. package/gui/dist/assets/index-B5r7LNHN.js +0 -115
  97. package/gui/dist/assets/index-D5SiRo8X.css +0 -1
@@ -38,6 +38,9 @@ import {
38
38
  } from "./request-log";
39
39
  import { responseWithDeferredRequestLog } from "./relay";
40
40
  import { handleResponses } from "./responses";
41
+ import { providerConsumesCallerAuthorization } from "../providers/caller-authorization";
42
+ import { captureExplicitOpenAiCallerAuth } from "../providers/openai-sidecar";
43
+ import { captureCallerDirectAuth } from "../providers/caller-authorization";
41
44
  import type { AdmissionLease } from "../lib/admission";
42
45
  import type { DataPlaneAdmission } from "./auth-cors";
43
46
  import { tryClaimNativeMainProfileForTurn } from "../codex/native-main-admission";
@@ -133,7 +136,8 @@ async function handleChatCompletionsWithBudget(
133
136
  // it registers (extra_headers, sent verbatim by upstream Grok). Dashboard usage
134
137
  // bucketing only — never an auth or billing signal.
135
138
  if (req.headers.get("x-opencodex-grok") === "1") logCtx.surface = "grok";
136
- let directRoute = false;
139
+ let callerAuthorizationRoute = false;
140
+ let routeMayChangeCredentialDomain = false;
137
141
  let settledRoute: ReturnType<typeof routeModel> | null = null;
138
142
  let chatNativeRoute: ReturnType<typeof routeModel> | null = null;
139
143
  try {
@@ -150,9 +154,9 @@ async function handleChatCompletionsWithBudget(
150
154
  logCtx.provider = route.providerName;
151
155
  logCtx.routeDecision = route.routeDecision;
152
156
  settledRoute = route;
153
- if (route.provider.adapter === "openai-responses") {
154
- directRoute = route.codexAccountMode === "direct";
155
- }
157
+ routeMayChangeCredentialDomain = route.combo !== undefined || route.routeKind === "policy";
158
+ callerAuthorizationRoute = !routeMayChangeCredentialDomain
159
+ && providerConsumesCallerAuthorization(route.provider);
156
160
  if (route.provider.adapter === "cursor" || route.provider.adapter === "kiro") {
157
161
  const parts: string[] = [];
158
162
  if (chatBody.messages !== undefined) parts.push(JSON.stringify(chatBody.messages));
@@ -240,17 +244,23 @@ async function handleChatCompletionsWithBudget(
240
244
  && isCodexReserveHelperUnsupported(config, settledRoute.modelId, logIds?.admission, visionDescribeTerminal)) {
241
245
  return chatCompletionsErrorResponse(400, CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE, "invalid_request_error");
242
246
  }
247
+ const nativeCallerAuth = captureExplicitOpenAiCallerAuth(req.headers, config);
248
+ // Caller-owned only: stored-main enrichment below is sidecar authority, never Direct authority.
249
+ const callerDirectAuth = captureCallerDirectAuth(req.headers, config);
250
+ let openAiSidecarAuth = nativeCallerAuth;
243
251
  const headers = new Headers({ "content-type": "application/json" });
244
252
  // Internal bridge metadata; the Go resolver scopes and hashes it before upstream use.
245
253
  const openCodeSession = req.headers.get("x-opencode-session");
246
254
  if (openCodeSession) headers.set("x-opencode-session", openCodeSession);
247
255
  for (const name of FORWARD_HEADERS) {
248
- if (name === "authorization" && !directRoute) continue;
256
+ if (routeMayChangeCredentialDomain && (name === "authorization" || name === "chatgpt-account-id")) continue;
257
+ if (name === "authorization" && !callerAuthorizationRoute) continue;
249
258
  const value = req.headers.get(name);
250
259
  if (value) headers.set(name, value);
251
260
  }
252
- // Prefer main ChatGPT auth so OpenAI-backed sidecars remain reachable on routed turns.
253
- if (!directRoute) {
261
+ // A noncanonical caller-auth route can use stored main auth only through a sidecar snapshot.
262
+ // Later shadow/thread rewrites strip primary credentials at the actual Responses boundary.
263
+ if (!callerAuthorizationRoute || (settledRoute && !isCanonicalOpenAiForwardProvider(settledRoute.provider))) {
254
264
  // This enrichment is optional for routed/non-main providers. If native main
255
265
  // is fenced, omit it and let auth-context reject only a final physical-main
256
266
  // selection while healthy pool/provider routes continue.
@@ -259,8 +269,12 @@ async function handleChatCompletionsWithBudget(
259
269
  const { getMainAccountToken } = await import("../codex/main-account");
260
270
  const token = getMainAccountToken();
261
271
  if (token) {
262
- headers.set("authorization", `Bearer ${token.accessToken}`);
263
- headers.set("chatgpt-account-id", token.chatgptAccountId);
272
+ const mainHeaders = new Headers({ authorization: `Bearer ${token.accessToken}`, "chatgpt-account-id": token.chatgptAccountId });
273
+ openAiSidecarAuth ??= captureExplicitOpenAiCallerAuth(mainHeaders, config);
274
+ if (!callerAuthorizationRoute && !routeMayChangeCredentialDomain) {
275
+ headers.set("authorization", `Bearer ${token.accessToken}`);
276
+ headers.set("chatgpt-account-id", token.chatgptAccountId);
277
+ }
264
278
  }
265
279
  } catch {
266
280
  /* optional */
@@ -299,6 +313,9 @@ async function handleChatCompletionsWithBudget(
299
313
  addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, meta);
300
314
  };
301
315
  const upstream = await handleResponses(internalReq, config, logCtx, {
316
+ openAiSidecarAuth,
317
+ nativeCallerAuth,
318
+ callerDirectAuth,
302
319
  ...(logIds?.turnAdmissionLease ? { turnAdmissionLease: logIds.turnAdmissionLease } : {}),
303
320
  // #1686: the Chat surface translates its body and replays here, so the admission fact has
304
321
  // to ride along or a bearer-admitted Chat caller would still be refused by Direct.
@@ -603,7 +603,7 @@ export async function fetchWithHeaderDeadline(
603
603
  ): Promise<HeaderDeadlineFetchResult> {
604
604
  const deadline = makeDeadline(timeoutMs, parent);
605
605
  try {
606
- const upstream = await fetchImpl(input, { ...init, signal: deadline.signal, timeout: 0 });
606
+ const upstream = await fetchImpl(input, { ...init, redirect: "manual", signal: deadline.signal, timeout: 0 });
607
607
  return { kind: "response", upstream };
608
608
  } catch (error) {
609
609
  if (deadline.didExpire()) return { kind: "timeout" };
@@ -837,6 +837,7 @@ async function handleClaudeMessagesWithBudget(
837
837
  }
838
838
 
839
839
  const headers = new Headers({ "content-type": "application/json" });
840
+ let trustedClaudeMainAuth: { authorization: string; chatgptAccountId?: string } | undefined;
840
841
  for (const name of FORWARD_HEADERS) {
841
842
  // The caller's bearer is the proxy admission token (ocx claude placeholder), never a
842
843
  // ChatGPT credential — forwarding it upstream turns into {"detail":"Unauthorized"}.
@@ -852,8 +853,13 @@ async function handleClaudeMessagesWithBudget(
852
853
  const { getMainAccountToken } = await import("../codex/main-account");
853
854
  const token = getMainAccountToken();
854
855
  if (token) {
855
- headers.set("authorization", `Bearer ${token.accessToken}`);
856
+ const authorization = `Bearer ${token.accessToken}`;
857
+ headers.set("authorization", authorization);
856
858
  headers.set("chatgpt-account-id", token.chatgptAccountId);
859
+ trustedClaudeMainAuth = {
860
+ authorization,
861
+ ...(token.chatgptAccountId ? { chatgptAccountId: token.chatgptAccountId } : {}),
862
+ };
857
863
  }
858
864
  }
859
865
  if (opencodeGoRoute) {
@@ -923,6 +929,10 @@ async function handleClaudeMessagesWithBudget(
923
929
  // would fire, disagreeing with the pre-flight decision above.
924
930
  inboundWire: "anthropic",
925
931
  stripClaudeMainAuthForNoncanonicalForward: true,
932
+ ...(trustedClaudeMainAuth ? { trustedClaudeMainAuth } : {}),
933
+ // Claude's internal stored-main enrichment is not an original caller credential.
934
+ nativeCallerAuth: null,
935
+ callerDirectAuth: null,
926
936
  translatorBudget,
927
937
  ...(logIds ? { onFirstOutput: () => recordFirstOutput(logCtx, logIds.start) } : {}),
928
938
  onNativePassthroughTerminal: status => finalizeNativeLog(httpStatusForRequestLogTerminal(status, logCtx), { terminalStatus: status, closeReason: "terminal" }),
@@ -288,6 +288,7 @@ async function tryCcaImageGeneration(
288
288
  try {
289
289
  upstream = await fetch(`${baseUrl}/v1internal:generateContent`, {
290
290
  method: "POST",
291
+ redirect: "manual",
291
292
  headers: {
292
293
  "Content-Type": "application/json",
293
294
  "Authorization": `Bearer ${token}`,
@@ -3,6 +3,7 @@ import { registerOptionalShutdownHook } from "../../lib/optional-shutdown-hooks"
3
3
 
4
4
  const MAX_SELECTION_STREAMS = 64;
5
5
  const HEARTBEAT_MS = 15_000;
6
+ const STREAM_QUEUE_HIGH_WATER_MARK = 16;
6
7
  const encoder = new TextEncoder();
7
8
  const connections = new Set<() => void>();
8
9
 
@@ -36,9 +37,17 @@ export function accountSelectionStream(request: Request, validate: () => boolean
36
37
  const send = (frame: string) => {
37
38
  if (closed) return;
38
39
  if (!authorized()) {
39
- // Error clears queued frames as well, so a revoked consumer cannot drain them.
40
- try { controller.error(new DOMException("Management session is no longer authorized", "NotAllowedError")); }
41
- finally { close(); }
40
+ // A revoked consumer must not drain frames queued before revocation: error() is
41
+ // what discards a non-empty queue. When nothing is queued — the common expired-session
42
+ // path, including the heartbeat — close() alone terminates quietly, so an expired
43
+ // dashboard session does not dump an expected DOMException into the server console.
44
+ // desiredSize equals the high water mark exactly when the queue is empty.
45
+ if (controller.desiredSize !== null && controller.desiredSize < STREAM_QUEUE_HIGH_WATER_MARK) {
46
+ try { controller.error(new DOMException("Management session is no longer authorized", "NotAllowedError")); }
47
+ finally { close(); }
48
+ } else {
49
+ close();
50
+ }
42
51
  return;
43
52
  }
44
53
  // Reconnection sends a ready event, so a slow reader can reconcile without an
@@ -61,7 +70,7 @@ export function accountSelectionStream(request: Request, validate: () => boolean
61
70
  heartbeat.unref?.();
62
71
  },
63
72
  cancel() { cleanup(); },
64
- }, { highWaterMark: 16 });
73
+ }, { highWaterMark: STREAM_QUEUE_HIGH_WATER_MARK });
65
74
  return new Response(body, { headers: {
66
75
  "Content-Type": "text/event-stream; charset=utf-8",
67
76
  "Cache-Control": "no-cache, no-transform",
@@ -329,6 +329,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
329
329
  oauthOpenBrowser: config.oauthOpenBrowser !== false,
330
330
  // Absent means off (today's Design B injection), so the GUI/CLI render a plain switch.
331
331
  codexDesktopAuthless: config.codexDesktopAuthless === true,
332
+ // Absent keeps Design B remote compaction; true selects the dedicated provider identity.
333
+ codexClientCompaction: config.codexClientCompaction === true,
332
334
  startupHealth: await readStartupHealth(config),
333
335
  codexRuntime: {
334
336
  path: displayCodexRuntimePath(resolved.runtime.command),
@@ -419,6 +421,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
419
421
  ultraFastTier?: unknown;
420
422
  codexMainAccountHardLock?: unknown;
421
423
  codexDesktopAuthless?: unknown;
424
+ codexClientCompaction?: unknown;
422
425
  };
423
426
  if (body.codexAutoStart === undefined
424
427
  && body.streamMode === undefined
@@ -429,8 +432,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
429
432
  && body.showCodexSparkQuota === undefined
430
433
  && body.ultraFastTier === undefined
431
434
  && body.codexMainAccountHardLock === undefined
432
- && body.codexDesktopAuthless === undefined) {
433
- return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, codexAccountPickerEnabled, codexQuotaAutoRefresh, oauthOpenBrowser, showCodexSparkQuota, ultraFastTier, codexMainAccountHardLock, or codexDesktopAuthless" }, 400);
435
+ && body.codexDesktopAuthless === undefined
436
+ && body.codexClientCompaction === undefined) {
437
+ return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, codexAccountPickerEnabled, codexQuotaAutoRefresh, oauthOpenBrowser, showCodexSparkQuota, ultraFastTier, codexMainAccountHardLock, codexDesktopAuthless, or codexClientCompaction" }, 400);
434
438
  }
435
439
  if (body.codexAutoStart !== undefined && typeof body.codexAutoStart !== "boolean") {
436
440
  return jsonResponse({ error: "codexAutoStart boolean is required" }, 400);
@@ -457,6 +461,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
457
461
  if (body.codexDesktopAuthless !== undefined && typeof body.codexDesktopAuthless !== "boolean") {
458
462
  return jsonResponse({ error: "codexDesktopAuthless boolean is required" }, 400);
459
463
  }
464
+ if (body.codexClientCompaction !== undefined && typeof body.codexClientCompaction !== "boolean") {
465
+ return jsonResponse({ error: "codexClientCompaction boolean is required" }, 400);
466
+ }
460
467
  let quotaAutoRefreshChange: { id: string; window: "fiveHour" | "weekly"; enabled: boolean } | undefined;
461
468
  if (body.codexQuotaAutoRefresh !== undefined) {
462
469
  if (!isPlainRecord(body.codexQuotaAutoRefresh)) {
@@ -509,10 +516,13 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
509
516
  hasCodexMainAccountHardLock: Object.hasOwn(config, "codexMainAccountHardLock"),
510
517
  codexDesktopAuthless: config.codexDesktopAuthless,
511
518
  hasCodexDesktopAuthless: Object.hasOwn(config, "codexDesktopAuthless"),
519
+ codexClientCompaction: config.codexClientCompaction,
520
+ hasCodexClientCompaction: Object.hasOwn(config, "codexClientCompaction"),
512
521
  };
513
522
  const pickerWasEnabled = codexAccountPickerEnabled(config);
514
523
  let pickerIsEnabled = pickerWasEnabled;
515
524
  const authlessWasEnabled = config.codexDesktopAuthless === true;
525
+ const clientCompactionWasEnabled = config.codexClientCompaction === true;
516
526
  try {
517
527
  if (typeof body.codexAutoStart === "boolean") {
518
528
  config.codexAutoStart = body.codexAutoStart;
@@ -547,6 +557,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
547
557
  else if (body.codexMainAccountHardLock === false) deleteConfigTopLevelKey(config, "codexMainAccountHardLock");
548
558
  if (body.codexDesktopAuthless === true) config.codexDesktopAuthless = true;
549
559
  else if (body.codexDesktopAuthless === false) deleteConfigTopLevelKey(config, "codexDesktopAuthless");
560
+ if (body.codexClientCompaction === true) config.codexClientCompaction = true;
561
+ else if (body.codexClientCompaction === false) deleteConfigTopLevelKey(config, "codexClientCompaction");
550
562
  if (quotaAutoRefreshChange) {
551
563
  const { id, window, enabled } = quotaAutoRefreshChange;
552
564
  const setting = { ...(config.codexQuotaAutoRefresh?.[id] ?? {}) };
@@ -592,16 +604,22 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
592
604
  if (previousSettings.hasCodexDesktopAuthless) {
593
605
  config.codexDesktopAuthless = previousSettings.codexDesktopAuthless;
594
606
  } else deleteConfigTopLevelKey(config, "codexDesktopAuthless");
607
+ if (previousSettings.hasCodexClientCompaction) {
608
+ config.codexClientCompaction = previousSettings.codexClientCompaction;
609
+ } else deleteConfigTopLevelKey(config, "codexClientCompaction");
595
610
  throw error;
596
611
  }
597
612
  if (typeof body.appOwnedMemoryBudgetMb === "number") {
598
613
  configureAppOwnedMemoryBudget(resolveAppOwnedMemoryBudgetBytes(body.appOwnedMemoryBudgetMb));
599
614
  enforceAppOwnedMemoryBudget();
600
615
  }
601
- // The authless switch changes the injected config.toml shape, so converge now rather than
602
- // waiting for the next start; the injector re-reads config and rewrites the form.
616
+ // Both Desktop compatibility switches change the injected config.toml shape, so converge now
617
+ // rather than waiting for the next start; the injector re-reads config and rewrites the form.
603
618
  const authlessIsEnabled = config.codexDesktopAuthless === true;
604
- const catalogRefresh = pickerWasEnabled !== pickerIsEnabled || authlessWasEnabled !== authlessIsEnabled
619
+ const clientCompactionIsEnabled = config.codexClientCompaction === true;
620
+ const catalogRefresh = pickerWasEnabled !== pickerIsEnabled
621
+ || authlessWasEnabled !== authlessIsEnabled
622
+ || clientCompactionWasEnabled !== clientCompactionIsEnabled
605
623
  ? await convergeCodexCatalog()
606
624
  : undefined;
607
625
  const catalogRefreshPending = catalogRefresh
@@ -620,6 +638,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
620
638
  catalogRefreshPending,
621
639
  showCodexSparkQuota: config.showCodexSparkQuota === true,
622
640
  codexDesktopAuthless: authlessIsEnabled,
641
+ codexClientCompaction: clientCompactionIsEnabled,
623
642
  codexMainAccountHardLock: config.codexMainAccountHardLock === true,
624
643
  mainAccountHardLock: getMainAccountHardLockStatus(config),
625
644
  startupHealth: await readStartupHealth(config),
@@ -425,6 +425,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
425
425
  bytes: result.bytes,
426
426
  ...(result.trashDir ? { trashDir: result.trashDir } : {}),
427
427
  removedPaths: result.removedPaths,
428
+ ...(result.skippedReferencedPaths?.length ? { skippedReferencedPaths: result.skippedReferencedPaths } : {}),
428
429
  });
429
430
  } catch {
430
431
  return jsonResponse({
@@ -366,13 +366,14 @@ async function handleCodexToggle(ctx: ManagementContext): Promise<Response> {
366
366
  }
367
367
  }
368
368
  const { restoreNativeCodexAsync } = await import("../../codex/inject");
369
+ const { OCX_NATIVE_REPLAY_RECOVERY_NOTE } = await import("../../responses/compaction");
369
370
  const restored = await restoreNativeCodexAsync({ revalidateDesiredState: true });
370
371
  return jsonResponse({
371
372
  ok: true, clientId: "codex", changed: durable && persisted.status === "committed",
372
373
  state: restored.success ? "absent" : "unsafe",
373
374
  desiredEnabled: enabled,
374
375
  message: restored.success
375
- ? "Codex restored to its native path; the proxy is still serving other clients"
376
+ ? `Codex restored to its native path; the proxy is still serving other clients. ${OCX_NATIVE_REPLAY_RECOVERY_NOTE}`
376
377
  : `Codex intent saved, but restoring the native path did not complete: ${restored.message}`,
377
378
  ...(restored.success
378
379
  ? (durable ? {} : { reason: "not_durable" })
@@ -40,6 +40,8 @@ import { providerDestinationResolvedError } from "../../lib/destination-policy";
40
40
  import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
41
41
  import { ProviderOutboundPolicyError, providerOutboundGet, providerOutboundPost, providerRedirectError } from "../../lib/provider-outbound";
42
42
  import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
43
+ import { fetchQoderModels } from "../../adapters/qoder/live-models";
44
+ import { resolveQoderProfile } from "../../adapters/qoder/profiles";
43
45
  import { parseAntigravityAvailableModels } from "../../providers/antigravity-models";
44
46
  import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
45
47
  import { deriveProviderPresets, providerConfigSeed } from "../../providers/derive";
@@ -1350,6 +1352,28 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
1350
1352
  message: `Connected. ${live.models.length} models.`,
1351
1353
  });
1352
1354
  }
1355
+ if (prov.adapter === "qoder") {
1356
+ const started = Date.now();
1357
+ const profile = resolveQoderProfile(prov.baseUrl);
1358
+ if (!profile) {
1359
+ return jsonResponse({ ok: false, latencyMs: 0, error: "qoder discovery refused a non-canonical destination" });
1360
+ }
1361
+ const live = await fetchQoderModels(profile, apiKey ?? "");
1362
+ const latencyMs = Date.now() - started;
1363
+ if (!live.ok) {
1364
+ return jsonResponse({
1365
+ ok: false,
1366
+ latencyMs,
1367
+ error: `qoder discovery ${live.error}${live.detail ? `: ${live.detail}` : ""}`,
1368
+ });
1369
+ }
1370
+ return jsonResponse({
1371
+ ok: true,
1372
+ latencyMs,
1373
+ models: live.models.length,
1374
+ message: `Connected. ${live.models.length} models.`,
1375
+ });
1376
+ }
1353
1377
  const project = prov.project ?? snapshot?.projectId;
1354
1378
  if (antigravity && !project) {
1355
1379
  return jsonResponse({ ok: false, latencyMs: 0, error: "Antigravity project unavailable — re-run `ocx login google-antigravity`" });
@@ -300,6 +300,20 @@ export async function handleManagementAPI(
300
300
  message: "This proxy is managed by a Task Scheduler wrapper that can respawn it, so the stop must be run by `ocx stop`, which verifies the respawn window. Nothing was changed.",
301
301
  }, 409, req, config);
302
302
  }
303
+ if (respawnRisk === "self-unload") {
304
+ // This proxy IS the launchd/systemd job, so stopping the manager below would
305
+ // terminate the handler before the shared teardown at the end of this route restores
306
+ // the native Codex keys — the dashboard Stop button left `openai_base_url`,
307
+ // `experimental_realtime_ws_base_url` and `model_catalog_json` pointed at a dead
308
+ // proxy (#4023). Refuse before touching anything, like the Windows branch above.
309
+ // `ocx stop` is safe because it runs outside this process and owns the teardown
310
+ // through its receipt, which is why the receipt-backed caller never reaches here.
311
+ return jsonResponse({
312
+ success: false,
313
+ code: "self_unload_service",
314
+ message: "This proxy is running as the installed service, so stopping the manager from inside it would end this process before native Codex is restored. Run `ocx stop`, which stops the service from outside and completes the restore. Nothing was changed.",
315
+ }, 409, req, config);
316
+ }
303
317
  if (respawnRisk === "unknown") {
304
318
  // Do NOT send them to `ocx stop`: it maps the same unanswerable probe to a stop
305
319
  // failure, so that advice would be a loop. The scheduler query itself is what needs
@@ -2,11 +2,9 @@
2
2
  * Reclaim a listen port after stop/update so restart can stay on the configured
3
3
  * port instead of hopping to an ephemeral one (Windows CLOSE_WAIT / leftover ocx).
4
4
  *
5
- * Killing is never the default. A process may be killed only when the caller
6
- * sets `killOcxHolders` and either supplies a non-empty `onlyKillPids` allowlist
7
- * (trusted teardown PIDs, including allowlisted holders that fail ocx revalidate)
8
- * or enables `killAllOcxOnPort` for revalidated ocx listeners. Unknown foreign
9
- * (non-ocx, non-allowlisted) processes are never killed.
5
+ * Killing is never the default. It requires `killOcxHolders`, an allowed PID or
6
+ * `killAllOcxOnPort`, and successful ocx verification. A historical PID allowlist
7
+ * never overrides a rejected verifier result; rejected live holders stay protected.
10
8
  */
11
9
  import { execFileSync } from "node:child_process";
12
10
  import { verifyPidIdentity } from "../config/process-state";
@@ -29,6 +27,9 @@ export type ReclaimListenPortOptions = WaitForPortOptions & {
29
27
  /**
30
28
  * Explicit PIDs the caller just stopped / hard-killed. An omitted or empty
31
29
  * list means no process may be killed — unless {@link killAllOcxOnPort} is set.
30
+ * The allowlist only narrows kill candidates: every candidate, allowlisted or
31
+ * not, still requires verifier acceptance (`verifyOcxFn(pid) === pid`) on each
32
+ * scan, and a rejected live holder is never killed or TCP-row dropped.
32
33
  */
33
34
  onlyKillPids?: number[];
34
35
  /**
@@ -36,8 +37,7 @@ export type ReclaimListenPortOptions = WaitForPortOptions & {
36
37
  * killed (re-checked each scan). Used by post-update restart so a Windows
37
38
  * service wrapper that respawns a *new* bun PID mid-reclaim cannot stay
38
39
  * protected just because it was absent from the pre-wait allowlist snapshot.
39
- * Never kills foreign (non-ocx) processes — only allowlisted teardown PIDs
40
- * and revalidated ocx listeners.
40
+ * Every candidate still requires ocx verifier acceptance before termination.
41
41
  */
42
42
  killAllOcxOnPort?: boolean;
43
43
  /**
@@ -174,8 +174,8 @@ export function listListenPids(port: number): number[] {
174
174
  * Never kills a process unless `killOcxHolders === true` and either
175
175
  * `onlyKillPids` is a non-empty allowlist or `killAllOcxOnPort` is set — then
176
176
  * revalidates immediately before each kill.
177
- * Never kills foreign processes. Never drops TCP rows while a live foreign or
178
- * protected ocx listener owns the port, or when the listener scan failed.
177
+ * Never overrides a rejected ocx verifier result. Never drops TCP rows while a
178
+ * rejected live or protected ocx listener owns the port, or when the scan failed.
179
179
  */
180
180
  export async function reclaimListenPort(
181
181
  port: number,
@@ -231,24 +231,9 @@ export async function reclaimListenPort(
231
231
  }
232
232
  const isOcx = verifyOcxFn(pid) === pid;
233
233
  const allowlisted = allowedKillPids.has(pid);
234
- // Pre-update PIDs can fail verify while still LISTENing (dead owner still
235
- // listed, or cmdline probe raced). Allowlisted teardown PIDs may be killed;
236
- // unknown foreign claimants must remain fail-closed.
237
234
  if (!isOcx) {
238
- if (mayKill && allowlisted) {
239
- if (!killed.has(pid)) {
240
- try {
241
- killFn(pid);
242
- killed.add(pid);
243
- } catch {
244
- // Kill failed: never SetTcpEntry while the process may still own the port.
245
- protectedOcxListener = true;
246
- }
247
- }
248
- if (!isAliveFn(pid)) killed.delete(pid);
249
- else protectedOcxListener = true;
250
- continue;
251
- }
235
+ // A saved PID narrows eligible candidates; it cannot override verifier rejection.
236
+ // Dead ghost owners have already been skipped by the liveness check above.
252
237
  foreignLive = true;
253
238
  continue;
254
239
  }
@@ -22,6 +22,8 @@ import {
22
22
  appendUsageEntry,
23
23
  isKnownAdmissionKind,
24
24
  isKnownInboundProtocol,
25
+ isKnownTerminalSource,
26
+ isKnownTransportPhase,
25
27
  isKnownUsageSurface,
26
28
  isCodexUsageAccountLogLabel,
27
29
  isValidReasoningWireValue,
@@ -320,6 +322,8 @@ export function requestLogEntryFromPersistedUsage(entry: PersistedUsageEntry): R
320
322
  ...(entry.usage ? { usage: entry.usage } : {}),
321
323
  ...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}),
322
324
  ...(entry.attempts !== undefined ? { attempts: entry.attempts } : {}),
325
+ ...(isKnownTransportPhase(entry.transportPhase) ? { transportPhase: entry.transportPhase } : {}),
326
+ ...(isKnownTerminalSource(entry.terminalSource) ? { terminalSource: entry.terminalSource } : {}),
323
327
  ...(routeDecision ? { routeDecision } : {}),
324
328
  ...(claudeCompatibility ? { claudeCompatibility } : {}),
325
329
  };
@@ -441,6 +445,8 @@ export function addRequestLog(entry: RequestLogEntry) {
441
445
  ...(entry.usage ? { usage: entry.usage } : {}),
442
446
  ...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}),
443
447
  ...(entry.attempts !== undefined ? { attempts: entry.attempts } : {}),
448
+ ...(isKnownTransportPhase(entry.transportPhase) ? { transportPhase: entry.transportPhase } : {}),
449
+ ...(isKnownTerminalSource(entry.terminalSource) ? { terminalSource: entry.terminalSource } : {}),
444
450
  ...failureDiagnostics,
445
451
  ...(entry.routeDecision ? { routeDecision: entry.routeDecision } : {}),
446
452
  ...(entry.claudeCompatibility ? { claudeCompatibility: entry.claudeCompatibility } : {}),
@@ -8,6 +8,7 @@ import {
8
8
  } from "../../config";
9
9
  import { parseRequest } from "../../responses/parser";
10
10
  import { externalTaskInputContent } from "../../responses/task-input";
11
+ import { MULTI_AGENT_MODE_HINT_RECOMMENDATION } from "../../codex/multi-agent-mode-policy";
11
12
  import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
12
13
  import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../../adapters/openai-responses";
13
14
  import { expandPreviousResponseInput, previousResponseProviderState, rememberResponseState } from "../../responses/state";
@@ -233,13 +234,7 @@ export function buildToolBridgeMaps(parsed: OcxParsedRequest, budget?: Translato
233
234
 
234
235
 
235
236
 
236
- export const PROACTIVE_MULTI_AGENT_MODE_TEXT = [
237
- "Proactive multi-agent delegation is active.",
238
- "Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies.",
239
- "Delegate independent sub-tasks to sub-agents whenever parallel work would materially improve speed or quality — do not serialize work that can run concurrently.",
240
- "Each sub-agent runs in its own context and can use all available tools; prefer spawning specialists over doing everything yourself.",
241
- "This mode remains active until a later multi-agent mode developer message changes it.",
242
- ].join(" ");
237
+ export const PROACTIVE_MULTI_AGENT_MODE_TEXT = MULTI_AGENT_MODE_HINT_RECOMMENDATION.text;
243
238
 
244
239
  const OPENCODEX_SUBAGENT_GUIDANCE_OPEN_TAG = "<opencodex_subagent_guidance>";
245
240
  const OPENCODEX_SUBAGENT_GUIDANCE_CLOSE_TAG = "</opencodex_subagent_guidance>";
@@ -491,8 +486,8 @@ export async function multiAgentGuidanceText(
491
486
  }
492
487
 
493
488
  const effort = parsed.options.reasoning;
494
- // v1 keeps only the upstream-parity behavior: Proactive text at the top tier
495
- // (ultra arrives as max on the wire). No designation/roster payload here.
489
+ // v1 changes only the delegation trigger at the top tier; other rules still apply.
490
+ // Ultra arrives as max on the wire. No designation/roster payload here.
496
491
  if (effort !== "max" && effort !== "ultra") return null;
497
492
  return `<multi_agent_mode>${PROACTIVE_MULTI_AGENT_MODE_TEXT}</multi_agent_mode>`;
498
493
  }