@gajae-code/coding-agent 0.15.6 → 0.16.1

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 (270) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/dist/types/async/job-manager.d.ts +32 -1
  3. package/dist/types/browser-backend.d.ts +12 -0
  4. package/dist/types/cli/args.d.ts +2 -0
  5. package/dist/types/cli/root-flags.d.ts +14 -0
  6. package/dist/types/cli/update-cli.d.ts +14 -0
  7. package/dist/types/cli.d.ts +7 -0
  8. package/dist/types/commands/launch.d.ts +10 -0
  9. package/dist/types/commands/sdk.d.ts +2 -0
  10. package/dist/types/config/autorouting-tier-map.d.ts +24 -0
  11. package/dist/types/config/billing-path.d.ts +25 -0
  12. package/dist/types/config/config-schema-version.d.ts +13 -1
  13. package/dist/types/config/file-lock.d.ts +5 -0
  14. package/dist/types/config/model-registry.d.ts +22 -0
  15. package/dist/types/config/settings-schema.d.ts +27 -8
  16. package/dist/types/config/settings.d.ts +2 -0
  17. package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
  18. package/dist/types/crash/record-loader.d.ts +15 -0
  19. package/dist/types/extensibility/extensions/types.d.ts +8 -3
  20. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  21. package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
  22. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  23. package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
  24. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
  25. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  26. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  27. package/dist/types/main.d.ts +1 -0
  28. package/dist/types/master-mode/context.d.ts +21 -0
  29. package/dist/types/master-mode/first-request.d.ts +34 -0
  30. package/dist/types/master-mode/snapshot.d.ts +32 -0
  31. package/dist/types/modes/acp/acp-agent.d.ts +21 -21
  32. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  33. package/dist/types/modes/components/model-selector.d.ts +4 -0
  34. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  35. package/dist/types/modes/components/session-selector.d.ts +1 -1
  36. package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
  37. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  38. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  39. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  40. package/dist/types/modes/interactive-mode.d.ts +16 -2
  41. package/dist/types/modes/print-mode.d.ts +17 -0
  42. package/dist/types/modes/theme/theme.d.ts +2 -1
  43. package/dist/types/modes/types.d.ts +2 -0
  44. package/dist/types/modes/ui-language.d.ts +7 -1
  45. package/dist/types/sdk/broker/broker.d.ts +79 -2
  46. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  47. package/dist/types/sdk/broker/ensure.d.ts +3 -0
  48. package/dist/types/sdk/broker/index.d.ts +3 -0
  49. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
  50. package/dist/types/sdk/broker/lifecycle.d.ts +40 -0
  51. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  52. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  53. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  54. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  55. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  56. package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
  57. package/dist/types/sdk/broker/state-version.d.ts +21 -1
  58. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  59. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  60. package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
  61. package/dist/types/sdk/bus/index.d.ts +9 -1
  62. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
  63. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
  64. package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
  65. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  66. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  67. package/dist/types/sdk/cli/index.d.ts +1 -0
  68. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  69. package/dist/types/sdk/cli/rows.d.ts +12 -5
  70. package/dist/types/sdk/cli/session-cli.d.ts +20 -3
  71. package/dist/types/sdk/client/client.d.ts +3 -2
  72. package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
  73. package/dist/types/sdk/host/control/index.d.ts +1 -1
  74. package/dist/types/sdk/host/control/operations.d.ts +2 -0
  75. package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
  76. package/dist/types/sdk/host/events.d.ts +1 -0
  77. package/dist/types/sdk/host/host.d.ts +10 -0
  78. package/dist/types/sdk/host/query/cursor.d.ts +4 -2
  79. package/dist/types/sdk/host/query/handlers.d.ts +1 -0
  80. package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
  81. package/dist/types/sdk/host/session-runtime.d.ts +70 -17
  82. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  83. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  84. package/dist/types/sdk/lifecycle-session.d.ts +3 -1
  85. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
  86. package/dist/types/sdk/receipt-state.d.ts +2 -0
  87. package/dist/types/sdk/router/session-router.d.ts +6 -0
  88. package/dist/types/sdk/session.d.ts +16 -0
  89. package/dist/types/sdk/startup-capability.d.ts +2 -1
  90. package/dist/types/sdk/turn-result.d.ts +6 -0
  91. package/dist/types/session/agent-session.d.ts +52 -7
  92. package/dist/types/session/credential-store-errors.d.ts +1 -0
  93. package/dist/types/session/fold-coordinator.d.ts +29 -2
  94. package/dist/types/session/session-manager.d.ts +79 -2
  95. package/dist/types/session/yield-queue.d.ts +5 -4
  96. package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
  97. package/dist/types/tools/ask.d.ts +1 -0
  98. package/dist/types/tools/bash.d.ts +16 -0
  99. package/dist/types/tools/browser/launch.d.ts +10 -0
  100. package/dist/types/tools/browser/registry.d.ts +8 -0
  101. package/dist/types/tools/gh.d.ts +8 -0
  102. package/dist/types/tools/index.d.ts +10 -10
  103. package/dist/types/tools/job.d.ts +3 -1
  104. package/dist/types/utils/external-editor.d.ts +7 -0
  105. package/examples/extensions/README.md +1 -1
  106. package/examples/extensions/pirate.ts +7 -4
  107. package/package.json +7 -7
  108. package/scripts/generate-sdk-operation-inventory.ts +13 -1
  109. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  110. package/src/async/job-manager.ts +107 -9
  111. package/src/browser-backend.ts +29 -0
  112. package/src/cli/accounts-cli.ts +9 -5
  113. package/src/cli/args.ts +17 -0
  114. package/src/cli/fast-help.ts +1 -0
  115. package/src/cli/plugin-cli.ts +29 -0
  116. package/src/cli/root-flags.ts +5 -0
  117. package/src/cli/update-cli.ts +140 -23
  118. package/src/commands/launch.ts +86 -18
  119. package/src/commands/sdk.ts +135 -28
  120. package/src/config/autorouting-tier-map.ts +12 -0
  121. package/src/config/billing-path.ts +42 -0
  122. package/src/config/config-schema-version.ts +18 -1
  123. package/src/config/file-lock.ts +158 -83
  124. package/src/config/model-preset-registry.ts +178 -23
  125. package/src/config/model-profiles.ts +5 -5
  126. package/src/config/model-registry.ts +291 -42
  127. package/src/config/model-resolver.ts +5 -0
  128. package/src/config/settings-schema.ts +27 -8
  129. package/src/config/settings.ts +51 -15
  130. package/src/coordinator-mcp/projection-scan.ts +27 -6
  131. package/src/coordinator-mcp/question-state.ts +9 -11
  132. package/src/coordinator-mcp/server.ts +115 -21
  133. package/src/crash/index-store.ts +2 -0
  134. package/src/crash/record-loader.ts +10 -0
  135. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  136. package/src/extensibility/extensions/runner.ts +27 -0
  137. package/src/extensibility/extensions/types.ts +8 -3
  138. package/src/extensibility/gjc-plugins/installer.ts +51 -11
  139. package/src/extensibility/runtime-skill-discovery.ts +110 -3
  140. package/src/extensibility/skills.ts +22 -12
  141. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  142. package/src/gjc-runtime/launch-worktree.ts +285 -4
  143. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  144. package/src/gjc-runtime/session-state-lock.ts +213 -14
  145. package/src/gjc-runtime/session-state-sidecar.ts +938 -39
  146. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  147. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -3
  148. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  149. package/src/hooks/skill-state.ts +6 -0
  150. package/src/internal-urls/docs-index.generated.ts +15 -15
  151. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  152. package/src/main.ts +61 -3
  153. package/src/master-mode/context.ts +50 -0
  154. package/src/master-mode/first-request.ts +65 -0
  155. package/src/master-mode/snapshot.ts +205 -0
  156. package/src/modes/acp/acp-agent.ts +610 -161
  157. package/src/modes/components/footer.ts +4 -2
  158. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  159. package/src/modes/components/model-selector.ts +76 -8
  160. package/src/modes/components/oauth-selector.ts +6 -1
  161. package/src/modes/components/session-selector.ts +23 -15
  162. package/src/modes/components/sessions-dashboard.ts +8 -5
  163. package/src/modes/components/settings-selector.ts +1 -0
  164. package/src/modes/components/status-line/model-name.ts +53 -0
  165. package/src/modes/components/status-line/priority-row.ts +107 -0
  166. package/src/modes/components/status-line/segments.ts +23 -9
  167. package/src/modes/components/tool-status-header.ts +151 -15
  168. package/src/modes/controllers/command-controller.ts +1 -1
  169. package/src/modes/controllers/event-controller.ts +30 -4
  170. package/src/modes/controllers/extension-ui-controller.ts +13 -13
  171. package/src/modes/controllers/input-controller.ts +30 -2
  172. package/src/modes/controllers/selector-controller.ts +200 -98
  173. package/src/modes/interactive-mode.ts +36 -12
  174. package/src/modes/print-mode.ts +64 -0
  175. package/src/modes/prompt-action-autocomplete.ts +30 -4
  176. package/src/modes/runtime-init.ts +12 -9
  177. package/src/modes/theme/theme.ts +5 -0
  178. package/src/modes/types.ts +2 -0
  179. package/src/modes/ui-language.ts +49 -2
  180. package/src/modes/utils/ui-helpers.ts +22 -3
  181. package/src/prompts/agent-fragments/browser-aside.md +22 -0
  182. package/src/prompts/system/master-mode.md +28 -0
  183. package/src/prompts/tools/ask.md +1 -1
  184. package/src/prompts/tools/bash.md +1 -0
  185. package/src/prompts/tools/job.md +1 -1
  186. package/src/sdk/acp/adapter.ts +5 -0
  187. package/src/sdk/broker/broker.ts +1646 -27
  188. package/src/sdk/broker/endpoint-authority.ts +86 -0
  189. package/src/sdk/broker/ensure.ts +81 -3
  190. package/src/sdk/broker/index.ts +3 -0
  191. package/src/sdk/broker/lifecycle-ledger.ts +6 -0
  192. package/src/sdk/broker/lifecycle.ts +440 -75
  193. package/src/sdk/broker/master-capability.ts +232 -0
  194. package/src/sdk/broker/session-index.ts +121 -17
  195. package/src/sdk/broker/session-scope.ts +272 -0
  196. package/src/sdk/broker/spawn-authority.ts +1031 -0
  197. package/src/sdk/broker/spawn-substrate.ts +591 -0
  198. package/src/sdk/broker/startup-budget.ts +144 -2
  199. package/src/sdk/broker/state-version.ts +43 -14
  200. package/src/sdk/broker/transport.ts +1 -0
  201. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  202. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  203. package/src/sdk/bus/chat-daemon-runtime.ts +136 -18
  204. package/src/sdk/bus/discord-daemon.ts +50 -10
  205. package/src/sdk/bus/index.ts +84 -7
  206. package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
  207. package/src/sdk/bus/lifecycle-commands.ts +4 -0
  208. package/src/sdk/bus/notification-service.ts +22 -5
  209. package/src/sdk/bus/prompt-reconciliation.ts +7 -2
  210. package/src/sdk/bus/reconciliation-store.ts +55 -31
  211. package/src/sdk/bus/slack-conversation.ts +1 -1
  212. package/src/sdk/bus/slack-daemon.ts +72 -38
  213. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  214. package/src/sdk/cli/index.ts +1 -0
  215. package/src/sdk/cli/master-cli.ts +259 -0
  216. package/src/sdk/cli/rows.ts +41 -15
  217. package/src/sdk/cli/session-cli.ts +297 -26
  218. package/src/sdk/client/client.ts +14 -8
  219. package/src/sdk/host/control/dispatch.ts +101 -40
  220. package/src/sdk/host/control/index.ts +2 -0
  221. package/src/sdk/host/control/operations.ts +2 -0
  222. package/src/sdk/host/control/runtime-gate.ts +203 -11
  223. package/src/sdk/host/events.ts +1 -0
  224. package/src/sdk/host/host.ts +29 -2
  225. package/src/sdk/host/query/cursor.ts +52 -7
  226. package/src/sdk/host/query/handlers.ts +33 -12
  227. package/src/sdk/host/query/revision-store.ts +12 -0
  228. package/src/sdk/host/session-runtime.test.ts +673 -14
  229. package/src/sdk/host/session-runtime.ts +674 -103
  230. package/src/sdk/host/surface-policy.ts +3 -3
  231. package/src/sdk/lifecycle/broker-client.ts +28 -0
  232. package/src/sdk/lifecycle/service.ts +279 -20
  233. package/src/sdk/lifecycle-session.ts +17 -4
  234. package/src/sdk/prompt-deadline-manager.ts +2 -1
  235. package/src/sdk/protocol/operation-inventory.generated.json +105 -3
  236. package/src/sdk/protocol/operation-registry.ts +14 -4
  237. package/src/sdk/receipt-state.ts +10 -0
  238. package/src/sdk/router/session-router.ts +219 -118
  239. package/src/sdk/session.ts +416 -195
  240. package/src/sdk/startup-capability.ts +1 -0
  241. package/src/sdk/turn-result.ts +22 -0
  242. package/src/session/agent-session.ts +1628 -318
  243. package/src/session/blob-store.ts +15 -5
  244. package/src/session/credential-store-errors.ts +2 -0
  245. package/src/session/fold-coordinator.ts +40 -3
  246. package/src/session/internal/managed-session-storage.ts +153 -78
  247. package/src/session/session-manager.ts +850 -97
  248. package/src/session/yield-queue.ts +24 -10
  249. package/src/setup/frictionless-onboarding.ts +2 -0
  250. package/src/slash-commands/builtin-registry.ts +41 -0
  251. package/src/task/executor.ts +9 -4
  252. package/src/telemetry/events.ts +1138 -12
  253. package/src/tools/ask-contract.test.ts +64 -0
  254. package/src/tools/ask-contract.ts +36 -1
  255. package/src/tools/ask.ts +10 -8
  256. package/src/tools/bash-interactive.ts +11 -2
  257. package/src/tools/bash.ts +412 -75
  258. package/src/tools/browser/launch.ts +37 -0
  259. package/src/tools/browser/registry.ts +99 -9
  260. package/src/tools/cron.ts +6 -4
  261. package/src/tools/descriptors.test.ts +15 -0
  262. package/src/tools/descriptors.ts +5 -1
  263. package/src/tools/gh.ts +104 -48
  264. package/src/tools/github-cache.ts +2 -2
  265. package/src/tools/index.ts +10 -6
  266. package/src/tools/job.ts +4 -1
  267. package/src/tools/monitor.ts +12 -9
  268. package/src/tools/resolve.ts +30 -10
  269. package/src/tools/tool-catalog.generated.ts +2 -2
  270. package/src/utils/external-editor.ts +72 -1
