@bitkyc08/opencodex 2.52.0 → 2.53.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 (236) hide show
  1. package/gui/dist/assets/index-BBOZWGB6.css +1 -0
  2. package/gui/dist/assets/index-BlO4Yl6q.js +128 -0
  3. package/gui/dist/index.html +2 -2
  4. package/native/remote-workspace-helper/Cargo.lock +130 -0
  5. package/native/remote-workspace-helper/Cargo.toml +24 -0
  6. package/native/remote-workspace-helper/src/main.rs +49 -0
  7. package/native/remote-workspace-helper/src/protocol.rs +246 -0
  8. package/native/remote-workspace-helper/src/sandbox/macos.rs +19 -0
  9. package/native/remote-workspace-helper/src/sandbox/mod.rs +77 -0
  10. package/native/remote-workspace-helper/src/sandbox/windows.rs +15 -0
  11. package/package.json +6 -1
  12. package/src/adapters/anthropic-image-normalize.ts +30 -2
  13. package/src/adapters/anthropic.ts +1 -1
  14. package/src/adapters/base.ts +8 -2
  15. package/src/adapters/cursor/cursor-errors.ts +12 -0
  16. package/src/adapters/cursor/thread-continuity.ts +93 -0
  17. package/src/adapters/cursor.ts +104 -73
  18. package/src/adapters/devin/cloud-direct/chat.ts +312 -23
  19. package/src/adapters/devin/cloud-direct/metadata.ts +31 -2
  20. package/src/adapters/devin/live-models.ts +70 -3
  21. package/src/adapters/devin.ts +281 -21
  22. package/src/adapters/google-wire-compiler.ts +14 -6
  23. package/src/adapters/google.ts +22 -8
  24. package/src/adapters/kiro/adapter.ts +316 -0
  25. package/src/adapters/kiro/conversation.ts +136 -0
  26. package/src/adapters/kiro/payload.ts +432 -0
  27. package/src/adapters/kiro/reasoning.ts +56 -0
  28. package/src/adapters/kiro/stream.ts +1153 -0
  29. package/src/adapters/kiro/usage.ts +223 -0
  30. package/src/adapters/kiro/wire.ts +76 -0
  31. package/src/adapters/kiro.ts +8 -2319
  32. package/src/adapters/mimo-free.ts +1 -1
  33. package/src/adapters/openai-chat-images.ts +101 -0
  34. package/src/adapters/openai-chat.ts +201 -181
  35. package/src/adapters/openai-responses.ts +92 -224
  36. package/src/adapters/registry.ts +0 -7
  37. package/src/adapters/run-turn-queue.ts +13 -6
  38. package/src/bridge.ts +14 -15
  39. package/src/chat/inbound.ts +29 -4
  40. package/src/chat/outbound.ts +145 -107
  41. package/src/claude/desktop-profile.ts +4 -6
  42. package/src/cli/account-api.ts +14 -0
  43. package/src/cli/account-extended.ts +1 -1
  44. package/src/cli/account-history.ts +60 -0
  45. package/src/cli/account-main.ts +80 -0
  46. package/src/cli/account.ts +11 -3
  47. package/src/cli/capabilities.ts +113 -0
  48. package/src/cli/catalog.ts +109 -0
  49. package/src/cli/dispatch.ts +9 -0
  50. package/src/cli/help.ts +2 -0
  51. package/src/cli/index.ts +2 -2
  52. package/src/cli/observe.ts +28 -1
  53. package/src/cli/opencode.ts +42 -8
  54. package/src/cli/provider-runtime.ts +11 -1
  55. package/src/cli/provider.ts +22 -2
  56. package/src/cli/registry.ts +21 -0
  57. package/src/cli/remote-workspace.ts +154 -0
  58. package/src/cli/status.ts +39 -7
  59. package/src/cli/usage-report.ts +14 -2
  60. package/src/client/hub-client.ts +34 -0
  61. package/src/client/hub-state.ts +9 -1
  62. package/src/codex/account-store.ts +78 -0
  63. package/src/codex/auth-api.ts +81 -54
  64. package/src/codex/auth-context.ts +45 -16
  65. package/src/codex/catalog/effort.ts +1 -1
  66. package/src/codex/catalog/metadata.ts +3 -6
  67. package/src/codex/catalog/native-models.ts +4 -4
  68. package/src/codex/catalog/parsing.ts +2 -20
  69. package/src/codex/catalog/provider-fetch.ts +10 -1
  70. package/src/codex/catalog/remote.ts +233 -0
  71. package/src/codex/catalog/sync.ts +403 -35
  72. package/src/codex/convergence.ts +1 -1
  73. package/src/codex/history-manifest.ts +36 -0
  74. package/src/codex/history-provider.ts +32 -5
  75. package/src/codex/inject.ts +9 -0
  76. package/src/codex/main-account.ts +113 -0
  77. package/src/codex/main-device-reauth-api.ts +89 -0
  78. package/src/codex/main-device-reauth.ts +217 -0
  79. package/src/codex/native-residue.ts +9 -2
  80. package/src/codex/quota-auto-refresh.ts +3 -2
  81. package/src/codex/quota-capacity.ts +98 -0
  82. package/src/codex/quota-history.ts +160 -0
  83. package/src/codex/quota-types.ts +8 -0
  84. package/src/codex/quota.ts +118 -91
  85. package/src/codex/refresh.ts +2 -1
  86. package/src/codex/routing.ts +90 -17
  87. package/src/codex/sync.ts +33 -4
  88. package/src/combos/request.ts +19 -1
  89. package/src/config/multi-agent-surface.ts +61 -0
  90. package/src/config/provider-validation.ts +176 -0
  91. package/src/config.ts +213 -11
  92. package/src/generated/compatibility-version.json +436 -168
  93. package/src/images/loop.ts +119 -36
  94. package/src/lib/admission.ts +12 -6
  95. package/src/lib/redact.ts +7 -0
  96. package/src/lib/translator-budget.ts +4 -3
  97. package/src/lib/windows-atomic-replace.ts +1 -0
  98. package/src/lib/windows-elevation.ts +1 -1
  99. package/src/oauth/chatgpt-device.ts +62 -5
  100. package/src/oauth/devin/cli-import.ts +130 -0
  101. package/src/oauth/devin.ts +63 -8
  102. package/src/oauth/index.ts +29 -14
  103. package/src/oauth/kiro.ts +18 -6
  104. package/src/oauth/login-cli.ts +9 -1
  105. package/src/oauth/meta-muse-device.ts +464 -0
  106. package/src/oauth/meta-muse.ts +123 -32
  107. package/src/oauth/pool-kernel.ts +9 -0
  108. package/src/oauth/pool-settings-capability.ts +2 -2
  109. package/src/oauth/store.ts +57 -0
  110. package/src/oauth/types.ts +31 -0
  111. package/src/providers/derive.ts +13 -3
  112. package/src/providers/devin-cli-authmode-migration.ts +57 -35
  113. package/src/providers/devin-provider-merge-migration.ts +240 -0
  114. package/src/providers/muse-key-quota.ts +117 -0
  115. package/src/providers/muse-subscription-usage.ts +14 -2
  116. package/src/providers/openai-sidecar.ts +25 -3
  117. package/src/providers/opencode-zen-rate-limit.ts +58 -0
  118. package/src/providers/provider-id-rewrite.ts +20 -5
  119. package/src/providers/quota-types.ts +12 -0
  120. package/src/providers/quota.ts +143 -102
  121. package/src/providers/reasoning-metadata.ts +543 -0
  122. package/src/providers/registry.ts +80 -49
  123. package/src/reasoning-effort.ts +26 -2
  124. package/src/remote/hub-usage.ts +32 -0
  125. package/src/remote-control/index.ts +192 -41
  126. package/src/remote-control/workspace-activation.ts +9 -0
  127. package/src/remote-control/workspace-agent-connection.ts +366 -0
  128. package/src/remote-control/workspace-claude-runtime.ts +243 -0
  129. package/src/remote-control/workspace-codex-runtime.ts +531 -0
  130. package/src/remote-control/workspace-codex-sandbox.ts +115 -0
  131. package/src/remote-control/workspace-command-runner.ts +748 -0
  132. package/src/remote-control/workspace-coordinator.ts +231 -0
  133. package/src/remote-control/workspace-device.ts +585 -0
  134. package/src/remote-control/workspace-executable.ts +43 -0
  135. package/src/remote-control/workspace-executor.ts +397 -0
  136. package/src/remote-control/workspace-hub.ts +519 -0
  137. package/src/remote-control/workspace-pi-runtime.ts +382 -0
  138. package/src/remote-control/workspace-process.ts +129 -0
  139. package/src/remote-control/workspace-rpc.ts +304 -0
  140. package/src/remote-control/workspace-runtime.ts +60 -0
  141. package/src/remote-control/workspace-secret-store.ts +39 -0
  142. package/src/remote-control/workspace-sessions.ts +799 -0
  143. package/src/remote-control/workspace-tool-bridge.ts +192 -0
  144. package/src/responses/code-mode-helper-compat.ts +22 -3
  145. package/src/responses/hosted-tool-policy.ts +0 -1
  146. package/src/responses/muse-tool-name-alias.ts +379 -0
  147. package/src/responses/plaintext-v2-agent-messages.ts +902 -0
  148. package/src/router.ts +7 -0
  149. package/src/routing/compatibility/behavior.ts +0 -1
  150. package/src/server/audio-client.ts +64 -0
  151. package/src/server/audio-dictation.ts +91 -0
  152. package/src/server/audio-live.ts +185 -0
  153. package/src/server/audio-transcriptions.ts +183 -0
  154. package/src/server/audio-upstream.ts +153 -0
  155. package/src/server/auth-cors.ts +61 -2
  156. package/src/server/chat-completions.ts +1 -1
  157. package/src/server/chat-native-sse.ts +92 -48
  158. package/src/server/chat-native.ts +37 -15
  159. package/src/server/hub-usage.ts +57 -0
  160. package/src/server/images.ts +4 -0
  161. package/src/server/index.ts +722 -57
  162. package/src/server/lifecycle.ts +5 -6
  163. package/src/server/live-call-bindings.ts +60 -0
  164. package/src/server/live.ts +12 -1
  165. package/src/server/management/agent-settings-routes.ts +25 -4
  166. package/src/server/management/api-access.ts +37 -0
  167. package/src/server/management/api-key-usage.ts +7 -2
  168. package/src/server/management/config-routes.ts +1 -18
  169. package/src/server/management/context.ts +15 -0
  170. package/src/server/management/logs-usage-routes.ts +2 -0
  171. package/src/server/management/oauth-account-routes.ts +39 -12
  172. package/src/server/management/provider-routes.ts +125 -2
  173. package/src/server/management/remote-workspace-routes.ts +140 -0
  174. package/src/server/management/route-registry.ts +15 -0
  175. package/src/server/management/usage-aggregate-cache.ts +14 -15
  176. package/src/server/management/usage-summary-cache.ts +2 -0
  177. package/src/server/management-api.ts +23 -0
  178. package/src/server/ports.ts +17 -0
  179. package/src/server/relay-eager.ts +4 -1
  180. package/src/server/relay.ts +70 -10
  181. package/src/server/request-decompress.ts +6 -3
  182. package/src/server/responses/agent-task-recovery.ts +25 -32
  183. package/src/server/responses/codex-auth-error.ts +11 -0
  184. package/src/server/responses/codex-ws-exchange.ts +52 -3
  185. package/src/server/responses/codex-ws-wire.ts +55 -0
  186. package/src/server/responses/compact.ts +9 -1
  187. package/src/server/responses/core.ts +337 -73
  188. package/src/server/responses/encrypted-payload.ts +45 -2
  189. package/src/server/responses/ws-upstream.ts +4 -1
  190. package/src/server/responses-self-named-namespace-scrub.ts +1 -3
  191. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  192. package/src/server/search.ts +3 -0
  193. package/src/server/sse-payload-rewrite.ts +136 -51
  194. package/src/server/ws-bridge.ts +35 -1
  195. package/src/service/cli.ts +372 -0
  196. package/src/service/diagnostics.ts +340 -0
  197. package/src/service/guards.ts +303 -0
  198. package/src/service/health.ts +222 -0
  199. package/src/service/launchd.ts +853 -0
  200. package/src/service/orchestration.ts +617 -0
  201. package/src/service/repair.ts +334 -0
  202. package/src/service/state.ts +363 -0
  203. package/src/service/systemd.ts +229 -0
  204. package/src/service/windows-ops.ts +690 -0
  205. package/src/service/windows-scheduler.ts +769 -0
  206. package/src/service/windows-taskxml.ts +613 -0
  207. package/src/service.ts +22 -5550
  208. package/src/storage/cleanup/db.ts +258 -0
  209. package/src/storage/cleanup/execute.ts +358 -0
  210. package/src/storage/cleanup/paths.ts +189 -0
  211. package/src/storage/cleanup/pending.ts +140 -0
  212. package/src/storage/cleanup/preview.ts +292 -0
  213. package/src/storage/cleanup/reconcile.ts +347 -0
  214. package/src/storage/cleanup/restore.ts +932 -0
  215. package/src/storage/cleanup/satellite.ts +474 -0
  216. package/src/storage/cleanup/staging.ts +129 -0
  217. package/src/storage/cleanup/types.ts +98 -0
  218. package/src/storage/cleanup.ts +49 -3127
  219. package/src/types/accounts.ts +2 -0
  220. package/src/types/config.ts +13 -12
  221. package/src/types/provider.ts +37 -0
  222. package/src/types/request.ts +2 -0
  223. package/src/types/tools.ts +17 -5
  224. package/src/types.ts +1 -0
  225. package/src/usage/expected-prices.ts +127 -0
  226. package/src/usage/log.ts +58 -1
  227. package/src/vision/eligibility.ts +13 -2
  228. package/src/web-search/loop.ts +56 -3
  229. package/gui/dist/assets/index-CWXut3rG.js +0 -115
  230. package/gui/dist/assets/index-EdoPnm9_.css +0 -1
  231. package/src/adapters/devin-cli/acp.ts +0 -204
  232. package/src/adapters/devin-cli/adapter.ts +0 -345
  233. package/src/adapters/devin-cli/binary.ts +0 -69
  234. package/src/adapters/devin-cli/models.ts +0 -57
  235. package/src/oauth/devin-cli.ts +0 -149
  236. package/src/server/responses-reasoning-summary-rewrite.ts +0 -178
