@yansigit/opencodex 2.33.0 → 2.35.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 (196) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/index-BjCaHxdz.js +112 -0
  3. package/gui/dist/assets/index-DLkXOXLC.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/anthropic.ts +79 -2
  7. package/src/adapters/command-code.ts +141 -23
  8. package/src/adapters/cursor/call-id.ts +44 -0
  9. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  10. package/src/adapters/cursor/discovery.ts +60 -2
  11. package/src/adapters/cursor/effort-map.ts +79 -1
  12. package/src/adapters/cursor/envelope-echo.ts +162 -0
  13. package/src/adapters/cursor/live-models.ts +7 -2
  14. package/src/adapters/cursor/live-transport.ts +17 -1
  15. package/src/adapters/cursor/message-mapper.ts +4 -1
  16. package/src/adapters/cursor/native-exec-fs.ts +13 -12
  17. package/src/adapters/cursor/native-exec-network.ts +3 -5
  18. package/src/adapters/cursor/native-exec-policy.ts +47 -0
  19. package/src/adapters/cursor/native-exec-shell.ts +116 -31
  20. package/src/adapters/cursor/native-exec.ts +38 -10
  21. package/src/adapters/cursor/protobuf-events.ts +28 -2
  22. package/src/adapters/cursor/protobuf-request.ts +93 -41
  23. package/src/adapters/cursor/request-builder.ts +39 -10
  24. package/src/adapters/cursor/tool-definitions.ts +27 -3
  25. package/src/adapters/cursor/tool-result-normalize.ts +51 -6
  26. package/src/adapters/cursor/types.ts +23 -4
  27. package/src/adapters/cursor.ts +170 -29
  28. package/src/adapters/google-aistudio-parser.ts +49 -0
  29. package/src/adapters/google-antigravity-replay.ts +105 -25
  30. package/src/adapters/google-antigravity-wire.ts +5 -0
  31. package/src/adapters/google-errors.ts +41 -12
  32. package/src/adapters/google-http.ts +12 -11
  33. package/src/adapters/google.ts +219 -36
  34. package/src/adapters/image.ts +1 -1
  35. package/src/adapters/kiro-constants.ts +15 -0
  36. package/src/adapters/kiro-tools.ts +43 -15
  37. package/src/adapters/kiro.ts +54 -9
  38. package/src/adapters/openai-chat.ts +286 -242
  39. package/src/adapters/openai-responses.ts +335 -24
  40. package/src/adapters/run-turn-queue.ts +36 -1
  41. package/src/adapters/tool-catalog-nudge.ts +2 -2
  42. package/src/adapters/xai-tool-schema.ts +436 -0
  43. package/src/bridge.ts +67 -26
  44. package/src/chat/inbound.ts +29 -1
  45. package/src/chat/outbound.ts +15 -7
  46. package/src/claude/agents-inject.ts +8 -1
  47. package/src/claude/outbound.ts +10 -8
  48. package/src/cli/account-api.ts +27 -7
  49. package/src/cli/account-extended.ts +10 -3
  50. package/src/cli/account.ts +29 -5
  51. package/src/cli/alias.ts +66 -0
  52. package/src/cli/claude.ts +26 -1
  53. package/src/cli/dispatch.ts +13 -1
  54. package/src/cli/help.ts +1 -0
  55. package/src/cli/index.ts +6 -1
  56. package/src/cli/init.ts +1 -0
  57. package/src/cli/models-runtime.ts +95 -0
  58. package/src/cli/models.ts +13 -7
  59. package/src/cli/provider-runtime.ts +16 -2
  60. package/src/cli/registry.ts +6 -1
  61. package/src/cli/telemetry-commands.ts +25 -0
  62. package/src/cli/v2.ts +34 -10
  63. package/src/codex/account-pause.ts +2 -1
  64. package/src/codex/account-priority.ts +3 -2
  65. package/src/codex/app-server-processes.ts +80 -6
  66. package/src/codex/auth-api.ts +48 -8
  67. package/src/codex/auth-context.ts +21 -18
  68. package/src/codex/catalog/aggregation.ts +6 -0
  69. package/src/codex/catalog/model-metadata.ts +13 -1
  70. package/src/codex/catalog/native-models.ts +5 -2
  71. package/src/codex/catalog/parsing.ts +16 -0
  72. package/src/codex/catalog/provider-fetch.ts +20 -3
  73. package/src/codex/catalog/sync.ts +127 -2
  74. package/src/codex/catalog.ts +1 -1
  75. package/src/codex/codex-write-lock.ts +3 -1
  76. package/src/codex/convergence-types.ts +1 -1
  77. package/src/codex/convergence.ts +22 -2
  78. package/src/codex/desired-state.ts +2 -2
  79. package/src/codex/desktop-app-restart.ts +18 -5
  80. package/src/codex/inject-coordination.ts +83 -0
  81. package/src/codex/inject.ts +14 -1
  82. package/src/codex/log-guard/inspect.ts +22 -4
  83. package/src/codex/model-entitlements.ts +9 -2
  84. package/src/codex/prompt-layers.ts +371 -25
  85. package/src/codex/prompt-text-probe.ts +238 -0
  86. package/src/codex/quota.ts +123 -18
  87. package/src/codex/routing.ts +9 -0
  88. package/src/codex/subagent-model-fallback.ts +198 -27
  89. package/src/codex/transition-state.ts +107 -8
  90. package/src/combos/types.ts +10 -0
  91. package/src/compatibility/openai-responses.ts +33 -1
  92. package/src/config/autonomous-remediation.ts +21 -0
  93. package/src/config/provider-validation.ts +14 -0
  94. package/src/config/rebase-provenance.ts +68 -0
  95. package/src/config.ts +191 -17
  96. package/src/generated/compatibility-version.json +279 -159
  97. package/src/generated/model-metadata.ts +3 -0
  98. package/src/images/loop.ts +5 -4
  99. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  100. package/src/lab/fabric/producer-child.ts +1 -1
  101. package/src/lib/config-ownership.ts +20 -0
  102. package/src/lib/errors.ts +11 -2
  103. package/src/lib/package-tree-integrity.ts +101 -0
  104. package/src/oauth/aistudio-credentials.ts +65 -0
  105. package/src/oauth/aistudio-native-daemon.ts +116 -0
  106. package/src/oauth/aistudio-session-sync.ts +95 -0
  107. package/src/oauth/generic-account-failover.ts +231 -0
  108. package/src/oauth/google-aistudio-auth.ts +98 -0
  109. package/src/oauth/index.ts +57 -5
  110. package/src/oauth/key-providers.ts +18 -1
  111. package/src/oauth/kiro.ts +45 -0
  112. package/src/oauth/login-cli.ts +65 -1
  113. package/src/oauth/types.ts +15 -0
  114. package/src/providers/codex-capacity.ts +5 -2
  115. package/src/providers/command-code-efforts.ts +38 -6
  116. package/src/providers/context-cap.ts +4 -3
  117. package/src/providers/default-aliases.ts +65 -0
  118. package/src/providers/derive.ts +29 -1
  119. package/src/providers/fastwire.ts +7 -1
  120. package/src/providers/model-presets.ts +119 -0
  121. package/src/providers/new-model-policy.ts +146 -0
  122. package/src/providers/provider-id-rewrite.ts +2 -1
  123. package/src/providers/quota.ts +157 -46
  124. package/src/providers/registry.ts +184 -71
  125. package/src/providers/slug-codec.ts +52 -0
  126. package/src/responses/code-mode-helper-compat.ts +50 -0
  127. package/src/responses/custom-tool-compat.ts +34 -10
  128. package/src/responses/parser.ts +4 -0
  129. package/src/responses/schema.ts +5 -1
  130. package/src/responses/thought-signature-replay.ts +17 -0
  131. package/src/router.ts +43 -2
  132. package/src/routing/account-pool/cooldown.ts +8 -0
  133. package/src/routing/account-pool/index.ts +1 -0
  134. package/src/routing/analytics.ts +1 -0
  135. package/src/routing/quota.ts +10 -0
  136. package/src/server/auth-cors.ts +24 -0
  137. package/src/server/chat-completions.ts +26 -16
  138. package/src/server/chat-native-sse.ts +3 -3
  139. package/src/server/chat-native.ts +30 -11
  140. package/src/server/claude-messages.ts +1 -1
  141. package/src/server/effort-policy.ts +16 -0
  142. package/src/server/index.ts +180 -14
  143. package/src/server/lifecycle.ts +52 -1
  144. package/src/server/management/agent-settings-routes.ts +31 -15
  145. package/src/server/management/codex-prompt-routes.ts +570 -0
  146. package/src/server/management/combo-routes.ts +2 -1
  147. package/src/server/management/config-routes.ts +27 -9
  148. package/src/server/management/context.ts +9 -0
  149. package/src/server/management/logs-usage-routes.ts +11 -5
  150. package/src/server/management/model-routes.ts +266 -0
  151. package/src/server/management/oauth-account-routes.ts +13 -3
  152. package/src/server/management/provider-routes.ts +137 -3
  153. package/src/server/management/routing-profile-routes.ts +2 -2
  154. package/src/server/management-api.ts +2 -0
  155. package/src/server/port-reclaim.ts +19 -1
  156. package/src/server/relay-eager.ts +147 -20
  157. package/src/server/relay.ts +251 -19
  158. package/src/server/request-log-conversation.ts +33 -0
  159. package/src/server/request-log.ts +48 -21
  160. package/src/server/responses/collaboration.ts +42 -5
  161. package/src/server/responses/combo-stream-preflight.ts +10 -3
  162. package/src/server/responses/core.ts +575 -140
  163. package/src/server/responses/empty-completion-guard.ts +35 -0
  164. package/src/server/responses/fetch-helpers.ts +14 -6
  165. package/src/server/responses/input-admission.ts +3 -1
  166. package/src/server/responses/passthrough-error.ts +33 -9
  167. package/src/server/responses/policy-fallback.ts +1 -1
  168. package/src/server/responses/responses-field-backfill.ts +105 -13
  169. package/src/server/responses/ws-upstream.ts +35 -5
  170. package/src/server/responses-custom-tool-repair.ts +52 -7
  171. package/src/server/responses-terminal-repair.ts +25 -4
  172. package/src/server/sse-frame-buffer.ts +31 -4
  173. package/src/server/ws-bridge.ts +14 -2
  174. package/src/smoke/fingerprint-cache.ts +133 -0
  175. package/src/smoke/live-scenarios.ts +33 -0
  176. package/src/smoke/runner.ts +119 -0
  177. package/src/telemetry/dispatcher.ts +44 -0
  178. package/src/telemetry/fingerprint.ts +24 -0
  179. package/src/telemetry/hook.ts +43 -0
  180. package/src/telemetry/ledger.ts +54 -0
  181. package/src/telemetry/types.ts +23 -0
  182. package/src/types/config.ts +66 -14
  183. package/src/types/provider.ts +79 -1
  184. package/src/types/request.ts +18 -10
  185. package/src/types/tools.ts +30 -11
  186. package/src/types.ts +1 -0
  187. package/src/usage/command-code-manifest.ts +116 -0
  188. package/src/usage/cost.ts +2 -2
  189. package/src/usage/expected-prices.ts +126 -24
  190. package/src/usage/log.ts +18 -8
  191. package/src/usage/summary.ts +34 -12
  192. package/src/web-search/exa-executor.ts +40 -9
  193. package/src/web-search/index.ts +16 -8
  194. package/src/web-search/loop.ts +5 -4
  195. package/gui/dist/assets/index-DKLr4LTE.js +0 -102
  196. package/gui/dist/assets/index-DrSQdTRd.css +0 -1
