@bitkyc08/opencodex 2.75.0 → 2.76.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 (217) hide show
  1. package/README.md +6 -1
  2. package/bin/ocx.mjs +12 -1
  3. package/gui/dist/assets/App-CddorHdA.js +51 -0
  4. package/gui/dist/assets/App-DMudtISW.css +1 -0
  5. package/gui/dist/assets/{Tray-CGAD2AGk.js → Tray-DjNwwrWS.js} +1 -1
  6. package/gui/dist/assets/index-Btze1I3T.css +1 -0
  7. package/gui/dist/assets/index-CvnY7E6i.js +86 -0
  8. package/gui/dist/assets/quota-summary-B_AWpHfP.js +1 -0
  9. package/gui/dist/index.html +2 -2
  10. package/gui/dist/provider-icons/opengateway.svg +12 -0
  11. package/package.json +1 -1
  12. package/src/adapters/anthropic/account-metadata.ts +17 -0
  13. package/src/adapters/anthropic/client-identity.ts +69 -0
  14. package/src/adapters/anthropic/passthrough.ts +23 -4
  15. package/src/adapters/anthropic.ts +18 -1
  16. package/src/adapters/command-code-project-context.ts +7 -11
  17. package/src/adapters/devin/cloud-direct/tool-schema.ts +12 -48
  18. package/src/adapters/kiro-retry.ts +23 -8
  19. package/src/adapters/openai-chat/messages.ts +1 -1
  20. package/src/adapters/registry.ts +8 -1
  21. package/src/adapters/zed.ts +377 -0
  22. package/src/bridge/sse.ts +4 -2
  23. package/src/chatgpt/app-server-shim/app-server-rewrite.ts +45 -0
  24. package/src/chatgpt/app-server-shim/bundle-trust.ts +77 -0
  25. package/src/chatgpt/app-server-shim/filter.ts +157 -0
  26. package/src/chatgpt/app-server-shim/gate-rewrite.ts +99 -0
  27. package/src/chatgpt/app-server-shim/launcher.ts +91 -0
  28. package/src/claude/desktop-3p-startup.ts +62 -0
  29. package/src/claude/desktop-3p.ts +5 -0
  30. package/src/claude/first-party-settings.ts +14 -0
  31. package/src/claude/intercept/cli-catalog.ts +150 -0
  32. package/src/claude/intercept/cli-picker.ts +82 -0
  33. package/src/claude/intercept/listener.ts +21 -6
  34. package/src/claude/intercept/picker-bootstrap.ts +23 -5
  35. package/src/claude/intercept/picker-models.ts +57 -13
  36. package/src/claude/intercept/runtime.ts +30 -2
  37. package/src/claude/intercept/settings.ts +3 -0
  38. package/src/cli/agent.ts +172 -0
  39. package/src/cli/alias.ts +2 -1
  40. package/src/cli/capabilities-command.ts +3 -2
  41. package/src/cli/capabilities.ts +87 -0
  42. package/src/cli/chatgpt-command.ts +140 -0
  43. package/src/cli/combo.ts +144 -19
  44. package/src/cli/config-command.ts +3 -1
  45. package/src/cli/dispatch.ts +10 -1
  46. package/src/cli/help.ts +1 -0
  47. package/src/cli/index.ts +5 -24
  48. package/src/cli/init.ts +16 -2
  49. package/src/cli/integrations.ts +13 -0
  50. package/src/cli/internal-command.ts +12 -5
  51. package/src/cli/observe.ts +6 -1
  52. package/src/cli/registry.ts +6 -0
  53. package/src/cli/route-policy.ts +3 -1
  54. package/src/cli/runtime-api.ts +5 -2
  55. package/src/cli/status-oauth.ts +5 -0
  56. package/src/cli/status.ts +8 -1
  57. package/src/cli/stop-approval.ts +55 -16
  58. package/src/cli/stop-report.ts +5 -0
  59. package/src/clients/lazycodex.ts +48 -0
  60. package/src/clients/omo-role-models.ts +111 -0
  61. package/src/codex/account-credit-use.ts +99 -0
  62. package/src/codex/account-lifecycle.ts +2 -0
  63. package/src/codex/agent-role-models.ts +171 -0
  64. package/src/codex/auth-api/account-list.ts +10 -3
  65. package/src/codex/auth-api/routes.ts +34 -0
  66. package/src/codex/auth-context.ts +52 -11
  67. package/src/codex/catalog/discovered-natives.ts +11 -1
  68. package/src/codex/catalog/gather-capture.ts +6 -1
  69. package/src/codex/catalog/model-hints.ts +9 -1
  70. package/src/codex/catalog/provider-models.ts +60 -0
  71. package/src/codex/catalog/routed-gather.ts +30 -5
  72. package/src/codex/convergence-types.ts +2 -1
  73. package/src/codex/main-account-external-usage.ts +62 -0
  74. package/src/codex/main-account-hard-lock.ts +24 -13
  75. package/src/codex/native-profile-processes.ts +53 -16
  76. package/src/codex/prompt-layers/toml-edit.ts +1 -1
  77. package/src/codex/quota-types.ts +3 -7
  78. package/src/codex/quota.ts +19 -4
  79. package/src/codex/role-auto-assign.ts +159 -0
  80. package/src/codex/role-sizing-limits.ts +2 -0
  81. package/src/codex/role-sizing.ts +216 -0
  82. package/src/codex/routing/selection.ts +36 -0
  83. package/src/codex/routing/thread-affinity.ts +2 -0
  84. package/src/codex/routing.ts +4 -4
  85. package/src/codex/subagent-model-fallback.ts +82 -26
  86. package/src/codex/subagent-selectable-models.ts +32 -0
  87. package/src/combos/index.ts +18 -0
  88. package/src/combos/jev-decision-contract.ts +20 -0
  89. package/src/combos/jev-dispatch.ts +18 -0
  90. package/src/combos/jev-model-backend.ts +127 -0
  91. package/src/combos/jev.ts +249 -33
  92. package/src/combos/types.ts +125 -2
  93. package/src/config/diagnostics.ts +18 -0
  94. package/src/config/macos-system-proxy.ts +72 -15
  95. package/src/config/owner-registry.ts +13 -4
  96. package/src/config/process-state.ts +17 -8
  97. package/src/config/proxy-env.ts +35 -5
  98. package/src/config/schema/config-schema.ts +30 -1
  99. package/src/config/schema/leaf-validators.ts +4 -0
  100. package/src/config/serving-runtimes.ts +95 -1
  101. package/src/generated/compatibility-version.json +336 -156
  102. package/src/lib/errors.ts +7 -7
  103. package/src/lib/jsonc.ts +13 -0
  104. package/src/lib/local-chat-completion.ts +144 -0
  105. package/src/lib/provider-outbound.ts +87 -1
  106. package/src/lib/provider-runtime-fetch.ts +23 -0
  107. package/src/lib/provider-tls-profile.ts +221 -0
  108. package/src/lib/request-execution-budget.ts +16 -6
  109. package/src/lib/spend-reservation-ledger.ts +54 -20
  110. package/src/lib/sse-decoder.ts +20 -17
  111. package/src/lib/synced-state-location.ts +96 -0
  112. package/src/lib/winsw.ts +14 -5
  113. package/src/oauth/anthropic-account-refusal.ts +53 -4
  114. package/src/oauth/anthropic-model-quota.ts +83 -0
  115. package/src/oauth/anthropic-rate-limit-policy.ts +37 -0
  116. package/src/oauth/anthropic-routing.ts +112 -58
  117. package/src/oauth/health.ts +39 -3
  118. package/src/oauth/index.ts +14 -0
  119. package/src/oauth/kiro-account-load.ts +10 -0
  120. package/src/oauth/zed.ts +134 -0
  121. package/src/providers/model-discovery.ts +19 -8
  122. package/src/providers/model-rename-fields.ts +1 -0
  123. package/src/providers/provider-id-rewrite.ts +9 -2
  124. package/src/providers/quota/account-cache.ts +29 -7
  125. package/src/providers/quota/anthropic-cooldown-recovery.ts +18 -0
  126. package/src/providers/quota/anthropic-family-headers.ts +25 -0
  127. package/src/providers/quota/vendor-probes-oauth.ts +5 -1
  128. package/src/providers/quota-types.ts +4 -0
  129. package/src/providers/registry/entries-core.ts +18 -0
  130. package/src/providers/registry/entries-extended.ts +55 -0
  131. package/src/providers/registry/types.ts +3 -0
  132. package/src/providers/xai-fast-model.ts +12 -4
  133. package/src/providers/zed.ts +598 -0
  134. package/src/responses/state/snapshot-select.ts +22 -9
  135. package/src/responses/state.ts +2 -3
  136. package/src/router.ts +9 -0
  137. package/src/server/anthropic-rate-limit-headers.ts +7 -0
  138. package/src/server/auth-cors.ts +7 -0
  139. package/src/server/chat-completions.ts +9 -4
  140. package/src/server/claude-messages.ts +59 -21
  141. package/src/server/index/claude-intercept-lifecycle.ts +71 -25
  142. package/src/server/index/optional-listeners.ts +4 -0
  143. package/src/server/index/spend-ledger-lifecycle.ts +4 -0
  144. package/src/server/management/agent-settings-routes.ts +74 -37
  145. package/src/server/management/claude-desktop-picker-routes.ts +7 -0
  146. package/src/server/management/claude-intercept-routes.ts +35 -0
  147. package/src/server/management/codex-agent-role-routes.ts +122 -0
  148. package/src/server/management/codex-role-auto-assign.ts +208 -0
  149. package/src/server/management/combo-routes.ts +62 -5
  150. package/src/server/management/config-routes.ts +35 -3
  151. package/src/server/management/context.ts +4 -1
  152. package/src/server/management/decision-discovery.ts +74 -0
  153. package/src/server/management/decision-model-validation.ts +80 -0
  154. package/src/server/management/decision-routes.ts +160 -0
  155. package/src/server/management/native-integration-routes.ts +16 -0
  156. package/src/server/management/provider-routes.ts +30 -34
  157. package/src/server/management/route-registry.ts +12 -0
  158. package/src/server/management/sibling-guard.ts +1 -0
  159. package/src/server/management/zed-provider-probe.ts +38 -0
  160. package/src/server/management-api.ts +12 -1
  161. package/src/server/messages-native-oauth.ts +7 -2
  162. package/src/server/messages-native.ts +7 -3
  163. package/src/server/relay.ts +3 -1
  164. package/src/server/request-log.ts +2 -0
  165. package/src/server/responses/adapter-continuation.ts +3 -3
  166. package/src/server/responses/adapter-dispatch.ts +18 -3
  167. package/src/server/responses/compact.ts +14 -1
  168. package/src/server/responses/core-auth.ts +2 -0
  169. package/src/server/responses/core-combo.ts +67 -11
  170. package/src/server/responses/core-normalize.ts +21 -1
  171. package/src/server/responses/core-options.ts +11 -1
  172. package/src/server/responses/core.ts +5 -3
  173. package/src/server/responses/fetch-helpers.ts +6 -3
  174. package/src/server/responses/jev-model-invoke.ts +190 -0
  175. package/src/server/responses/passthrough-delivery.ts +16 -0
  176. package/src/server/responses/policy-fallback.ts +55 -3
  177. package/src/server/responses/policy-request-scope.ts +80 -0
  178. package/src/server/responses/request-prepare.ts +70 -18
  179. package/src/server/responses/request-transport.ts +54 -23
  180. package/src/server/responses/run-turn-execution.ts +1 -0
  181. package/src/server/responses/sidecar-execution.ts +9 -3
  182. package/src/server/responses-hosted-image-display.ts +225 -0
  183. package/src/service/managing-cli.ts +34 -4
  184. package/src/service/systemd-env.ts +75 -0
  185. package/src/service/windows-guarded-manager.ts +60 -19
  186. package/src/service/windows-scheduler.ts +22 -3
  187. package/src/service/windows-taskxml.ts +32 -10
  188. package/src/service-manager-probe.ts +72 -19
  189. package/src/types/config.ts +31 -1
  190. package/src/types/provider.ts +2 -0
  191. package/src/types/request.ts +2 -0
  192. package/src/update/index.ts +39 -6
  193. package/src/update/job.ts +63 -63
  194. package/src/update/npm-cache-preflight.d.mts +33 -1
  195. package/src/update/npm-cache-preflight.mjs +131 -18
  196. package/src/update/ownership-transaction.ts +19 -1
  197. package/src/update/restart-ownership.ts +80 -6
  198. package/src/update/transactional-install.d.mts +2 -0
  199. package/src/update/transactional-install.mjs +6 -1
  200. package/src/usage/jev-stats.ts +28 -1
  201. package/src/usage/log.ts +1 -1
  202. package/src/vision/anthropic-describe.ts +5 -3
  203. package/src/vision/index.ts +1 -0
  204. package/src/vision/plan.ts +2 -1
  205. package/src/vision/routed-describe.ts +29 -121
  206. package/src/web-search/alpha-search.ts +3 -3
  207. package/src/web-search/anthropic-executor.ts +6 -4
  208. package/src/web-search/index.ts +1 -1
  209. package/src/web-search/loop.ts +3 -3
  210. package/src/web-search/passthrough-bridge.ts +2 -1
  211. package/src/web-search/run-turn-loop.ts +1 -1
  212. package/src/web-search/sidecar-providers.ts +2 -1
  213. package/gui/dist/assets/App-ia4U0ufY.css +0 -1
  214. package/gui/dist/assets/App-xm0zGNOe.js +0 -51
  215. package/gui/dist/assets/index-DBT3Q31Y.css +0 -1
  216. package/gui/dist/assets/index-D_RGAT6Q.js +0 -86
  217. package/gui/dist/assets/quota-summary-CUFq48t-.js +0 -1