@@ -241,11 +241,11 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
241
241
  architect: ["anthropic/claude-opus-5:xhigh", "anthropic/claude-opus-4-6:xhigh"],
242
242
  }),
243
243
  profile("claude-fable", ["anthropic"], {
244
- default: "anthropic/claude-fable-5:xhigh",
244
+ default: "anthropic/claude-fable-5-1:xhigh",
245
245
  executor: "anthropic/claude-sonnet-5",
246
- planner: "anthropic/claude-fable-5:low",
247
- critic: "anthropic/claude-fable-5:high",
248
- architect: "anthropic/claude-fable-5:xhigh",
246
+ planner: "anthropic/claude-fable-5-1:low",
247
+ critic: "anthropic/claude-fable-5-1:high",
248
+ architect: "anthropic/claude-fable-5-1:xhigh",
249
249
  }),
250
250
  profile("glm-eco", ["zai"], {
251
251
  default: "zai/glm-5.3-flash:low",
@@ -482,7 +482,7 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
482
482
  architect: "openai-codex/gpt-5.6-sol:high",
483
483
  }),
484
484
  profile("fable-opus-codex", ["anthropic", "openai-codex"], {
485
- default: "anthropic/claude-fable-5:high",
485
+ default: "anthropic/claude-fable-5-1:high",
486
486
  executor: "openai-codex/gpt-5.6-terra:medium",
487
487
  planner: "anthropic/claude-opus-5:medium",
488
488
  critic: "anthropic/claude-opus-5:high",
@@ -39,7 +39,11 @@ import {
39
39
  UNK_MAX_TOKENS,
40
40
  unregisterCustomApis,
41
41
  } from "@gajae-code/ai/core";
42
- import { resolveLoopbackOpenAIBaseUrl } from "@gajae-code/ai/utils/discovery/openai-compatible";
42
+ import { fetchModelsDevPayload } from "@gajae-code/ai/provider-models/openai-compat";
43
+ import {
44
+ detectDiscoveredApiFamily,
45
+ resolveLoopbackOpenAIBaseUrl,
46
+ } from "@gajae-code/ai/utils/discovery/openai-compatible";
43
47
 
44
48
  // Sentinels for local-only OAuth tokens — declared inline to avoid loading provider
45
49
  // modules at startup. Must match the provider OAuth modules.
@@ -66,6 +70,7 @@ import {
66
70
  } from "../sdk/providers";
67
71
  import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
68
72
  import type { ActiveSearchModelContext, WebSearchMode } from "../web/search/types";
73
+ import { type BillingPath, deriveBillingPath } from "./billing-path";
69
74
  import { ConfigError, ConfigFile } from "./config-file";
70
75
  import { isAuthenticated, kNoAuth } from "./model-auth";
71
76
  import { type ConfiguredModelBindings, ModelBindingsApplier } from "./model-bindings-applier";
@@ -112,6 +117,7 @@ import {
112
117
  } from "./provider-selection-policy";
113
118
  import { type Settings, settings } from "./settings";
114
119
 
120
+ export type { BillingPath, BillingPathKind } from "./billing-path";
115
121
  export type { EffectiveProviderAuth, ProviderSelectionPolicy } from "./provider-selection-policy";
116
122
  export type { CanonicalModelIndex, CanonicalModelRecord, CanonicalModelVariant, ModelEquivalenceConfig };
117
123
 
@@ -135,6 +141,69 @@ function redactDiscoveryUrl(value: string | URL): string {
135
141
  return "(invalid URL)";
136
142
  }
137
143
  }
144
+
145
+ /** Whether a structured transport code proves that no listener accepted the connection. */
146
+ function isConnectionRefusedDiscoveryCode(value: unknown): boolean {
147
+ if (typeof value === "string") {
148
+ const normalized = value.replace(/[^A-Za-z0-9]/gu, "").toUpperCase();
149
+ return (
150
+ normalized === "ECONNREFUSED" || normalized === "CONNECTIONREFUSED" || normalized === "ERRCONNECTIONREFUSED"
151
+ );
152
+ }
153
+ if (typeof value !== "number" || !Number.isInteger(value)) return false;
154
+ // ECONNREFUSED errno values used by the supported runtimes/OSes.
155
+ return value === 61 || value === -61 || value === 111 || value === -111 || value === 10061 || value === -10061;
156
+ }
157
+
158
+ function isConnectionRefusedDiscoveryEvidence(error: unknown): boolean {
159
+ const seen = new Set<unknown>();
160
+ const queue: unknown[] = [error];
161
+ while (queue.length > 0) {
162
+ const current = queue.shift();
163
+ if (current === null || typeof current !== "object" || seen.has(current)) continue;
164
+ seen.add(current);
165
+ for (const field of ["code", "errno", "errorCode", "transportCode"]) {
166
+ let value: unknown;
167
+ try {
168
+ value = Reflect.get(current, field);
169
+ } catch {
170
+ value = undefined;
171
+ }
172
+ if (isConnectionRefusedDiscoveryCode(value)) return true;
173
+ }
174
+ for (const field of ["cause", "error", "transport"]) {
175
+ let nested: unknown;
176
+ try {
177
+ nested = Reflect.get(current, field);
178
+ } catch {
179
+ nested = undefined;
180
+ }
181
+ if (nested !== undefined) queue.push(nested);
182
+ }
183
+ }
184
+ return false;
185
+ }
186
+
187
+ /** Whether `hostname` (as `URL.hostname` reports it) names this machine's loopback. */
188
+ function isLoopbackDiscoveryHost(hostname: string): boolean {
189
+ if (hostname === "localhost" || hostname === "[::1]" || hostname === "::1") return true;
190
+ return /^127(?:\.\d{1,3}){3}$/u.test(hostname);
191
+ }
192
+
193
+ /**
194
+ * A loopback discovery endpoint that is proven to have no listener is the normal state of
195
+ * a machine without a local model server. Structured transport evidence is required when
196
+ * available; DNS, routing, proxy, TLS, and HTTP failures remain warnings.
197
+ */
198
+ export function isQuietLocalDiscoveryFailure(baseUrl: string, _warning: string, evidence?: unknown): boolean {
199
+ let host: string;
200
+ try {
201
+ host = new URL(baseUrl).hostname;
202
+ } catch {
203
+ return false;
204
+ }
205
+ return isLoopbackDiscoveryHost(host) && evidence !== undefined && isConnectionRefusedDiscoveryEvidence(evidence);
206
+ }
138
207
  function stripUrlQuery(value: string): string {
139
208
  const queryStart = value.indexOf("?");
140
209
  if (queryStart < 0) return value;
@@ -272,6 +341,44 @@ type ProviderValidationMode = "models-config" | "runtime-register";
272
341
 
273
342
  const OPENAI_REQUEST_TRANSFORM_APIS = new Set<Api>(["openai-completions", "openai-responses"]);
274
343
 
344
+ const OPENAI_FAMILY_APIS = new Set<Api>([
345
+ "openai-completions",
346
+ "openai-responses",
347
+ "openai-codex-responses",
348
+ "azure-openai-responses",
349
+ ]);
350
+
351
+ /** Whether an api uses an OpenAI-shaped request/response transport. */
352
+ function isOpenAIFamilyApi(api: Api): boolean {
353
+ return OPENAI_FAMILY_APIS.has(api);
354
+ }
355
+
356
+ /**
357
+ * The most frequent OpenAI-family api among statically-configured models, used
358
+ * as the default api for auto-enabled `/v1/models` discovery on a custom
359
+ * provider that sets `api` only per model. Returns `undefined` when no
360
+ * configured model uses an OpenAI-family api (e.g. an Anthropic-only base),
361
+ * which suppresses auto-discovery for that provider.
362
+ */
363
+ function dominantOpenAIFamilyModelApi(models: readonly { api?: string }[] | undefined): Api | undefined {
364
+ if (!models || models.length === 0) return undefined;
365
+ const counts = new Map<Api, number>();
366
+ for (const model of models) {
367
+ const api = model.api as Api | undefined;
368
+ if (api === undefined || !isOpenAIFamilyApi(api)) continue;
369
+ counts.set(api, (counts.get(api) ?? 0) + 1);
370
+ }
371
+ let best: Api | undefined;
372
+ let bestCount = 0;
373
+ for (const [api, count] of counts) {
374
+ if (count > bestCount) {
375
+ best = api;
376
+ bestCount = count;
377
+ }
378
+ }
379
+ return best;
380
+ }
381
+
275
382
  function getKnownProviderApis(providerName: string): Set<Api> {
276
383
  const apis = new Set<Api>();
277
384
  for (const model of getBundledModels(providerName as Parameters<typeof getBundledModels>[0])) {
@@ -1192,6 +1299,7 @@ type CustomModelOverlay = {
1192
1299
  wireModelId?: string;
1193
1300
  requestTransform?: ModelRequestTransform;
1194
1301
  cacheRetention?: CacheRetention;
1302
+ transport?: Model<Api>["transport"];
1195
1303
  isOAuth?: boolean;
1196
1304
  };
1197
1305
 
@@ -1303,6 +1411,7 @@ function buildCustomModelOverlay(
1303
1411
  providerRequestTransform: ModelRequestTransform | undefined,
1304
1412
  providerAuth: ProviderAuthMode | undefined,
1305
1413
  providerCacheRetention: CacheRetention | undefined,
1414
+ providerTransport: Model<Api>["transport"] | undefined,
1306
1415
  modelDef: CustomModelDefinitionLike,
1307
1416
  ): CustomModelOverlay | undefined {
1308
1417
  const api = modelDef.api ?? providerApi;
@@ -1331,6 +1440,7 @@ function buildCustomModelOverlay(
1331
1440
  contextPromotionTarget: modelDef.contextPromotionTarget,
1332
1441
  premiumMultiplier: modelDef.premiumMultiplier,
1333
1442
  cacheRetention: modelDef.cacheRetention ?? providerCacheRetention,
1443
+ transport: providerTransport,
1334
1444
  isOAuth: resolveCustomModelIsOAuth(api, providerAuth),
1335
1445
  };
1336
1446
  }
@@ -1466,6 +1576,7 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil
1466
1576
  wireModelId: resolvedModel.wireModelId,
1467
1577
  requestTransform: resolvedModel.requestTransform,
1468
1578
  cacheRetention: resolvedModel.cacheRetention ?? reference?.cacheRetention,
1579
+ transport: resolvedModel.transport,
1469
1580
  premiumMultiplier: resolvedModel.premiumMultiplier,
1470
1581
  isOAuth: resolvedModel.isOAuth,
1471
1582
  } as Model<Api>);
@@ -2291,6 +2402,7 @@ export class ModelRegistry {
2291
2402
  // re-merge bundled transport metadata here.
2292
2403
  headers: customModel.headers,
2293
2404
  compat: customModel.compat,
2405
+ transport: customModel.transport,
2294
2406
  contextPromotionTarget: customModel.contextPromotionTarget ?? existingModel.contextPromotionTarget,
2295
2407
  wireModelId: customModel.wireModelId,
2296
2408
  requestTransform: customModel.requestTransform,
@@ -2669,17 +2781,48 @@ export class ModelRegistry {
2669
2781
  keylessProviders.add(providerName);
2670
2782
  }
2671
2783
 
2672
- if (providerConfig.discovery && providerConfig.api) {
2784
+ // Explicit discovery config is authoritative. Otherwise auto-enable
2785
+ // OpenAI-compatible `/v1/models` discovery for any custom provider that
2786
+ // declares an OpenAI-family `api` and a `baseUrl`: a subscription-style
2787
+ // gateway (e.g. CLIProxyAPI) then auto-populates `/model` with its live
2788
+ // catalog and auto-routes each model's wire family, with no manual
2789
+ // `discovery:` block. It stays optional so an endpoint without
2790
+ // `/v1/models` degrades to the configured static models instead of
2791
+ // failing. Providers that route through the local `openaiCompat` proxy
2792
+ // already register their own discovery above, so skip them here.
2793
+ const effectiveDiscoveryBaseUrl = providerConfig.baseUrl ?? resolveProviderBaseUrlFromEnv(providerName);
2794
+ // The default api for discovered models: the provider-level `api` when
2795
+ // set, otherwise the most common OpenAI-family `api` among the
2796
+ // statically-configured models. For withfox-style configs that only set
2797
+ // `api` per model (`openai-responses` for gpt, `anthropic-messages` for
2798
+ // claude), this preserves the intended OpenAI transport for discovered
2799
+ // OpenAI models while `detectDiscoveredApiFamily` still flips claude ids
2800
+ // to Anthropic.
2801
+ const providerApi =
2802
+ (providerConfig.api as Api | undefined) ??
2803
+ dominantOpenAIFamilyModelApi(providerConfig.models as { api?: string }[] | undefined);
2804
+ const autoDiscovery: ProviderDiscovery | undefined =
2805
+ !providerConfig.discovery &&
2806
+ !localOpenAICompat &&
2807
+ providerName !== "vllm" &&
2808
+ providerName !== "sglang" &&
2809
+ providerApi !== undefined &&
2810
+ isOpenAIFamilyApi(providerApi) &&
2811
+ effectiveDiscoveryBaseUrl !== undefined
2812
+ ? { type: "openai-models-list" }
2813
+ : undefined;
2814
+ const effectiveDiscovery = providerConfig.discovery ?? autoDiscovery;
2815
+ if (effectiveDiscovery && providerApi) {
2673
2816
  discoverableProviders.push({
2674
2817
  provider: providerName,
2675
- api: providerConfig.api as Api,
2676
- baseUrl: providerConfig.baseUrl ?? resolveProviderBaseUrlFromEnv(providerName),
2818
+ api: providerApi,
2819
+ baseUrl: effectiveDiscoveryBaseUrl,
2677
2820
  headers: providerConfig.headers,
2678
2821
  compat: providerConfig.compat,
2679
2822
  requestTransform: providerConfig.requestTransform,
2680
2823
  cacheRetention: providerConfig.cacheRetention,
2681
- discovery: providerConfig.discovery,
2682
- optional: false,
2824
+ discovery: effectiveDiscovery,
2825
+ optional: !providerConfig.discovery,
2683
2826
  });
2684
2827
  }
2685
2828
 
@@ -2957,27 +3100,71 @@ export class ModelRegistry {
2957
3100
  ? { ...result, invalidatesPublishedState }
2958
3101
  : { ...result, current: false, models: [], fetched: false, invalidatesPublishedState };
2959
3102
  });
3103
+ // Every field below except `model.baseUrl` and `model.id` depends only on
3104
+ // `model.provider`. The built-in catalog carries thousands of models across
3105
+ // only a few dozen providers, and `#getProviderEvidenceGeneration` reaches
3106
+ // into AuthStorage while `#getProviderBaseUrlForDiscovery` parses URLs, so
3107
+ // recomputing per model turned this filter into a per-refresh UI-thread
3108
+ // stall. Memoize the provider-keyed work once per provider.
3109
+ interface BuiltInProviderEvidenceState {
3110
+ evidence:
3111
+ | {
3112
+ fresh: boolean;
3113
+ modelIds: ReadonlySet<string>;
3114
+ profileModelIds?: ReadonlySet<string>;
3115
+ profileFresh?: boolean;
3116
+ profileEndpoint?: string;
3117
+ authGeneration: string;
3118
+ endpoint: string;
3119
+ }
3120
+ | undefined;
3121
+ expectedAuthGeneration: string | undefined;
3122
+ authGenerationThrew: boolean;
3123
+ providerBaseEndpoint: string | undefined;
3124
+ canUseCredentialDerivedXiaomiEndpoint: boolean;
3125
+ }
3126
+ const builtInProviderStates = new Map<string, BuiltInProviderEvidenceState>();
3127
+ const builtInProviderStateFor = (provider: string): BuiltInProviderEvidenceState => {
3128
+ const cached = builtInProviderStates.get(provider);
3129
+ if (cached) return cached;
3130
+ let expectedAuthGeneration: string | undefined;
3131
+ let authGenerationThrew = false;
3132
+ try {
3133
+ expectedAuthGeneration = this.#getProviderEvidenceGeneration(
3134
+ provider,
3135
+ this.#providerEvidenceApiKeys.get(provider),
3136
+ );
3137
+ } catch {
3138
+ authGenerationThrew = true;
3139
+ }
3140
+ const providerBaseUrl = this.#getProviderBaseUrlForDiscovery(provider);
3141
+ const state: BuiltInProviderEvidenceState = {
3142
+ evidence: this.#descriptorDiscoveryEvidence.get(provider),
3143
+ expectedAuthGeneration,
3144
+ authGenerationThrew,
3145
+ providerBaseEndpoint:
3146
+ providerBaseUrl !== undefined ? this.#normalizeDiscoveryEvidenceEndpoint(providerBaseUrl) : undefined,
3147
+ canUseCredentialDerivedXiaomiEndpoint:
3148
+ provider === "xiaomi" &&
3149
+ this.#providerEvidenceApiKeys.get("xiaomi")?.startsWith("tp-") === true &&
3150
+ this.#runtimeProviderOverrides.get("xiaomi")?.baseUrl === undefined &&
3151
+ this.#providerOverrides.get("xiaomi")?.baseUrl === undefined &&
3152
+ resolveProviderBaseUrlFromEnv("xiaomi") === undefined,
3153
+ };
3154
+ builtInProviderStates.set(provider, state);
3155
+ return state;
3156
+ };
2960
3157
  const currentBuiltInDiscovered = builtInDiscovered.filter(model => {
2961
- const evidence = this.#descriptorDiscoveryEvidence.get(model.provider);
2962
- const currentEndpoint = this.#normalizeDiscoveryEvidenceEndpoint(
2963
- this.#getProviderBaseUrlForDiscovery(model.provider) ?? model.baseUrl ?? "",
2964
- );
2965
- const canUseCredentialDerivedXiaomiEndpoint =
2966
- model.provider === "xiaomi" &&
2967
- this.#providerEvidenceApiKeys.get("xiaomi")?.startsWith("tp-") === true &&
2968
- this.#runtimeProviderOverrides.get("xiaomi")?.baseUrl === undefined &&
2969
- this.#providerOverrides.get("xiaomi")?.baseUrl === undefined &&
2970
- resolveProviderBaseUrlFromEnv("xiaomi") === undefined;
3158
+ const state = builtInProviderStateFor(model.provider);
3159
+ const { evidence } = state;
3160
+ if (evidence === undefined || state.authGenerationThrew) return false;
3161
+ if (evidence.authGeneration !== state.expectedAuthGeneration) return false;
2971
3162
  try {
3163
+ const currentEndpoint =
3164
+ state.providerBaseEndpoint ?? this.#normalizeDiscoveryEvidenceEndpoint(model.baseUrl ?? "");
2972
3165
  return (
2973
- evidence !== undefined &&
2974
- evidence.authGeneration ===
2975
- this.#getProviderEvidenceGeneration(
2976
- model.provider,
2977
- this.#providerEvidenceApiKeys.get(model.provider),
2978
- ) &&
2979
3166
  (evidence.endpoint === currentEndpoint ||
2980
- (canUseCredentialDerivedXiaomiEndpoint &&
3167
+ (state.canUseCredentialDerivedXiaomiEndpoint &&
2981
3168
  evidence.endpoint === this.#normalizeDiscoveryEvidenceEndpoint(model.baseUrl ?? ""))) &&
2982
3169
  evidence.modelIds.has(model.id)
2983
3170
  );
@@ -3222,6 +3409,8 @@ export class ModelRegistry {
3222
3409
  (evidence.authGeneration !== authGenerationBeforeDiscovery || evidence.endpoint !== endpoint)))
3223
3410
  ? "online"
3224
3411
  : strategy;
3412
+ let discoveryFailureEvidence: unknown;
3413
+
3225
3414
  const mergeInput = await this.#discoveryManager.discover(effectiveProviderConfig, refreshStrategy, {
3226
3415
  cacheDbPath: this.#cacheDbPath,
3227
3416
  requiresAuth: provider =>
@@ -3234,11 +3423,17 @@ export class ModelRegistry {
3234
3423
  baseUrl: provider.baseUrl,
3235
3424
  }),
3236
3425
  isAuthenticated,
3237
- fetchModels: async (provider, apiKey) =>
3238
- this.#sanitizeDiscoverableModelsForCache(
3239
- provider,
3240
- await this.#discoverModelsByProviderType(provider, apiKey),
3241
- ),
3426
+ fetchModels: async (provider, apiKey) => {
3427
+ try {
3428
+ return this.#sanitizeDiscoverableModelsForCache(
3429
+ provider,
3430
+ await this.#discoverModelsByProviderType(provider, apiKey),
3431
+ );
3432
+ } catch (error) {
3433
+ discoveryFailureEvidence = error;
3434
+ throw error;
3435
+ }
3436
+ },
3242
3437
  getEvidenceGeneration: provider => this.#getProviderEvidenceGeneration(provider.provider, preflightApiKey),
3243
3438
  cacheDynamicModelProvenance: cacheLookupProvenance,
3244
3439
  canPublishCache: () => isCurrentEndpoint() && isCurrentProviderRefresh(),
@@ -3262,11 +3457,15 @@ export class ModelRegistry {
3262
3457
  };
3263
3458
  }
3264
3459
  if (mergeInput.warning) {
3265
- logger.warn("model discovery failed for provider", {
3460
+ const baseUrl = effectiveProviderConfig.baseUrl ?? "";
3461
+ const fields = {
3266
3462
  provider: effectiveProviderConfig.provider,
3267
- url: redactDiscoveryUrl(effectiveProviderConfig.baseUrl ?? ""),
3463
+ url: redactDiscoveryUrl(baseUrl),
3268
3464
  error: mergeInput.warning,
3269
- });
3465
+ };
3466
+ if (isQuietLocalDiscoveryFailure(baseUrl, mergeInput.warning, discoveryFailureEvidence))
3467
+ logger.debug("model discovery failed for provider", fields);
3468
+ else logger.warn("model discovery failed for provider", fields);
3270
3469
  }
3271
3470
  this.#providerEvidenceApiKeys.set(effectiveProviderConfig.provider, preflightApiKey);
3272
3471
  return {
@@ -3826,27 +4025,47 @@ export class ModelRegistry {
3826
4025
  return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
3827
4026
  }
3828
4027
 
3829
- #resolveDiscoveredModelApi(providerConfig: DiscoveryProviderConfig, modelId: string): Api {
3830
- let api = providerConfig.api;
4028
+ #resolveDiscoveredModelApi(
4029
+ providerConfig: DiscoveryProviderConfig,
4030
+ modelId: string,
4031
+ entry?: { id?: unknown; owned_by?: unknown },
4032
+ ): Api {
4033
+ // 1. Explicit per-prefix routing from models.yml always wins — the user
4034
+ // stated the transport for these ids, so never second-guess it.
3831
4035
  let matchedPrefixLength = -1;
4036
+ let prefixApi: Api | undefined;
3832
4037
  for (const [prefix, routedApi] of Object.entries(providerConfig.discovery.apiByModelPrefix ?? {})) {
3833
4038
  if (modelId.startsWith(prefix) && prefix.length > matchedPrefixLength) {
3834
- api = routedApi;
4039
+ prefixApi = routedApi;
3835
4040
  matchedPrefixLength = prefix.length;
3836
4041
  }
3837
4042
  }
3838
- return api;
4043
+ if (prefixApi !== undefined) return prefixApi;
4044
+ // 2. For OpenAI-compatible gateways that can front both OpenAI and
4045
+ // Anthropic upstreams (e.g. CLIProxyAPI), auto-detect the wire family
4046
+ // per model from the `/v1/models` `owned_by` owner and the model id.
4047
+ // A mixed gateway then routes `claude-*` through Anthropic Messages
4048
+ // with zero manual config. OpenAI-family models keep the provider's
4049
+ // configured OpenAI transport (e.g. `openai-responses` vs
4050
+ // `openai-completions`) rather than being forced to chat-completions,
4051
+ // because a `/v1/models` list cannot reveal which OpenAI API the
4052
+ // gateway expects. Only override to `openai-completions` when the
4053
+ // provider default is not itself an OpenAI-family api.
4054
+ if (providerConfig.discovery.type === "openai-models-list") {
4055
+ const detected = detectDiscoveredApiFamily(entry ?? { id: modelId });
4056
+ if (detected === "anthropic-messages") return "anthropic-messages";
4057
+ if (detected === "openai-completions") {
4058
+ return isOpenAIFamilyApi(providerConfig.api) ? providerConfig.api : "openai-completions";
4059
+ }
4060
+ }
4061
+ // 3. Fall back to the provider-level default api.
4062
+ return providerConfig.api;
3839
4063
  }
3840
4064
 
3841
4065
  async #discoverModelsDevProvider(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
3842
4066
  const baseUrl = providerConfig.baseUrl;
3843
4067
  if (!baseUrl) throw new Error(`Provider "${providerConfig.provider}" requires baseUrl for models.dev discovery.`);
3844
- const response = await fetch("https://models.dev/api.json", {
3845
- headers: { Accept: "application/json" },
3846
- signal: AbortSignal.timeout(5_000),
3847
- });
3848
- if (!response.ok) throw new Error(`HTTP ${response.status} from https://models.dev/api.json`);
3849
- const payload: unknown = await response.json();
4068
+ const payload: unknown = await fetchModelsDevPayload();
3850
4069
  if (!isRecord(payload)) return [];
3851
4070
  const catalogProvider = payload[providerConfig.discovery.modelsDevProvider ?? providerConfig.provider];
3852
4071
  if (!isRecord(catalogProvider) || !isRecord(catalogProvider.models)) return [];
@@ -3952,7 +4171,7 @@ export class ModelRegistry {
3952
4171
  item.max_tokens,
3953
4172
  item.max_output_tokens,
3954
4173
  );
3955
- const api = this.#resolveDiscoveredModelApi(providerConfig, id);
4174
+ const api = this.#resolveDiscoveredModelApi(providerConfig, id, item);
3956
4175
  discovered.push(
3957
4176
  enrichModelThinking({
3958
4177
  id,
@@ -4452,6 +4671,7 @@ export class ModelRegistry {
4452
4671
  providerConfig.requestTransform,
4453
4672
  (providerConfig.auth as ProviderAuthMode | undefined) ?? undefined,
4454
4673
  providerConfig.cacheRetention,
4674
+ providerConfig.transport,
4455
4675
  modelDef as CustomModelDefinitionLike,
4456
4676
  );
4457
4677
  if (!model) continue;
@@ -4584,6 +4804,22 @@ export class ModelRegistry {
4584
4804
  autoroutingProviderOrder(): readonly string[] {
4585
4805
  return projectCatalogProviderOrder(getConfiguredProviderOrderFromSettings(), this.#models);
4586
4806
  }
4807
+
4808
+ /**
4809
+ * Provider priority for automatic model selection. Explicit provider order
4810
+ * wins first, followed by omitted OAuth providers, then other providers;
4811
+ * catalog order breaks ties within each policy band.
4812
+ */
4813
+ automaticProviderOrder(sessionId?: string): readonly string[] {
4814
+ const policy = this.#buildProviderSelectionPolicy(sessionId);
4815
+ return [...policy.orderedProviders()].sort((left, right) => {
4816
+ const rankDifference = policy.rank(left) - policy.rank(right);
4817
+ return rankDifference !== 0
4818
+ ? rankDifference
4819
+ : policy.providerCatalogIndex(left) - policy.providerCatalogIndex(right);
4820
+ });
4821
+ }
4822
+
4587
4823
  #providerRankMap(policy: ProviderSelectionPolicy): Map<string, number> {
4588
4824
  const providerRank = new Map<string, number>();
4589
4825
  for (const provider of policy.orderedProviders()) {
@@ -4820,6 +5056,18 @@ export class ModelRegistry {
4820
5056
  return this.#effectiveProviderAuth(provider, sessionId);
4821
5057
  }
4822
5058
 
5059
+ /**
5060
+ * Billing path for a resolved binding — derived at the point the binding
5061
+ * resolves credentials (model.api + credential provenance). Not a
5062
+ * provider-name allowlist: a custom OpenAI-compatible provider bound
5063
+ * to a metered gateway is "metered-api" exactly like a first-party
5064
+ * key. Only the billing category is ever disclosed.
5065
+ */
5066
+ getBillingPath(model: Model<Api>, sessionId?: string): BillingPath | undefined {
5067
+ const effectiveAuth = this.#effectiveProviderAuth(model.provider, sessionId);
5068
+ return deriveBillingPath(model.provider, model.api as Api, effectiveAuth);
5069
+ }
5070
+
4823
5071
  /**
4824
5072
  * Forget a session's remembered canonical variant so the next resolution
4825
5073
  * for that session re-ranks from scratch (explicit reselection
@@ -5608,6 +5856,7 @@ export class ModelRegistry {
5608
5856
  config.requestTransform,
5609
5857
  undefined,
5610
5858
  undefined,
5859
+ config.transport,
5611
5860
  modelDef as CustomModelDefinitionLike,
5612
5861
  );
5613
5862
  if (!overlay) {
@@ -1580,6 +1580,11 @@ export async function findInitialModel(options: {
1580
1580
  thinkingLevel = resolveThinkingLevelForModel(found, defaultThinkingSelector);
1581
1581
  return { model, thinkingLevel, fallbackMessage: undefined };
1582
1582
  }
1583
+ return {
1584
+ model: undefined,
1585
+ thinkingLevel: undefined,
1586
+ fallbackMessage: `Model ${defaultProvider}/${defaultModelId} not found`,
1587
+ };
1583
1588
  }
1584
1589
 
1585
1590
  // 4. Try first available model with valid API key
@@ -421,6 +421,12 @@ export const SETTINGS_SCHEMA = {
421
421
  description: "Hard maximum runtime for an SDK prompt from acceptance, bounding progress-aware renewals.",
422
422
  validate: (value: number) => Number.isSafeInteger(value) && value >= 60_000 && value <= 86_400_000,
423
423
  },
424
+ "sdk.masterOrphanGraceMs": {
425
+ type: "number",
426
+ default: 120_000,
427
+ description: "Grace period before a master-spawned child orphaned by confirmed master loss is closed.",
428
+ validate: (value: number) => Number.isSafeInteger(value) && value >= 60_000 && value <= 3_600_000,
429
+ },
424
430
 
425
431
  // Notifications (shared daemon with Telegram/Discord/Slack presentation adapters)
426
432
  "notifications.enabled": { type: "boolean", default: false },
@@ -745,7 +751,7 @@ export const SETTINGS_SCHEMA = {
745
751
  // ────────────────────────────────────────────────────────────────────────
746
752
  "ui.language": {
747
753
  type: "enum",
748
- values: ["en", "ko", "ja"] as const,
754
+ values: ["en", "ko", "zh", "ja"] as const,
749
755
  default: "en",
750
756
  ui: {
751
757
  tab: "appearance",
@@ -754,6 +760,7 @@ export const SETTINGS_SCHEMA = {
754
760
  options: [
755
761
  { value: "en", label: "English" },
756
762
  { value: "ko", label: "Korean (한국어)" },
763
+ { value: "zh", label: "Chinese (简体中文)" },
757
764
  { value: "ja", label: "Japanese (日本語)" },
758
765
  ],
759
766
  },
@@ -1572,14 +1579,15 @@ export const SETTINGS_SCHEMA = {
1572
1579
  },
1573
1580
  },
1574
1581
 
1575
- interruptMode: {
1582
+ toolInterruptPolicy: {
1576
1583
  type: "enum",
1577
- values: ["immediate", "wait"] as const,
1578
- default: "immediate",
1584
+ values: ["abort_tools", "finish_tools"] as const,
1585
+ default: "abort_tools",
1579
1586
  ui: {
1580
1587
  tab: "interaction",
1581
- label: "Interrupt Mode",
1582
- description: "When steering messages interrupt tool execution",
1588
+ label: "Tool Interrupt Policy",
1589
+ description:
1590
+ "Whether a steering message aborts the tools still running in the current batch (abort_tools) or lets them finish first (finish_tools). Never changes when a steer is consumed: it is always picked up at the next tool/turn boundary either way.",
1583
1591
  },
1584
1592
  },
1585
1593
 
@@ -3036,6 +3044,17 @@ export const SETTINGS_SCHEMA = {
3036
3044
  description: "Enable the browser tool (Ulixee Hero)",
3037
3045
  },
3038
3046
  },
3047
+ "browser.backend": {
3048
+ type: "enum",
3049
+ values: ["native", "aside"] as const,
3050
+ default: "native",
3051
+ ui: {
3052
+ tab: "tools",
3053
+ label: "Browser Backend",
3054
+ description:
3055
+ "Browser surface offered to the model. 'native' exposes the built-in Puppeteer browser tool. 'aside' hides it and requires every browser task to run through the Aside CLI in your live logged-in browser (requires the Aside CLI).",
3056
+ },
3057
+ },
3039
3058
 
3040
3059
  "browser.headless": {
3041
3060
  type: "boolean",
@@ -3065,7 +3084,7 @@ export const SETTINGS_SCHEMA = {
3065
3084
  tab: "tools",
3066
3085
  label: "Profile reuse posture",
3067
3086
  description:
3068
- "'auto' (default): when a usable real Chrome profile is available, the browser tool uses an isolated copy of it (cookies/session/cache) for stronger stealth, warns, and falls back to synthetic. 'opt-in': stay synthetic unless a real profile is explicitly requested.",
3087
+ "'auto' (default): when a usable real Chrome profile is available, the browser tool uses an isolated copy of it (cookies/session/cache) for stronger stealth, warns, and falls back to synthetic. 'opt-in': stay synthetic unless a real profile is explicitly requested. On macOS, cookie decryption is brand-specific (Chrome Safe Storage vs Chromium Safe Storage Keychain); a Chrome profile copied into bundled Chromium decrypts to garbage, so auto reuses the profile only when the headless executable can decrypt it — detected system Chrome/Chromium or PUPPETEER_EXECUTABLE_PATH — otherwise it falls back to synthetic with a warning.",
3069
3088
  },
3070
3089
  },
3071
3090
 
@@ -4117,7 +4136,7 @@ export function getEnumValues(path: SettingPath): readonly string[] | undefined
4117
4136
  return "values" in def ? (def.values as readonly string[]) : undefined;
4118
4137
  }
4119
4138
 
4120
- export { CONFIG_SCHEMA_VERSION } from "./config-schema-version";
4139
+ export { CONFIG_SCHEMA_VERSION, normalizeConfigSchemaVersion } from "./config-schema-version";
4121
4140
 
4122
4141
  export type SettingsSchemaIssue = {
4123
4142
  path: string;