@@ -365,12 +365,11 @@ export async function runListenerShutdown(
365
365
  always: () => Promise<void>,
366
366
  ): Promise<void> {
367
367
  const failures: unknown[] = [];
368
- for (const step of steps) {
369
- try {
370
- await step();
371
- } catch (error) {
372
- failures.push(error);
373
- }
368
+ // Close admission and start connection-owner cleanup before waiting for any drain.
369
+ // A graceful listener stop can itself depend on a later owner closing its sockets.
370
+ const results = await Promise.allSettled(steps.map(async step => { await step(); }));
371
+ for (const result of results) {
372
+ if (result.status === "rejected") failures.push(result.reason);
374
373
  }
375
374
  try {
376
375
  await always();
@@ -0,0 +1,60 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ export const EXTERNAL_CALL_PREFIX = "rtc_ocx_";
4
+ export const LIVE_CALL_TTL_MS = 30 * 60_000;
5
+ const MAX_CALL_BINDINGS = 1024;
6
+
7
+ export interface LiveCallBinding {
8
+ owner: string;
9
+ upstreamCallId: string;
10
+ joinStyle: "frameless-path" | "realtime-query";
11
+ providerName: string;
12
+ accountId?: string;
13
+ chatgptAccountId?: string;
14
+ keyedCredentialDigest?: string;
15
+ callerOwned: boolean;
16
+ sidebandBaseUrl?: string;
17
+ }
18
+
19
+ /** Caller-visible IDs are opaque aliases; expired aliases never become legacy upstream IDs. */
20
+ export class LiveCallBindings {
21
+ private entries = new Map<string, { binding: LiveCallBinding; expiresAt: number }>();
22
+
23
+ constructor(private readonly now: () => number = Date.now) {}
24
+
25
+ private prune(): void {
26
+ const now = this.now();
27
+ for (const [id, value] of this.entries) if (value.expiresAt <= now) this.entries.delete(id);
28
+ }
29
+
30
+ hasCapacity(): boolean {
31
+ this.prune();
32
+ return this.entries.size < MAX_CALL_BINDINGS;
33
+ }
34
+
35
+ create(binding: LiveCallBinding): string | null {
36
+ if (!this.hasCapacity()) return null;
37
+ const id = EXTERNAL_CALL_PREFIX + randomUUID().replaceAll("-", "");
38
+ this.entries.set(id, { binding: { ...binding }, expiresAt: this.now() + LIVE_CALL_TTL_MS });
39
+ return id;
40
+ }
41
+
42
+ get(id: string, owner: string): LiveCallBinding | undefined {
43
+ this.prune();
44
+ const value = this.entries.get(id);
45
+ return value?.binding.owner === owner ? { ...value.binding } : undefined;
46
+ }
47
+
48
+ clear(): void { this.entries.clear(); }
49
+ }
50
+
51
+ export function upstreamLiveCallId(location: string | null): string | null {
52
+ if (!location || location.length > 4096) return null;
53
+ try {
54
+ const url = new URL(location, "https://unused.invalid");
55
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) return null;
56
+ const segment = url.pathname.replace(/\/+$/, "").split("/").at(-1) ?? "";
57
+ const id = decodeURIComponent(segment);
58
+ return /^(?:rtc_[A-Za-z0-9_-]+|[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12})$/.test(id) && id.length <= 128 ? id : null;
59
+ } catch { return null; }
60
+ }
@@ -35,6 +35,7 @@ import {
35
35
  cooldownErrorResponse,
36
36
  CodexAuthContextError,
37
37
  CodexMainProfileDrainingError,
38
+ CodexModelAvailabilityError,
38
39
  CodexPoolAuthenticationError,
39
40
  CodexThreadAffinityExpiredError,
40
41
  } from "../codex/auth-context";
@@ -48,6 +49,7 @@ import type { RequestLogContext } from "./request-log";
48
49
  import { codexLogAccountId } from "./responses";
49
50
  import type { AdmissionLease } from "../lib/admission";
50
51
  import { codexAccountSelectionForTurn } from "./lifecycle";
52
+ import { codexModelAvailabilityErrorResponse } from "./responses/codex-auth-error";
51
53
 
52
54
  /** Voice call create can wait on SDP negotiation; bound a hung upstream. */
53
55
  const LIVE_UPSTREAM_TIMEOUT_MS = 120_000;
@@ -371,7 +373,7 @@ export function buildLiveSidebandUpstreamWsUrl(
371
373
  );
372
374
  }
373
375
 
374
- async function backendJsonBodyFromApiMultipart(
376
+ export async function backendJsonBodyFromApiMultipart(
375
377
  body: ArrayBuffer,
376
378
  contentType: string,
377
379
  ): Promise<{ body: Uint8Array; contentType: string } | Response> {
@@ -424,14 +426,20 @@ export async function readBodyCapped(
424
426
  stream: ReadableStream<Uint8Array> | null,
425
427
  maxBytes: number,
426
428
  tooLargeMessage: (total: number) => string,
429
+ signal?: AbortSignal,
427
430
  ): Promise<ArrayBuffer | Response> {
428
431
  if (!stream) return new ArrayBuffer(0);
429
432
  const reader = stream.getReader();
433
+ const abortRead = () => { void reader.cancel(signal?.reason).catch(() => {}); };
434
+ signal?.addEventListener("abort", abortRead, { once: true });
430
435
  const chunks: Uint8Array[] = [];
431
436
  let total = 0;
432
437
  try {
438
+ if (signal?.aborted) abortRead();
439
+ signal?.throwIfAborted();
433
440
  for (;;) {
434
441
  const { done, value } = await reader.read();
442
+ signal?.throwIfAborted();
435
443
  if (done) break;
436
444
  if (!value || value.byteLength === 0) continue;
437
445
  total += value.byteLength;
@@ -450,6 +458,7 @@ export async function readBodyCapped(
450
458
  await reader.cancel(err).catch(() => {});
451
459
  throw err;
452
460
  } finally {
461
+ signal?.removeEventListener("abort", abortRead);
453
462
  try {
454
463
  // Always release: `reader.cancel()` does NOT drop the lock, and holding it would leave
455
464
  // the stream permanently locked for any later consumer (audit R-WP5-2).
@@ -558,6 +567,8 @@ export async function resolveLiveRelay(
558
567
  "authentication_error",
559
568
  "Selected Codex account needs reauthentication",
560
569
  );
570
+ } else if (err instanceof CodexModelAvailabilityError) {
571
+ forwardAuthError = codexModelAvailabilityErrorResponse(err);
561
572
  } else if (err instanceof CodexPoolAuthenticationError) {
562
573
  forwardAuthError = formatErrorResponse(401, "authentication_error", err.message);
563
574
  } else {
@@ -3,6 +3,7 @@ import { readFileSync } from "node:fs";
3
3
  import type { CatalogModel } from "../../codex/catalog";
4
4
  import { catalogModelSlug, filterCatalogVisibleModels, invalidateCodexModelsCache, nativeContextLimits, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
5
5
  import { mergeModelPinnedEfforts, modelPinnedEffortsConfigError } from "../../config/provider-validation";
6
+ import { MULTI_AGENT_SURFACE_ADVISORY_VERSION, multiAgentSurfaceAdvisory, resolveMultiAgentMode } from "../../config/multi-agent-surface";
6
7
  import { captureConfigTopLevelRollback, parsedConfigRebaseDeletionKeys, projectConfigRebaseProvenance } from "../../config/rebase-provenance";
7
8
  import {
8
9
  DEFAULT_SUBAGENT_MODELS,
@@ -243,7 +244,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
243
244
  enabled,
244
245
  agentsMaxThreadsConflict: enabled && hasAgentsMaxThreads(),
245
246
  maxConcurrentThreadsPerSession: getLogicalMaxThreads(),
246
- multiAgentMode: config.multiAgentMode ?? "default",
247
+ // Resolved, not raw: a hand-edited unsupported value survives config parsing, and the
248
+ // advisory below already reports the resolved surface. Two answers would disagree.
249
+ multiAgentMode: resolveMultiAgentMode(config),
250
+ multiAgentSurfaceAdvisory: multiAgentSurfaceAdvisory(config),
247
251
  keepNativeChatGptOnV1: config.keepNativeChatGptOnV1 === true,
248
252
  agentsEnabled: getAgentsEnabled(),
249
253
  agentsMaxDepth: getAgentsMaxDepth(),
@@ -265,6 +269,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
265
269
  agentsMaxDepth?: unknown;
266
270
  subagentDeveloperInstructions?: unknown;
267
271
  multiAgentModeHintText?: unknown;
272
+ multiAgentSurfaceAdvisoryAcknowledged?: unknown;
268
273
  };
269
274
  try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
270
275
  const wantsFlag = body.enabled !== undefined;
@@ -275,8 +280,9 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
275
280
  const wantsMaxDepth = body.agentsMaxDepth !== undefined;
276
281
  const wantsSubagentInstructions = body.subagentDeveloperInstructions !== undefined;
277
282
  const wantsModeHintText = body.multiAgentModeHintText !== undefined;
278
- if (!wantsFlag && !wantsThreads && !wantsMode && !wantsKeepNative && !wantsAgentsEnabled && !wantsMaxDepth && !wantsSubagentInstructions && !wantsModeHintText) {
279
- return jsonResponse({ error: "body must set enabled, multiAgentMode, keepNativeChatGptOnV1, maxConcurrentThreadsPerSession, agentsEnabled, agentsMaxDepth, subagentDeveloperInstructions, and/or multiAgentModeHintText" }, 400);
283
+ const wantsAdvisoryAck = body.multiAgentSurfaceAdvisoryAcknowledged !== undefined;
284
+ if (!wantsFlag && !wantsThreads && !wantsMode && !wantsKeepNative && !wantsAgentsEnabled && !wantsMaxDepth && !wantsSubagentInstructions && !wantsModeHintText && !wantsAdvisoryAck) {
285
+ return jsonResponse({ error: "body must set enabled, multiAgentMode, keepNativeChatGptOnV1, maxConcurrentThreadsPerSession, agentsEnabled, agentsMaxDepth, subagentDeveloperInstructions, multiAgentModeHintText, and/or multiAgentSurfaceAdvisoryAcknowledged" }, 400);
280
286
  }
281
287
  if (wantsFlag && typeof body.enabled !== "boolean") return jsonResponse({ error: "body.enabled must be a boolean" }, 400);
282
288
  if (wantsMode && body.multiAgentMode !== "v1" && body.multiAgentMode !== "default" && body.multiAgentMode !== "v2") {
@@ -311,6 +317,11 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
311
317
  && (typeof body.multiAgentModeHintText !== "string" || body.multiAgentModeHintText.trim().length === 0)) {
312
318
  return jsonResponse({ error: "body.multiAgentModeHintText must be a non-empty string or null" }, 400);
313
319
  }
320
+ // A boolean, and only `true` acknowledges. `false` is an explicit no-op so a client
321
+ // that always sends the field cannot un-answer an advisory it already dismissed.
322
+ if (wantsAdvisoryAck && typeof body.multiAgentSurfaceAdvisoryAcknowledged !== "boolean") {
323
+ return jsonResponse({ error: "body.multiAgentSurfaceAdvisoryAcknowledged must be a boolean" }, 400);
324
+ }
314
325
  const mode = wantsMode ? body.multiAgentMode as "v1" | "default" | "v2" : undefined;
315
326
  const effectiveMode = mode ?? config.multiAgentMode ?? "default";
316
327
  const effectiveKeepNative = wantsKeepNative
@@ -378,6 +389,15 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
378
389
  : "keepNativeChatGptOnV1 is stored but inactive until multi-agent mode is v2. Applies to new sessions.")
379
390
  : "ChatGPT-native models follow the selected v1/v2/base surface. Applies to new sessions.");
380
391
  }
392
+ // Written after the mode, so a landed acknowledgement implies the mode write it was
393
+ // sent with already landed. The converse does not hold: this writer throws rather than
394
+ // reporting, exactly as the mode write above it does, so a failure here surfaces the
395
+ // mode change with the advisory still raised. That is the benign direction — the
396
+ // operator is asked again — and it is why this is not claimed as a transaction.
397
+ if (wantsAdvisoryAck && body.multiAgentSurfaceAdvisoryAcknowledged === true) {
398
+ config.multiAgentSurfaceAdvisoryVersion = MULTI_AGENT_SURFACE_ADVISORY_VERSION;
399
+ saveConfigPreservingClaudeCode(config);
400
+ }
381
401
  // New-key scalar writes: each writer is individually atomic, so apply them in
382
402
  // sequence after the transition. A failure here is a persistence failure (the
383
403
  // writers' ok:false result or a throw from the underlying atomic write helper),
@@ -417,7 +437,8 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
417
437
  enabled,
418
438
  agentsMaxThreadsConflict: enabled && hasAgentsMaxThreads(),
419
439
  maxConcurrentThreadsPerSession: getLogicalMaxThreads(),
420
- multiAgentMode: config.multiAgentMode ?? "default",
440
+ multiAgentMode: resolveMultiAgentMode(config),
441
+ multiAgentSurfaceAdvisory: multiAgentSurfaceAdvisory(config),
421
442
  keepNativeChatGptOnV1: config.keepNativeChatGptOnV1 === true,
422
443
  agentsEnabled: getAgentsEnabled(),
423
444
  agentsMaxDepth: getAgentsMaxDepth(),
@@ -2,6 +2,21 @@ import type { OcxConfig } from "../../types";
2
2
  import { isWildcardHostname } from "../../codex/loopback-target";
3
3
  import { localInferenceDestination } from "../../lib/local-destinations";
4
4
  import { probeHostname } from "../proxy-liveness";
5
+ import { isCanonicalOpenAiForwardProvider, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers-destination";
6
+ import { LIVE_AUDIO_MODEL, TRANSCRIPTION_MODEL } from "../audio-upstream";
7
+
8
+ export interface AudioApiAccess {
9
+ transcriptionEndpoint: string;
10
+ dictationStreamEndpoint: string;
11
+ liveEndpoint: string;
12
+ realtimeCallsEndpoint: string;
13
+ transcriptionModel: string;
14
+ liveModel: string;
15
+ /** Configuration only, not account health, entitlement or observed connectivity. */
16
+ transcriptionConfigured: boolean;
17
+ dictationConfigured: boolean;
18
+ liveConfigured: boolean;
19
+ }
5
20
 
6
21
  export interface ApiAccessEndpoints {
7
22
  baseUrl: string;
@@ -10,6 +25,7 @@ export interface ApiAccessEndpoints {
10
25
  messagesEndpoint: string;
11
26
  modelsEndpoint: string;
12
27
  claudeCodeEnabled: boolean;
28
+ audio: AudioApiAccess;
13
29
  /** Back-compat alias for older GUI clients. */
14
30
  endpoint: string;
15
31
  }
@@ -140,6 +156,16 @@ export function buildApiAccessEndpoints(
140
156
  ): ApiAccessEndpoints {
141
157
  const baseUrl = resolveApiAccessBaseUrl(config, opts);
142
158
  const responsesEndpoint = `${baseUrl}/responses`;
159
+ const socketBase = new URL(baseUrl);
160
+ socketBase.protocol = socketBase.protocol === "https:" ? "wss:" : "ws:";
161
+ const forward = config.providers?.[OPENAI_CODEX_PROVIDER_ID];
162
+ const chatgptConfigured = !!forward && forward.disabled !== true
163
+ && isCanonicalOpenAiForwardProvider({ ...forward, authMode: forward.authMode ?? "forward" });
164
+ const keyed = config.providers?.[OPENAI_API_PROVIDER_ID];
165
+ // Do not resolve key references or inspect accounts on a management metadata read.
166
+ const apiConfigured = !!keyed && keyed.disabled !== true && keyed.adapter === "openai-responses"
167
+ && keyed.authMode !== "forward" && keyed.baseUrl.replace(/\/+$/, "") === "https://api.openai.com/v1"
168
+ && typeof keyed.apiKey === "string" && !!keyed.apiKey.trim();
143
169
  return {
144
170
  baseUrl,
145
171
  responsesEndpoint,
@@ -147,6 +173,17 @@ export function buildApiAccessEndpoints(
147
173
  messagesEndpoint: `${baseUrl}/messages`,
148
174
  modelsEndpoint: `${baseUrl}/models`,
149
175
  claudeCodeEnabled: config.claudeCode?.enabled !== false,
176
+ audio: {
177
+ transcriptionEndpoint: `${baseUrl}/audio/transcriptions`,
178
+ dictationStreamEndpoint: `${socketBase.href}/audio/transcriptions/stream`,
179
+ liveEndpoint: `${socketBase.href}/live`,
180
+ realtimeCallsEndpoint: `${baseUrl}/realtime/calls`,
181
+ transcriptionModel: TRANSCRIPTION_MODEL,
182
+ liveModel: LIVE_AUDIO_MODEL,
183
+ transcriptionConfigured: chatgptConfigured || apiConfigured,
184
+ dictationConfigured: chatgptConfigured,
185
+ liveConfigured: chatgptConfigured,
186
+ },
150
187
  endpoint: responsesEndpoint,
151
188
  };
152
189
  }
@@ -19,6 +19,9 @@ export type ApiKeyUsage =
19
19
  export interface ApiKeyUsageSnapshot {
20
20
  rollup: Map<string, ApiKeyUsage>;
21
21
  historyTruncated?: true;
22
+ /** Positive evidence of skipped oversized rows; absence is not a completeness guarantee. */
23
+ usageIncomplete?: true;
24
+ usageIncompleteReason?: "oversized_rows";
22
25
  /**
23
26
  * Earliest row carrying a recognized `admissionKind`. A property of the DATA
24
27
  * SET, not of a key, so it is singular and lives beside the map: it is what
@@ -223,9 +226,11 @@ export async function readApiKeyUsageRollup(configuredIds: string[], maxReadByte
223
226
  const flight = (async (): Promise<ApiKeyUsageSnapshot> => {
224
227
  const accumulator = createApiKeyUsageAccumulator(configuredIds, now);
225
228
  const scan = await scanUsageLedgerCooperatively({ onEntry: entry => accumulator.add(entry) });
226
- if (scan.oversizedRows > 0) throw new Error("usage ledger contains an oversized row");
227
229
  return cacheApiKeyUsageFromRollup(
228
- accumulator.snapshot(),
230
+ {
231
+ ...accumulator.snapshot(),
232
+ ...(scan.oversizedRows > 0 ? { usageIncomplete: true as const, usageIncompleteReason: "oversized_rows" as const } : {}),
233
+ },
229
234
  configuredIds,
230
235
  usageLogIdentityKey(scan.revision),
231
236
  scan.revision?.size ?? 0,
@@ -316,9 +316,6 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
316
316
  appOwnedMemoryBudgetMb: config.appOwnedMemoryBudgetMb ?? 256,
317
317
  codexAccountPickerEnabled: codexAccountPickerEnabled(config),
318
318
  codexQuotaAutoRefresh: quotaAutoRefreshSettings(config),
319
- // Absent means hidden, so the GUI renders the switch without having to know that
320
- // `undefined` and `false` mean the same thing.
321
- showCodexSparkQuota: config.showCodexSparkQuota === true,
322
319
  // Absent means off, same convention: the GUI renders a plain switch without
323
320
  // needing to know that `undefined` and `false` mean the same thing here.
324
321
  ultraFastTier: config.ultraFastTier === true,
@@ -417,7 +414,6 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
417
414
  codexAccountPickerEnabled?: unknown;
418
415
  codexQuotaAutoRefresh?: unknown;
419
416
  oauthOpenBrowser?: unknown;
420
- showCodexSparkQuota?: unknown;
421
417
  ultraFastTier?: unknown;
422
418
  codexMainAccountHardLock?: unknown;
423
419
  codexDesktopAuthless?: unknown;
@@ -429,12 +425,11 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
429
425
  && body.codexAccountPickerEnabled === undefined
430
426
  && body.codexQuotaAutoRefresh === undefined
431
427
  && body.oauthOpenBrowser === undefined
432
- && body.showCodexSparkQuota === undefined
433
428
  && body.ultraFastTier === undefined
434
429
  && body.codexMainAccountHardLock === undefined
435
430
  && body.codexDesktopAuthless === undefined
436
431
  && body.codexClientCompaction === undefined) {
437
- return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, codexAccountPickerEnabled, codexQuotaAutoRefresh, oauthOpenBrowser, showCodexSparkQuota, ultraFastTier, codexMainAccountHardLock, codexDesktopAuthless, or codexClientCompaction" }, 400);
432
+ return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, codexAccountPickerEnabled, codexQuotaAutoRefresh, oauthOpenBrowser, ultraFastTier, codexMainAccountHardLock, codexDesktopAuthless, or codexClientCompaction" }, 400);
438
433
  }
439
434
  if (body.codexAutoStart !== undefined && typeof body.codexAutoStart !== "boolean") {
440
435
  return jsonResponse({ error: "codexAutoStart boolean is required" }, 400);
@@ -449,9 +444,6 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
449
444
  && typeof body.codexAccountPickerEnabled !== "boolean") {
450
445
  return jsonResponse({ error: "codexAccountPickerEnabled boolean is required" }, 400);
451
446
  }
452
- if (body.showCodexSparkQuota !== undefined && typeof body.showCodexSparkQuota !== "boolean") {
453
- return jsonResponse({ error: "showCodexSparkQuota boolean is required" }, 400);
454
- }
455
447
  if (body.ultraFastTier !== undefined && typeof body.ultraFastTier !== "boolean") {
456
448
  return jsonResponse({ error: "ultraFastTier boolean is required" }, 400);
457
449
  }
@@ -508,8 +500,6 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
508
500
  hasCodexQuotaAutoRefresh: Object.hasOwn(config, "codexQuotaAutoRefresh"),
509
501
  oauthOpenBrowser: config.oauthOpenBrowser,
510
502
  hasOauthOpenBrowser: Object.hasOwn(config, "oauthOpenBrowser"),
511
- showCodexSparkQuota: config.showCodexSparkQuota,
512
- hasShowCodexSparkQuota: Object.hasOwn(config, "showCodexSparkQuota"),
513
503
  ultraFastTier: config.ultraFastTier,
514
504
  hasUltraFastTier: Object.hasOwn(config, "ultraFastTier"),
515
505
  codexMainAccountHardLock: config.codexMainAccountHardLock,
@@ -546,9 +536,6 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
546
536
  if (typeof body.oauthOpenBrowser === "boolean") {
547
537
  config.oauthOpenBrowser = body.oauthOpenBrowser;
548
538
  }
549
- if (typeof body.showCodexSparkQuota === "boolean") {
550
- config.showCodexSparkQuota = body.showCodexSparkQuota;
551
- }
552
539
  // Off deletes the key rather than persisting `false`: absent is the documented
553
540
  // default, and a written `false` would survive as a decision nobody made.
554
541
  if (body.ultraFastTier === true) config.ultraFastTier = true;
@@ -592,9 +579,6 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
592
579
  if (previousSettings.hasOauthOpenBrowser) {
593
580
  config.oauthOpenBrowser = previousSettings.oauthOpenBrowser;
594
581
  } else deleteConfigTopLevelKey(config, "oauthOpenBrowser");
595
- if (previousSettings.hasShowCodexSparkQuota) {
596
- config.showCodexSparkQuota = previousSettings.showCodexSparkQuota;
597
- } else deleteConfigTopLevelKey(config, "showCodexSparkQuota");
598
582
  if (previousSettings.hasUltraFastTier) {
599
583
  config.ultraFastTier = previousSettings.ultraFastTier;
600
584
  } else deleteConfigTopLevelKey(config, "ultraFastTier");
@@ -636,7 +620,6 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
636
620
  codexQuotaAutoRefresh: quotaAutoRefreshSettings(config),
637
621
  oauthOpenBrowser: config.oauthOpenBrowser !== false,
638
622
  catalogRefreshPending,
639
- showCodexSparkQuota: config.showCodexSparkQuota === true,
640
623
  codexDesktopAuthless: authlessIsEnabled,
641
624
  codexClientCompaction: clientCompactionIsEnabled,
642
625
  codexMainAccountHardLock: config.codexMainAccountHardLock === true,
@@ -20,7 +20,22 @@ import type {
20
20
  readCodexAppServerState,
21
21
  } from "../../codex/app-server-restart-service";
22
22
 
23
+ import type { RemoteWorkspaceHub } from "../../remote-control/workspace-hub";
24
+ import type { RemoteWorkspaceSessionService } from "../../remote-control/workspace-sessions";
25
+
26
+ export type RemoteWorkspaceHubApi = Pick<RemoteWorkspaceHub,
27
+ "identity" | "createPairingGrant" | "assertPairingSourceAllowed" | "pairDevice"
28
+ | "authenticateDeviceToken" | "attachConnection" | "updateDeviceCapabilities"
29
+ | "detachConnection" | "listDevices" | "revokeDevice" | "closeAllConnections">;
30
+ export type RemoteWorkspaceSessionsApi = Pick<RemoteWorkspaceSessionService,
31
+ "availability" | "list" | "create" | "prompt" | "submitPrompt" | "stop" | "shutdown">;
32
+
23
33
  export interface ManagementApiDeps {
34
+ remoteWorkspaceHub?: RemoteWorkspaceHubApi;
35
+ remoteWorkspaceSessions?: RemoteWorkspaceSessionsApi;
36
+ /** The listener retains and awaits teardown only after this optional subsystem activates. */
37
+ remoteWorkspaceStopping?: () => boolean;
38
+ onRemoteWorkspaceShutdown?: (shutdown: () => Promise<void>) => void;
24
39
  /** Isolates automatic owned-client writes in route tests. */
25
40
  refreshOwnedCatalogIntegrations?: typeof refreshOwnedCatalogIntegrations;
26
41
  /** Platform seam for capability projections; does not alter host-level startup behavior. */
@@ -225,6 +225,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
225
225
  const accumulator = filteredAggregate.accumulator;
226
226
  return jsonResponse({
227
227
  ...accumulator.summarize(range, now, surface),
228
+ ...(filteredAggregate.usageIncomplete ? { usageIncomplete: true as const, usageIncompleteReason: "oversized_rows" as const } : {}),
228
229
  historyTruncated: false,
229
230
  truncatedPrefixBytes: 0,
230
231
  entriesTruncated: false,
@@ -247,6 +248,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
247
248
  const revisionKey = `${usageLogRevisionKey(aggregate.revision)}\0${effectiveReadLimit}`;
248
249
  const lastSeenSize = aggregate.revision?.size ?? 0;
249
250
  const baseReadMetadata = {
251
+ ...(aggregate.usageIncomplete ? { usageIncomplete: true as const, usageIncompleteReason: "oversized_rows" as const } : {}),
250
252
  historyTruncated: false,
251
253
  truncatedPrefixBytes: 0,
252
254
  entriesTruncated: false,
@@ -39,6 +39,7 @@ import {
39
39
  normalizeAccountPoolStrategy,
40
40
  parseAccountPoolStickyLimit,
41
41
  parseAccountPoolStrategy,
42
+ parseCodexAccountPoolStrategy,
42
43
  } from "../../codex/pool-rotation";
43
44
  import { normalizeAccountPoolQuotaWindow, parseAccountPoolQuotaWindow } from "../../oauth/anthropic-routing";
44
45
  import { primeCodexPoolQuotas } from "../../codex/auth-api";
@@ -78,6 +79,25 @@ import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, C
78
79
  import type { ManagementContext } from "./context";
79
80
  import { readManagementJsonBody, readManagementJsonBodyOr, rethrowManagementBodyTooLarge } from "./body";
80
81
  import { codexAccountNamespaceProviderCollisionError } from "../../codex/account-namespace-match";
82
+
83
+ /**
84
+ * Provider ids that share the Devin cloud-direct client, and therefore share its
85
+ * process-memory caches.
86
+ *
87
+ * `devin-cli` is a deprecated alias for the merged `devin` provider, but a
88
+ * config row the startup migration has not rekeyed yet can still arrive here —
89
+ * and its logout/removal must clear the same caches, because both ids hand the
90
+ * same api_key to the same client and one cache serves both.
91
+ */
92
+ function isDevinCloudDirectProvider(provider: string): boolean {
93
+ return provider === "devin" || provider === "devin-cli";
94
+ }
95
+
96
+ async function clearDevinCloudDirectCaches(): Promise<void> {
97
+ const { clearCachedUserJwt, clearCachedCatalog } = await import("../../adapters/devin/cloud-direct");
98
+ clearCachedUserJwt();
99
+ clearCachedCatalog();
100
+ }
81
101
  import { ACCOUNT_IMPORT_DEADLINE_MS, ACCOUNT_IMPORT_MAX_REQUEST_BYTES } from "../../oauth/account-import";
82
102
  import { readBoundedJsonRequestBody } from "../request-decompress";
83
103
 
@@ -255,14 +275,12 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
255
275
  const { clearProviderQuotaCache, clearAccountQuotaCache } = await import("../../providers/quota");
256
276
  clearProviderQuotaCache();
257
277
  clearAccountQuotaCache(provider);
258
- if (provider === "devin") {
259
- // The cached user_jwt's payload contains the api_key, and the catalog is
260
- // keyed by that key. Without this they outlive the credential in process
261
- // memory until the JWT's own ~24 minute expiry.
262
- const { clearCachedUserJwt, clearCachedCatalog } = await import("../../adapters/devin/cloud-direct");
263
- clearCachedUserJwt();
264
- clearCachedCatalog();
265
- }
278
+ // The cached user_jwt's payload contains the api_key, and the catalog is
279
+ // keyed by that key. Without this they outlive the credential in process
280
+ // memory until the JWT's own ~24 minute expiry. `devin-cli` is a deprecated
281
+ // alias whose unmigrated rows share the one cache, so gating on `devin`
282
+ // alone left a CLI-imported key's JWT resident after its own logout.
283
+ if (isDevinCloudDirectProvider(provider)) await clearDevinCloudDirectCaches();
266
284
  return jsonResponse({ success: true });
267
285
  }
268
286
 
@@ -325,7 +343,9 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
325
343
  return {
326
344
  ...account,
327
345
  quota: row.quota,
328
- ...(quotaMode === "probe" ? { quotaUnavailable: row.unavailable === true } : {}),
346
+ ...(quotaMode === "probe" ? { quotaUnavailable: row.unavailable === true,
347
+ ...(row.unavailable && row.quotaFailure && row.quotaFailureIsCurrent?.() === true ? { quotaFailure: row.quotaFailure } : {}),
348
+ } : {}),
329
349
  };
330
350
  }),
331
351
  });
@@ -383,8 +403,10 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
383
403
  // sticky limit is refused identically whichever pool is addressed.
384
404
  let strategy: string | undefined;
385
405
  if (fields.strategy !== undefined) {
386
- const parsed = parseGenericPoolStrategy(fields.strategy);
387
- if (parsed === null) return jsonResponse({ error: "strategy must be one of: quota, round-robin, fill-first" }, 400);
406
+ const parsed = kind === "codex" ? parseCodexAccountPoolStrategy(fields.strategy) : parseGenericPoolStrategy(fields.strategy);
407
+ if (parsed === null) return jsonResponse({ error: kind === "codex"
408
+ ? "strategy must be one of: quota, round-robin, fill-first, reset-first"
409
+ : "strategy must be one of: quota, round-robin, fill-first" }, 400);
388
410
  strategy = parsed;
389
411
  }
390
412
  let stickyLimit: number | undefined;
@@ -685,6 +707,10 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
685
707
  const { clearProviderQuotaCache, clearAccountQuotaCache } = await import("../../providers/quota");
686
708
  clearProviderQuotaCache();
687
709
  clearAccountQuotaCache(provider);
710
+ // Same reasoning as logout. Removing the last account for a provider used to
711
+ // leave the JWT and catalog in memory, because only the logout route cleared
712
+ // them.
713
+ if (isDevinCloudDirectProvider(provider)) await clearDevinCloudDirectCaches();
688
714
  return jsonResponse({ ok: true });
689
715
  }
690
716
 
@@ -833,7 +859,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
833
859
  requestOrigin: req.headers.get("origin"),
834
860
  });
835
861
  const { readApiKeyUsageRollup } = await import("./api-key-usage");
836
- const { rollup, attributionSince, historyTruncated } = await readApiKeyUsageRollup(keys.map(k => k.id), config.managementUsageMaxReadBytes);
862
+ const { rollup, attributionSince, historyTruncated, usageIncomplete, usageIncompleteReason } = await readApiKeyUsageRollup(keys.map(k => k.id), config.managementUsageMaxReadBytes);
837
863
  return jsonResponse({
838
864
  // 8 random hex past the fixed `ocx_data_` literal: enough to tell two keys
839
865
  // apart in a list, with 128 bits of the tail still unrevealed. Masking only
@@ -853,6 +879,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
853
879
  // Dataset-level and singular: it describes the usage log, not any one key.
854
880
  ...(attributionSince ? { attributionSince } : {}),
855
881
  ...(historyTruncated ? { historyTruncated: true } : {}),
882
+ ...(usageIncomplete ? { usageIncomplete: true, usageIncompleteReason } : {}),
856
883
  authMatrix: AUTH_MATRIX,
857
884
  ...endpoints,
858
885
  }, 200, req, config);