@gajae-code/coding-agent 0.16.0 → 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.
- package/CHANGELOG.md +41 -1
- package/dist/types/async/job-manager.d.ts +32 -1
- package/dist/types/browser-backend.d.ts +12 -0
- package/dist/types/cli/update-cli.d.ts +1 -0
- package/dist/types/commands/sdk.d.ts +2 -0
- package/dist/types/config/billing-path.d.ts +25 -0
- package/dist/types/config/config-schema-version.d.ts +13 -1
- package/dist/types/config/model-registry.d.ts +16 -0
- package/dist/types/config/settings-schema.d.ts +17 -7
- package/dist/types/extensibility/extensions/types.d.ts +8 -3
- package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
- package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/acp/acp-agent.d.ts +21 -21
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +16 -2
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -1
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +14 -0
- package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
- package/dist/types/sdk/broker/state-version.d.ts +17 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/index.d.ts +3 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/rows.d.ts +8 -2
- package/dist/types/sdk/cli/session-cli.d.ts +2 -1
- package/dist/types/sdk/host/events.d.ts +1 -0
- package/dist/types/sdk/host/query/cursor.d.ts +4 -2
- package/dist/types/sdk/host/query/handlers.d.ts +1 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +18 -17
- package/dist/types/sdk/lifecycle-session.d.ts +1 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
- package/dist/types/sdk/receipt-state.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +4 -0
- package/dist/types/sdk/turn-result.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +47 -7
- package/dist/types/session/fold-coordinator.d.ts +29 -2
- package/dist/types/session/session-manager.d.ts +14 -2
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +12 -0
- package/dist/types/tools/browser/launch.d.ts +10 -0
- package/dist/types/tools/browser/registry.d.ts +8 -0
- package/dist/types/tools/index.d.ts +6 -10
- package/dist/types/tools/job.d.ts +3 -1
- package/dist/types/utils/external-editor.d.ts +7 -0
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/pirate.ts +7 -4
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +7 -1
- package/src/async/job-manager.ts +107 -9
- package/src/browser-backend.ts +29 -0
- package/src/cli/plugin-cli.ts +29 -0
- package/src/cli/update-cli.ts +63 -12
- package/src/commands/sdk.ts +22 -7
- package/src/config/billing-path.ts +42 -0
- package/src/config/config-schema-version.ts +18 -1
- package/src/config/model-preset-registry.ts +177 -18
- package/src/config/model-registry.ts +268 -42
- package/src/config/settings-schema.ts +19 -7
- package/src/config/settings.ts +46 -15
- package/src/coordinator-mcp/projection-scan.ts +6 -6
- package/src/coordinator-mcp/server.ts +21 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/extensibility/extensions/runner.ts +27 -0
- package/src/extensibility/extensions/types.ts +8 -3
- package/src/extensibility/gjc-plugins/installer.ts +51 -11
- package/src/extensibility/runtime-skill-discovery.ts +110 -3
- package/src/extensibility/skills.ts +22 -12
- package/src/gjc-runtime/launch-worktree.ts +285 -4
- package/src/gjc-runtime/session-state-lock.ts +154 -28
- package/src/gjc-runtime/session-state-sidecar.ts +10 -17
- package/src/gjc-runtime/tmux-owner-isolation.ts +16 -1
- package/src/internal-urls/docs-index.generated.ts +10 -10
- package/src/main.ts +4 -1
- package/src/modes/acp/acp-agent.ts +5 -5
- package/src/modes/components/model-selector.ts +76 -8
- package/src/modes/components/session-selector.ts +23 -15
- package/src/modes/components/sessions-dashboard.ts +8 -5
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +30 -4
- package/src/modes/controllers/extension-ui-controller.ts +13 -13
- package/src/modes/controllers/input-controller.ts +2 -2
- package/src/modes/controllers/selector-controller.ts +10 -3
- package/src/modes/interactive-mode.ts +36 -12
- package/src/modes/prompt-action-autocomplete.ts +30 -4
- package/src/modes/runtime-init.ts +12 -9
- package/src/modes/theme/theme.ts +5 -0
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +22 -3
- package/src/prompts/agent-fragments/browser-aside.md +22 -0
- package/src/prompts/tools/ask.md +1 -1
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +5 -0
- package/src/sdk/broker/broker.ts +10 -1
- package/src/sdk/broker/ensure.ts +76 -3
- package/src/sdk/broker/lifecycle-ledger.ts +6 -0
- package/src/sdk/broker/lifecycle.ts +254 -34
- package/src/sdk/broker/spawn-substrate.ts +45 -2
- package/src/sdk/broker/startup-budget.ts +144 -2
- package/src/sdk/broker/state-version.ts +17 -0
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +111 -24
- package/src/sdk/bus/discord-daemon.ts +50 -10
- package/src/sdk/bus/index.ts +33 -3
- package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
- package/src/sdk/bus/lifecycle-commands.ts +4 -0
- package/src/sdk/bus/notification-service.ts +22 -5
- package/src/sdk/bus/prompt-reconciliation.ts +7 -2
- package/src/sdk/bus/reconciliation-store.ts +55 -31
- package/src/sdk/bus/slack-conversation.ts +1 -1
- package/src/sdk/bus/slack-daemon.ts +72 -38
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/rows.ts +30 -7
- package/src/sdk/cli/session-cli.ts +33 -21
- package/src/sdk/host/control/dispatch.ts +2 -2
- package/src/sdk/host/events.ts +1 -0
- package/src/sdk/host/host.ts +2 -0
- package/src/sdk/host/query/cursor.ts +52 -7
- package/src/sdk/host/query/handlers.ts +33 -12
- package/src/sdk/host/query/revision-store.ts +12 -0
- package/src/sdk/host/session-runtime.test.ts +463 -8
- package/src/sdk/host/session-runtime.ts +383 -54
- package/src/sdk/host/surface-policy.ts +3 -3
- package/src/sdk/lifecycle-session.ts +7 -2
- package/src/sdk/prompt-deadline-manager.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +54 -3
- package/src/sdk/protocol/operation-registry.ts +1 -1
- package/src/sdk/receipt-state.ts +10 -0
- package/src/sdk/router/session-router.ts +152 -59
- package/src/sdk/session.ts +60 -41
- package/src/sdk/turn-result.ts +22 -0
- package/src/session/agent-session.ts +1033 -260
- package/src/session/fold-coordinator.ts +40 -3
- package/src/session/session-manager.ts +517 -77
- package/src/slash-commands/builtin-registry.ts +41 -0
- package/src/task/executor.ts +9 -4
- package/src/tools/ask-contract.test.ts +64 -0
- package/src/tools/ask-contract.ts +36 -1
- package/src/tools/ask.ts +10 -8
- package/src/tools/bash-interactive.ts +11 -2
- package/src/tools/bash.ts +156 -33
- package/src/tools/browser/launch.ts +37 -0
- package/src/tools/browser/registry.ts +99 -9
- package/src/tools/cron.ts +6 -4
- package/src/tools/descriptors.test.ts +15 -0
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/index.ts +6 -6
- package/src/tools/job.ts +4 -1
- package/src/tools/monitor.ts +12 -9
- package/src/tools/resolve.ts +30 -10
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/utils/external-editor.ts +72 -1
|
@@ -39,7 +39,11 @@ import {
|
|
|
39
39
|
UNK_MAX_TOKENS,
|
|
40
40
|
unregisterCustomApis,
|
|
41
41
|
} from "@gajae-code/ai/core";
|
|
42
|
-
import {
|
|
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])) {
|
|
@@ -2674,17 +2781,48 @@ export class ModelRegistry {
|
|
|
2674
2781
|
keylessProviders.add(providerName);
|
|
2675
2782
|
}
|
|
2676
2783
|
|
|
2677
|
-
|
|
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) {
|
|
2678
2816
|
discoverableProviders.push({
|
|
2679
2817
|
provider: providerName,
|
|
2680
|
-
api:
|
|
2681
|
-
baseUrl:
|
|
2818
|
+
api: providerApi,
|
|
2819
|
+
baseUrl: effectiveDiscoveryBaseUrl,
|
|
2682
2820
|
headers: providerConfig.headers,
|
|
2683
2821
|
compat: providerConfig.compat,
|
|
2684
2822
|
requestTransform: providerConfig.requestTransform,
|
|
2685
2823
|
cacheRetention: providerConfig.cacheRetention,
|
|
2686
|
-
discovery:
|
|
2687
|
-
optional:
|
|
2824
|
+
discovery: effectiveDiscovery,
|
|
2825
|
+
optional: !providerConfig.discovery,
|
|
2688
2826
|
});
|
|
2689
2827
|
}
|
|
2690
2828
|
|
|
@@ -2962,27 +3100,71 @@ export class ModelRegistry {
|
|
|
2962
3100
|
? { ...result, invalidatesPublishedState }
|
|
2963
3101
|
: { ...result, current: false, models: [], fetched: false, invalidatesPublishedState };
|
|
2964
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
|
+
};
|
|
2965
3157
|
const currentBuiltInDiscovered = builtInDiscovered.filter(model => {
|
|
2966
|
-
const
|
|
2967
|
-
const
|
|
2968
|
-
|
|
2969
|
-
);
|
|
2970
|
-
const canUseCredentialDerivedXiaomiEndpoint =
|
|
2971
|
-
model.provider === "xiaomi" &&
|
|
2972
|
-
this.#providerEvidenceApiKeys.get("xiaomi")?.startsWith("tp-") === true &&
|
|
2973
|
-
this.#runtimeProviderOverrides.get("xiaomi")?.baseUrl === undefined &&
|
|
2974
|
-
this.#providerOverrides.get("xiaomi")?.baseUrl === undefined &&
|
|
2975
|
-
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;
|
|
2976
3162
|
try {
|
|
3163
|
+
const currentEndpoint =
|
|
3164
|
+
state.providerBaseEndpoint ?? this.#normalizeDiscoveryEvidenceEndpoint(model.baseUrl ?? "");
|
|
2977
3165
|
return (
|
|
2978
|
-
evidence !== undefined &&
|
|
2979
|
-
evidence.authGeneration ===
|
|
2980
|
-
this.#getProviderEvidenceGeneration(
|
|
2981
|
-
model.provider,
|
|
2982
|
-
this.#providerEvidenceApiKeys.get(model.provider),
|
|
2983
|
-
) &&
|
|
2984
3166
|
(evidence.endpoint === currentEndpoint ||
|
|
2985
|
-
(canUseCredentialDerivedXiaomiEndpoint &&
|
|
3167
|
+
(state.canUseCredentialDerivedXiaomiEndpoint &&
|
|
2986
3168
|
evidence.endpoint === this.#normalizeDiscoveryEvidenceEndpoint(model.baseUrl ?? ""))) &&
|
|
2987
3169
|
evidence.modelIds.has(model.id)
|
|
2988
3170
|
);
|
|
@@ -3227,6 +3409,8 @@ export class ModelRegistry {
|
|
|
3227
3409
|
(evidence.authGeneration !== authGenerationBeforeDiscovery || evidence.endpoint !== endpoint)))
|
|
3228
3410
|
? "online"
|
|
3229
3411
|
: strategy;
|
|
3412
|
+
let discoveryFailureEvidence: unknown;
|
|
3413
|
+
|
|
3230
3414
|
const mergeInput = await this.#discoveryManager.discover(effectiveProviderConfig, refreshStrategy, {
|
|
3231
3415
|
cacheDbPath: this.#cacheDbPath,
|
|
3232
3416
|
requiresAuth: provider =>
|
|
@@ -3239,11 +3423,17 @@ export class ModelRegistry {
|
|
|
3239
3423
|
baseUrl: provider.baseUrl,
|
|
3240
3424
|
}),
|
|
3241
3425
|
isAuthenticated,
|
|
3242
|
-
fetchModels: async (provider, apiKey) =>
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
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
|
+
},
|
|
3247
3437
|
getEvidenceGeneration: provider => this.#getProviderEvidenceGeneration(provider.provider, preflightApiKey),
|
|
3248
3438
|
cacheDynamicModelProvenance: cacheLookupProvenance,
|
|
3249
3439
|
canPublishCache: () => isCurrentEndpoint() && isCurrentProviderRefresh(),
|
|
@@ -3267,11 +3457,15 @@ export class ModelRegistry {
|
|
|
3267
3457
|
};
|
|
3268
3458
|
}
|
|
3269
3459
|
if (mergeInput.warning) {
|
|
3270
|
-
|
|
3460
|
+
const baseUrl = effectiveProviderConfig.baseUrl ?? "";
|
|
3461
|
+
const fields = {
|
|
3271
3462
|
provider: effectiveProviderConfig.provider,
|
|
3272
|
-
url: redactDiscoveryUrl(
|
|
3463
|
+
url: redactDiscoveryUrl(baseUrl),
|
|
3273
3464
|
error: mergeInput.warning,
|
|
3274
|
-
}
|
|
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);
|
|
3275
3469
|
}
|
|
3276
3470
|
this.#providerEvidenceApiKeys.set(effectiveProviderConfig.provider, preflightApiKey);
|
|
3277
3471
|
return {
|
|
@@ -3831,27 +4025,47 @@ export class ModelRegistry {
|
|
|
3831
4025
|
return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
|
|
3832
4026
|
}
|
|
3833
4027
|
|
|
3834
|
-
#resolveDiscoveredModelApi(
|
|
3835
|
-
|
|
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.
|
|
3836
4035
|
let matchedPrefixLength = -1;
|
|
4036
|
+
let prefixApi: Api | undefined;
|
|
3837
4037
|
for (const [prefix, routedApi] of Object.entries(providerConfig.discovery.apiByModelPrefix ?? {})) {
|
|
3838
4038
|
if (modelId.startsWith(prefix) && prefix.length > matchedPrefixLength) {
|
|
3839
|
-
|
|
4039
|
+
prefixApi = routedApi;
|
|
3840
4040
|
matchedPrefixLength = prefix.length;
|
|
3841
4041
|
}
|
|
3842
4042
|
}
|
|
3843
|
-
return
|
|
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;
|
|
3844
4063
|
}
|
|
3845
4064
|
|
|
3846
4065
|
async #discoverModelsDevProvider(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
|
|
3847
4066
|
const baseUrl = providerConfig.baseUrl;
|
|
3848
4067
|
if (!baseUrl) throw new Error(`Provider "${providerConfig.provider}" requires baseUrl for models.dev discovery.`);
|
|
3849
|
-
const
|
|
3850
|
-
headers: { Accept: "application/json" },
|
|
3851
|
-
signal: AbortSignal.timeout(5_000),
|
|
3852
|
-
});
|
|
3853
|
-
if (!response.ok) throw new Error(`HTTP ${response.status} from https://models.dev/api.json`);
|
|
3854
|
-
const payload: unknown = await response.json();
|
|
4068
|
+
const payload: unknown = await fetchModelsDevPayload();
|
|
3855
4069
|
if (!isRecord(payload)) return [];
|
|
3856
4070
|
const catalogProvider = payload[providerConfig.discovery.modelsDevProvider ?? providerConfig.provider];
|
|
3857
4071
|
if (!isRecord(catalogProvider) || !isRecord(catalogProvider.models)) return [];
|
|
@@ -3957,7 +4171,7 @@ export class ModelRegistry {
|
|
|
3957
4171
|
item.max_tokens,
|
|
3958
4172
|
item.max_output_tokens,
|
|
3959
4173
|
);
|
|
3960
|
-
const api = this.#resolveDiscoveredModelApi(providerConfig, id);
|
|
4174
|
+
const api = this.#resolveDiscoveredModelApi(providerConfig, id, item);
|
|
3961
4175
|
discovered.push(
|
|
3962
4176
|
enrichModelThinking({
|
|
3963
4177
|
id,
|
|
@@ -4842,6 +5056,18 @@ export class ModelRegistry {
|
|
|
4842
5056
|
return this.#effectiveProviderAuth(provider, sessionId);
|
|
4843
5057
|
}
|
|
4844
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
|
+
|
|
4845
5071
|
/**
|
|
4846
5072
|
* Forget a session's remembered canonical variant so the next resolution
|
|
4847
5073
|
* for that session re-ranks from scratch (explicit reselection
|
|
@@ -1579,14 +1579,15 @@ export const SETTINGS_SCHEMA = {
|
|
|
1579
1579
|
},
|
|
1580
1580
|
},
|
|
1581
1581
|
|
|
1582
|
-
|
|
1582
|
+
toolInterruptPolicy: {
|
|
1583
1583
|
type: "enum",
|
|
1584
|
-
values: ["
|
|
1585
|
-
default: "
|
|
1584
|
+
values: ["abort_tools", "finish_tools"] as const,
|
|
1585
|
+
default: "abort_tools",
|
|
1586
1586
|
ui: {
|
|
1587
1587
|
tab: "interaction",
|
|
1588
|
-
label: "Interrupt
|
|
1589
|
-
description:
|
|
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.",
|
|
1590
1591
|
},
|
|
1591
1592
|
},
|
|
1592
1593
|
|
|
@@ -3043,6 +3044,17 @@ export const SETTINGS_SCHEMA = {
|
|
|
3043
3044
|
description: "Enable the browser tool (Ulixee Hero)",
|
|
3044
3045
|
},
|
|
3045
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
|
+
},
|
|
3046
3058
|
|
|
3047
3059
|
"browser.headless": {
|
|
3048
3060
|
type: "boolean",
|
|
@@ -3072,7 +3084,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
3072
3084
|
tab: "tools",
|
|
3073
3085
|
label: "Profile reuse posture",
|
|
3074
3086
|
description:
|
|
3075
|
-
"'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.",
|
|
3076
3088
|
},
|
|
3077
3089
|
},
|
|
3078
3090
|
|
|
@@ -4124,7 +4136,7 @@ export function getEnumValues(path: SettingPath): readonly string[] | undefined
|
|
|
4124
4136
|
return "values" in def ? (def.values as readonly string[]) : undefined;
|
|
4125
4137
|
}
|
|
4126
4138
|
|
|
4127
|
-
export { CONFIG_SCHEMA_VERSION } from "./config-schema-version";
|
|
4139
|
+
export { CONFIG_SCHEMA_VERSION, normalizeConfigSchemaVersion } from "./config-schema-version";
|
|
4128
4140
|
|
|
4129
4141
|
export type SettingsSchemaIssue = {
|
|
4130
4142
|
path: string;
|
package/src/config/settings.ts
CHANGED
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
type GroupPrefix,
|
|
71
71
|
type GroupTypeMap,
|
|
72
72
|
getDefault,
|
|
73
|
+
normalizeConfigSchemaVersion,
|
|
73
74
|
reconcileSettingsSchema,
|
|
74
75
|
SETTINGS_SCHEMA,
|
|
75
76
|
type SettingPath,
|
|
@@ -1525,15 +1526,19 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1525
1526
|
this.#hasInvalidNotificationGlobal = true;
|
|
1526
1527
|
}
|
|
1527
1528
|
}
|
|
1529
|
+
// ONE normalized marker feeds every version decision (future-schema,
|
|
1530
|
+
// migration-pending, and the ordered registry in #migrateRawSettings): a
|
|
1531
|
+
// malformed value must never be read as a future schema here — which would
|
|
1532
|
+
// skip legacy migration entirely — and as unmigrated by the registry.
|
|
1533
|
+
const configSchemaVersion = normalizeConfigSchemaVersion(parsedRaw.configSchemaVersion);
|
|
1528
1534
|
this.#futureSchemaVersion =
|
|
1529
1535
|
filePath === this.#configPath &&
|
|
1530
|
-
|
|
1531
|
-
|
|
1536
|
+
configSchemaVersion !== undefined &&
|
|
1537
|
+
configSchemaVersion > CONFIG_SCHEMA_VERSION;
|
|
1532
1538
|
|
|
1533
|
-
const configSchemaVersion = parsedRaw.configSchemaVersion;
|
|
1534
1539
|
if (
|
|
1535
1540
|
filePath === this.#configPath &&
|
|
1536
|
-
(
|
|
1541
|
+
(configSchemaVersion === undefined || configSchemaVersion < CONFIG_SCHEMA_VERSION)
|
|
1537
1542
|
) {
|
|
1538
1543
|
this.#schemaMigrationPending = true;
|
|
1539
1544
|
}
|
|
@@ -2087,7 +2092,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
2087
2092
|
return false;
|
|
2088
2093
|
}
|
|
2089
2094
|
const schemaVersion = (root as Record<string, unknown> | null | undefined)?.configSchemaVersion;
|
|
2090
|
-
if (
|
|
2095
|
+
if ((normalizeConfigSchemaVersion(schemaVersion) ?? 0) > CONFIG_SCHEMA_VERSION) {
|
|
2091
2096
|
this.#warnLegacyFallbackMigration(
|
|
2092
2097
|
`Settings: database workflow migration skipped: ${this.#configPath} is a future config schema (configSchemaVersion ${schemaVersion} > ${CONFIG_SCHEMA_VERSION}); leaving legacy rows for the next load`,
|
|
2093
2098
|
);
|
|
@@ -2338,7 +2343,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
2338
2343
|
// sets #futureSchemaVersion, so it must check the target schema
|
|
2339
2344
|
// itself and never patch it or consume the legacy source.
|
|
2340
2345
|
const targetSchemaVersion = (tx.root as Record<string, unknown> | null | undefined)?.configSchemaVersion;
|
|
2341
|
-
if (
|
|
2346
|
+
if ((normalizeConfigSchemaVersion(targetSchemaVersion) ?? 0) > CONFIG_SCHEMA_VERSION) {
|
|
2342
2347
|
this.#warnLegacyFallbackMigration(
|
|
2343
2348
|
`Settings: config-root workflow migration skipped: ${target} is a future config schema (configSchemaVersion ${targetSchemaVersion} > ${CONFIG_SCHEMA_VERSION})`,
|
|
2344
2349
|
);
|
|
@@ -3580,7 +3585,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
3580
3585
|
// read-only across Settings; never patch it (mirrors the config-root
|
|
3581
3586
|
// migration guard) so a future schema cannot inherit stale keys.
|
|
3582
3587
|
const targetSchemaVersion = (tx.root as Record<string, unknown> | null | undefined)?.configSchemaVersion;
|
|
3583
|
-
if (
|
|
3588
|
+
if ((normalizeConfigSchemaVersion(targetSchemaVersion) ?? 0) > CONFIG_SCHEMA_VERSION) {
|
|
3584
3589
|
this.#warnLegacyFallbackMigration(
|
|
3585
3590
|
`Settings: project workflow migration skipped: ${target} is a future config schema (configSchemaVersion ${targetSchemaVersion} > ${CONFIG_SCHEMA_VERSION})`,
|
|
3586
3591
|
);
|
|
@@ -4097,7 +4102,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
4097
4102
|
markerPath = `${source}.fallback-invalid`,
|
|
4098
4103
|
): Promise<void> {
|
|
4099
4104
|
const targetSchemaVersion = (tx.root as Record<string, unknown> | null | undefined)?.configSchemaVersion;
|
|
4100
|
-
if (
|
|
4105
|
+
if ((normalizeConfigSchemaVersion(targetSchemaVersion) ?? 0) > CONFIG_SCHEMA_VERSION) return;
|
|
4101
4106
|
if (tx.root !== undefined && (tx.root === null || typeof tx.root !== "object" || Array.isArray(tx.root))) {
|
|
4102
4107
|
return;
|
|
4103
4108
|
}
|
|
@@ -4826,7 +4831,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
4826
4831
|
// guard) would rewrite configuration an older binary treats as
|
|
4827
4832
|
// read-only.
|
|
4828
4833
|
const targetSchemaVersion = (tx.root as Record<string, unknown> | null | undefined)?.configSchemaVersion;
|
|
4829
|
-
if (
|
|
4834
|
+
if ((normalizeConfigSchemaVersion(targetSchemaVersion) ?? 0) > CONFIG_SCHEMA_VERSION) {
|
|
4830
4835
|
return;
|
|
4831
4836
|
}
|
|
4832
4837
|
// Unset only while the target still matches the migration's fallback
|
|
@@ -5404,11 +5409,41 @@ export class Settings implements NotificationSettingsReader {
|
|
|
5404
5409
|
|
|
5405
5410
|
/** Apply registered schema migrations once, using configSchemaVersion as the durable marker. */
|
|
5406
5411
|
#migrateRawSettings(raw: RawSettings): RawSettings {
|
|
5407
|
-
const
|
|
5412
|
+
const rawVersion = raw.configSchemaVersion;
|
|
5413
|
+
const configuredVersion = normalizeConfigSchemaVersion(rawVersion);
|
|
5414
|
+
if (rawVersion !== undefined && rawVersion !== configuredVersion) {
|
|
5415
|
+
logger.warn("Settings: config has a malformed configSchemaVersion; migrating from the normalized version", {
|
|
5416
|
+
configSchemaVersion: String(rawVersion),
|
|
5417
|
+
normalized: String(configuredVersion),
|
|
5418
|
+
});
|
|
5419
|
+
}
|
|
5408
5420
|
if (configuredVersion === CONFIG_SCHEMA_VERSION) return raw;
|
|
5409
5421
|
if (typeof configuredVersion === "number" && configuredVersion > CONFIG_SCHEMA_VERSION) return raw;
|
|
5410
5422
|
|
|
5411
|
-
//
|
|
5423
|
+
// Ordered migration registry. Each step runs ONLY for files below its
|
|
5424
|
+
// target version: the v0 steps are not all idempotent (ask.timeout's
|
|
5425
|
+
// milliseconds-to-seconds conversion would re-divide a valid v1 value),
|
|
5426
|
+
// so a v1 file must skip straight to the v2 step.
|
|
5427
|
+
const fromVersion = typeof configuredVersion === "number" ? configuredVersion : 0;
|
|
5428
|
+
if (fromVersion < 1) this.#migrateRawSettingsV0ToV1(raw);
|
|
5429
|
+
// v1 -> v2: interruptMode -> toolInterruptPolicy.
|
|
5430
|
+
if (fromVersion < 2 && "interruptMode" in raw) {
|
|
5431
|
+
if (!("toolInterruptPolicy" in raw)) {
|
|
5432
|
+
const legacy = raw.interruptMode;
|
|
5433
|
+
if (legacy === "wait") raw.toolInterruptPolicy = "finish_tools";
|
|
5434
|
+
else if (legacy === "immediate") raw.toolInterruptPolicy = "abort_tools";
|
|
5435
|
+
// Any other value is malformed: drop it and let the schema default apply.
|
|
5436
|
+
}
|
|
5437
|
+
delete raw.interruptMode;
|
|
5438
|
+
}
|
|
5439
|
+
|
|
5440
|
+
raw.configSchemaVersion = CONFIG_SCHEMA_VERSION;
|
|
5441
|
+
|
|
5442
|
+
return raw;
|
|
5443
|
+
}
|
|
5444
|
+
|
|
5445
|
+
/** v0 -> v1 transforms. Runs only for files that predate schema version 1. */
|
|
5446
|
+
#migrateRawSettingsV0ToV1(raw: RawSettings): void {
|
|
5412
5447
|
// queueMode -> steeringMode
|
|
5413
5448
|
normalizeSessionDirectoryMigration(raw);
|
|
5414
5449
|
if ("queueMode" in raw && !("steeringMode" in raw)) {
|
|
@@ -5573,10 +5608,6 @@ export class Settings implements NotificationSettingsReader {
|
|
|
5573
5608
|
delete providersObj.imageCustomKeyEnv;
|
|
5574
5609
|
}
|
|
5575
5610
|
}
|
|
5576
|
-
|
|
5577
|
-
raw.configSchemaVersion = CONFIG_SCHEMA_VERSION;
|
|
5578
|
-
|
|
5579
|
-
return raw;
|
|
5580
5611
|
}
|
|
5581
5612
|
|
|
5582
5613
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -195,10 +195,10 @@ function trimProjectionRootPath(dir: string): string {
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
/**
|
|
198
|
-
*
|
|
199
|
-
* authoritative by checking its identity
|
|
198
|
+
* Node exposes no descriptor-relative `openat` binding on Windows or macOS. Keep the
|
|
199
|
+
* root pathname authoritative by checking its identity around every path operation.
|
|
200
200
|
*/
|
|
201
|
-
async function
|
|
201
|
+
async function openProjectionDirectoryPathBracketed(dir: string): Promise<ProjectionScanDirectory> {
|
|
202
202
|
const rootPath = trimProjectionRootPath(dir);
|
|
203
203
|
let opened: import("node:fs").BigIntStats;
|
|
204
204
|
try {
|
|
@@ -226,8 +226,8 @@ async function openProjectionDirectoryWindowsSafe(dir: string): Promise<Projecti
|
|
|
226
226
|
};
|
|
227
227
|
|
|
228
228
|
const entryPath = (entry: string): string => {
|
|
229
|
-
// Directory entries come from readdir and are therefore single components
|
|
230
|
-
//
|
|
229
|
+
// Directory entries come from readdir and are therefore single components.
|
|
230
|
+
// Keep the invariant explicit so an injected or corrupted entry can
|
|
231
231
|
// never turn a child operation into mutable pathname traversal.
|
|
232
232
|
if (
|
|
233
233
|
entry.length === 0 ||
|
|
@@ -290,7 +290,7 @@ async function openProjectionDirectoryWindowsSafe(dir: string): Promise<Projecti
|
|
|
290
290
|
*/
|
|
291
291
|
async function openProjectionDirectorySafe(dir: string): Promise<ProjectionScanDirectory> {
|
|
292
292
|
const platform = projectionPlatform();
|
|
293
|
-
if (platform === "win32") return
|
|
293
|
+
if (platform === "win32" || platform === "darwin") return openProjectionDirectoryPathBracketed(dir);
|
|
294
294
|
if (platform !== "linux") throw new ProjectionScanUnsupportedError();
|
|
295
295
|
const noFollow = fs.constants.O_NOFOLLOW;
|
|
296
296
|
const nonBlock = fs.constants.O_NONBLOCK;
|