@bitkyc08/opencodex 2.52.0 → 2.53.0-preview.20260913

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-D7ynYo2K.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
package/src/config.ts CHANGED
@@ -1,9 +1,11 @@
1
+ import { modelCapabilitiesConfigError, mergeModelCapabilities, sanitizeModelCapabilitiesForLoad } from "./config/provider-validation";
1
2
  import { createHash } from "node:crypto";
2
3
  import { chmodSync, constants as fsConstants, copyFileSync, existsSync, linkSync, lstatSync, mkdirSync, readFileSync, truncateSync, unlinkSync, writeFileSync } from "node:fs";
3
4
  import { dirname, join } from "node:path";
4
5
  import { Database } from "bun:sqlite";
5
6
  import * as z from "zod/v4";
6
7
  import { isValidProviderName, hasOwnProvider } from "./config/provider-name";
8
+ import { MULTI_AGENT_SURFACE_ADVISORY_VERSION } from "./config/multi-agent-surface";
7
9
  import { DEFAULT_SUBAGENT_MODELS, SUBAGENT_MODELS_VERSION } from "./config/subagent-models";
8
10
  export { DEFAULT_SUBAGENT_MODELS } from "./config/subagent-models";
9
11
  import {
@@ -14,8 +16,11 @@ import {
14
16
  pinnedReasoningEffortConfigError,
15
17
  modelAdapterRecordConfigError,
16
18
  modelDisplayNamesConfigError,
19
+ autoReviewModelOverridesConfigError,
20
+ autoReviewModelTargetConfigError,
17
21
  nonBlankStringArrayConfigError,
18
22
  normalizeNonBlankStringArray,
23
+ normalizeAutoReviewModelOverrides,
19
24
  positiveIntegerConfigError,
20
25
  positiveIntegerRecordConfigError,
21
26
  providerBaseUrlConfigError,
@@ -573,17 +578,39 @@ const modelPinnedEffortsSchema = z.unknown().superRefine((value, ctx) => {
573
578
  Object.entries(value as Record<string, string>).map(([key, effort]) => [key.trim(), effort]),
574
579
  ));
575
580
 
581
+ const autoReviewModelSchema = z.unknown().superRefine((value, ctx) => {
582
+ const error = autoReviewModelTargetConfigError(value, "autoReviewModel", true);
583
+ if (error) ctx.addIssue({ code: "custom", message: error });
584
+ }).transform(value => {
585
+ if (typeof value !== "string") return undefined;
586
+ const trimmed = value.trim();
587
+ return trimmed ? trimmed : undefined;
588
+ });
589
+
590
+ const autoReviewModelOverridesSchema = z.unknown().superRefine((value, ctx) => {
591
+ const error = autoReviewModelOverridesConfigError(value, "autoReviewModelOverrides", true);
592
+ if (error) ctx.addIssue({ code: "custom", message: error });
593
+ }).transform(value => normalizeAutoReviewModelOverrides(value));
594
+
595
+ const modelCapabilitiesSchema = z.unknown().superRefine((value, ctx) => {
596
+ const error = modelCapabilitiesConfigError(value);
597
+ if (error) ctx.addIssue({ code: "custom", message: error });
598
+ }).transform(value => mergeModelCapabilities(undefined, value));
599
+
576
600
  /**
577
601
  * Zod schema for one provider entry: known fields are validated strictly while unknown
578
602
  * fields pass through (preserved for runtime extensions).
579
603
  */
580
604
  const providerConfigSchema = z.object({
605
+ modelCapabilities: modelCapabilitiesSchema.optional(),
581
606
  pinnedReasoningEffort: pinnedReasoningEffortSchema.optional(),
582
607
  modelPinnedReasoningEfforts: modelPinnedEffortsSchema.optional(),
583
608
  // Validated rather than left to passthrough: an unrecognized strategy would otherwise
584
609
  // load silently and then be ignored at selection time, which reads as a broken feature
585
610
  // rather than a rejected setting.
586
611
  apiKeyPoolStrategy: z.enum(["round-robin", "fill-first", "quota"]).optional(),
612
+ autoReviewModel: autoReviewModelSchema.optional(),
613
+ autoReviewModelOverrides: autoReviewModelOverridesSchema.optional(),
587
614
  adapter: z.string().min(1),
588
615
  baseUrl: z.string().min(1),
589
616
  alias: z.string().optional(),
@@ -665,9 +692,12 @@ export {
665
692
  apiKeyTransportConfigError,
666
693
  booleanRecordConfigError,
667
694
  modelAdapterRecordConfigError,
695
+ autoReviewModelOverridesConfigError,
696
+ autoReviewModelTargetConfigError,
668
697
  modelDisplayNamesConfigError,
669
698
  nonBlankStringArrayConfigError,
670
699
  normalizeNonBlankStringArray,
700
+ normalizeAutoReviewModelOverrides,
671
701
  positiveIntegerConfigError,
672
702
  positiveIntegerRecordConfigError,
673
703
  providerBaseUrlConfigError,
@@ -1041,6 +1071,11 @@ function canonicalHttpOrigin(value: string): string | null {
1041
1071
  }
1042
1072
  }
1043
1073
 
1074
+ const managementIngressSchema = z.union([
1075
+ z.object({ enabled: z.literal(false) }).strict(),
1076
+ z.object({ enabled: z.literal(true), port: z.number().int().min(1).max(65535) }).strict(),
1077
+ ]);
1078
+
1044
1079
  const hubConfigSchema = z.object({
1045
1080
  managementPublicOrigin: z.string().transform((value, ctx) => {
1046
1081
  const origin = canonicalHttpOrigin(value);
@@ -1064,10 +1099,7 @@ const hubConfigSchema = z.object({
1064
1099
  }).optional(),
1065
1100
  // A malformed hand edit disables only the optional ingress. Live writes are rejected by
1066
1101
  // managementIngressConfigError before this load-time degradation can hide the mistake.
1067
- managementIngress: z.union([
1068
- z.object({ enabled: z.literal(false) }).strict(),
1069
- z.object({ enabled: z.literal(true), port: z.number().int().min(1).max(65535) }).strict(),
1070
- ]).optional().catch(undefined),
1102
+ managementIngress: managementIngressSchema.optional().catch(undefined),
1071
1103
  }).strict();
1072
1104
 
1073
1105
  const tailscaleUserSchema = z.string().trim().min(1).superRefine((value, ctx) => {
@@ -1253,6 +1285,8 @@ const configSchema = z.object({
1253
1285
  configRebaseProvenance: z.unknown().optional(),
1254
1286
  // A retry can be billable, so absence and malformed hand edits both stay off.
1255
1287
  emptyCompletionRetry: z.boolean().optional().catch(false),
1288
+ // Header suppression changes what Codex sees, so absence and malformed edits stay off.
1289
+ dropCodexSafetyBuffering: z.boolean().optional().catch(false),
1256
1290
  // A malformed hand edit must not silently stop opening the browser: fall back
1257
1291
  // to undefined, which resolves to the historical auto-open behavior.
1258
1292
  oauthOpenBrowser: z.boolean().optional().catch(undefined),
@@ -1261,6 +1295,9 @@ const configSchema = z.object({
1261
1295
  googleAntigravityStaticCatalogVersion: z.union([z.literal(1), z.literal(2)]).optional().catch(undefined),
1262
1296
  subagentModelsVersion: z.number().int().positive().optional().catch(undefined),
1263
1297
  subagentModels: z.array(z.string().min(1)).optional().catch(undefined),
1298
+ // A hand-edited advisory version must not cost the operator their providers; a bad
1299
+ // value degrades to undefined, which simply raises the notice again.
1300
+ multiAgentSurfaceAdvisoryVersion: z.number().int().nonnegative().optional().catch(undefined),
1264
1301
  clientIntegrations: clientIntegrationsSchema.optional().catch(undefined),
1265
1302
  // A malformed profile policy must not fall back to legacy all-profile activation.
1266
1303
  asideProfileSync: asideProfileSyncSchema.optional().catch({ allProfiles: false }),
@@ -1269,6 +1306,7 @@ const configSchema = z.object({
1269
1306
  contextCapValue: z.number().int().positive().optional(),
1270
1307
  multiAgentGuidanceEnabled: z.boolean().optional(),
1271
1308
  // Invalid optional recovery config must not discard unrelated provider/account state.
1309
+ plaintextV2AgentMessages: z.boolean().optional().catch(undefined),
1272
1310
  agentTaskRecovery: agentTaskRecoverySchema.optional().catch(undefined),
1273
1311
  // Same rationale: a bad notify section must not cost the operator their providers.
1274
1312
  quotaResetNotify: quotaResetNotifySchema.optional().catch(undefined),
@@ -1304,9 +1342,6 @@ const configSchema = z.object({
1304
1342
  // A malformed hand edit must degrade to false without discarding providers, accounts,
1305
1343
  // or the exact selector map. Live writes remain strict.
1306
1344
  codexAccountPickerEnabled: z.boolean().optional().catch(false),
1307
- // Same degrade-not-reject rule: a malformed hand edit hides Spark rather than discarding the
1308
- // whole config. Hidden is also the default, so `catch(false)` and the default agree.
1309
- showCodexSparkQuota: z.boolean().optional().catch(false),
1310
1345
  resetCreditAutoRedeem: z.object({
1311
1346
  enabled: z.boolean().optional(),
1312
1347
  leadTimeMinutes: z.number().int().min(1).max(60).optional(),
@@ -1898,6 +1933,23 @@ export function retryOn429PolicyConfigError(policy: unknown): string | null {
1898
1933
  return `retryOn429.${field} is invalid (${first.message})`;
1899
1934
  }
1900
1935
 
1936
+ function sanitizeCapabilityDeclarationsForLoad(parsed: unknown): void {
1937
+ if (!parsed || typeof parsed !== "object") return;
1938
+ const providers = (parsed as Record<string, unknown>).providers;
1939
+ if (!providers || typeof providers !== "object" || Array.isArray(providers)) return;
1940
+ for (const [name, value] of Object.entries(providers)) {
1941
+ if (!value || typeof value !== "object" || Array.isArray(value)) continue;
1942
+ const provider = value as Record<string, unknown>;
1943
+ if (provider.modelCapabilities === undefined) continue;
1944
+ if (modelCapabilitiesConfigError(provider.modelCapabilities) !== null) {
1945
+ console.warn(`config.json provider ${JSON.stringify(redactSecretString(name))} has malformed modelCapabilities; retaining valid axes and restricting malformed input modalities to text`);
1946
+ const repaired = sanitizeModelCapabilitiesForLoad(provider.modelCapabilities);
1947
+ if (repaired) provider.modelCapabilities = repaired;
1948
+ else delete provider.modelCapabilities;
1949
+ }
1950
+ }
1951
+ }
1952
+
1901
1953
  /**
1902
1954
  * Load-time degradation for `providers.<name>.modelCosts`, mirroring
1903
1955
  * {@link sanitizeRetryOn429ForLoad}. A hand-edited malformed display-price row
@@ -1946,6 +1998,44 @@ function sanitizeModelCostsForLoad(parsed: unknown): void {
1946
1998
  }
1947
1999
  }
1948
2000
 
2001
+ /**
2002
+ * Load-time degradation for provider-scoped auto-review selectors. A malformed
2003
+ * hand edit must not fail the whole config parse; the management boundary stays
2004
+ * strict and rejects the same shapes before they can be written.
2005
+ */
2006
+ function sanitizeAutoReviewForLoad(parsed: unknown): void {
2007
+ if (!parsed || typeof parsed !== "object") return;
2008
+ const root = parsed as Record<string, unknown>;
2009
+ const providers = root.providers;
2010
+ if (!providers || typeof providers !== "object" || Array.isArray(providers)) return;
2011
+ for (const [name, providerValue] of Object.entries(providers as Record<string, unknown>)) {
2012
+ if (!providerValue || typeof providerValue !== "object" || Array.isArray(providerValue)) continue;
2013
+ const provider = providerValue as Record<string, unknown>;
2014
+ const safeProviderName = JSON.stringify(redactSecretString(name));
2015
+ if (name === "openai") {
2016
+ delete provider.autoReviewModel;
2017
+ delete provider.autoReviewModelOverrides;
2018
+ continue;
2019
+ }
2020
+ if (provider.autoReviewModel !== undefined
2021
+ && autoReviewModelTargetConfigError(provider.autoReviewModel, "autoReviewModel", true) !== null) {
2022
+ console.warn(`⚠️ config.json providers.${safeProviderName}.autoReviewModel is invalid — ignoring the selector`);
2023
+ delete provider.autoReviewModel;
2024
+ }
2025
+ if (provider.autoReviewModelOverrides !== undefined) {
2026
+ const overridesError = autoReviewModelOverridesConfigError(
2027
+ provider.autoReviewModelOverrides,
2028
+ "autoReviewModelOverrides",
2029
+ true,
2030
+ );
2031
+ if (overridesError) {
2032
+ console.warn(`⚠️ config.json providers.${safeProviderName}.autoReviewModelOverrides is invalid — ignoring the map`);
2033
+ delete provider.autoReviewModelOverrides;
2034
+ }
2035
+ }
2036
+ }
2037
+ }
2038
+
1949
2039
  /**
1950
2040
  * Companion to {@link warnDegradedStreamMode} for a blank persisted `hostname`. The bind
1951
2041
  * falls back to loopback, which is the safe direction but not what the file asked for —
@@ -1959,6 +2049,26 @@ function warnDegradedHostname(rawParsed: unknown, validated: OcxConfig): void {
1959
2049
  }
1960
2050
  }
1961
2051
 
2052
+ function degradedListenerWarnings(rawParsed: unknown, validated: OcxConfig): string[] {
2053
+ const raw = rawConfigRecord(rawParsed);
2054
+ if (!raw) return [];
2055
+ const warnings: string[] = [];
2056
+ if (raw.unauthenticatedLoopbackListener !== undefined && validated.unauthenticatedLoopbackListener === undefined) {
2057
+ warnings.push("unauthenticatedLoopbackListener ignored: invalid listener configuration; repair config.json before enabling the listener");
2058
+ }
2059
+ const hub = rawConfigRecord(raw.hub);
2060
+ if (hub?.managementIngress !== undefined && !managementIngressSchema.safeParse(hub.managementIngress).success) {
2061
+ warnings.push("hub.managementIngress ignored: invalid management listener configuration; repair config.json before enabling the listener");
2062
+ }
2063
+ return warnings;
2064
+ }
2065
+
2066
+ function warnDegradedListeners(rawParsed: unknown, validated: OcxConfig): void {
2067
+ for (const warning of degradedListenerWarnings(rawParsed, validated)) {
2068
+ console.warn(`⚠️ config.json ${warning}. Other settings were preserved.`);
2069
+ }
2070
+ }
2071
+
1962
2072
  /**
1963
2073
  * Companion to {@link warnDegradedStreamMode} for a malformed selection-order map.
1964
2074
  * Priority is a preference, so the schema drops the whole map rather than failing
@@ -2124,6 +2234,13 @@ function normalizePersistedClaudeCode(claudeCode: unknown): OcxConfig["claudeCod
2124
2234
  if (kept.length > 0) normalized.classifierFallbacks = kept;
2125
2235
  else delete normalized.classifierFallbacks;
2126
2236
  }
2237
+ const desktopProfile = normalized.desktopProfile;
2238
+ if (desktopProfile && typeof desktopProfile === "object" && !Array.isArray(desktopProfile)) {
2239
+ const profile = { ...desktopProfile } as Record<string, unknown>;
2240
+ if (typeof profile.appliedFingerprint !== "string") delete profile.appliedFingerprint;
2241
+ if (typeof profile.appliedAt !== "string") delete profile.appliedAt;
2242
+ normalized.desktopProfile = profile;
2243
+ }
2127
2244
  return normalized as OcxConfig["claudeCode"];
2128
2245
  }
2129
2246
 
@@ -2159,6 +2276,17 @@ function warnDegradedUpstreamHostCircuitThreshold(rawParsed: unknown): void {
2159
2276
  if (warning) console.warn(`⚠️ config.json ${warning}. Other settings were preserved.`);
2160
2277
  }
2161
2278
 
2279
+ function malformedPlaintextV2AgentMessagesWarning(value: unknown): string | null {
2280
+ const raw = rawConfigRecord(value);
2281
+ if (!raw || raw.plaintextV2AgentMessages === undefined || typeof raw.plaintextV2AgentMessages === "boolean") return null;
2282
+ return "plaintextV2AgentMessages ignored: expected a boolean";
2283
+ }
2284
+
2285
+ function warnDegradedPlaintextV2AgentMessages(value: unknown): void {
2286
+ const warning = malformedPlaintextV2AgentMessagesWarning(value);
2287
+ if (warning) console.warn(`⚠️ config.json ${warning}. Other settings were preserved.`);
2288
+ }
2289
+
2162
2290
  function malformedAgentTaskRecoveryWarning(rawParsed: unknown): string | null {
2163
2291
  const raw = rawConfigRecord(rawParsed);
2164
2292
  if (!raw || !Object.hasOwn(raw, "agentTaskRecovery")) return null;
@@ -2401,14 +2529,17 @@ export function loadConfig(): OcxConfig {
2401
2529
  sanitizeAliasesForLoad(parsed);
2402
2530
  sanitizeReasoningPinsForLoad(parsed);
2403
2531
  sanitizeModelDisplayNamesForLoad(parsed);
2532
+ sanitizeAutoReviewForLoad(parsed);
2404
2533
  sanitizeRetryOn429ForLoad(parsed);
2405
2534
  sanitizeModelCostsForLoad(parsed);
2535
+ sanitizeCapabilityDeclarationsForLoad(parsed);
2406
2536
  const result = configSchema.safeParse(parsed);
2407
2537
  if (result.success) {
2408
2538
  const config = normalizeApiKeyIds(result.data as OcxConfig);
2409
2539
  warnInheritedFastWireConflicts(configPath, config);
2410
2540
  warnDegradedStreamMode(parsed, config);
2411
2541
  warnDegradedHostname(parsed, config);
2542
+ warnDegradedListeners(parsed, config);
2412
2543
  warnDegradedApiKeys(parsed, config);
2413
2544
  warnDegradedCodexAccountPriorities(parsed, config);
2414
2545
  warnDegradedCodexQuotaAutoRefresh(parsed, config);
@@ -2416,6 +2547,7 @@ export function loadConfig(): OcxConfig {
2416
2547
  warnDegradedNativeSubagentConfig(parsed, config);
2417
2548
  warnDegradedCodexAccountPicker(parsed);
2418
2549
  warnDegradedUpstreamHostCircuitThreshold(parsed);
2550
+ warnDegradedPlaintextV2AgentMessages(parsed);
2419
2551
  warnDegradedAgentTaskRecovery(parsed);
2420
2552
  warnDegradedRuntimeRole(parsed);
2421
2553
  warnDegradedOptionalRemoteBlocks(parsed);
@@ -2427,7 +2559,18 @@ export function loadConfig(): OcxConfig {
2427
2559
  // discarding it entirely, so pool accounts and providers survive a missing
2428
2560
  // field like defaultProvider.
2429
2561
  const defaults = getDefaultConfig();
2430
- const merged = { ...defaults, ...parsed, subagentModelsVersion: parsed.subagentModelsVersion };
2562
+ // Pin the keys whose ABSENCE is meaningful. Spreading defaults underneath means any
2563
+ // key the stored document lacks is inherited, which is right for additive defaults and
2564
+ // wrong for a behavioral mode: a config that reaches this path only because it lost
2565
+ // `defaultProvider` would be repaired into v1 sub-agents and a pre-answered advisory,
2566
+ // silently changing a setting its operator never touched.
2567
+ const merged = {
2568
+ ...defaults,
2569
+ ...parsed,
2570
+ subagentModelsVersion: parsed.subagentModelsVersion,
2571
+ multiAgentMode: parsed.multiAgentMode,
2572
+ multiAgentSurfaceAdvisoryVersion: parsed.multiAgentSurfaceAdvisoryVersion,
2573
+ };
2431
2574
  // Ensure providers from both sides survive
2432
2575
  if (parsed.providers && defaults.providers) {
2433
2576
  merged.providers = { ...defaults.providers, ...parsed.providers };
@@ -2438,6 +2581,7 @@ export function loadConfig(): OcxConfig {
2438
2581
  const config = normalizeApiKeyIds(retryResult.data as OcxConfig);
2439
2582
  warnInheritedFastWireConflicts(configPath, config);
2440
2583
  warnDegradedHostname(parsed, config);
2584
+ warnDegradedListeners(parsed, config);
2441
2585
  warnDegradedApiKeys(parsed, config);
2442
2586
  warnDegradedCodexAccountPriorities(parsed, config);
2443
2587
  warnDegradedCodexQuotaAutoRefresh(parsed, config);
@@ -2445,6 +2589,7 @@ export function loadConfig(): OcxConfig {
2445
2589
  warnDegradedNativeSubagentConfig(parsed, config);
2446
2590
  warnDegradedCodexAccountPicker(parsed);
2447
2591
  warnDegradedUpstreamHostCircuitThreshold(parsed);
2592
+ warnDegradedPlaintextV2AgentMessages(parsed);
2448
2593
  warnDegradedAgentTaskRecovery(parsed);
2449
2594
  warnDegradedRuntimeRole(parsed);
2450
2595
  warnDegradedOptionalRemoteBlocks(parsed);
@@ -2463,6 +2608,7 @@ export function loadConfig(): OcxConfig {
2463
2608
  const config = normalizeApiKeyIds(salvaged.parsed);
2464
2609
  warnInheritedFastWireConflicts(configPath, config);
2465
2610
  warnDegradedHostname(parsed, config);
2611
+ warnDegradedListeners(parsed, config);
2466
2612
  warnDegradedApiKeys(parsed, config);
2467
2613
  warnDegradedCodexAccountPriorities(parsed, config);
2468
2614
  warnDegradedCodexQuotaAutoRefresh(parsed, config);
@@ -2470,6 +2616,7 @@ export function loadConfig(): OcxConfig {
2470
2616
  warnDegradedNativeSubagentConfig(parsed, config);
2471
2617
  warnDegradedCodexAccountPicker(parsed);
2472
2618
  warnDegradedUpstreamHostCircuitThreshold(parsed);
2619
+ warnDegradedPlaintextV2AgentMessages(parsed);
2473
2620
  warnDegradedAgentTaskRecovery(parsed);
2474
2621
  warnDegradedRuntimeRole(parsed);
2475
2622
  warnDegradedOptionalRemoteBlocks(parsed);
@@ -2597,6 +2744,7 @@ function validFileConfigDiagnostics(config: OcxConfig, rawParsed: unknown): Conf
2597
2744
  const warnings = configPlaceholderWarnings(normalized);
2598
2745
  warnings.push(...inheritedFastWireConflictProviderNames(normalized).map(inheritedFastWireConflictWarning));
2599
2746
  warnings.push(...degradedCodexAccountPriorityWarnings(rawParsed, normalized));
2747
+ warnings.push(...degradedListenerWarnings(rawParsed, normalized));
2600
2748
  const quotaAutoRefreshWarning = degradedCodexQuotaAutoRefreshWarning(rawParsed, normalized);
2601
2749
  if (quotaAutoRefreshWarning) warnings.push(quotaAutoRefreshWarning);
2602
2750
  if (rawEffort !== undefined && !isClaudeSubagentEffort(rawEffort)) {
@@ -2621,6 +2769,8 @@ function validFileConfigDiagnostics(config: OcxConfig, rawParsed: unknown): Conf
2621
2769
  if (notifyWarning) warnings.push(notifyWarning);
2622
2770
  const codexPoolWarning = malformedCodexPoolWarning(rawParsed);
2623
2771
  if (codexPoolWarning) warnings.push(codexPoolWarning);
2772
+ const plaintextWarning = malformedPlaintextV2AgentMessagesWarning(rawParsed);
2773
+ if (plaintextWarning) warnings.push(plaintextWarning);
2624
2774
  if (syncDisabledReason) {
2625
2775
  warnings.push(`syncCodexSubagentDefaults ignored: ${syncDisabledReason}`);
2626
2776
  }
@@ -2642,7 +2792,14 @@ function mergeConfigDefaults(parsed: unknown): unknown {
2642
2792
  if (!parsed || typeof parsed !== "object") return parsed;
2643
2793
  const defaults = getDefaultConfig();
2644
2794
  const raw = parsed as Record<string, unknown>;
2645
- const merged: Record<string, unknown> = { ...defaults, ...raw, subagentModelsVersion: raw.subagentModelsVersion };
2795
+ // Same absence-is-meaningful pin as the repair merge above.
2796
+ const merged: Record<string, unknown> = {
2797
+ ...defaults,
2798
+ ...raw,
2799
+ subagentModelsVersion: raw.subagentModelsVersion,
2800
+ multiAgentMode: raw.multiAgentMode,
2801
+ multiAgentSurfaceAdvisoryVersion: raw.multiAgentSurfaceAdvisoryVersion,
2802
+ };
2646
2803
  if (raw.providers && typeof raw.providers === "object" && defaults.providers) {
2647
2804
  merged.providers = { ...defaults.providers, ...(raw.providers as Record<string, unknown>) };
2648
2805
  }
@@ -2702,6 +2859,12 @@ function upstreamHostCircuitThresholdError(value: unknown): string | null {
2702
2859
  return `schema_invalid: upstreamHostCircuitThreshold: must be an integer from 0 to ${UPSTREAM_HOST_CIRCUIT_MAX_THRESHOLD}`;
2703
2860
  }
2704
2861
 
2862
+ function plaintextV2AgentMessagesError(value: unknown): string | null {
2863
+ return malformedPlaintextV2AgentMessagesWarning(value)
2864
+ ? "schema_invalid: plaintextV2AgentMessages: must be a boolean or omitted"
2865
+ : null;
2866
+ }
2867
+
2705
2868
  function agentTaskRecoveryError(value: unknown): string | null {
2706
2869
  const raw = rawConfigRecord(value);
2707
2870
  if (!raw || !Object.hasOwn(raw, "agentTaskRecovery") || raw.agentTaskRecovery === undefined) return null;
@@ -2858,6 +3021,14 @@ function emptyCompletionRetryError(value: unknown): string | null {
2858
3021
  return "schema_invalid: emptyCompletionRetry: must be a boolean or omitted";
2859
3022
  }
2860
3023
 
3024
+ function dropCodexSafetyBufferingError(value: unknown): string | null {
3025
+ const raw = rawConfigRecord(value);
3026
+ if (!raw || !Object.hasOwn(raw, "dropCodexSafetyBuffering")) return null;
3027
+ const enabled = raw.dropCodexSafetyBuffering;
3028
+ if (enabled === undefined || typeof enabled === "boolean") return null;
3029
+ return "schema_invalid: dropCodexSafetyBuffering: must be a boolean or omitted";
3030
+ }
3031
+
2861
3032
  function oauthOpenBrowserError(value: unknown): string | null {
2862
3033
  const raw = rawConfigRecord(value);
2863
3034
  if (!raw || !Object.hasOwn(raw, "oauthOpenBrowser")) return null;
@@ -2993,6 +3164,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
2993
3164
  ?? claudeSubagentEffortError(value)
2994
3165
  ?? appOwnedMemoryBudgetError(value)
2995
3166
  ?? upstreamHostCircuitThresholdError(value)
3167
+ ?? plaintextV2AgentMessagesError(value)
2996
3168
  ?? agentTaskRecoveryError(value)
2997
3169
  ?? quotaResetNotifyError(value)
2998
3170
  ?? codexPoolError(value)
@@ -3001,6 +3173,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
3001
3173
  ?? codexQuotaAutoRefreshError(value)
3002
3174
  ?? codexAccountPickerEnabledError(value)
3003
3175
  ?? emptyCompletionRetryError(value)
3176
+ ?? dropCodexSafetyBufferingError(value)
3004
3177
  ?? oauthOpenBrowserError(value)
3005
3178
  ?? runtimeRoleError(value)
3006
3179
  ?? remoteGuiConfigError(value)
@@ -3025,8 +3198,10 @@ function configDiagnosticsFromRaw(raw: string): ConfigDiagnostics {
3025
3198
  // schema and send the caller a default-config fallback (the config command could then
3026
3199
  // persist that fallback over the user's providers/keys).
3027
3200
  sanitizeModelDisplayNamesForLoad(parsed);
3201
+ sanitizeAutoReviewForLoad(parsed);
3028
3202
  sanitizeRetryOn429ForLoad(parsed);
3029
3203
  sanitizeModelCostsForLoad(parsed);
3204
+ sanitizeCapabilityDeclarationsForLoad(parsed);
3030
3205
  const result = configSchema.safeParse(parsed);
3031
3206
  if (result.success) {
3032
3207
  return validFileConfigDiagnostics(normalizeApiKeyIds(result.data as OcxConfig), parsed);
@@ -3050,10 +3225,13 @@ function configDiagnosticsFromRaw(raw: string): ConfigDiagnostics {
3050
3225
  // that ignores the error and writes it back preserves what the operator configured.
3051
3226
  const salvaged = salvageConfigCandidate(merged, retryResult.error);
3052
3227
  if (salvaged) {
3228
+ const config = normalizeApiKeyIds(salvaged.parsed);
3229
+ const warnings = degradedListenerWarnings(parsed, config);
3053
3230
  return {
3054
- config: normalizeApiKeyIds(salvaged.parsed),
3231
+ config,
3055
3232
  source: "fallback",
3056
3233
  error: schemaDiagnosticsError(result.error),
3234
+ ...(warnings.length > 0 ? { warnings } : {}),
3057
3235
  };
3058
3236
  }
3059
3237
 
@@ -4022,6 +4200,7 @@ export function getDefaultConfig(): OcxConfig {
4022
4200
  return {
4023
4201
  port: 10100,
4024
4202
  emptyCompletionRetry: false,
4203
+ dropCodexSafetyBuffering: false,
4025
4204
  fastRows: true,
4026
4205
  managementUsageMaxReadBytes: 64 * 1024 * 1024,
4027
4206
  appOwnedMemoryBudgetMb: DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES / (1024 * 1024),
@@ -4040,6 +4219,12 @@ export function getDefaultConfig(): OcxConfig {
4040
4219
  defaultProvider: "openai",
4041
4220
  subagentModels: [...DEFAULT_SUBAGENT_MODELS],
4042
4221
  subagentModelsVersion: SUBAGENT_MODELS_VERSION,
4222
+ // v1 is the shipped surface while a v2 native-to-routed task is undeliverable
4223
+ // ciphertext. Written explicitly rather than left absent, because an absent key
4224
+ // means base everywhere else. A fresh install starts already acknowledged: there is
4225
+ // nothing to advise an operator who is on the recommended surface.
4226
+ multiAgentMode: "v1",
4227
+ multiAgentSurfaceAdvisoryVersion: MULTI_AGENT_SURFACE_ADVISORY_VERSION,
4043
4228
  multiAgentGuidanceEnabled: true,
4044
4229
  websockets: false,
4045
4230
  codexAutoStart: true,
@@ -4185,6 +4370,16 @@ function warnConfigRepaired(configPath: string, error: z.ZodError): void {
4185
4370
  */
4186
4371
  const SALVAGEABLE_CONFIG_SECTIONS = ["routingProfiles", "combos"] as const;
4187
4372
 
4373
+ /** Optional nested fields that can be dropped whole without changing the rest of the document. */
4374
+ const SALVAGEABLE_OPTIONAL_FIELDS: ReadonlyArray<readonly [string, string]> = [
4375
+ ["claudeCode", "desktopProfile"],
4376
+ ];
4377
+
4378
+ function isSalvageableConfigPath(section: string, id: string): boolean {
4379
+ if ((SALVAGEABLE_CONFIG_SECTIONS as readonly string[]).includes(section)) return true;
4380
+ return SALVAGEABLE_OPTIONAL_FIELDS.some(path => path[0] === section && path[1] === id);
4381
+ }
4382
+
4188
4383
  /**
4189
4384
  * Drop just the named entries a parse failure blamed, so the rest of the
4190
4385
  * document survives.
@@ -4210,7 +4405,7 @@ function dropInvalidConfigSections(
4210
4405
  if (isUnsalvageableIssue(issue)) return null;
4211
4406
  const [section, id] = issue.path;
4212
4407
  if (typeof section !== "string" || typeof id !== "string") return null;
4213
- if (!(SALVAGEABLE_CONFIG_SECTIONS as readonly string[]).includes(section)) return null;
4408
+ if (!isSalvageableConfigPath(section, id)) return null;
4214
4409
  // A complaint about the container itself ("combos must be an object") is
4215
4410
  // not about one entry, so there is nothing selective to drop.
4216
4411
  if (issue.path.length < 2) return null;
@@ -4310,6 +4505,13 @@ function countSalvageableEntries(document: unknown): number {
4310
4505
  total += Object.keys(value as Record<string, unknown>).length;
4311
4506
  }
4312
4507
  }
4508
+ for (const [section, id] of SALVAGEABLE_OPTIONAL_FIELDS) {
4509
+ const container = (document as Record<string, unknown>)[section];
4510
+ if (container && typeof container === "object" && !Array.isArray(container)
4511
+ && Object.hasOwn(container as Record<string, unknown>, id)) {
4512
+ total += 1;
4513
+ }
4514
+ }
4313
4515
  return total;
4314
4516
  }
4315
4517