@@ -97,6 +97,9 @@ import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostR
97
97
  import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
98
98
  import type { ManagementContext } from "./context";
99
99
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
100
+ import { resolveAiStudioCredentials } from "../../oauth/aistudio-credentials";
101
+ import { buildAiStudioHeaders, parseGoogleCookieJar } from "../../oauth/google-aistudio-auth";
102
+ import { maxWsFrameBytesConfigError, wsUpstreamConfigError } from "../../config/provider-validation";
100
103
 
101
104
  type ProviderPatchApplication =
102
105
  | { error: string }
@@ -108,6 +111,93 @@ type ProviderPatchApplication =
108
111
  headersTouched: boolean;
109
112
  };
110
113
 
114
+ const AI_STUDIO_REAUTH_ERROR = "Session expired or missing — re-authentication required";
115
+ const AI_STUDIO_PROBE_TIMEOUT_MS = 8_000;
116
+ const AI_STUDIO_PROBE_MODEL = "gemini-2.5-flash";
117
+ const AI_STUDIO_ORIGIN = "https://aistudio.google.com";
118
+
119
+ let aiStudioProbeFetchForTests: typeof fetch | undefined;
120
+
121
+ export function setAiStudioProbeFetchForTests(fetchImpl?: typeof fetch): void {
122
+ aiStudioProbeFetchForTests = fetchImpl;
123
+ }
124
+
125
+ function isAiStudioHtmlSignIn(text: string): boolean {
126
+ const lower = text.trim().toLowerCase();
127
+ return lower.startsWith("<!doctype") || lower.startsWith("<html") || lower.includes("accounts.google.com/v3/signin");
128
+ }
129
+
130
+ async function probeAiStudioLiveSession(
131
+ name: string,
132
+ prov: OcxProviderConfig,
133
+ ): Promise<{ ok: boolean; latencyMs: number; authState?: "connected" | "checking" | "needs_reauth" | "unsupported"; message?: string; error?: string }> {
134
+ const credentials = resolveAiStudioCredentials(prov);
135
+ if (credentials.kind !== "ready") {
136
+ return { ok: false, latencyMs: 0, error: AI_STUDIO_REAUTH_ERROR };
137
+ }
138
+ if (process.platform !== "darwin") {
139
+ return {
140
+ ok: true,
141
+ latencyMs: 0,
142
+ authState: "unsupported",
143
+ message: "AI Studio credentials configured; native login is unsupported on this platform",
144
+ };
145
+ }
146
+
147
+ const base = (prov.baseUrl || "https://alkalimakersuite-pa.clients6.google.com").replace(/\/+$/, "");
148
+ const url = base + "/v1internal:generateContent";
149
+ const jar = parseGoogleCookieJar(credentials.cookieHeader);
150
+ const headers = await buildAiStudioHeaders(jar, AI_STUDIO_ORIGIN);
151
+ const body = JSON.stringify({
152
+ model: AI_STUDIO_PROBE_MODEL,
153
+ contents: [{ role: "user", parts: [{ text: "ping" }] }],
154
+ generationConfig: { maxOutputTokens: 1 },
155
+ });
156
+
157
+ const controller = new AbortController();
158
+ const timer = setTimeout(() => controller.abort(), AI_STUDIO_PROBE_TIMEOUT_MS);
159
+ const started = Date.now();
160
+ try {
161
+ const outboundProvider = aiStudioProbeFetchForTests
162
+ ? { ...prov, fetch: aiStudioProbeFetchForTests }
163
+ : prov;
164
+ const response = await providerOutboundPost(name, outboundProvider, url, {
165
+ headers,
166
+ body,
167
+ signal: controller.signal,
168
+ });
169
+ const latencyMs = Date.now() - started;
170
+ const contentType = (response.headers.get("content-type") ?? "").toLowerCase();
171
+ const text = await response.text().catch(() => "");
172
+
173
+ if ((response.status >= 300 && response.status < 400) || response.status === 401 || response.status === 403) {
174
+ return { ok: false, latencyMs, error: AI_STUDIO_REAUTH_ERROR };
175
+ }
176
+ if (contentType.includes("text/html") || isAiStudioHtmlSignIn(text)) {
177
+ return { ok: false, latencyMs, error: AI_STUDIO_REAUTH_ERROR };
178
+ }
179
+ if (response.status !== 200) {
180
+ return { ok: false, latencyMs, error: "AI Studio connection probe failed" };
181
+ }
182
+ try {
183
+ JSON.parse(text);
184
+ } catch {
185
+ return { ok: false, latencyMs, error: "AI Studio connection probe failed" };
186
+ }
187
+ return {
188
+ ok: true,
189
+ latencyMs,
190
+ authState: "connected",
191
+ message: "AI Studio session verified",
192
+ };
193
+ } catch {
194
+ return { ok: false, latencyMs: Date.now() - started, error: "AI Studio connection probe failed" };
195
+ } finally {
196
+ clearTimeout(timer);
197
+ }
198
+ }
199
+
200
+
111
201
  /**
112
202
  * Apply the recognized PATCH field mask onto a provider copy. The caller runs this once
113
203
  * for validation and again inside the config mutation lock against the newest provider,
@@ -206,6 +296,28 @@ function applyProviderPatchFields(
206
296
  next.liveModels = rawBody.liveModels;
207
297
  touched = true;
208
298
  }
299
+ if (Object.hasOwn(rawBody, "wsUpstream")) {
300
+ const value = rawBody.wsUpstream;
301
+ if (value === null) {
302
+ delete next.wsUpstream;
303
+ } else {
304
+ const error = wsUpstreamConfigError(value);
305
+ if (error) return { error };
306
+ next.wsUpstream = value as boolean;
307
+ }
308
+ touched = true;
309
+ }
310
+ if (Object.hasOwn(rawBody, "maxWsFrameBytes")) {
311
+ const value = rawBody.maxWsFrameBytes;
312
+ if (value === null) {
313
+ delete next.maxWsFrameBytes;
314
+ } else {
315
+ const error = maxWsFrameBytesConfigError(value);
316
+ if (error) return { error };
317
+ next.maxWsFrameBytes = value as number;
318
+ }
319
+ touched = true;
320
+ }
209
321
  if (Object.hasOwn(rawBody, "xaiResponsesOptIn")) {
210
322
  if (name !== "xai") return { error: "xaiResponsesOptIn is valid only for provider xai" };
211
323
  if (typeof rawBody.xaiResponsesOptIn !== "boolean") {
@@ -482,6 +594,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
482
594
  contextWindow: p.contextWindow,
483
595
  modelContextWindows: p.modelContextWindows,
484
596
  modelAutoCompactTokenLimits: p.modelAutoCompactTokenLimits,
597
+ wsUpstream: p.wsUpstream,
598
+ maxWsFrameBytes: p.maxWsFrameBytes,
485
599
  modelSupportsServiceTier: p.modelSupportsServiceTier,
486
600
  noStructuredOutputModels: p.noStructuredOutputModels,
487
601
  upstreamHttpVersion: p.upstreamHttpVersion,
@@ -575,15 +689,26 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
575
689
  let body: { name?: unknown; provider?: unknown; setDefault?: boolean };
576
690
  try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
577
691
  const name = typeof body.name === "string" ? body.name.trim() : "";
578
- const providerError = providerManagementConfigError(name, body.provider);
692
+ const submittedProvider = isPlainRecord(body.provider)
693
+ ? structuredClone(body.provider) as Record<string, unknown>
694
+ : body.provider;
695
+ const submittedCredential = isPlainRecord(submittedProvider)
696
+ && isPlainRecord(submittedProvider.azureCredential)
697
+ ? submittedProvider.azureCredential as Record<string, unknown>
698
+ : undefined;
699
+ if (typeof submittedCredential?.managedIdentityClientId === "string") {
700
+ submittedCredential.managedIdentityClientId = submittedCredential.managedIdentityClientId.trim();
701
+ }
702
+ const providerError = providerManagementConfigError(name, submittedProvider);
579
703
  if (providerError) return jsonResponse({ error: providerError }, 400);
580
- const serviceTierError = providerServiceTierConfigError(name, body.provider);
704
+ const serviceTierError = providerServiceTierConfigError(name, submittedProvider);
581
705
  if (serviceTierError) return jsonResponse({ error: serviceTierError }, 400);
582
- const prov = body.provider ? stripCodexRuntimeProviderFields(body.provider as OcxProviderConfig) : undefined;
706
+ const prov = submittedProvider ? stripCodexRuntimeProviderFields(submittedProvider as OcxProviderConfig) : undefined;
583
707
  // PATCH already clears on null; POST persisted the body as submitted, so a `null` here
584
708
  // reached disk and the next loadConfig() refused it. Canonicalize to absent, which is what
585
709
  // "clear" means everywhere else.
586
710
  if (prov && prov.upstreamHttpVersion === null) delete prov.upstreamHttpVersion;
711
+ if (prov && prov.wsUpstream === null) delete prov.wsUpstream;
587
712
  if (!name || !prov?.adapter || !prov?.baseUrl) {
588
713
  return jsonResponse({ error: "name, provider.adapter and provider.baseUrl are required" }, 400);
589
714
  }
@@ -628,6 +753,12 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
628
753
  // erase hand-edited per-model prices from Logs/Usage estimates.
629
754
  const existingCosts = config.providers[name]?.modelCosts;
630
755
  if (existingCosts && !prov.modelCosts) prov.modelCosts = existingCosts;
756
+ // And to the per-provider account-failover opt-out (#2568d). `ProviderPayload` has no
757
+ // member for it either, so an add/edit save structurally cannot carry it — and dropping it
758
+ // silently ENABLES rotation, because activation is presence-driven once the knob is gone.
759
+ // An overwrite must not spend a second subscription account's quota as a side effect.
760
+ const existingFailover = config.providers[name]?.oauthAccountFailover;
761
+ if (existingFailover && !prov.oauthAccountFailover) prov.oauthAccountFailover = existingFailover;
631
762
  // ...and to hand-edited context windows. `ProviderPayload` (gui/src/provider-payload.ts)
632
763
  // has no member for either field, so the add/edit form structurally cannot send them:
633
764
  // absence in the request means "not carried", never "the user deleted it". Deletion goes
@@ -838,6 +969,9 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
838
969
  message: "Passthrough provider is configured (forwards your Codex login; no upstream /models).",
839
970
  });
840
971
  }
972
+ if (prov.googleMode === "ai-studio-web" || name === "google-aistudio") {
973
+ return jsonResponse(await probeAiStudioLiveSession(name, prov));
974
+ }
841
975
  if (prov.liveModels === false) {
842
976
  // A static catalog has no live discovery endpoint to test. This is neither
843
977
  // positive connectivity evidence nor an outage, and it must stay before
@@ -20,7 +20,7 @@ import { assemblePolicyCandidateEvidence } from "../../routing/compatibility/ass
20
20
  import { activateLab, labActivationRequired } from "../../lib/lab-activation";
21
21
  import { quotaEvidenceForCandidate } from "../../routing/quota";
22
22
  import { routedProviderConfig } from "../../router";
23
- import { saveConfigPreservingClaudeCode, getConfigDir } from "../../config";
23
+ import { deleteConfigTopLevelKey, saveConfigPreservingClaudeCode, getConfigDir } from "../../config";
24
24
  import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
25
25
  import { isPlainRecord } from "./shared";
26
26
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
@@ -337,7 +337,7 @@ export async function handleRoutingProfileRoutes(ctx: ManagementContext): Promis
337
337
  const nextProfiles = { ...(config.routingProfiles ?? {}) };
338
338
  delete nextProfiles[id];
339
339
  if (Object.keys(nextProfiles).length > 0) config.routingProfiles = nextProfiles;
340
- else delete config.routingProfiles;
340
+ else deleteConfigTopLevelKey(config, "routingProfiles");
341
341
  const saveConfigPreservingClaudeCodeSafe = deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode;
342
342
  saveConfigPreservingClaudeCodeSafe(config);
343
343
  reconcileLiveStateStores();
@@ -70,6 +70,7 @@ import { handleOauthAccountRoutes } from "./management/oauth-account-routes";
70
70
  import { handleComboRoutes } from "./management/combo-routes";
71
71
  import { handleSystemRoutes } from "./management/system-routes";
72
72
  import { handleSidebarRoutes } from "./management/sidebar-routes";
73
+ import { handleCodexPromptRoutes } from "./management/codex-prompt-routes";
73
74
  import { handleIntegrationRoutes } from "./management/integration-routes";
74
75
  import { handleNativeIntegrationRoutes } from "./management/native-integration-routes";
75
76
  import type { ManagementContext } from "./management/context";
@@ -229,6 +230,7 @@ export async function handleManagementAPI(
229
230
  ?? (await handleIntegrationRoutes(ctx))
230
231
  ?? (await handleNativeIntegrationRoutes(ctx))
231
232
  ?? (await handleAgentSettingsRoutes(ctx))
233
+ ?? (await handleCodexPromptRoutes(ctx))
232
234
  ?? (await handleOauthAccountRoutes(ctx))
233
235
  ?? (await handleComboRoutes(ctx))
234
236
  ?? (await handleSystemRoutes(ctx))
@@ -144,6 +144,24 @@ export function scanListenPids(port: number): ListenPidScan {
144
144
  .filter(pid => Number.isSafeInteger(pid) && pid > 0),
145
145
  };
146
146
  } catch (lsofErr) {
147
+ if (lsofErr && typeof lsofErr === "object" && "status" in lsofErr && lsofErr.status === 1) {
148
+ return { ok: true, pids: [] };
149
+ }
150
+ try {
151
+ const output = execFileSync("ss", ["-tlnp", `sport = :${port}`], {
152
+ encoding: "utf-8",
153
+ stdio: ["ignore", "pipe", "ignore"],
154
+ timeout: 3000,
155
+ });
156
+ const pids = new Set<number>();
157
+ for (const match of output.matchAll(/pid=(\d+)/g)) {
158
+ const pid = Number(match[1]);
159
+ if (Number.isSafeInteger(pid) && pid > 0) pids.add(pid);
160
+ }
161
+ return { ok: true, pids: [...pids] };
162
+ } catch {
163
+ /* ss unavailable or unsupported on this platform; try netstat */
164
+ }
147
165
  try {
148
166
  const output = execFileSync("netstat", ["-anlp"], {
149
167
  encoding: "utf-8",
@@ -154,7 +172,7 @@ export function scanListenPids(port: number): ListenPidScan {
154
172
  } catch (netstatErr) {
155
173
  return {
156
174
  ok: false,
157
- error: `lsof/netstat unavailable: ${String(lsofErr)} / ${String(netstatErr)}`,
175
+ error: `lsof/ss/netstat unavailable: ${String(lsofErr)} / ${String(netstatErr)}`,
158
176
  };
159
177
  }
160
178
  }
@@ -24,7 +24,12 @@
24
24
  * up to the drain window.
25
25
  */
26
26
 
27
- import { buildFailedTailPayload, createSseTerminalOutputBoundary } from "./relay";
27
+ import {
28
+ adapterEofIncompleteFrame,
29
+ createSseTerminalOutputBoundary,
30
+ doneFrame,
31
+ failedTailFrame,
32
+ } from "./relay";
28
33
  import {
29
34
  nextSseBlock,
30
35
  payloadRewriteAsBlockRewrite,
@@ -100,9 +105,19 @@ export function relaySseEagerBounded(
100
105
  const now = opts?.now ?? Date.now;
101
106
 
102
107
  const reader = body.getReader();
108
+ const terminalEncoder = new TextEncoder();
109
+ const adapterEofFrame = adapterEofIncompleteFrame(terminalEncoder);
110
+ const terminalSentinel = doneFrame(terminalEncoder);
103
111
  const terminalBoundary = createSseTerminalOutputBoundary();
104
112
  const activeRewrite: SseBlockRewrite | undefined = hooks.rewriteBlocks
105
113
  ?? (hooks.rewritePayload ? payloadRewriteAsBlockRewrite(hooks.rewritePayload) : undefined);
114
+ const encodeFailedTail = (error: unknown): Uint8Array | null => {
115
+ try {
116
+ return failedTailFrame(terminalEncoder, error);
117
+ } catch {
118
+ return null;
119
+ }
120
+ };
106
121
  const rewriteDecoder = activeRewrite ? new TextDecoder() : null;
107
122
  const rewriteEncoder = activeRewrite ? new TextEncoder() : null;
108
123
  const rewriteBudget = opts?.rewriteBudget;
@@ -164,13 +179,21 @@ export function relaySseEagerBounded(
164
179
  let queuedBytes = 0;
165
180
  let cancelled = false;
166
181
  let done = false;
167
- const terminalSentinel = new TextEncoder().encode("data: [DONE]\n\n");
168
182
  // Pause gate: resolved by client pull, client cancel, or upstream abort so a
169
183
  // paused producer ALWAYS resumes (audit blocker 2 — no deadlock; onDone and
170
184
  // turn unregistration stay reachable, drainAndShutdown never hangs).
171
185
  let wake: (() => void) | null = null;
172
186
  const wakeUp = () => { const w = wake; wake = null; w?.(); };
173
- const paused = () => new Promise<void>(resolve => { wake = resolve; });
187
+ const paused = () => new Promise<void>(resolve => {
188
+ wake = resolve;
189
+ // A pull, cancel, or abort can win the tiny window between the loop's
190
+ // predicate check and installing this resolver. Re-check every wake
191
+ // predicate after installation so that an already-fired wake cannot leave
192
+ // the producer parked forever.
193
+ if (queuedBytes <= maxQueueBytes || cancelled || upstream.signal.aborted) {
194
+ wakeUp();
195
+ }
196
+ });
174
197
  upstream.signal.addEventListener("abort", wakeUp, { once: true });
175
198
 
176
199
  let controllerRef: ReadableStreamDefaultController<Uint8Array> | null = null;
@@ -196,6 +219,9 @@ export function relaySseEagerBounded(
196
219
 
197
220
  const producer = async () => {
198
221
  let syntheticKind: "incomplete" | "failed" | null = null;
222
+ let deliveryFallbackSent = false;
223
+ let priorRewriteFailure = false;
224
+ let priorRewriteError: unknown;
199
225
  // reader.read() is not intrinsically tied to the upstream AbortController
200
226
  // (a fetch body usually rejects on abort, but that coupling is the fetch
201
227
  // implementation's, not the stream's), so abort must break a parked read on
@@ -221,18 +247,46 @@ export function relaySseEagerBounded(
221
247
  if (upstreamDone) {
222
248
  hooks.finishInspection();
223
249
  const boundedTail = terminalBoundary.finish();
250
+ let clientTail = boundedTail;
251
+ let rewriteFailed = false;
252
+ let rewriteError: unknown;
224
253
  if (activeRewrite) {
225
- const rewritten = rewriteOutbound(boundedTail);
226
- const tail = joinUint8Arrays(rewritten, flushRewriteTail());
227
- if (tail.byteLength > 0 && !cancelled) {
228
- queuedBytes += tail.byteLength;
229
- try { controllerRef?.enqueue(tail); } catch { /* client already gone */ }
254
+ try {
255
+ const rewritten = rewriteOutbound(boundedTail);
256
+ clientTail = joinUint8Arrays(rewritten, flushRewriteTail());
257
+ } catch (error) {
258
+ rewriteFailed = true;
259
+ rewriteError = error;
260
+ clientTail = new Uint8Array(0);
261
+ }
262
+ }
263
+ if (rewriteFailed) {
264
+ const safeTail = encodeFailedTail(rewriteError);
265
+ if (safeTail && !cancelled && !upstream.signal.aborted) {
266
+ if (!hooks.sawTerminal()) syntheticKind = "failed";
267
+ queuedBytes += safeTail.byteLength;
268
+ try { controllerRef?.enqueue(safeTail); } catch { /* client already torn down */ }
269
+ try { controllerRef?.close(); } catch { /* client already gone */ }
230
270
  }
231
- } else if (boundedTail.byteLength > 0 && !cancelled) {
232
- queuedBytes += boundedTail.byteLength;
233
- try { controllerRef?.enqueue(boundedTail); } catch { /* client already gone */ }
271
+ break;
272
+ }
273
+ if (clientTail.byteLength > 0 && !cancelled) {
274
+ queuedBytes += clientTail.byteLength;
275
+ try { controllerRef?.enqueue(clientTail); } catch { /* client already gone */ }
234
276
  }
235
- if (!hooks.sawTerminal() && !cancelled && !upstream.signal.aborted) {
277
+ if (terminalBoundary.terminalSeen()) {
278
+ if (!terminalBoundary.doneSeen() && !cancelled) {
279
+ queuedBytes += terminalSentinel.byteLength;
280
+ try { controllerRef?.enqueue(terminalSentinel); } catch { /* client already gone */ }
281
+ }
282
+ } else if (!hooks.sawTerminal() && !cancelled && !upstream.signal.aborted) {
283
+ // A clean 200 EOF without a Responses terminal must be visible to
284
+ // Codex as one incomplete turn, followed by the normal sentinel.
285
+ queuedBytes += adapterEofFrame.byteLength + terminalSentinel.byteLength;
286
+ try {
287
+ controllerRef?.enqueue(adapterEofFrame);
288
+ controllerRef?.enqueue(terminalSentinel);
289
+ } catch { /* client already gone */ }
236
290
  syntheticKind = "incomplete";
237
291
  }
238
292
  break;
@@ -247,7 +301,21 @@ export function relaySseEagerBounded(
247
301
  continue;
248
302
  }
249
303
  const terminalBounded = terminalBoundary.feed(value);
250
- const outbound = activeRewrite ? rewriteOutbound(terminalBounded) : terminalBounded;
304
+ let outbound: Uint8Array;
305
+ if (activeRewrite) {
306
+ try {
307
+ outbound = rewriteOutbound(terminalBounded);
308
+ } catch (error) {
309
+ // Preserve the first rewrite failure across the teardown flush. A
310
+ // second empty flush may succeed, but the terminal bytes still
311
+ // must not bypass the failed rewrite or become DONE-only output.
312
+ priorRewriteFailure = true;
313
+ priorRewriteError = error;
314
+ throw error;
315
+ }
316
+ } else {
317
+ outbound = terminalBounded;
318
+ }
251
319
  if (outbound.byteLength > 0) {
252
320
  queuedBytes += outbound.byteLength;
253
321
  try {
@@ -278,16 +346,75 @@ export function relaySseEagerBounded(
278
346
  } catch (err) {
279
347
  // Upstream read failure. Distinguish genuine mid-stream reset from
280
348
  // abort-driven teardown (shutdown/cancel-expiry) — audit M3.
281
- if (!hooks.sawTerminal() && !cancelled && !upstream.signal.aborted) {
349
+ // A read can fail after delivering an unterminated terminal block. Flush
350
+ // both observers before deciding whether this is a synthetic reset so
351
+ // eager mode matches the tee/pull boundary semantics at EOF.
352
+ let boundedTail: Uint8Array<ArrayBufferLike> = new Uint8Array(0);
353
+ let tailTerminal = false;
354
+ let tailDone = false;
355
+ try { hooks.finishInspection(); } catch { /* preserve the original read failure */ }
356
+ try {
357
+ boundedTail = terminalBoundary.finish();
358
+ tailTerminal = terminalBoundary.terminalSeen();
359
+ tailDone = terminalBoundary.doneSeen();
360
+ } catch {
361
+ // A near-cap ambiguous delimiter tail may itself overflow at EOF.
362
+ // Preserve the original read/framing failure and continue emitting
363
+ // the bounded failed tail instead of letting cleanup throw again.
364
+ }
365
+ let clientTail: Uint8Array<ArrayBufferLike> = boundedTail;
366
+ let rewriteFailed = false;
367
+ let rewriteError: unknown;
368
+ if (priorRewriteFailure) {
369
+ rewriteFailed = true;
370
+ rewriteError = priorRewriteError;
371
+ clientTail = new Uint8Array(0);
372
+ } else if (activeRewrite) {
373
+ try {
374
+ const rewritten = rewriteOutbound(boundedTail);
375
+ clientTail = joinUint8Arrays(rewritten, flushRewriteTail());
376
+ } catch (error) {
377
+ rewriteFailed = true;
378
+ rewriteError = error;
379
+ clientTail = new Uint8Array(0);
380
+ }
381
+ }
382
+ if (clientTail.byteLength > 0 && !cancelled && !upstream.signal.aborted) {
383
+ queuedBytes += clientTail.byteLength;
384
+ try { controllerRef?.enqueue(clientTail); } catch { /* client already torn down */ }
385
+ }
386
+ if (rewriteFailed && !cancelled && !upstream.signal.aborted) {
387
+ // Never bypass a client rewrite after it fails: boundedTail can contain
388
+ // provider metadata or content that the active rewrite was required to
389
+ // remove. Emit one safe failed envelope instead. When inspection has
390
+ // already reported the real upstream terminal, this is a delivery
391
+ // fallback only and must not create a second accounting outcome.
392
+ const safeTail = encodeFailedTail(rewriteError ?? err);
393
+ if (safeTail && !cancelled && !upstream.signal.aborted) {
394
+ if (!hooks.sawTerminal()) syntheticKind = "failed";
395
+ deliveryFallbackSent = true;
396
+ queuedBytes += safeTail.byteLength;
397
+ try { controllerRef?.enqueue(safeTail); } catch { /* client already torn down */ }
398
+ try { controllerRef?.close(); } catch { /* client already gone */ }
399
+ }
400
+ } else if (tailTerminal && !cancelled && !upstream.signal.aborted) {
401
+ if (!tailDone) {
402
+ queuedBytes += terminalSentinel.byteLength;
403
+ try { controllerRef?.enqueue(terminalSentinel); } catch { /* client already gone */ }
404
+ }
405
+ } else if (!tailTerminal && !cancelled && !upstream.signal.aborted) {
282
406
  // Serializing `err` can run user-defined accessors (Error.message
283
407
  // getters, toString) that re-entrantly cancel the client or abort the
284
408
  // upstream. Build the tail FIRST, then re-check eligibility before
285
409
  // committing to the synthetic terminal (adversarial review blocker).
286
- const tail = new TextEncoder().encode(
287
- `\n\nevent: response.failed\ndata: ${buildFailedTailPayload(err)}\n\ndata: [DONE]\n\n`,
288
- );
289
- if (!hooks.sawTerminal() && !cancelled && !upstream.signal.aborted) {
290
- syntheticKind = "failed";
410
+ const tail = encodeFailedTail(err);
411
+ if (tail && !cancelled && !upstream.signal.aborted) {
412
+ // Inspection and client framing have separate bounded parsers. If
413
+ // inspection resynchronized after an oversized frame and observed a
414
+ // later real terminal, it still must not suppress a terminal delivery
415
+ // to the client. Only accounting remains tied to the inspected result.
416
+ if (!hooks.sawTerminal()) syntheticKind = "failed";
417
+ deliveryFallbackSent = true;
291
418
  queuedBytes += tail.byteLength;
292
419
  try { controllerRef?.enqueue(tail); } catch { /* client already torn down */ }
293
420
  try { controllerRef?.close(); } catch { /* client already torn down */ }
@@ -306,7 +433,7 @@ export function relaySseEagerBounded(
306
433
  if (cancelled && !hooks.sawTerminal()) {
307
434
  hooks.onClientCancel();
308
435
  }
309
- if (cancelled || upstream.signal.aborted || syntheticKind === "failed") {
436
+ if (cancelled || upstream.signal.aborted || syntheticKind === "failed" || deliveryFallbackSent) {
310
437
  upstream.abort();
311
438
  reader.cancel().catch(() => {});
312
439
  }