/** * ServerSettings - Server-authoritative settings service. * * Owns persistence, validation, and change notification of settings that affect * server-side behavior (binary paths, streaming mode, env mode, custom models, * text generation model selection). * * Follows the same pattern as `keybindings.ts`: JSON file + Cache + PubSub + * Semaphore + FileSystem.watch for concurrency and external edit detection. * * @module ServerSettings */import { DEFAULT_TEXT_GENERATION_MODEL, DEFAULT_TEXT_GENERATION_MODEL_BY_PROVIDER, DEFAULT_MODEL_BY_PROVIDER, DEFAULT_SERVER_SETTINGS, type ModelSelection, type ProviderInstanceConfig, type ProviderInstanceEnvironmentVariable, type UsageLimitSourceConfig, ProviderDriverKind, ProviderInstanceId, resolveProviderInstanceEnabled, ServerSettings, ServerSettingsError, type ServerSettingsPatch,} from "@t3tools/contracts";import * as Cache from "effect/Cache";import * as Cause from "effect/Cause";import * as Context from "effect/Context";import * as Deferred from "effect/Deferred";import * as Duration from "effect/Duration";import * as Equal from "effect/Equal";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Path from "effect/Path";import * as PubSub from "effect/PubSub";import * as Ref from "effect/Ref";import * as Schema from "effect/Schema";import * as Semaphore from "effect/Semaphore";import * as Scope from "effect/Scope";import * as Stream from "effect/Stream";import * as SqlClient from "effect/unstable/sql/SqlClient";import { writeFileStringAtomically } from "./atomicWrite.ts";import * as ServerConfig from "./config.ts";import { type DeepPartial, deepMerge } from "@t3tools/shared/Struct";import { fromJsonStringPretty, fromLenientJson } from "@t3tools/shared/schemaJson";import { applyServerSettingsPatch, isModelSelectionProviderEnabled,} from "@t3tools/shared/serverSettings";import * as ServerSecretStore from "./auth/ServerSecretStore.ts"; export { resolveSourceControlWriterModelSelection } from "@t3tools/shared/serverSettings"; const encodeServerSettings = Schema.encodeEffect(ServerSettings);const encodeServerSettingsJson = Schema.encodeUnknownEffect(fromJsonStringPretty(ServerSettings));const decodeServerSettings = Schema.decodeUnknownEffect(ServerSettings); const textEncoder = new TextEncoder();const textDecoder = new TextDecoder(); /** * Fold the legacy in-config `enabled` flag into the envelope-level * `ProviderInstanceConfig.enabled` and strip it from the config blob, so * explicit provider instances carry exactly one enabled flag. Old settings * files can hold both flags with conflicting values; an explicit false on * either side wins so a user's disable is never silently undone. Runs on * every load and update — the file converges on the next write. */const foldProviderInstanceEnabledFlags = (settings: ServerSettings): ServerSettings => { let changed = false; const providerInstances: Record<string, ProviderInstanceConfig> = {}; for (const [instanceId, instance] of Object.entries(settings.providerInstances)) { const config = instance.config; // Only fold boolean flags: a malformed `enabled` (e.g. `"false"`) must // stay in the blob so driver schema validation flags it instead of the // fold silently repairing the config. if ( config === null || typeof config !== "object" || Array.isArray(config) || typeof (config as { readonly enabled?: unknown }).enabled !== "boolean" ) { providerInstances[instanceId] = instance; continue; } const { enabled: configEnabled, ...restConfig } = config as Record<string, unknown> & { readonly enabled: boolean; }; const resolved = instance.enabled === false || configEnabled === false ? false : (instance.enabled ?? configEnabled); changed = true; providerInstances[instanceId] = { ...instance, enabled: resolved, config: restConfig, } satisfies ProviderInstanceConfig; } if (!changed) { return settings; } return { ...settings, providerInstances: providerInstances as ServerSettings["providerInstances"], };}; const normalizeServerSettings = ( settings: ServerSettings,): Effect.Effect<ServerSettings, ServerSettingsError> => encodeServerSettings(settings).pipe( Effect.flatMap(decodeServerSettings), Effect.map(foldProviderInstanceEnabledFlags), Effect.mapError( (cause) => new ServerSettingsError({ settingsPath: "<memory>", operation: "normalize", cause, }), ), ); function providerEnvironmentSecretName(input: { readonly instanceId: string; readonly name: string;}): string { return `provider-env-${Buffer.from(input.instanceId, "utf8").toString("base64url")}-${Buffer.from(input.name, "utf8").toString("base64url")}`;} /** * On disk the hub key is replaced by this marker and the real value lives in * the secret store, mirroring provider environment secrets. A client that * sends the marker back means "keep what you have". */const USAGE_LIMIT_SOURCE_KEY_REDACTED = "\u2022\u2022\u2022\u2022\u2022\u2022"; function usageLimitSourceSecretName(sourceId: string): string { return `usage-limit-source-${Buffer.from(sourceId, "utf8").toString("base64url")}`;} function redactProviderEnvironmentVariable( variable: ProviderInstanceEnvironmentVariable,): ProviderInstanceEnvironmentVariable { if (!variable.sensitive) { const { valueRedacted: _omit, ...rest } = variable; return rest; } return { ...variable, value: "", ...(variable.value.length > 0 || variable.valueRedacted ? { valueRedacted: true } : {}), };} export function redactServerSettingsForClient(settings: ServerSettings): ServerSettings { const providerInstances = Object.fromEntries( Object.entries(settings.providerInstances).map(([instanceId, instance]) => [ instanceId, instance.environment ? { ...instance, environment: instance.environment.map(redactProviderEnvironmentVariable), } : instance, ]), ); // The hub key is a bearer secret; clients only need to know one is set. const usageLimitSources = Object.fromEntries( Object.entries(settings.usageLimitSources).map(([id, source]) => [ id, { ...source, managementKey: source.managementKey.length > 0 ? USAGE_LIMIT_SOURCE_KEY_REDACTED : "", }, ]), ); return { ...settings, providerInstances, usageLimitSources };} export class ServerSettingsService extends Context.Service< ServerSettingsService, { /** Start the settings runtime and attach file watching. */ readonly start: Effect.Effect<void, ServerSettingsError>; /** Await settings runtime readiness. */ readonly ready: Effect.Effect<void, ServerSettingsError>; /** Read the current settings. */ readonly getSettings: Effect.Effect<ServerSettings, ServerSettingsError>; /** Patch settings and persist. Returns the new full settings object. */ readonly updateSettings: ( patch: ServerSettingsPatch, ) => Effect.Effect<ServerSettings, ServerSettingsError>; /** Stream of settings change events. */ readonly streamChanges: Stream.Stream<ServerSettings>; /** * Acquire a settings change subscription synchronously in the current * fiber. Use this before reading a snapshot when changes between the * snapshot and a lazily started stream must not be lost. */ readonly subscribeChanges: Effect.Effect<Stream.Stream<ServerSettings>, never, Scope.Scope>; }>()("t3/serverSettings/ServerSettingsService") { /** @deprecated Import and use `layerTest` from this module. */ static readonly layerTest = (overrides: DeepPartial<ServerSettings> = {}) => layerTest(overrides);} const makeTest = (overrides: DeepPartial<ServerSettings> = {}) => Effect.gen(function* () { const { automaticGitFetchInterval, providerHealthRefreshInterval, ...overridesForMerge } = overrides; const merged = deepMerge(DEFAULT_SERVER_SETTINGS, overridesForMerge); const initialSettings = yield* normalizeServerSettings({ ...merged, ...(automaticGitFetchInterval !== undefined ? { automaticGitFetchInterval: automaticGitFetchInterval as Duration.Duration } : {}), ...(providerHealthRefreshInterval !== undefined ? { providerHealthRefreshInterval: providerHealthRefreshInterval as Duration.Duration } : {}), }); const currentSettingsRef = yield* Ref.make<ServerSettings>(initialSettings); return { start: Effect.void, ready: Effect.void, getSettings: Ref.get(currentSettingsRef).pipe(Effect.map(resolveTextGenerationProvider)), updateSettings: (patch) => Ref.get(currentSettingsRef).pipe( Effect.map((currentSettings) => applyServerSettingsPatch(currentSettings, patch)), Effect.flatMap(normalizeServerSettings), Effect.tap((nextSettings) => Ref.set(currentSettingsRef, nextSettings)), Effect.map(resolveTextGenerationProvider), ), streamChanges: Stream.empty, subscribeChanges: Effect.succeed(Stream.empty), } satisfies ServerSettingsService["Service"]; }); export const layerTest = (overrides: DeepPartial<ServerSettings> = {}) => Layer.effect(ServerSettingsService, makeTest(overrides)); const ServerSettingsJson = fromLenientJson(ServerSettings);const decodeServerSettingsJsonExit = Schema.decodeUnknownExit(ServerSettingsJson);const PersistedOptionalProviderSettings = Schema.Struct({ providers: Schema.optionalKey( Schema.Struct({ cursor: Schema.optionalKey(Schema.Struct({ enabled: Schema.optionalKey(Schema.Boolean) })), grok: Schema.optionalKey(Schema.Struct({ enabled: Schema.optionalKey(Schema.Boolean) })), opencode: Schema.optionalKey(Schema.Struct({ enabled: Schema.optionalKey(Schema.Boolean) })), }), ),});const decodePersistedOptionalProviderSettingsJsonExit = Schema.decodeUnknownExit( fromLenientJson(PersistedOptionalProviderSettings),); function restoreUsedProviders( settings: ServerSettings, persisted: typeof PersistedOptionalProviderSettings.Type, providerHistory: ReadonlyArray<{ readonly providerName: string; readonly providerInstanceId: string | null; }>,): ServerSettings { const usedProviders = new Set(providerHistory.map(({ providerName }) => providerName)); const usedProviderInstances = new Set( providerHistory.map( ({ providerName, providerInstanceId }) => providerInstanceId ?? providerName, ), ); const providerInstances = Object.fromEntries( Object.entries(settings.providerInstances).map(([instanceId, instance]) => [ instanceId, instance.enabled === undefined && (instance.driver === "cursor" || instance.driver === "grok" || instance.driver === "opencode") && usedProviderInstances.has(instanceId) ? { ...instance, enabled: true } : instance, ]), ); return { ...settings, providers: { ...settings.providers, cursor: { ...settings.providers.cursor, enabled: persisted.providers?.cursor?.enabled ?? usedProviders.has("cursor"), }, grok: { ...settings.providers.grok, enabled: persisted.providers?.grok?.enabled ?? usedProviders.has("grok"), }, opencode: { ...settings.providers.opencode, enabled: persisted.providers?.opencode?.enabled ?? usedProviders.has("opencode"), }, }, providerInstances, };} function resolveTextGenerationProvider(settings: ServerSettings): ServerSettings { return isModelSelectionProviderEnabled(settings, settings.textGenerationModelSelection) ? settings : fallbackTextGenerationProvider(settings);} function fallbackTextGenerationProvider(settings: ServerSettings): ServerSettings { // Same precedence as isModelSelectionProviderEnabled: an explicit provider // instance wins over the legacy providers map, which decodes to defaults // (codex enabled) when the Providers UI has only written providerInstances. const fallbackEntry = Object.entries(settings.providers).find(([driver, provider]) => { const instance = settings.providerInstances[ProviderInstanceId.make(driver)]; return instance === undefined ? provider.enabled : resolveProviderInstanceEnabled(instance); }); const fallback = fallbackEntry ? ProviderDriverKind.make(fallbackEntry[0]) : undefined; if (!fallback) { return settings; } return { ...settings, textGenerationModelSelection: { instanceId: ProviderInstanceId.make(fallback), model: DEFAULT_TEXT_GENERATION_MODEL_BY_PROVIDER[fallback] ?? DEFAULT_MODEL_BY_PROVIDER[fallback] ?? DEFAULT_TEXT_GENERATION_MODEL, } satisfies ModelSelection, };} // Values under these keys are compared as a whole — never stripped field-by-field.const ATOMIC_SETTINGS_KEYS: ReadonlySet<string> = new Set([ "backgroundActivity", "automaticGitFetchInterval", "providerHealthRefreshInterval", "sourceControlWriterModelSelection", "textGenerationModelSelection",]); // Preserve both enabled states because provider history cannot recover a new opt-in.const PERSISTED_SERVER_SETTINGS_DEFAULTS = { ...DEFAULT_SERVER_SETTINGS, providers: { ...DEFAULT_SERVER_SETTINGS.providers, cursor: { ...DEFAULT_SERVER_SETTINGS.providers.cursor, enabled: undefined }, grok: { ...DEFAULT_SERVER_SETTINGS.providers.grok, enabled: undefined }, opencode: { ...DEFAULT_SERVER_SETTINGS.providers.opencode, enabled: undefined }, },}; function stripDefaultServerSettings(current: unknown, defaults: unknown): unknown | undefined { if (Array.isArray(current) || Array.isArray(defaults)) { return Equal.equals(current, defaults) ? undefined : current; } if ( current !== null && defaults !== null && typeof current === "object" && typeof defaults === "object" ) { const currentRecord = current as Record<string, unknown>; const defaultsRecord = defaults as Record<string, unknown>; const next: Record<string, unknown> = {}; for (const key of Object.keys(currentRecord)) { if (ATOMIC_SETTINGS_KEYS.has(key)) { if (!Equal.equals(currentRecord[key], defaultsRecord[key])) { next[key] = currentRecord[key]; } } else { const stripped = stripDefaultServerSettings(currentRecord[key], defaultsRecord[key]); if (stripped !== undefined) { next[key] = stripped; } } } return Object.keys(next).length > 0 ? next : undefined; } return Object.is(current, defaults) ? undefined : current;} const make = Effect.gen(function* () { const { settingsPath } = yield* ServerConfig.ServerConfig; const fs = yield* FileSystem.FileSystem; const pathService = yield* Path.Path; const secretStore = yield* ServerSecretStore.ServerSecretStore; const sql = yield* SqlClient.SqlClient; const writeSemaphore = yield* Semaphore.make(1); const cacheKey = "settings" as const; const changesPubSub = yield* PubSub.unbounded<ServerSettings>(); const startedRef = yield* Ref.make(false); const startedDeferred = yield* Deferred.make<void, ServerSettingsError>(); const watcherScope = yield* Scope.make("sequential"); yield* Effect.addFinalizer(() => Scope.close(watcherScope, Exit.void)); const emitChange = (settings: ServerSettings) => PubSub.publish(changesPubSub, settings).pipe(Effect.asVoid); const readConfigExists = fs.exists(settingsPath).pipe( Effect.mapError( (cause) => new ServerSettingsError({ settingsPath, operation: "check-exists", cause, }), ), ); const readRawConfig = fs.readFileString(settingsPath).pipe( Effect.mapError( (cause) => new ServerSettingsError({ settingsPath, operation: "read-file", cause, }), ), ); const loadSettingsFromDisk = Effect.gen(function* () { let settings = DEFAULT_SERVER_SETTINGS; let persisted: typeof PersistedOptionalProviderSettings.Type = {}; if (yield* readConfigExists) { const raw = yield* readRawConfig; const decoded = decodeServerSettingsJsonExit(raw); const persistedSettings = decodePersistedOptionalProviderSettingsJsonExit(raw); if (persistedSettings._tag === "Success") { persisted = persistedSettings.value; } if (decoded._tag === "Failure" || persistedSettings._tag === "Failure") { const failure = decoded._tag === "Failure" ? decoded : persistedSettings; if (failure._tag === "Failure") { yield* Effect.logWarning("failed to parse settings.json, using defaults", { path: settingsPath, issues: Cause.pretty(failure.cause), cause: failure.cause, }); } } else { settings = decoded.value; } } const providerHistory = yield* sql<{ readonly providerName: string; readonly providerInstanceId: string | null; }>` SELECT DISTINCT provider_name AS "providerName", provider_instance_id AS "providerInstanceId" FROM projection_thread_sessions WHERE provider_name IN ('cursor', 'grok', 'opencode') UNION SELECT DISTINCT provider_name AS "providerName", provider_instance_id AS "providerInstanceId" FROM provider_session_runtime WHERE provider_name IN ('cursor', 'grok', 'opencode') `.pipe( Effect.mapError( (cause) => new ServerSettingsError({ settingsPath, operation: "read-provider-history", cause, }), ), ); return foldProviderInstanceEnabledFlags( restoreUsedProviders(settings, persisted, providerHistory), ); }); const settingsCache = yield* Cache.make<typeof cacheKey, ServerSettings, ServerSettingsError>({ capacity: 1, lookup: () => loadSettingsFromDisk,Lines 1–500 of 867. Load more
/** * ServerSettings - Server-authoritative settings service. * * Owns persistence, validation, and change notification of settings that affect * server-side behavior (binary paths, streaming mode, env mode, custom models, * text generation model selection). * * Follows the same pattern as `keybindings.ts`: JSON file + Cache + PubSub + * Semaphore + FileSystem.watch for concurrency and external edit detection. * * @module ServerSettings */ import { DEFAULT_TEXT_GENERATION_MODEL, DEFAULT_TEXT_GENERATION_MODEL_BY_PROVIDER, DEFAULT_MODEL_BY_PROVIDER, DEFAULT_SERVER_SETTINGS, type ModelSelection, type ProviderInstanceConfig, type ProviderInstanceEnvironmentVariable, type UsageLimitSourceConfig, ProviderDriverKind, ProviderInstanceId, resolveProviderInstanceEnabled, ServerSettings, ServerSettingsError, type ServerSettingsPatch, } from "@t3tools/contracts"; import * as Cache from "effect/Cache"; import * as Cause from "effect/Cause"; import * as Context from "effect/Context"; import * as Deferred from "effect/Deferred"; import * as Duration from "effect/Duration"; import * as Equal from "effect/Equal"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as PubSub from "effect/PubSub"; import * as Ref from "effect/Ref"; import * as Schema from "effect/Schema"; import * as Semaphore from "effect/Semaphore"; import * as Scope from "effect/Scope"; import * as Stream from "effect/Stream"; import * as SqlClient from "effect/unstable/sql/SqlClient"; import { writeFileStringAtomically } from "./atomicWrite.ts"; import * as ServerConfig from "./config.ts"; import { type DeepPartial, deepMerge } from "@t3tools/shared/Struct"; import { fromJsonStringPretty, fromLenientJson } from "@t3tools/shared/schemaJson"; import { applyServerSettingsPatch, isModelSelectionProviderEnabled, } from "@t3tools/shared/serverSettings"; import * as ServerSecretStore from "./auth/ServerSecretStore.ts"; export { resolveSourceControlWriterModelSelection } from "@t3tools/shared/serverSettings"; const encodeServerSettings = Schema.encodeEffect(ServerSettings); const encodeServerSettingsJson = Schema.encodeUnknownEffect(fromJsonStringPretty(ServerSettings)); const decodeServerSettings = Schema.decodeUnknownEffect(ServerSettings); const textEncoder = new TextEncoder(); const textDecoder = new TextDecoder(); /** * Fold the legacy in-config `enabled` flag into the envelope-level * `ProviderInstanceConfig.enabled` and strip it from the config blob, so * explicit provider instances carry exactly one enabled flag. Old settings * files can hold both flags with conflicting values; an explicit false on * either side wins so a user's disable is never silently undone. Runs on * every load and update — the file converges on the next write. */ const foldProviderInstanceEnabledFlags = (settings: ServerSettings): ServerSettings => { let changed = false; const providerInstances: Record<string, ProviderInstanceConfig> = {}; for (const [instanceId, instance] of Object.entries(settings.providerInstances)) { const config = instance.config; // Only fold boolean flags: a malformed `enabled` (e.g. `"false"`) must // stay in the blob so driver schema validation flags it instead of the // fold silently repairing the config. if ( config === null || typeof config !== "object" || Array.isArray(config) || typeof (config as { readonly enabled?: unknown }).enabled !== "boolean" ) { providerInstances[instanceId] = instance; continue; } const { enabled: configEnabled, ...restConfig } = config as Record<string, unknown> & { readonly enabled: boolean; }; const resolved = instance.enabled === false || configEnabled === false ? false : (instance.enabled ?? configEnabled); changed = true; providerInstances[instanceId] = { ...instance, enabled: resolved, config: restConfig, } satisfies ProviderInstanceConfig; } if (!changed) { return settings; } return { ...settings, providerInstances: providerInstances as ServerSettings["providerInstances"], }; }; const normalizeServerSettings = ( settings: ServerSettings, ): Effect.Effect<ServerSettings, ServerSettingsError> => encodeServerSettings(settings).pipe( Effect.flatMap(decodeServerSettings), Effect.map(foldProviderInstanceEnabledFlags), Effect.mapError( (cause) => new ServerSettingsError({ settingsPath: "<memory>", operation: "normalize", cause, }), ), ); function providerEnvironmentSecretName(input: { readonly instanceId: string; readonly name: string; }): string { return `provider-env-${Buffer.from(input.instanceId, "utf8").toString("base64url")}-${Buffer.from(input.name, "utf8").toString("base64url")}`; } /** * On disk the hub key is replaced by this marker and the real value lives in * the secret store, mirroring provider environment secrets. A client that * sends the marker back means "keep what you have". */ const USAGE_LIMIT_SOURCE_KEY_REDACTED = "\u2022\u2022\u2022\u2022\u2022\u2022"; function usageLimitSourceSecretName(sourceId: string): string { return `usage-limit-source-${Buffer.from(sourceId, "utf8").toString("base64url")}`; } function redactProviderEnvironmentVariable( variable: ProviderInstanceEnvironmentVariable, ): ProviderInstanceEnvironmentVariable { if (!variable.sensitive) { const { valueRedacted: _omit, ...rest } = variable; return rest; } return { ...variable, value: "", ...(variable.value.length > 0 || variable.valueRedacted ? { valueRedacted: true } : {}), }; } export function redactServerSettingsForClient(settings: ServerSettings): ServerSettings { const providerInstances = Object.fromEntries( Object.entries(settings.providerInstances).map(([instanceId, instance]) => [ instanceId, instance.environment ? { ...instance, environment: instance.environment.map(redactProviderEnvironmentVariable), } : instance, ]), ); // The hub key is a bearer secret; clients only need to know one is set. const usageLimitSources = Object.fromEntries( Object.entries(settings.usageLimitSources).map(([id, source]) => [ id, { ...source, managementKey: source.managementKey.length > 0 ? USAGE_LIMIT_SOURCE_KEY_REDACTED : "", }, ]), ); return { ...settings, providerInstances, usageLimitSources }; } export class ServerSettingsService extends Context.Service< ServerSettingsService, { /** Start the settings runtime and attach file watching. */ readonly start: Effect.Effect<void, ServerSettingsError>; /** Await settings runtime readiness. */ readonly ready: Effect.Effect<void, ServerSettingsError>; /** Read the current settings. */ readonly getSettings: Effect.Effect<ServerSettings, ServerSettingsError>; /** Patch settings and persist. Returns the new full settings object. */ readonly updateSettings: ( patch: ServerSettingsPatch, ) => Effect.Effect<ServerSettings, ServerSettingsError>; /** Stream of settings change events. */ readonly streamChanges: Stream.Stream<ServerSettings>; /** * Acquire a settings change subscription synchronously in the current * fiber. Use this before reading a snapshot when changes between the * snapshot and a lazily started stream must not be lost. */ readonly subscribeChanges: Effect.Effect<Stream.Stream<ServerSettings>, never, Scope.Scope>; } >()("t3/serverSettings/ServerSettingsService") { /** @deprecated Import and use `layerTest` from this module. */ static readonly layerTest = (overrides: DeepPartial<ServerSettings> = {}) => layerTest(overrides); } const makeTest = (overrides: DeepPartial<ServerSettings> = {}) => Effect.gen(function* () { const { automaticGitFetchInterval, providerHealthRefreshInterval, ...overridesForMerge } = overrides; const merged = deepMerge(DEFAULT_SERVER_SETTINGS, overridesForMerge); const initialSettings = yield* normalizeServerSettings({ ...merged, ...(automaticGitFetchInterval !== undefined ? { automaticGitFetchInterval: automaticGitFetchInterval as Duration.Duration } : {}), ...(providerHealthRefreshInterval !== undefined ? { providerHealthRefreshInterval: providerHealthRefreshInterval as Duration.Duration } : {}), }); const currentSettingsRef = yield* Ref.make<ServerSettings>(initialSettings); return { start: Effect.void, ready: Effect.void, getSettings: Ref.get(currentSettingsRef).pipe(Effect.map(resolveTextGenerationProvider)), updateSettings: (patch) => Ref.get(currentSettingsRef).pipe( Effect.map((currentSettings) => applyServerSettingsPatch(currentSettings, patch)), Effect.flatMap(normalizeServerSettings), Effect.tap((nextSettings) => Ref.set(currentSettingsRef, nextSettings)), Effect.map(resolveTextGenerationProvider), ), streamChanges: Stream.empty, subscribeChanges: Effect.succeed(Stream.empty), } satisfies ServerSettingsService["Service"]; }); export const layerTest = (overrides: DeepPartial<ServerSettings> = {}) => Layer.effect(ServerSettingsService, makeTest(overrides)); const ServerSettingsJson = fromLenientJson(ServerSettings); const decodeServerSettingsJsonExit = Schema.decodeUnknownExit(ServerSettingsJson); const PersistedOptionalProviderSettings = Schema.Struct({ providers: Schema.optionalKey( Schema.Struct({ cursor: Schema.optionalKey(Schema.Struct({ enabled: Schema.optionalKey(Schema.Boolean) })), grok: Schema.optionalKey(Schema.Struct({ enabled: Schema.optionalKey(Schema.Boolean) })), opencode: Schema.optionalKey(Schema.Struct({ enabled: Schema.optionalKey(Schema.Boolean) })), }), ), }); const decodePersistedOptionalProviderSettingsJsonExit = Schema.decodeUnknownExit( fromLenientJson(PersistedOptionalProviderSettings), ); function restoreUsedProviders( settings: ServerSettings, persisted: typeof PersistedOptionalProviderSettings.Type, providerHistory: ReadonlyArray<{ readonly providerName: string; readonly providerInstanceId: string | null; }>, ): ServerSettings { const usedProviders = new Set(providerHistory.map(({ providerName }) => providerName)); const usedProviderInstances = new Set( providerHistory.map( ({ providerName, providerInstanceId }) => providerInstanceId ?? providerName, ), ); const providerInstances = Object.fromEntries( Object.entries(settings.providerInstances).map(([instanceId, instance]) => [ instanceId, instance.enabled === undefined && (instance.driver === "cursor" || instance.driver === "grok" || instance.driver === "opencode") && usedProviderInstances.has(instanceId) ? { ...instance, enabled: true } : instance, ]), ); return { ...settings, providers: { ...settings.providers, cursor: { ...settings.providers.cursor, enabled: persisted.providers?.cursor?.enabled ?? usedProviders.has("cursor"), }, grok: { ...settings.providers.grok, enabled: persisted.providers?.grok?.enabled ?? usedProviders.has("grok"), }, opencode: { ...settings.providers.opencode, enabled: persisted.providers?.opencode?.enabled ?? usedProviders.has("opencode"), }, }, providerInstances, }; } function resolveTextGenerationProvider(settings: ServerSettings): ServerSettings { return isModelSelectionProviderEnabled(settings, settings.textGenerationModelSelection) ? settings : fallbackTextGenerationProvider(settings); } function fallbackTextGenerationProvider(settings: ServerSettings): ServerSettings { // Same precedence as isModelSelectionProviderEnabled: an explicit provider // instance wins over the legacy providers map, which decodes to defaults // (codex enabled) when the Providers UI has only written providerInstances. const fallbackEntry = Object.entries(settings.providers).find(([driver, provider]) => { const instance = settings.providerInstances[ProviderInstanceId.make(driver)]; return instance === undefined ? provider.enabled : resolveProviderInstanceEnabled(instance); }); const fallback = fallbackEntry ? ProviderDriverKind.make(fallbackEntry[0]) : undefined; if (!fallback) { return settings; } return { ...settings, textGenerationModelSelection: { instanceId: ProviderInstanceId.make(fallback), model: DEFAULT_TEXT_GENERATION_MODEL_BY_PROVIDER[fallback] ?? DEFAULT_MODEL_BY_PROVIDER[fallback] ?? DEFAULT_TEXT_GENERATION_MODEL, } satisfies ModelSelection, }; } // Values under these keys are compared as a whole — never stripped field-by-field. const ATOMIC_SETTINGS_KEYS: ReadonlySet<string> = new Set([ "backgroundActivity", "automaticGitFetchInterval", "providerHealthRefreshInterval", "sourceControlWriterModelSelection", "textGenerationModelSelection", ]); // Preserve both enabled states because provider history cannot recover a new opt-in. const PERSISTED_SERVER_SETTINGS_DEFAULTS = { ...DEFAULT_SERVER_SETTINGS, providers: { ...DEFAULT_SERVER_SETTINGS.providers, cursor: { ...DEFAULT_SERVER_SETTINGS.providers.cursor, enabled: undefined }, grok: { ...DEFAULT_SERVER_SETTINGS.providers.grok, enabled: undefined }, opencode: { ...DEFAULT_SERVER_SETTINGS.providers.opencode, enabled: undefined }, }, }; function stripDefaultServerSettings(current: unknown, defaults: unknown): unknown | undefined { if (Array.isArray(current) || Array.isArray(defaults)) { return Equal.equals(current, defaults) ? undefined : current; } if ( current !== null && defaults !== null && typeof current === "object" && typeof defaults === "object" ) { const currentRecord = current as Record<string, unknown>; const defaultsRecord = defaults as Record<string, unknown>; const next: Record<string, unknown> = {}; for (const key of Object.keys(currentRecord)) { if (ATOMIC_SETTINGS_KEYS.has(key)) { if (!Equal.equals(currentRecord[key], defaultsRecord[key])) { next[key] = currentRecord[key]; } } else { const stripped = stripDefaultServerSettings(currentRecord[key], defaultsRecord[key]); if (stripped !== undefined) { next[key] = stripped; } } } return Object.keys(next).length > 0 ? next : undefined; } return Object.is(current, defaults) ? undefined : current; } const make = Effect.gen(function* () { const { settingsPath } = yield* ServerConfig.ServerConfig; const fs = yield* FileSystem.FileSystem; const pathService = yield* Path.Path; const secretStore = yield* ServerSecretStore.ServerSecretStore; const sql = yield* SqlClient.SqlClient; const writeSemaphore = yield* Semaphore.make(1); const cacheKey = "settings" as const; const changesPubSub = yield* PubSub.unbounded<ServerSettings>(); const startedRef = yield* Ref.make(false); const startedDeferred = yield* Deferred.make<void, ServerSettingsError>(); const watcherScope = yield* Scope.make("sequential"); yield* Effect.addFinalizer(() => Scope.close(watcherScope, Exit.void)); const emitChange = (settings: ServerSettings) => PubSub.publish(changesPubSub, settings).pipe(Effect.asVoid); const readConfigExists = fs.exists(settingsPath).pipe( Effect.mapError( (cause) => new ServerSettingsError({ settingsPath, operation: "check-exists", cause, }), ), ); const readRawConfig = fs.readFileString(settingsPath).pipe( Effect.mapError( (cause) => new ServerSettingsError({ settingsPath, operation: "read-file", cause, }), ), ); const loadSettingsFromDisk = Effect.gen(function* () { let settings = DEFAULT_SERVER_SETTINGS; let persisted: typeof PersistedOptionalProviderSettings.Type = {}; if (yield* readConfigExists) { const raw = yield* readRawConfig; const decoded = decodeServerSettingsJsonExit(raw); const persistedSettings = decodePersistedOptionalProviderSettingsJsonExit(raw); if (persistedSettings._tag === "Success") { persisted = persistedSettings.value; } if (decoded._tag === "Failure" || persistedSettings._tag === "Failure") { const failure = decoded._tag === "Failure" ? decoded : persistedSettings; if (failure._tag === "Failure") { yield* Effect.logWarning("failed to parse settings.json, using defaults", { path: settingsPath, issues: Cause.pretty(failure.cause), cause: failure.cause, }); } } else { settings = decoded.value; } } const providerHistory = yield* sql<{ readonly providerName: string; readonly providerInstanceId: string | null; }>` SELECT DISTINCT provider_name AS "providerName", provider_instance_id AS "providerInstanceId" FROM projection_thread_sessions WHERE provider_name IN ('cursor', 'grok', 'opencode') UNION SELECT DISTINCT provider_name AS "providerName", provider_instance_id AS "providerInstanceId" FROM provider_session_runtime WHERE provider_name IN ('cursor', 'grok', 'opencode') `.pipe( Effect.mapError( (cause) => new ServerSettingsError({ settingsPath, operation: "read-provider-history", cause, }), ), ); return foldProviderInstanceEnabledFlags( restoreUsedProviders(settings, persisted, providerHistory), ); }); const settingsCache = yield* Cache.make<typeof cacheKey, ServerSettings, ServerSettingsError>({ capacity: 1, lookup: () => loadSettingsFromDisk,