authorgravatar for 45952970+repparw@users.noreply.github.comUlises Britos <45952970+repparw@users.noreply.github.com> 2026-06-19 15:07:23-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-19 11:07:23-07:00
log5d4e2fae012b74cfb323cc2cb45687caf93997cc
treeff7a70b3799b9ab0fd1eb2fc3ba5c0467b219bd6
parent3e01c4bc572ea6a938c9a47143c914ff042ea41a
signature Signed by PGP key B5690EEEBB952194

feat: allow disabling provider update checks (#3130)

Co-authored-by: codex <codex@users.noreply.github.com>

14 files changed, 239 insertions(+), 57 deletions(-)

apps/server/src/provider/Drivers/ClaudeDriver.ts+19-9
......@@ -20,12 +20,12 @@ import * as Effect from "effect/Effect";
2020import * as FileSystem from "effect/FileSystem";
2121import * as Path from "effect/Path";
2222import * as Schema from "effect/Schema";
23import * as Stream from "effect/Stream";
2423import { HttpClient } from "effect/unstable/http";
2524import { ChildProcessSpawner } from "effect/unstable/process";
2625
2726import { makeClaudeTextGeneration } from "../../textGeneration/ClaudeTextGeneration.ts";
2827import { ServerConfig } from "../../config.ts";
28import { ServerSettingsService } from "../../serverSettings.ts";
2929import { ProviderDriverError } from "../Errors.ts";
3030import { makeClaudeAdapter } from "../Layers/ClaudeAdapter.ts";
3131import {
......@@ -48,6 +48,11 @@ import {
4848 normalizeCommandPath,
4949 resolveProviderMaintenanceCapabilitiesEffect,
5050} from "../providerMaintenance.ts";
51import {
52 haveProviderSnapshotSettingsChanged,
53 makeProviderSnapshotSettingsSource,
54 type ProviderSnapshotSettings,
55} from "../providerUpdateSettings.ts";
5156import { makeClaudeCapabilitiesCacheKey, makeClaudeContinuationGroupKey } from "./ClaudeHome.ts";
5257const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings);
5358
......@@ -83,7 +88,8 @@ export type ClaudeDriverEnv =
8388 | HttpClient.HttpClient
8489 | Path.Path
8590 | ProviderEventLoggers
86 | ServerConfig;
91 | ServerConfig
92 | ServerSettingsService;
8793
8894const withInstanceIdentity =
8995 (input: {
......@@ -114,6 +120,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
114120 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
115121 const path = yield* Path.Path;
116122 const httpClient = yield* HttpClient.HttpClient;
123 const serverSettings = yield* ServerSettingsService;
117124 const eventLoggers = yield* ProviderEventLoggers;
118125 const processEnv = mergeProviderInstanceEnvironment(environment);
119126 const fallbackContinuationIdentity = defaultProviderContinuationIdentity({
......@@ -163,16 +170,19 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
163170 Effect.provideService(Path.Path, path),
164171 );
165172
166 const snapshot = yield* makeManagedServerProvider<ClaudeSettings>({
173 const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
174 const snapshot = yield* makeManagedServerProvider<ProviderSnapshotSettings<ClaudeSettings>>({
167175 maintenanceCapabilities,
168 getSettings: Effect.succeed(effectiveConfig),
169 streamSettings: Stream.never,
170 haveSettingsChanged: () => false,
176 getSettings: snapshotSettings.getSettings,
177 streamSettings: snapshotSettings.streamSettings,
178 haveSettingsChanged: haveProviderSnapshotSettingsChanged,
171179 initialSnapshot: (settings) =>
172 makePendingClaudeProvider(settings).pipe(Effect.map(stampIdentity)),
180 makePendingClaudeProvider(settings.provider).pipe(Effect.map(stampIdentity)),
173181 checkProvider,
174 enrichSnapshot: ({ snapshot, publishSnapshot }) =>
175 enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities).pipe(
182 enrichSnapshot: ({ settings, snapshot, publishSnapshot }) =>
183 enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities, {
184 enableProviderUpdateChecks: settings.enableProviderUpdateChecks,
185 }).pipe(
176186 Effect.provideService(HttpClient.HttpClient, httpClient),
177187 Effect.flatMap((enrichedSnapshot) => publishSnapshot(enrichedSnapshot)),
178188 ),
apps/server/src/provider/Drivers/CodexDriver.ts+19-9
......@@ -28,12 +28,12 @@ import * as Effect from "effect/Effect";
2828import * as FileSystem from "effect/FileSystem";
2929import * as Path from "effect/Path";
3030import * as Schema from "effect/Schema";
31import * as Stream from "effect/Stream";
3231import { HttpClient } from "effect/unstable/http";
3332import { ChildProcessSpawner } from "effect/unstable/process";
3433
3534import { makeCodexTextGeneration } from "../../textGeneration/CodexTextGeneration.ts";
3635import { ServerConfig } from "../../config.ts";
36import { ServerSettingsService } from "../../serverSettings.ts";
3737import { ProviderDriverError } from "../Errors.ts";
3838import { makeCodexAdapter } from "../Layers/CodexAdapter.ts";
3939import { checkCodexProviderStatus, makePendingCodexProvider } from "../Layers/CodexProvider.ts";
......@@ -47,6 +47,11 @@ import {
4747 makePackageManagedProviderMaintenanceResolver,
4848 resolveProviderMaintenanceCapabilitiesEffect,
4949} from "../providerMaintenance.ts";
50import {
51 haveProviderSnapshotSettingsChanged,
52 makeProviderSnapshotSettingsSource,
53 type ProviderSnapshotSettings,
54} from "../providerUpdateSettings.ts";
5055import {
5156 codexContinuationIdentity,
5257 materializeCodexShadowHome,
......@@ -75,7 +80,8 @@ export type CodexDriverEnv =
7580 | HttpClient.HttpClient
7681 | Path.Path
7782 | ProviderEventLoggers
78 | ServerConfig;
83 | ServerConfig
84 | ServerSettingsService;
7985
8086/**
8187 * Stamp instance identity onto a `ServerProvider` snapshot produced by the
......@@ -111,6 +117,7 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {
111117 Effect.gen(function* () {
112118 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
113119 const httpClient = yield* HttpClient.HttpClient;
120 const serverSettings = yield* ServerSettingsService;
114121 const eventLoggers = yield* ProviderEventLoggers;
115122 const processEnv = mergeProviderInstanceEnvironment(environment);
116123 const homeLayout = yield* resolveCodexHomeLayout(config);
......@@ -163,16 +170,19 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {
163170 Effect.map(stampIdentity),
164171 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
165172 );
166 const snapshot = yield* makeManagedServerProvider<CodexSettings>({
173 const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
174 const snapshot = yield* makeManagedServerProvider<ProviderSnapshotSettings<CodexSettings>>({
167175 maintenanceCapabilities,
168 getSettings: Effect.succeed(effectiveConfig),
169 streamSettings: Stream.never,
170 haveSettingsChanged: () => false,
176 getSettings: snapshotSettings.getSettings,
177 streamSettings: snapshotSettings.streamSettings,
178 haveSettingsChanged: haveProviderSnapshotSettingsChanged,
171179 initialSnapshot: (settings) =>
172 makePendingCodexProvider(settings).pipe(Effect.map(stampIdentity)),
180 makePendingCodexProvider(settings.provider).pipe(Effect.map(stampIdentity)),
173181 checkProvider,
174 enrichSnapshot: ({ snapshot, publishSnapshot }) =>
175 enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities).pipe(
182 enrichSnapshot: ({ settings, snapshot, publishSnapshot }) =>
183 enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities, {
184 enableProviderUpdateChecks: settings.enableProviderUpdateChecks,
185 }).pipe(
176186 Effect.provideService(HttpClient.HttpClient, httpClient),
177187 Effect.flatMap((enrichedSnapshot) => publishSnapshot(enrichedSnapshot)),
178188 ),
apps/server/src/provider/Drivers/CursorDriver.ts+17-8
......@@ -18,11 +18,11 @@ import * as Effect from "effect/Effect";
1818import * as FileSystem from "effect/FileSystem";
1919import * as Path from "effect/Path";
2020import * as Schema from "effect/Schema";
21import * as Stream from "effect/Stream";
2221import { HttpClient } from "effect/unstable/http";
2322import { ChildProcessSpawner } from "effect/unstable/process";
2423
2524import { ServerConfig } from "../../config.ts";
25import { ServerSettingsService } from "../../serverSettings.ts";
2626import { makeCursorTextGeneration } from "../../textGeneration/CursorTextGeneration.ts";
2727import { ProviderDriverError } from "../Errors.ts";
2828import { makeCursorAdapter } from "../Layers/CursorAdapter.ts";
......@@ -45,6 +45,11 @@ import {
4545 makeStaticProviderMaintenanceResolver,
4646 resolveProviderMaintenanceCapabilitiesEffect,
4747} from "../providerMaintenance.ts";
48import {
49 haveProviderSnapshotSettingsChanged,
50 makeProviderSnapshotSettingsSource,
51 type ProviderSnapshotSettings,
52} from "../providerUpdateSettings.ts";
4853const decodeCursorSettings = Schema.decodeSync(CursorSettings);
4954
5055const DRIVER_KIND = ProviderDriverKind.make("cursor");
......@@ -66,7 +71,8 @@ export type CursorDriverEnv =
6671 | HttpClient.HttpClient
6772 | Path.Path
6873 | ProviderEventLoggers
69 | ServerConfig;
74 | ServerConfig
75 | ServerSettingsService;
7076
7177const withInstanceIdentity =
7278 (input: {
......@@ -98,6 +104,7 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {
98104 const fileSystem = yield* FileSystem.FileSystem;
99105 const path = yield* Path.Path;
100106 const httpClient = yield* HttpClient.HttpClient;
107 const serverSettings = yield* ServerSettingsService;
101108 const eventLoggers = yield* ProviderEventLoggers;
102109 const processEnv = mergeProviderInstanceEnvironment(environment);
103110 const continuationIdentity = defaultProviderContinuationIdentity({
......@@ -130,21 +137,23 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {
130137 Effect.provideService(Path.Path, path),
131138 );
132139
133 const snapshot = yield* makeManagedServerProvider<CursorSettings>({
140 const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
141 const snapshot = yield* makeManagedServerProvider<ProviderSnapshotSettings<CursorSettings>>({
134142 maintenanceCapabilities,
135 getSettings: Effect.succeed(effectiveConfig),
136 streamSettings: Stream.never,
137 haveSettingsChanged: () => false,
143 getSettings: snapshotSettings.getSettings,
144 streamSettings: snapshotSettings.streamSettings,
145 haveSettingsChanged: haveProviderSnapshotSettingsChanged,
138146 initialSnapshot: (settings) =>
139 buildInitialCursorProviderSnapshot(settings).pipe(Effect.map(stampIdentity)),
147 buildInitialCursorProviderSnapshot(settings.provider).pipe(Effect.map(stampIdentity)),
140148 checkProvider,
141149 // Model catalog and capabilities come exclusively from Cursor's
142150 // list_available_models extension method during provider checks.
143151 enrichSnapshot: ({ settings, snapshot: currentSnapshot, publishSnapshot }) =>
144152 enrichCursorSnapshot({
145 settings,
153 settings: settings.provider,
146154 snapshot: currentSnapshot,
147155 maintenanceCapabilities,
156 enableProviderUpdateChecks: settings.enableProviderUpdateChecks,
148157 publishSnapshot,
149158 stampIdentity,
150159 httpClient,
apps/server/src/provider/Drivers/GrokDriver.ts+17-8
......@@ -5,11 +5,11 @@ import * as Effect from "effect/Effect";
55import * as FileSystem from "effect/FileSystem";
66import * as Path from "effect/Path";
77import * as Schema from "effect/Schema";
8import * as Stream from "effect/Stream";
98import { HttpClient } from "effect/unstable/http";
109import { ChildProcessSpawner } from "effect/unstable/process";
1110
1211import { ServerConfig } from "../../config.ts";
12import { ServerSettingsService } from "../../serverSettings.ts";
1313import { makeGrokTextGeneration } from "../../textGeneration/GrokTextGeneration.ts";
1414import { ProviderDriverError } from "../Errors.ts";
1515import { makeGrokAdapter } from "../Layers/GrokAdapter.ts";
......@@ -32,6 +32,11 @@ import {
3232 makeStaticProviderMaintenanceResolver,
3333 resolveProviderMaintenanceCapabilitiesEffect,
3434} from "../providerMaintenance.ts";
35import {
36 haveProviderSnapshotSettingsChanged,
37 makeProviderSnapshotSettingsSource,
38 type ProviderSnapshotSettings,
39} from "../providerUpdateSettings.ts";
3540const decodeGrokSettings = Schema.decodeSync(GrokSettings);
3641
3742const DRIVER_KIND = ProviderDriverKind.make("grok");
......@@ -50,7 +55,8 @@ export type GrokDriverEnv =
5055 | HttpClient.HttpClient
5156 | Path.Path
5257 | ProviderEventLoggers
53 | ServerConfig;
58 | ServerConfig
59 | ServerSettingsService;
5460
5561const withInstanceIdentity =
5662 (input: {
......@@ -80,6 +86,7 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = {
8086 Effect.gen(function* () {
8187 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
8288 const httpClient = yield* HttpClient.HttpClient;
89 const serverSettings = yield* ServerSettingsService;
8390 const eventLoggers = yield* ProviderEventLoggers;
8491 const processEnv = mergeProviderInstanceEnvironment(environment);
8592 const continuationIdentity = defaultProviderContinuationIdentity({
......@@ -110,18 +117,20 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = {
110117 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
111118 );
112119
113 const snapshot = yield* makeManagedServerProvider<GrokSettings>({
120 const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
121 const snapshot = yield* makeManagedServerProvider<ProviderSnapshotSettings<GrokSettings>>({
114122 maintenanceCapabilities,
115 getSettings: Effect.succeed(effectiveConfig),
116 streamSettings: Stream.never,
117 haveSettingsChanged: () => false,
123 getSettings: snapshotSettings.getSettings,
124 streamSettings: snapshotSettings.streamSettings,
125 haveSettingsChanged: haveProviderSnapshotSettingsChanged,
118126 initialSnapshot: (settings) =>
119 buildInitialGrokProviderSnapshot(settings).pipe(Effect.map(stampIdentity)),
127 buildInitialGrokProviderSnapshot(settings.provider).pipe(Effect.map(stampIdentity)),
120128 checkProvider,
121 enrichSnapshot: ({ snapshot: currentSnapshot, publishSnapshot }) =>
129 enrichSnapshot: ({ settings, snapshot: currentSnapshot, publishSnapshot }) =>
122130 enrichGrokSnapshot({
123131 snapshot: currentSnapshot,
124132 maintenanceCapabilities,
133 enableProviderUpdateChecks: settings.enableProviderUpdateChecks,
125134 publishSnapshot,
126135 httpClient,
127136 }),
apps/server/src/provider/Drivers/OpenCodeDriver.ts+29-17
......@@ -19,12 +19,12 @@ import * as Effect from "effect/Effect";
1919import * as FileSystem from "effect/FileSystem";
2020import * as Path from "effect/Path";
2121import * as Schema from "effect/Schema";
22import * as Stream from "effect/Stream";
2322import { HttpClient } from "effect/unstable/http";
2423import { ChildProcessSpawner } from "effect/unstable/process";
2524
2625import { makeOpenCodeTextGeneration } from "../../textGeneration/OpenCodeTextGeneration.ts";
2726import { ServerConfig } from "../../config.ts";
27import { ServerSettingsService } from "../../serverSettings.ts";
2828import { ProviderDriverError } from "../Errors.ts";
2929import { makeOpenCodeAdapter } from "../Layers/OpenCodeAdapter.ts";
3030import {
......@@ -47,6 +47,11 @@ import {
4747 normalizeCommandPath,
4848 resolveProviderMaintenanceCapabilitiesEffect,
4949} from "../providerMaintenance.ts";
50import {
51 haveProviderSnapshotSettingsChanged,
52 makeProviderSnapshotSettingsSource,
53 type ProviderSnapshotSettings,
54} from "../providerUpdateSettings.ts";
5055const decodeOpenCodeSettings = Schema.decodeSync(OpenCodeSettings);
5156
5257const DRIVER_KIND = ProviderDriverKind.make("opencode");
......@@ -80,7 +85,8 @@ export type OpenCodeDriverEnv =
8085 | OpenCodeRuntime
8186 | Path.Path
8287 | ProviderEventLoggers
83 | ServerConfig;
88 | ServerConfig
89 | ServerSettingsService;
8490
8591const withInstanceIdentity =
8692 (input: {
......@@ -111,6 +117,7 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv>
111117 const openCodeRuntime = yield* OpenCodeRuntime;
112118 const serverConfig = yield* ServerConfig;
113119 const httpClient = yield* HttpClient.HttpClient;
120 const serverSettings = yield* ServerSettingsService;
114121 const eventLoggers = yield* ProviderEventLoggers;
115122 const processEnv = mergeProviderInstanceEnvironment(environment);
116123 const continuationIdentity = defaultProviderContinuationIdentity({
......@@ -142,21 +149,26 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv>
142149 processEnv,
143150 ).pipe(Effect.map(stampIdentity), Effect.provideService(OpenCodeRuntime, openCodeRuntime));
144151
145 const snapshot = yield* makeManagedServerProvider<OpenCodeSettings>({
146 maintenanceCapabilities,
147 getSettings: Effect.succeed(effectiveConfig),
148 streamSettings: Stream.never,
149 haveSettingsChanged: () => false,
150 initialSnapshot: (settings) =>
151 makePendingOpenCodeProvider(settings).pipe(Effect.map(stampIdentity)),
152 checkProvider,
153 enrichSnapshot: ({ snapshot, publishSnapshot }) =>
154 enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities).pipe(
155 Effect.provideService(HttpClient.HttpClient, httpClient),
156 Effect.flatMap((enrichedSnapshot) => publishSnapshot(enrichedSnapshot)),
157 ),
158 refreshInterval: SNAPSHOT_REFRESH_INTERVAL,
159 }).pipe(
152 const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
153 const snapshot = yield* makeManagedServerProvider<ProviderSnapshotSettings<OpenCodeSettings>>(
154 {
155 maintenanceCapabilities,
156 getSettings: snapshotSettings.getSettings,
157 streamSettings: snapshotSettings.streamSettings,
158 haveSettingsChanged: haveProviderSnapshotSettingsChanged,
159 initialSnapshot: (settings) =>
160 makePendingOpenCodeProvider(settings.provider).pipe(Effect.map(stampIdentity)),
161 checkProvider,
162 enrichSnapshot: ({ settings, snapshot, publishSnapshot }) =>
163 enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities, {
164 enableProviderUpdateChecks: settings.enableProviderUpdateChecks,
165 }).pipe(
166 Effect.provideService(HttpClient.HttpClient, httpClient),
167 Effect.flatMap((enrichedSnapshot) => publishSnapshot(enrichedSnapshot)),
168 ),
169 refreshInterval: SNAPSHOT_REFRESH_INTERVAL,
170 },
171 ).pipe(
160172 Effect.mapError(
161173 (cause) =>
162174 new ProviderDriverError({
apps/server/src/provider/Layers/CursorProvider.ts+4-1
......@@ -1106,6 +1106,7 @@ export const enrichCursorSnapshot = (input: {
11061106 readonly settings: CursorSettings;
11071107 readonly snapshot: ServerProvider;
11081108 readonly maintenanceCapabilities: ProviderMaintenanceCapabilities;
1109 readonly enableProviderUpdateChecks?: boolean;
11091110 readonly publishSnapshot: (snapshot: ServerProvider) => Effect.Effect<void>;
11101111 readonly stampIdentity?: (snapshot: ServerProvider) => ServerProvider;
11111112 readonly httpClient: HttpClient.HttpClient;
......@@ -1117,7 +1118,9 @@ export const enrichCursorSnapshot = (input: {
11171118 return Effect.void;
11181119 }
11191120
1120 return enrichProviderSnapshotWithVersionAdvisory(snapshot, input.maintenanceCapabilities).pipe(
1121 return enrichProviderSnapshotWithVersionAdvisory(snapshot, input.maintenanceCapabilities, {
1122 enableProviderUpdateChecks: input.enableProviderUpdateChecks,
1123 }).pipe(
11211124 Effect.provideService(HttpClient.HttpClient, input.httpClient),
11221125 Effect.flatMap((enrichedSnapshot) =>
11231126 publishSnapshot(stampIdentity(enrichedSnapshot)).pipe(Effect.as(enrichedSnapshot)),
apps/server/src/provider/Layers/GrokProvider.ts+4-1
......@@ -311,12 +311,15 @@ export const checkGrokProviderStatus = Effect.fn("checkGrokProviderStatus")(func
311311export const enrichGrokSnapshot = (input: {
312312 readonly snapshot: ServerProvider;
313313 readonly maintenanceCapabilities: ProviderMaintenanceCapabilities;
314 readonly enableProviderUpdateChecks?: boolean;
314315 readonly publishSnapshot: (snapshot: ServerProvider) => Effect.Effect<void>;
315316 readonly httpClient: HttpClient.HttpClient;
316317}): Effect.Effect<void> => {
317318 const { snapshot, publishSnapshot } = input;
318319
319 return enrichProviderSnapshotWithVersionAdvisory(snapshot, input.maintenanceCapabilities).pipe(
320 return enrichProviderSnapshotWithVersionAdvisory(snapshot, input.maintenanceCapabilities, {
321 enableProviderUpdateChecks: input.enableProviderUpdateChecks,
322 }).pipe(
320323 Effect.provideService(HttpClient.HttpClient, input.httpClient),
321324 Effect.flatMap((enrichedSnapshot) => publishSnapshot(enrichedSnapshot)),
322325 Effect.catchCause((cause) =>
apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts+3
......@@ -39,6 +39,7 @@ import * as Layer from "effect/Layer";
3939import { HttpClient, HttpClientResponse } from "effect/unstable/http";
4040
4141import { ServerConfig } from "../../config.ts";
42import { ServerSettingsService } from "../../serverSettings.ts";
4243import { ClaudeDriver } from "../Drivers/ClaudeDriver.ts";
4344import { CodexDriver } from "../Drivers/CodexDriver.ts";
4445import { CursorDriver } from "../Drivers/CursorDriver.ts";
......@@ -107,6 +108,7 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => {
107108 prefix: "provider-instance-registry-test",
108109 }).pipe(
109110 Layer.provideMerge(NodeServices.layer),
111 Layer.provideMerge(ServerSettingsService.layerTest()),
110112 Layer.provideMerge(TestHttpClientLive),
111113 Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)),
112114 );
......@@ -244,6 +246,7 @@ describe("ProviderInstanceRegistryLive — all drivers slice", () => {
244246 prefix: "provider-instance-registry-all-drivers-test",
245247 }).pipe(
246248 Layer.provideMerge(infraLayer),
249 Layer.provideMerge(ServerSettingsService.layerTest()),
247250 Layer.provideMerge(TestHttpClientLive),
248251 Layer.provideMerge(Layer.succeed(ProviderEventLoggers, NoOpProviderEventLoggers)),
249252 );
apps/server/src/provider/makeManagedServerProvider.ts+1-1
......@@ -21,7 +21,7 @@ export const makeManagedServerProvider = Effect.fn("makeManagedServerProvider")(
2121 Settings,
2222>(input: {
2323 readonly maintenanceCapabilities: ServerProviderShape["maintenanceCapabilities"];
24 readonly getSettings: Effect.Effect<Settings>;
24 readonly getSettings: Effect.Effect<Settings, ServerSettingsError>;
2525 readonly streamSettings: Stream.Stream<Settings>;
2626 readonly haveSettingsChanged: (previous: Settings, next: Settings) => boolean;
2727 readonly initialSnapshot: (settings: Settings) => Effect.Effect<ServerProvider>;
apps/server/src/provider/providerMaintenance.test.ts+41-1
......@@ -4,13 +4,14 @@ import { chmodSync, mkdirSync, symlinkSync, writeFileSync } from "node:fs";
44import * as NodeServices from "@effect/platform-node/NodeServices";
55import * as NodeOS from "node:os";
66import path from "node:path";
7import { ProviderDriverKind } from "@t3tools/contracts";
7import { ProviderDriverKind, ProviderInstanceId, type ServerProvider } from "@t3tools/contracts";
88import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
99import * as Crypto from "effect/Crypto";
1010import * as Effect from "effect/Effect";
1111import { HttpClient } from "effect/unstable/http";
1212import {
1313 createProviderVersionAdvisory,
14 enrichProviderSnapshotWithVersionAdvisory,
1415 makePackageManagedProviderMaintenanceResolver,
1516 makeProviderMaintenanceCapabilities,
1617 makeStaticProviderMaintenanceResolver,
......@@ -67,6 +68,19 @@ const staticToolUpdate = makeStaticProviderMaintenanceResolver(
6768 updateLockKey: "static-tool",
6869 }),
6970);
71const installedPackageToolProvider: ServerProvider = {
72 instanceId: ProviderInstanceId.make("packageTool"),
73 driver: driver("packageTool"),
74 enabled: true,
75 installed: true,
76 version: "1.0.0",
77 status: "ready",
78 auth: { status: "authenticated" },
79 checkedAt: "2026-04-10T00:00:00.000Z",
80 models: [],
81 slashCommands: [],
82 skills: [],
83};
7084
7185it.layer(NodeServices.layer)("providerMaintenance", (it) => {
7286 it.effect("reads cached versions through the injectable cache reference", () =>
......@@ -95,6 +109,32 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => {
95109 ),
96110 );
97111
112 it.effect("does not fetch latest provider versions when update checks are disabled", () =>
113 enrichProviderSnapshotWithVersionAdvisory(
114 installedPackageToolProvider,
115 packageToolUpdate.resolve(),
116 {
117 enableProviderUpdateChecks: false,
118 },
119 ).pipe(
120 Effect.provideService(ProviderVersionCache, new Map()),
121 Effect.provideService(
122 HttpClient.HttpClient,
123 HttpClient.make(() =>
124 Effect.die("disabled provider update checks should not make an HTTP request"),
125 ),
126 ),
127 Effect.map((provider) => {
128 expect(provider.versionAdvisory).toMatchObject({
129 status: "unknown",
130 currentVersion: "1.0.0",
131 latestVersion: null,
132 checkedAt: "2026-04-10T00:00:00.000Z",
133 });
134 }),
135 ),
136 );
137
98138 it("marks providers with unknown current versions as unknown", () => {
99139 expect(
100140 createProviderVersionAdvisory({
apps/server/src/provider/providerMaintenance.ts+13-2
......@@ -468,10 +468,21 @@ export const resolveLatestProviderVersion = Effect.fn("resolveLatestProviderVers
468468
469469export const enrichProviderSnapshotWithVersionAdvisory = Effect.fn(
470470 "enrichProviderSnapshotWithVersionAdvisory",
471)(function* (snapshot: ServerProvider, maintenanceCapabilities?: ProviderMaintenanceCapabilities) {
471)(function* (
472 snapshot: ServerProvider,
473 maintenanceCapabilities?: ProviderMaintenanceCapabilities,
474 options?: {
475 readonly enableProviderUpdateChecks: boolean | undefined;
476 },
477) {
472478 const capabilities =
473479 maintenanceCapabilities ?? makeManualProviderMaintenanceCapabilities(snapshot.driver);
474 if (!snapshot.enabled || !snapshot.installed || !snapshot.version) {
480 const shouldResolveLatestVersion =
481 options?.enableProviderUpdateChecks !== false &&
482 snapshot.enabled &&
483 snapshot.installed &&
484 Boolean(snapshot.version);
485 if (!shouldResolveLatestVersion) {
475486 return {
476487 ...snapshot,
477488 versionAdvisory: createProviderVersionAdvisory({
apps/server/src/provider/providerUpdateSettings.ts created+43
......@@ -0,0 +1,43 @@
1import type { ServerSettings, ServerSettingsError } from "@t3tools/contracts";
2import * as Effect from "effect/Effect";
3import * as Equal from "effect/Equal";
4import * as Stream from "effect/Stream";
5
6import type { ServerSettingsShape } from "../serverSettings.ts";
7
8export interface ProviderSnapshotSettings<Settings> {
9 readonly provider: Settings;
10 readonly enableProviderUpdateChecks: boolean;
11}
12
13export function makeProviderSnapshotSettings<Settings>(
14 provider: Settings,
15 settings: ServerSettings,
16): ProviderSnapshotSettings<Settings> {
17 return {
18 provider,
19 enableProviderUpdateChecks: settings.enableProviderUpdateChecks,
20 };
21}
22
23export function haveProviderSnapshotSettingsChanged<Settings>(
24 previous: ProviderSnapshotSettings<Settings>,
25 next: ProviderSnapshotSettings<Settings>,
26): boolean {
27 return !Equal.equals(previous, next);
28}
29
30export function makeProviderSnapshotSettingsSource<Settings>(
31 provider: Settings,
32 serverSettings: ServerSettingsShape,
33): {
34 readonly getSettings: Effect.Effect<ProviderSnapshotSettings<Settings>, ServerSettingsError>;
35 readonly streamSettings: Stream.Stream<ProviderSnapshotSettings<Settings>>;
36} {
37 const mapSettings = (settings: ServerSettings) =>
38 makeProviderSnapshotSettings(provider, settings);
39 return {
40 getSettings: serverSettings.getSettings.pipe(Effect.map(mapSettings)),
41 streamSettings: serverSettings.streamChanges.pipe(Stream.map(mapSettings)),
42 };
43}
apps/web/src/components/settings/SettingsPanels.tsx+27
......@@ -668,6 +668,33 @@ export function GeneralSettingsPanel() {
668668 }
669669 />
670670
671 <SettingsRow
672 title="Provider update checks"
673 description="Check installed provider CLIs for newer available versions."
674 resetAction={
675 settings.enableProviderUpdateChecks !==
676 DEFAULT_UNIFIED_SETTINGS.enableProviderUpdateChecks ? (
677 <SettingResetButton
678 label="provider update checks"
679 onClick={() =>
680 updateSettings({
681 enableProviderUpdateChecks: DEFAULT_UNIFIED_SETTINGS.enableProviderUpdateChecks,
682 })
683 }
684 />
685 ) : null
686 }
687 control={
688 <Switch
689 checked={settings.enableProviderUpdateChecks}
690 onCheckedChange={(checked) =>
691 updateSettings({ enableProviderUpdateChecks: Boolean(checked) })
692 }
693 aria-label="Check provider versions"
694 />
695 }
696 />
697
671698 <SettingsRow
672699 title="Auto-open task panel"
673700 description="Open the right-side plan and task panel automatically when steps appear."
packages/contracts/src/settings.ts+2
......@@ -365,6 +365,7 @@ export const DEFAULT_AUTOMATIC_GIT_FETCH_INTERVAL = Duration.seconds(30);
365365
366366export const ServerSettings = Schema.Struct({
367367 enableAssistantStreaming: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
368 enableProviderUpdateChecks: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
368369 automaticGitFetchInterval: Schema.DurationFromMillis.pipe(
369370 Schema.withDecodingDefault(
370371 Effect.succeed(Duration.toMillis(DEFAULT_AUTOMATIC_GIT_FETCH_INTERVAL)),
......@@ -482,6 +483,7 @@ const OpenCodeSettingsPatch = Schema.Struct({
482483export const ServerSettingsPatch = Schema.Struct({
483484 // Server settings
484485 enableAssistantStreaming: Schema.optionalKey(Schema.Boolean),
486 enableProviderUpdateChecks: Schema.optionalKey(Schema.Boolean),
485487 automaticGitFetchInterval: Schema.optionalKey(Schema.DurationFromMillis),
486488 defaultThreadEnvMode: Schema.optionalKey(ThreadEnvMode),
487489 newWorktreesStartFromOrigin: Schema.optionalKey(Schema.Boolean),