@@ -1,3 +1,4 @@
1
+ import { ensureManagementClaudeIntercept, interceptStartRefusal } from "./claude-intercept-routes";
1
2
  /**
2
3
  * Claude Desktop picker mode over the management API, and the transition helpers every Desktop
3
4
  * mode change uses.
@@ -76,6 +77,12 @@ export async function handleClaudeDesktopPickerRoutes(ctx: ManagementContext): P
76
77
  }
77
78
  const body = parsePickerBody(raw);
78
79
  if (typeof body === "string") return jsonResponse({ error: body }, 400);
80
+ if (body.enabled) {
81
+ const refusal = interceptStartRefusal(ctx);
82
+ if (refusal) return refusal;
83
+ const started = await ensureManagementClaudeIntercept(ctx);
84
+ if (!started.ok) return jsonResponse({ ...started, code: started.reason }, 409);
85
+ }
79
86
  const controller = await currentPickerController();
80
87
  if (body.enabled) {
81
88
  if (!controller) {
@@ -0,0 +1,35 @@
1
+ import type { ManagementContext } from "./context";
2
+ import { jsonResponse } from "../auth-cors";
3
+ import { claudeInterceptEnabled, getClaudeInterceptState, type ClaudeInterceptOutcome } from "../../claude/intercept/runtime";
4
+
5
+ /** Local management authority only; data-plane credentials never grant this capability. */
6
+ export function interceptStartRefusal(ctx: ManagementContext): Response | null {
7
+ return ctx.trustedLoopbackIngress && (ctx.principal === "gui-session" || ctx.principal === "admin-token")
8
+ ? null : jsonResponse({ ok: false, code: "intercept_start_forbidden", error: "Local management authority is required." }, 403);
9
+ }
10
+
11
+ export async function ensureManagementClaudeIntercept(ctx: ManagementContext): Promise<ClaudeInterceptOutcome> {
12
+ if (ctx.config.runtimeRole === "client") return { ok: false, reason: "client_role" };
13
+ if (!claudeInterceptEnabled(ctx.config)) return { ok: false, reason: "disabled" };
14
+ if (ctx.deps.ensureClaudeIntercept) return ctx.deps.ensureClaudeIntercept();
15
+ const injected = ctx.deps.getClaudeInterceptState?.();
16
+ if (injected) return { ok: true, state: injected };
17
+ const listener = ctx.deps.linkListener?.();
18
+ if (listener?.ensureClaudeIntercept) return listener.ensureClaudeIntercept();
19
+ const state = (ctx.deps.getClaudeInterceptState ?? getClaudeInterceptState)();
20
+ return state ? { ok: true, state } : { ok: false, reason: "failed" };
21
+ }
22
+
23
+ export function interceptStatus(ctx: ManagementContext): { interceptReason: string | null; pickerReason: string | null; pickerFailurePort?: number; interceptFailurePort?: number } {
24
+ const outcome = ctx.deps.linkListener?.()?.claudeInterceptOutcome?.();
25
+ const state = (ctx.deps.getClaudeInterceptState ?? getClaudeInterceptState)();
26
+ return { ...(outcome && !outcome.ok ? { interceptFailurePort: outcome.port } : {}), interceptReason: outcome && !outcome.ok ? outcome.reason : null, pickerReason: state?.pickerReason ?? null, pickerFailurePort: state?.pickerFailurePort };
27
+ }
28
+
29
+ export async function handleClaudeInterceptRoutes(ctx: ManagementContext): Promise<Response | null> {
30
+ if (ctx.url.pathname !== "/api/claude-intercept/start" || ctx.req.method !== "POST") return null;
31
+ const refusal = interceptStartRefusal(ctx);
32
+ if (refusal) return refusal;
33
+ const outcome = await ensureManagementClaudeIntercept(ctx);
34
+ return jsonResponse(outcome, outcome.ok ? 200 : 409);
35
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Per-role model for omo (Codex / LazyCodex): `$CODEX_HOME/agents/<role>.toml` plus LazyCodex's
3
+ * `codex.agents.<role>.model` mirror in omo.jsonc. Both halves exist only when LazyCodex is
4
+ * detected; Pi-based and OpenCode-based omo are never read here.
5
+ *
6
+ * Loaded on demand from `src/server/management-api.ts`, like the quota-reset handler, so a
7
+ * dashboard request that never opens the omo tab loads neither writer.
8
+ */
9
+ import { jsonResponse } from "../auth-cors";
10
+ import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
11
+ import type { ManagementContext } from "./context";
12
+
13
+ const ROLE_PATH_PREFIX = "/api/codex-agent-roles/";
14
+ const AUTO_ASSIGN_PATH = "/api/codex-agent-roles/auto-assign";
15
+
16
+ function lazycodexNotDetected(ctx: ManagementContext): Response {
17
+ return jsonResponse({
18
+ error: "omo (Codex / LazyCodex) is not installed in this CODEX_HOME",
19
+ code: "lazycodex_not_detected",
20
+ }, 409, ctx.req, ctx.config);
21
+ }
22
+
23
+ export async function handleCodexAgentRoleRoutes(ctx: ManagementContext): Promise<Response | null> {
24
+ const { req, url, config, deps } = ctx;
25
+ const [{ getCodexHome }, roles, omo, { detectLazyCodex }] = await Promise.all([
26
+ import("../../codex/paths"),
27
+ import("../../codex/agent-role-models"),
28
+ import("../../clients/omo-role-models"),
29
+ import("../../clients/lazycodex"),
30
+ ]);
31
+
32
+ if (url.pathname === "/api/codex-agent-roles" && req.method === "GET") {
33
+ const codexHome = getCodexHome();
34
+ const lazycodex = detectLazyCodex(codexHome);
35
+ if (!lazycodex.detected) return jsonResponse({ lazycodex, omoJsonc: null, roles: [] }, 200, req, config);
36
+ const omoState = omo.readOmoRoleModels(omo.omoJsoncPath());
37
+ const omoModels = omoState.state === "present" ? omoState.models : {};
38
+ return jsonResponse({
39
+ lazycodex,
40
+ omoJsonc: { state: omoState.state },
41
+ roles: roles.listCodexAgentRoleModels(codexHome).map(entry => ({
42
+ ...entry,
43
+ omoJsoncModel: omoModels[entry.role] ?? null,
44
+ })),
45
+ }, 200, req, config);
46
+ }
47
+
48
+ if (url.pathname === AUTO_ASSIGN_PATH && req.method === "POST") {
49
+ const codexHome = getCodexHome();
50
+ if (!detectLazyCodex(codexHome).detected) return lazycodexNotDetected(ctx);
51
+ let body: { model?: unknown };
52
+ try {
53
+ body = await readManagementJsonBody(req);
54
+ } catch (error) {
55
+ rethrowManagementBodyTooLarge(error);
56
+ return jsonResponse({ error: "invalid JSON body" }, 400, req, config);
57
+ }
58
+ if (body?.model !== undefined && typeof body.model !== "string") {
59
+ return jsonResponse({ error: "model must be a string", code: "invalid_model" }, 400, req, config);
60
+ }
61
+ const [{ proposeCodexRoleModels, NoSizingModelError }, { fetchAllModels }] = await Promise.all([
62
+ import("./codex-role-auto-assign"),
63
+ import("./shared"),
64
+ ]);
65
+ try {
66
+ return jsonResponse(await proposeCodexRoleModels({
67
+ config,
68
+ codexHome,
69
+ ...(typeof body?.model === "string" ? { sizingModel: body.model } : {}),
70
+ fetchAllModels: deps.fetchAllModels ?? fetchAllModels,
71
+ ...(deps.completeCodexRoleSizing ? { completeRoleSizing: deps.completeCodexRoleSizing } : {}),
72
+ }), 200, req, config);
73
+ } catch (error) {
74
+ if (error instanceof NoSizingModelError) {
75
+ return jsonResponse({ error: error.message, code: "no_sizing_model" }, 409, req, config);
76
+ }
77
+ throw error;
78
+ }
79
+ }
80
+
81
+ if (!url.pathname.startsWith(ROLE_PATH_PREFIX) || req.method !== "PUT") return null;
82
+ const codexHome = getCodexHome();
83
+ if (!detectLazyCodex(codexHome).detected) return lazycodexNotDetected(ctx);
84
+ let role: string;
85
+ try {
86
+ role = decodeURIComponent(url.pathname.slice(ROLE_PATH_PREFIX.length));
87
+ } catch {
88
+ return jsonResponse({ error: "role must be URL-encoded", code: "invalid_role" }, 400, req, config);
89
+ }
90
+ let body: { model?: unknown; effort?: unknown };
91
+ try {
92
+ body = await readManagementJsonBody(req);
93
+ } catch (error) {
94
+ rethrowManagementBodyTooLarge(error);
95
+ return jsonResponse({ error: "invalid JSON body" }, 400, req, config);
96
+ }
97
+
98
+ let model: string;
99
+ let effort: string | undefined;
100
+ let toml: { status: "written" | "unchanged" };
101
+ try {
102
+ model = roles.validateAgentRoleModel(body?.model);
103
+ effort = body?.effort === undefined || body.effort === null ? undefined : roles.validateAgentRoleEffort(body.effort);
104
+ toml = roles.writeCodexAgentRoleModel(role, model, codexHome, effort);
105
+ } catch (error) {
106
+ if (error instanceof roles.AgentRoleModelError) {
107
+ const status = error.code === "unknown_role" ? 404 : error.code === "invalid_model" || error.code === "invalid_effort" ? 400 : 409;
108
+ return jsonResponse({ error: error.message, code: error.code }, status, req, config);
109
+ }
110
+ // Filesystem and ownership errors carry absolute paths and UIDs; keep them out of the response.
111
+ return jsonResponse({ error: "could not write the role file", code: "write_failed" }, 500, req, config);
112
+ }
113
+
114
+ // The role TOML is what Codex obeys, so its write stands even when the omo mirror cannot follow.
115
+ let omoStatus: ReturnType<typeof omo.writeOmoRoleModel> | "write_failed";
116
+ try {
117
+ omoStatus = omo.writeOmoRoleModel(role, model, omo.omoJsoncPath());
118
+ } catch {
119
+ omoStatus = "write_failed";
120
+ }
121
+ return jsonResponse({ ok: true, role, model, ...(effort ? { effort } : {}), toml, omoJsonc: { status: omoStatus } }, 200, req, config);
122
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * The read-only half of role auto-assign: size every Codex agent role with one model call, map
3
+ * each size to a concrete model and effort, and return the proposals. Nothing is written here;
4
+ * the dashboard and "ocx agent roles suggest --apply" apply a proposal through the ordinary
5
+ * PUT /api/codex-agent-roles/{role}.
6
+ */
7
+ import { readFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import type { OcxConfig } from "../../types";
10
+ import type { CatalogModel } from "../../codex/catalog";
11
+ import type { RoleModelCandidate } from "../../codex/role-auto-assign";
12
+
13
+ export interface RoleSizingCall {
14
+ readonly model: string;
15
+ readonly system: string;
16
+ readonly user: string;
17
+ }
18
+
19
+ export type CompleteRoleSizing = (call: RoleSizingCall, config: OcxConfig) => Promise<{ text: string; error?: string }>;
20
+
21
+ const SIZING_TIMEOUT_MS = 180_000;
22
+ const MAX_SIZING_RESPONSE_BYTES = 1024 * 1024;
23
+
24
+ /**
25
+ * The completion error can carry upstream response text and exception messages, which may
26
+ * include paths, account identifiers or secrets the redactor does not recognise. Only a fixed
27
+ * category, plus a bare HTTP status, leaves the management response and the CLI.
28
+ */
29
+ export function publicSizingError(error: string): string {
30
+ const http = /^[\w .-]* HTTP (\d{3}):/.exec(error);
31
+ if (http) return `HTTP ${http[1]}`;
32
+ if (error.endsWith("response exceeded byte bound")) return "the response was too large";
33
+ if (error.endsWith("returned non-JSON")) return "the response was not JSON";
34
+ if (error.endsWith("returned no text")) return "the response had no text";
35
+ return "the request could not be completed";
36
+ }
37
+
38
+ async function completeThroughProxy(call: RoleSizingCall, config: OcxConfig) {
39
+ const { postLocalChatCompletion } = await import("../../lib/local-chat-completion");
40
+ return postLocalChatCompletion({
41
+ config,
42
+ label: "role sizing",
43
+ logTag: "role-sizing",
44
+ timeoutMs: SIZING_TIMEOUT_MS,
45
+ maxResponseBytes: MAX_SIZING_RESPONSE_BYTES,
46
+ boundWhileStreaming: true,
47
+ body: {
48
+ model: call.model,
49
+ messages: [{ role: "system", content: call.system }, { role: "user", content: call.user }],
50
+ },
51
+ });
52
+ }
53
+
54
+ async function modelCandidates(slugs: readonly string[], models: readonly CatalogModel[]): Promise<RoleModelCandidate[]> {
55
+ const [catalog, { resolveMatchedPrice }] = await Promise.all([
56
+ import("../../codex/catalog"),
57
+ import("../../usage/cost"),
58
+ ]);
59
+ const nativeSlugs = catalog.listCatalogNativeSlugs();
60
+ const routed = new Map(models.map(model => [catalog.catalogModelSlug(model), model]));
61
+ const unitPrice = (provider: string, modelId: string): number | null => {
62
+ const cost = resolveMatchedPrice(provider, modelId)?.cost4;
63
+ return cost ? cost.input + cost.output : null;
64
+ };
65
+ return slugs.map(slug => {
66
+ const row = routed.get(slug);
67
+ if (row) {
68
+ return {
69
+ model: slug,
70
+ unitPrice: unitPrice(row.provider, row.id),
71
+ efforts: row.reasoningEfforts ?? [],
72
+ ...(row.defaultReasoningEffort ? { defaultEffort: row.defaultReasoningEffort } : {}),
73
+ };
74
+ }
75
+ if (nativeSlugs.includes(slug)) {
76
+ const defaultEffort = catalog.nativeDefaultReasoningEffort(slug);
77
+ return {
78
+ model: slug,
79
+ unitPrice: unitPrice("openai", slug),
80
+ efforts: catalog.nativeReasoningEfforts(slug),
81
+ ...(defaultEffort ? { defaultEffort } : {}),
82
+ };
83
+ }
84
+ return { model: slug, unitPrice: null, efforts: [] };
85
+ });
86
+ }
87
+
88
+ async function roleModelCandidates(config: OcxConfig, models: readonly CatalogModel[]): Promise<RoleModelCandidate[]> {
89
+ const [{ listCatalogNativeSlugs }, { subagentSelectableModels }] = await Promise.all([
90
+ import("../../codex/catalog"),
91
+ import("../../codex/subagent-selectable-models"),
92
+ ]);
93
+ return modelCandidates(subagentSelectableModels(config, models, listCatalogNativeSlugs()), models);
94
+ }
95
+
96
+ export class NoSizingModelError extends Error {}
97
+
98
+ function resolveSizingModel(sizingModel: string | undefined, readConfiguredDefaultModel: () => string | null): string {
99
+ const model = sizingModel?.trim() || readConfiguredDefaultModel();
100
+ if (!model) throw new NoSizingModelError("no default model is set in Codex config.toml; pass a sizing model");
101
+ return model;
102
+ }
103
+
104
+ export async function proposeCodexRoleModels(options: {
105
+ config: OcxConfig;
106
+ codexHome: string;
107
+ sizingModel?: string;
108
+ fetchAllModels: (config: OcxConfig) => Promise<CatalogModel[]>;
109
+ completeRoleSizing?: CompleteRoleSizing;
110
+ }) {
111
+ const [roles, sizing, assign, { readConfiguredDefaultModel }] = await Promise.all([
112
+ import("../../codex/agent-role-models"),
113
+ import("../../codex/role-sizing"),
114
+ import("../../codex/role-auto-assign"),
115
+ import("../../codex/catalog/parsing"),
116
+ ]);
117
+ const sizingModel = resolveSizingModel(options.sizingModel, readConfiguredDefaultModel);
118
+
119
+ const roleRows = roles.listCodexAgentRoleModels(options.codexHome).map(row => ({
120
+ role: row.role,
121
+ model: row.model,
122
+ effort: roles.readCodexAgentRoleEffort(row.role, options.codexHome),
123
+ }));
124
+ const inputs: { role: string; instructions: string }[] = [];
125
+ const outcomes = new Map<string, import("../../codex/role-sizing").RoleSizingOutcome>();
126
+ for (const row of roleRows) {
127
+ let excerpt: string | null = null;
128
+ try {
129
+ excerpt = sizing.roleInstructionsExcerpt(readFileSync(join(options.codexHome, "agents", `${row.role}.toml`), "utf8"));
130
+ } catch { /* an unreadable role is reported unsized below */ }
131
+ if (excerpt === null) outcomes.set(row.role, { unsized: "the role file has no description or developer_instructions to size from" });
132
+ else inputs.push({ role: row.role, instructions: excerpt });
133
+ }
134
+
135
+ let sizingError: string | null = null;
136
+ if (inputs.length > 0) {
137
+ const answer = await (options.completeRoleSizing ?? completeThroughProxy)({
138
+ model: sizingModel,
139
+ system: sizing.ROLE_SIZING_SYSTEM_PROMPT,
140
+ user: sizing.buildRoleSizingUserMessage(inputs),
141
+ }, options.config);
142
+ const names = inputs.map(input => input.role);
143
+ if (answer.error) {
144
+ sizingError = publicSizingError(answer.error);
145
+ for (const role of names) outcomes.set(role, { unsized: `the sizing call failed: ${sizingError}` });
146
+ } else {
147
+ for (const [role, outcome] of sizing.parseRoleSizingResponse(answer.text, names)) outcomes.set(role, outcome);
148
+ }
149
+ }
150
+
151
+ const classified = assign.classifyRoleModelCandidates(
152
+ await roleModelCandidates(options.config, await options.fetchAllModels(options.config)),
153
+ options.config.codexRoleTiers,
154
+ );
155
+ return {
156
+ sizingModel,
157
+ sizingError,
158
+ proposals: assign.buildRoleProposals(roleRows, outcomes, classified),
159
+ candidates: classified.map(({ model, tier, tierSource, unitPrice }) => ({ model, tier, tierSource, unitPrice })),
160
+ };
161
+ }
162
+
163
+ export const DELEGATED_WORK_ROLE = "delegated-work";
164
+
165
+ /**
166
+ * The same sizing for the Subagents page's delegation default: the work a parent hands its
167
+ * preferred subagent is sized as one standing role, and the pick comes from the models that page
168
+ * offers. Effort is always proposed, restricted to the Codex levels the page can save.
169
+ */
170
+ export async function proposeDelegationModel(options: {
171
+ config: OcxConfig;
172
+ work: string;
173
+ offered: readonly string[];
174
+ sizingModel?: string;
175
+ models: readonly CatalogModel[];
176
+ completeRoleSizing?: CompleteRoleSizing;
177
+ }) {
178
+ const [sizing, assign, { readConfiguredDefaultModel }, { isCodexReasoningEffort }] = await Promise.all([
179
+ import("../../codex/role-sizing"),
180
+ import("../../codex/role-auto-assign"),
181
+ import("../../codex/catalog/parsing"),
182
+ import("../../reasoning-effort"),
183
+ ]);
184
+ const sizingModel = resolveSizingModel(options.sizingModel, readConfiguredDefaultModel);
185
+ const answer = await (options.completeRoleSizing ?? completeThroughProxy)({
186
+ model: sizingModel,
187
+ system: sizing.DELEGATED_WORK_SIZING_SYSTEM_PROMPT,
188
+ user: sizing.buildRoleSizingUserMessage([{ role: DELEGATED_WORK_ROLE, instructions: options.work }]),
189
+ }, options.config);
190
+ const sizingError = answer.error ? publicSizingError(answer.error) : null;
191
+ const outcomes = sizingError
192
+ ? new Map([[DELEGATED_WORK_ROLE, { unsized: `the sizing call failed: ${sizingError}` }]])
193
+ : sizing.parseRoleSizingResponse(answer.text, [DELEGATED_WORK_ROLE]);
194
+ const candidates = (await modelCandidates(options.offered, options.models))
195
+ .map(candidate => ({ ...candidate, efforts: candidate.efforts.filter(isCodexReasoningEffort) }));
196
+ const classified = assign.classifyRoleModelCandidates(candidates, options.config.codexRoleTiers);
197
+ const [proposal] = assign.buildRoleProposals([{
198
+ role: DELEGATED_WORK_ROLE,
199
+ model: options.config.injectionModel ?? null,
200
+ effort: options.config.injectionEffort ?? null,
201
+ }], outcomes, classified, { alwaysProposeEffort: true });
202
+ return {
203
+ sizingModel,
204
+ sizingError,
205
+ proposal: proposal!,
206
+ candidates: classified.map(({ model, tier, tierSource, unitPrice }) => ({ model, tier, tierSource, unitPrice })),
207
+ };
208
+ }
@@ -66,6 +66,8 @@ import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, C
66
66
  import type { ManagementContext } from "./context";
67
67
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
68
68
  import { shadowCallTargetError } from "./shadow-call-validation";
69
+ import { decisionModelRouteError, normalizeDecisionModelSelector } from "./decision-model-validation";
70
+ import { comboConfigIssues, resolveComboId } from "../../combos/types";
69
71
  import { COMBO_DEFAULT_WAIT_FOR_COOLDOWN_MS } from "../../combos";
70
72
 
71
73
 
@@ -221,13 +223,62 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
221
223
  ...(!Object.hasOwn(requestedCombo, "defaultEffortMode") && previous?.defaultEffortMode !== undefined
222
224
  ? { defaultEffortMode: previous.defaultEffortMode }
223
225
  : {}),
226
+ // An API or CLI round-trip of a JEV combo that omits these keeps them; the dashboard sends
227
+ // both explicitly (null = default). Switching to another strategy drops them rather than
228
+ // failing validation.
229
+ ...(!Object.hasOwn(requestedCombo, "decisionProvider")
230
+ && !Object.hasOwn(requestedCombo, "decisionModel")
231
+ && previous?.decisionProvider !== undefined
232
+ && requestedCombo.strategy === "jev"
233
+ ? { decisionProvider: previous.decisionProvider }
234
+ : {}),
235
+ ...(!Object.hasOwn(requestedCombo, "decisionModel")
236
+ && !Object.hasOwn(requestedCombo, "decisionProvider")
237
+ && previous?.decisionModel !== undefined
238
+ && requestedCombo.strategy === "jev"
239
+ ? { decisionModel: previous.decisionModel }
240
+ : {}),
241
+ ...(!Object.hasOwn(requestedCombo, "decisionTimeoutMs")
242
+ && previous?.decisionTimeoutMs !== undefined
243
+ && requestedCombo.strategy === "jev"
244
+ ? { decisionTimeoutMs: previous.decisionTimeoutMs }
245
+ : {}),
224
246
  };
225
- const error = comboConfigError(id, effectiveCombo, config.providers, {
247
+ const nextCombos = { ...(config.combos ?? {}) };
248
+ if (renameFrom) delete nextCombos[renameFrom];
249
+ nextCombos[id] = effectiveCombo as unknown as OcxComboConfig;
250
+ const prospectiveConfig = { ...config, combos: nextCombos };
251
+ // Decision routes follow the same combo identity migration as agent/shadow references,
252
+ // retaining any synthetic suffix while resolving the old identity against the old map.
253
+ if (previous) {
254
+ const nextPublicModel = comboPublicModelId(id, nextCombos[id]!);
255
+ for (const [otherId, combo] of Object.entries(nextCombos)) {
256
+ if (otherId === id || typeof combo.decisionModel !== "string") continue;
257
+ const model = combo.decisionModel.trim();
258
+ const selector = normalizeDecisionModelSelector(config, model);
259
+ if (resolveComboId(config, selector) !== sourceId) continue;
260
+ const nextSelector = selector === comboModelId(sourceId) ? comboModelId(id) : nextPublicModel;
261
+ nextCombos[otherId] = { ...combo, decisionModel: nextSelector + model.slice(selector.length) };
262
+ }
263
+ }
264
+ const validationOptions = {
226
265
  requireEnabledTarget: true,
227
- combos: config.combos,
266
+ requireUsableDecisionService: true,
267
+ combos: nextCombos,
228
268
  excludeComboId: sourceId,
229
- });
269
+ normalizeDecisionModel: (model: string) => normalizeDecisionModelSelector(prospectiveConfig, model),
270
+ };
271
+ const error = comboConfigError(id, effectiveCombo, config.providers, validationOptions);
230
272
  if (error) return jsonResponse({ error }, 400);
273
+ for (const [otherId, combo] of Object.entries(nextCombos)) {
274
+ if (typeof combo.decisionModel !== "string") continue;
275
+ const issue = otherId === id ? undefined : comboConfigIssues(otherId, combo, config.providers, {
276
+ combos: nextCombos,
277
+ normalizeDecisionModel: validationOptions.normalizeDecisionModel,
278
+ }).find(issue => issue.path[0] === "decisionModel");
279
+ const routeError = issue?.message ?? decisionModelRouteError(prospectiveConfig, otherId, combo.decisionModel);
280
+ if (routeError) return jsonResponse({ error: issue ? `combo "${otherId}": ${routeError}` : routeError }, 400);
281
+ }
231
282
  const normalized = normalizeComboConfig(effectiveCombo as unknown as OcxComboConfig);
232
283
  // Persist only non-default identity/capability fields so config stays sparse.
233
284
  // Capability defaults (`imageInput`, `reasoningEffortMode`) go through the same
@@ -267,8 +318,6 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
267
318
  if (codexAccountNamespaceForModel(config.codexAccountNamespaces, newPublicModel)) {
268
319
  return jsonResponse({ error: CODEX_ACCOUNT_NAMESPACE_COMBO_ALIAS_COLLISION_ERROR }, 409);
269
320
  }
270
- const nextCombos = { ...(config.combos ?? {}) };
271
- if (renameFrom) delete nextCombos[renameFrom];
272
321
  nextCombos[id] = stored;
273
322
  let shouldSyncClaudeAgentDefs = false;
274
323
  const migratedModels = new Map<string, string>();
@@ -362,6 +411,14 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
362
411
  if (!Object.hasOwn(config.combos ?? {}, id)) {
363
412
  return jsonResponse({ error: "unknown combo" }, 404);
364
413
  }
414
+ const dependents = Object.entries(config.combos ?? {}).filter(([otherId, combo]) =>
415
+ otherId !== id && typeof combo.decisionModel === "string"
416
+ && resolveComboId(config, normalizeDecisionModelSelector(config, combo.decisionModel.trim())) === id,
417
+ ).map(([otherId]) => otherId);
418
+ if (dependents.length) {
419
+ return jsonResponse({ error: `combo "${id}" is referenced by decisionModel in combos: ${dependents.join(", ")}`,
420
+ code: "combo_has_dependent_combos", combos: dependents }, 409);
421
+ }
365
422
  const { clearComboSelectionState, clearComboTargetCooldowns } = await import("../../combos");
366
423
  delete config.combos![id];
367
424
  if (Object.keys(config.combos!).length === 0) deleteConfigTopLevelKey(config, "combos");
@@ -1,3 +1,7 @@
1
+ import { MAIN_ACCOUNT_HARD_LOCK_MIN_PERCENT } from "../../codex/quota-types";
2
+ import { resolveMainAccountHardLockThresholds } from "../../codex/main-account-hard-lock";
3
+ import { getMainAccountExternalUsageWarning } from "../../codex/main-account-external-usage";
4
+ import { getObservedMainQuotaIdentityKey } from "../../codex/main-account-cache";
1
5
  import { compactionRoutingSchema, memoryModelsSchema } from "../../config/schema/leaf-validators";
2
6
  import { compactionRecoverySchema } from "../../config/schema/compaction-recovery";
3
7
  import { captureConfigTopLevelRollback } from "../../config/rebase-provenance";
@@ -363,7 +367,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
363
367
  // Absent means on by default: the GUI renders a switch enabled unless explicit false.
364
368
  fastRows: config.fastRows !== false,
365
369
  codexMainAccountHardLock: isMainAccountHardLockEnabled(config),
366
- mainAccountHardLock: getMainAccountHardLockStatus(config),
370
+ mainAccountHardLock: { ...getMainAccountHardLockStatus(config),
371
+ externalUsage: getMainAccountExternalUsageWarning(getObservedMainQuotaIdentityKey()) },
367
372
  // Absent means the historical auto-open, so the GUI can render the toggle
368
373
  // without having to know that `undefined` and `true` mean the same thing.
369
374
  oauthOpenBrowser: config.oauthOpenBrowser !== false,
@@ -467,6 +472,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
467
472
  ultraFastTier?: unknown;
468
473
  fastRows?: unknown;
469
474
  codexMainAccountHardLock?: unknown;
475
+ codexMainAccountHardLockThresholds?: unknown;
470
476
  codexDesktopAuthless?: unknown;
471
477
  codexClientCompaction?: unknown;
472
478
  compactionRouting?: unknown;
@@ -483,12 +489,13 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
483
489
  && body.ultraFastTier === undefined
484
490
  && body.fastRows === undefined
485
491
  && body.codexMainAccountHardLock === undefined
492
+ && body.codexMainAccountHardLockThresholds === undefined
486
493
  && body.codexDesktopAuthless === undefined
487
494
  && body.codexClientCompaction === undefined
488
495
  && body.compactionRouting === undefined
489
496
  && body.compactionRecovery === undefined
490
497
  && body.memoryModels === undefined) {
491
- return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, codexAccountPickerEnabled, codexQuotaAutoRefresh, oauthOpenBrowser, showCodexCredits, ultraFastTier, fastRows, codexMainAccountHardLock, codexDesktopAuthless, codexClientCompaction, compactionRouting, compactionRecovery, or memoryModels" }, 400);
498
+ return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, codexAccountPickerEnabled, codexQuotaAutoRefresh, oauthOpenBrowser, showCodexCredits, ultraFastTier, fastRows, codexMainAccountHardLock, codexMainAccountHardLockThresholds, codexDesktopAuthless, codexClientCompaction, compactionRouting, compactionRecovery, or memoryModels" }, 400);
492
499
  }
493
500
  if (body.codexAutoStart !== undefined && typeof body.codexAutoStart !== "boolean") {
494
501
  return jsonResponse({ error: "codexAutoStart boolean is required" }, 400);
@@ -515,6 +522,24 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
515
522
  if (body.codexMainAccountHardLock !== undefined && typeof body.codexMainAccountHardLock !== "boolean") {
516
523
  return jsonResponse({ error: "codexMainAccountHardLock boolean is required" }, 400);
517
524
  }
525
+ let hardLockThresholds: OcxConfig["codexMainAccountHardLockThresholds"];
526
+ if (body.codexMainAccountHardLockThresholds !== undefined) {
527
+ const value = body.codexMainAccountHardLockThresholds;
528
+ if (!value || typeof value !== "object" || Array.isArray(value)
529
+ || Object.keys(value).length === 0
530
+ || Object.keys(value).some(key => key !== "short" && key !== "long")
531
+ || Object.values(value).some(percent => typeof percent !== "number" || !Number.isInteger(percent)
532
+ || percent < MAIN_ACCOUNT_HARD_LOCK_MIN_PERCENT || percent > 100)) {
533
+ return jsonResponse({ error: "codexMainAccountHardLockThresholds requires integer short/long percentages from 80 to 100" }, 400);
534
+ }
535
+ // A partial body updates one window and keeps the other stored value.
536
+ hardLockThresholds = { ...config.codexMainAccountHardLockThresholds,
537
+ ...(value as NonNullable<OcxConfig["codexMainAccountHardLockThresholds"]>) };
538
+ const defaults = resolveMainAccountHardLockThresholds(undefined);
539
+ if ((hardLockThresholds.short ?? defaults.short) > (hardLockThresholds.long ?? defaults.long)) {
540
+ return jsonResponse({ error: "codexMainAccountHardLockThresholds short must not exceed long" }, 400);
541
+ }
542
+ }
518
543
  if (body.codexDesktopAuthless !== undefined && typeof body.codexDesktopAuthless !== "boolean") {
519
544
  return jsonResponse({ error: "codexDesktopAuthless boolean is required" }, 400);
520
545
  }
@@ -586,6 +611,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
586
611
  hasUltraFastTier: Object.hasOwn(config, "ultraFastTier"),
587
612
  fastRows: config.fastRows,
588
613
  hasFastRows: Object.hasOwn(config, "fastRows"),
614
+ codexMainAccountHardLockThresholds: config.codexMainAccountHardLockThresholds,
615
+ hasCodexMainAccountHardLockThresholds: Object.hasOwn(config, "codexMainAccountHardLockThresholds"),
589
616
  codexMainAccountHardLock: config.codexMainAccountHardLock,
590
617
  hasCodexMainAccountHardLock: Object.hasOwn(config, "codexMainAccountHardLock"),
591
618
  codexDesktopAuthless: config.codexDesktopAuthless,
@@ -630,6 +657,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
630
657
  else if (body.fastRows === true) deleteConfigTopLevelKey(config, "fastRows");
631
658
  // Inverted from the pair above because the default is on (#5694): off is the persisted
632
659
  // decision, so it writes `false`, while on deletes the key and returns to the default.
660
+ if (hardLockThresholds !== undefined) config.codexMainAccountHardLockThresholds = { ...hardLockThresholds };
633
661
  if (body.codexMainAccountHardLock === false) config.codexMainAccountHardLock = false;
634
662
  else if (body.codexMainAccountHardLock === true) deleteConfigTopLevelKey(config, "codexMainAccountHardLock");
635
663
  if (body.codexDesktopAuthless === true) config.codexDesktopAuthless = true;
@@ -684,6 +712,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
684
712
  if (previousSettings.hasFastRows) {
685
713
  config.fastRows = previousSettings.fastRows;
686
714
  } else deleteConfigTopLevelKey(config, "fastRows");
715
+ if (previousSettings.hasCodexMainAccountHardLockThresholds) {
716
+ config.codexMainAccountHardLockThresholds = previousSettings.codexMainAccountHardLockThresholds;
717
+ } else deleteConfigTopLevelKey(config, "codexMainAccountHardLockThresholds");
687
718
  if (previousSettings.hasCodexMainAccountHardLock) {
688
719
  config.codexMainAccountHardLock = previousSettings.codexMainAccountHardLock;
689
720
  } else deleteConfigTopLevelKey(config, "codexMainAccountHardLock");
@@ -752,7 +783,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
752
783
  // phases as "Off" while the server kept them.
753
784
  memoryModels: config.memoryModels ?? null,
754
785
  codexMainAccountHardLock: isMainAccountHardLockEnabled(config),
755
- mainAccountHardLock: getMainAccountHardLockStatus(config),
786
+ mainAccountHardLock: { ...getMainAccountHardLockStatus(config),
787
+ externalUsage: getMainAccountExternalUsageWarning(getObservedMainQuotaIdentityKey()) },
756
788
  startupHealth: await readStartupHealthSnapshot(config),
757
789
  });
758
790
  }
@@ -48,6 +48,7 @@ export interface ManagementApiDeps {
48
48
  /** Bound to this server's lifecycle owner; absent in direct route tests. */
49
49
  listLowQuotaEvents?: (limit?: number) => LowQuotaEvent[];
50
50
  /** Bound Claude intercept state, injectable for isolated management-route tests. */
51
+ ensureClaudeIntercept?: () => Promise<import("../../claude/intercept/runtime").ClaudeInterceptOutcome>;
51
52
  getClaudeInterceptState?: typeof import("../../claude/intercept/runtime").getClaudeInterceptState;
52
53
  /** Reconciliation seam for field-scoped rollback tests. */
53
54
  reconcileClaudeFirstPartySettings?: typeof import("../../claude/first-party-settings").reconcileClaudeFirstPartySettings;
@@ -84,6 +85,8 @@ export interface ManagementApiDeps {
84
85
  * Tests stub it to orphan the fixture file mid-fetch (the r7 recheck test).
85
86
  */
86
87
  fetchAllModels?: (config: OcxConfig) => Promise<CatalogModel[]>;
88
+ /** Codex role auto-assign's one sizing model call; route tests answer it without a provider. */
89
+ completeCodexRoleSizing?: import("./codex-role-auto-assign").CompleteRoleSizing;
87
90
  /**
88
91
  * Writer seam for the Grok toggle: lets a test place the file in any state
89
92
  * between the pre-write recheck and the write itself (the r8 post-inspection
@@ -149,7 +152,7 @@ export interface ManagementApiDeps {
149
152
  codexPromptPaths?: CodexPromptPaths;
150
153
  /** Link seams are getters so the optional listener and supervisor are singletons. */
151
154
  linkSupervisor?: () => LinkSupervisor;
152
- linkListener?: () => Pick<LinkListenerLifecycle<unknown>, "ensureStarted" | "status" | "close" | "onAuthenticatedCatalog">;
155
+ linkListener?: () => Pick<LinkListenerLifecycle<unknown>, "ensureStarted" | "status" | "close" | "onAuthenticatedCatalog"> & Partial<Pick<import("../index/optional-listeners").OptionalListenerSet<unknown>, "ensureClaudeIntercept" | "claudeInterceptOutcome">>;
153
156
  readLinkStore?: () => LinkStore;
154
157
  writeLinkStore?: (store: LinkStore) => void;
155
158
  linkKnownHostsPath?: () => string;