| author | |
| committer | |
| log | f193a6863c494fdfa36a1ef3132e42a7d7b42926 |
| tree | 05f033d22f9cc16f11ef4b60cdb97470fc512f44 |
| parent | 83bbfa7e87e16a3c21413fe55524c1d05ce88dab |
| signature | Signed by PGP key B5690EEEBB952194 |
10 files changed, 233 insertions(+), 10 deletions(-)
apps/server/src/provider/Drivers/ClaudeDriver.ts+1| ... | ... | @@ -261,6 +261,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = { |
| 261 | 261 | accentColor, |
| 262 | 262 | enabled, |
| 263 | 263 | snapshot, |
| 264 | invalidateCaches: Cache.invalidateAll(capabilitiesProbeCache), | |
| 264 | 265 | snapshotForCwd, |
| 265 | 266 | adapter, |
| 266 | 267 | textGeneration, |
apps/server/src/provider/Drivers/CursorDriver.ts+3-2| ... | ... | @@ -134,11 +134,11 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = { |
| 134 | 134 | |
| 135 | 135 | const textGeneration = yield* makeCursorTextGeneration(effectiveConfig, processEnv); |
| 136 | 136 | |
| 137 | const discoverModels = yield* makeCursorModelDiscovery(effectiveConfig, processEnv); | |
| 137 | const modelDiscovery = yield* makeCursorModelDiscovery(effectiveConfig, processEnv); | |
| 138 | 138 | const checkProvider = checkCursorProviderStatus( |
| 139 | 139 | effectiveConfig, |
| 140 | 140 | processEnv, |
| 141 | discoverModels, | |
| 141 | modelDiscovery.discover, | |
| 142 | 142 | ).pipe( |
| 143 | 143 | Effect.flatMap((snapshot) => |
| 144 | 144 | effectiveConfig.enabled && snapshot.installed && snapshot.auth.status === "authenticated" |
| ... | ... | @@ -217,6 +217,7 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = { |
| 217 | 217 | accentColor, |
| 218 | 218 | enabled, |
| 219 | 219 | snapshot, |
| 220 | invalidateCaches: modelDiscovery.invalidate, | |
| 220 | 221 | snapshotForCwd: (cwd) => |
| 221 | 222 | !effectiveConfig.enabled |
| 222 | 223 | ? snapshot.getSnapshot |
apps/server/src/provider/Layers/CursorProvider.test.ts+5-1| ... | ... | @@ -742,7 +742,7 @@ describe("discoverCursorModelsViaAcp", () => { |
| 742 | 742 | apiEndpoint: "", |
| 743 | 743 | customModels: [], |
| 744 | 744 | }; |
| 745 | const discover = yield* makeCursorModelDiscovery(settings, { | |
| 745 | const { discover, invalidate } = yield* makeCursorModelDiscovery(settings, { | |
| 746 | 746 | ...process.env, |
| 747 | 747 | T3_ACP_REQUEST_LOG_PATH: requestLogPath, |
| 748 | 748 | }); |
| ... | ... | @@ -755,6 +755,10 @@ describe("discoverCursorModelsViaAcp", () => { |
| 755 | 755 | yield* fileSystem.writeFileString(requestLogPath, ""); |
| 756 | 756 | expect(yield* discover(about)).toEqual(first); |
| 757 | 757 | expect(yield* fileSystem.readFileString(requestLogPath)).toBe(""); |
| 758 | yield* invalidate; | |
| 759 | expect(yield* discover(about)).toEqual(first); | |
| 760 | expect(yield* fileSystem.readFileString(requestLogPath)).toContain("initialize"); | |
| 761 | yield* fileSystem.writeFileString(requestLogPath, ""); | |
| 758 | 762 | yield* discover({ ...about, version: "2026.08.12" }); |
| 759 | 763 | expect(yield* fileSystem.readFileString(requestLogPath)).toContain("initialize"); |
| 760 | 764 | yield* fileSystem.writeFileString(requestLogPath, ""); |
apps/server/src/provider/Layers/CursorProvider.ts+5-2| ... | ... | @@ -678,8 +678,11 @@ export const makeCursorModelDiscovery = Effect.fn("makeCursorModelDiscovery")(fu |
| 678 | 678 | Exit.isSuccess(exit) && exit.value.length > 0 ? Duration.minutes(30) : Duration.zero, |
| 679 | 679 | }, |
| 680 | 680 | ); |
| 681 | return (about: Pick<CursorAboutResult, "version" | "auth">) => | |
| 682 | Cache.get(cache, JSON.stringify([about.version, about.auth])); | |
| 681 | return { | |
| 682 | discover: (about: Pick<CursorAboutResult, "version" | "auth">) => | |
| 683 | Cache.get(cache, JSON.stringify([about.version, about.auth])), | |
| 684 | invalidate: Cache.invalidateAll(cache), | |
| 685 | }; | |
| 683 | 686 | }); |
| 684 | 687 | |
| 685 | 688 | function getCursorFallbackModels( |
apps/server/src/provider/ModelManifest.test.ts+79| ... | ... | @@ -342,6 +342,84 @@ const serviceLayers = (input: { |
| 342 | 342 | ); |
| 343 | 343 | |
| 344 | 344 | describe("ModelManifest service", () => { |
| 345 | it.live("explicit refresh bypasses fresh memory and disk caches", () => { | |
| 346 | let fetchCount = 0; | |
| 347 | const updated: ModelManifestData = { | |
| 348 | ...REMOTE_MANIFEST, | |
| 349 | currentModels: { codex: ["gpt-reloaded"] }, | |
| 350 | }; | |
| 351 | return Effect.gen(function* () { | |
| 352 | const service = yield* make; | |
| 353 | assert.deepStrictEqual(yield* service.refresh, REMOTE_MANIFEST); | |
| 354 | assert.deepStrictEqual(yield* service.refresh, REMOTE_MANIFEST); | |
| 355 | assert.strictEqual(fetchCount, 1); | |
| 356 | ||
| 357 | const rebooted = yield* make; | |
| 358 | assert.deepStrictEqual(yield* rebooted.refresh, REMOTE_MANIFEST); | |
| 359 | assert.strictEqual(fetchCount, 1); | |
| 360 | assert.deepStrictEqual(yield* rebooted.forceRefresh, updated); | |
| 361 | assert.strictEqual(fetchCount, 2); | |
| 362 | assert.deepStrictEqual(yield* rebooted.current, updated); | |
| 363 | assert.deepStrictEqual(yield* (yield* make).current, updated); | |
| 364 | }).pipe( | |
| 365 | Effect.scoped, | |
| 366 | Effect.provide( | |
| 367 | serviceLayers({ | |
| 368 | prefix: "model-manifest-force-refresh-test", | |
| 369 | response: () => Response.json(fetchCount++ === 0 ? REMOTE_MANIFEST : updated), | |
| 370 | }), | |
| 371 | ), | |
| 372 | ); | |
| 373 | }); | |
| 374 | ||
| 375 | it.live("explicit refresh retries immediately after failure and preserves last-good data", () => { | |
| 376 | let fetchCount = 0; | |
| 377 | return Effect.gen(function* () { | |
| 378 | const service = yield* make; | |
| 379 | assert.deepStrictEqual(yield* service.refresh, REMOTE_MANIFEST); | |
| 380 | assert.deepStrictEqual(yield* service.forceRefresh, REMOTE_MANIFEST); | |
| 381 | assert.deepStrictEqual(yield* service.current, REMOTE_MANIFEST); | |
| 382 | assert.deepStrictEqual(yield* (yield* make).current, REMOTE_MANIFEST); | |
| 383 | assert.strictEqual(fetchCount, 2); | |
| 384 | assert.deepStrictEqual(yield* service.forceRefresh, REMOTE_MANIFEST); | |
| 385 | assert.strictEqual(fetchCount, 3); | |
| 386 | }).pipe( | |
| 387 | Effect.scoped, | |
| 388 | Effect.provide( | |
| 389 | serviceLayers({ | |
| 390 | prefix: "model-manifest-force-retry-test", | |
| 391 | response: () => | |
| 392 | fetchCount++ === 1 | |
| 393 | ? new Response(null, { status: 503 }) | |
| 394 | : Response.json(REMOTE_MANIFEST), | |
| 395 | }), | |
| 396 | ), | |
| 397 | ); | |
| 398 | }); | |
| 399 | ||
| 400 | it.live("explicit refresh bypasses the retry delay after an initial failure", () => { | |
| 401 | let fetchCount = 0; | |
| 402 | return Effect.gen(function* () { | |
| 403 | const service = yield* make; | |
| 404 | assert.deepStrictEqual(yield* service.refresh, BUNDLED_MODEL_MANIFEST); | |
| 405 | assert.deepStrictEqual(yield* service.refresh, BUNDLED_MODEL_MANIFEST); | |
| 406 | assert.strictEqual(fetchCount, 1); | |
| 407 | assert.deepStrictEqual(yield* service.forceRefresh, REMOTE_MANIFEST); | |
| 408 | assert.strictEqual(fetchCount, 2); | |
| 409 | }).pipe( | |
| 410 | Effect.scoped, | |
| 411 | Effect.provide( | |
| 412 | serviceLayers({ | |
| 413 | prefix: "model-manifest-force-initial-retry-test", | |
| 414 | response: () => | |
| 415 | fetchCount++ === 0 | |
| 416 | ? new Response(null, { status: 503 }) | |
| 417 | : Response.json(REMOTE_MANIFEST), | |
| 418 | }), | |
| 419 | ), | |
| 420 | ); | |
| 421 | }); | |
| 422 | ||
| 345 | 423 | it.live("prefers a fetched manifest over the bundle and caches it to disk", () => |
| 346 | 424 | Effect.gen(function* () { |
| 347 | 425 | const service = yield* make; |
| ... | ... | @@ -457,6 +535,7 @@ describe("ModelManifest service", () => { |
| 457 | 535 | ), |
| 458 | 536 | ); |
| 459 | 537 | assert.deepStrictEqual(yield* service.refresh, BUNDLED_MODEL_MANIFEST); |
| 538 | assert.deepStrictEqual(yield* service.forceRefresh, BUNDLED_MODEL_MANIFEST); | |
| 460 | 539 | assert.strictEqual(fetchCount, 0); |
| 461 | 540 | }).pipe( |
| 462 | 541 | Effect.scoped, |
apps/server/src/provider/ModelManifest.ts+7-3| ... | ... | @@ -315,6 +315,8 @@ export class ModelManifest extends Context.Service< |
| 315 | 315 | readonly current: Effect.Effect<ModelManifestData>; |
| 316 | 316 | /** Manifest after a TTL-gated remote refresh; never fails. */ |
| 317 | 317 | readonly refresh: Effect.Effect<ModelManifestData>; |
| 318 | /** Explicit refresh bypasses freshness and retry timers, retaining last-good data. */ | |
| 319 | readonly forceRefresh: Effect.Effect<ModelManifestData>; | |
| 318 | 320 | /** Forks `refresh` into the service's own scope. Drivers call this from |
| 319 | 321 | * provider checks: the fetch is process-shared state, so it must survive |
| 320 | 322 | * the teardown of whichever instance happened to trigger it. */ |
| ... | ... | @@ -326,6 +328,7 @@ export class ModelManifest extends Context.Service< |
| 326 | 328 | const BundledOnlyModelManifest: ModelManifest["Service"] = { |
| 327 | 329 | current: Effect.succeed(BUNDLED_MODEL_MANIFEST), |
| 328 | 330 | refresh: Effect.succeed(BUNDLED_MODEL_MANIFEST), |
| 331 | forceRefresh: Effect.succeed(BUNDLED_MODEL_MANIFEST), | |
| 329 | 332 | refreshInBackground: Effect.void, |
| 330 | 333 | }; |
| 331 | 334 | |
| ... | ... | @@ -369,7 +372,7 @@ export const make = Effect.gen(function* () { |
| 369 | 372 | }), |
| 370 | 373 | ); |
| 371 | 374 | |
| 372 | const refresh = Effect.fn("ModelManifest.refresh")(function* () { | |
| 375 | const refresh = Effect.fn("ModelManifest.refresh")(function* (force = false) { | |
| 373 | 376 | yield* ensureDiskCacheLoaded; |
| 374 | 377 | const now = yield* Clock.currentTimeMillis; |
| 375 | 378 | // A timestamp in the future means the wall clock moved backwards (the |
| ... | ... | @@ -377,8 +380,8 @@ export const make = Effect.gen(function* () { |
| 377 | 380 | // it as expired: the refetch rewrites both timestamps and self-heals. |
| 378 | 381 | const isWithin = (sinceMs: number | null, windowMs: number) => |
| 379 | 382 | sinceMs !== null && now >= sinceMs && now - sinceMs < windowMs; |
| 380 | if (isWithin(fetchedAtMs, MANIFEST_TTL_MS)) return manifest; | |
| 381 | if (isWithin(lastAttemptMs, MANIFEST_RETRY_MS)) return manifest; | |
| 383 | if (!force && isWithin(fetchedAtMs, MANIFEST_TTL_MS)) return manifest; | |
| 384 | if (!force && isWithin(lastAttemptMs, MANIFEST_RETRY_MS)) return manifest; | |
| 382 | 385 | |
| 383 | 386 | // The same switch that gates provider CLI update checks. It stops network |
| 384 | 387 | // fetches only: a manifest already cached on disk from an earlier fetch |
| ... | ... | @@ -413,6 +416,7 @@ export const make = Effect.gen(function* () { |
| 413 | 416 | return ModelManifest.of({ |
| 414 | 417 | current: ensureDiskCacheLoaded.pipe(Effect.map(() => manifest)), |
| 415 | 418 | refresh: guardedRefresh, |
| 419 | forceRefresh: refreshSemaphore.withPermits(1)(refresh(true)), | |
| 416 | 420 | refreshInBackground: Effect.forkIn(guardedRefresh, serviceScope).pipe(Effect.asVoid), |
| 417 | 421 | }); |
| 418 | 422 | }); |
apps/server/src/provider/ProviderDriver.ts+2| ... | ... | @@ -74,6 +74,8 @@ export interface ProviderInstance { |
| 74 | 74 | readonly snapshot: ServerProviderShape; |
| 75 | 75 | readonly snapshotForCwd?: (cwd: string) => Effect.Effect<ServerProvider, ProviderDriverError>; |
| 76 | 76 | readonly refreshModels?: () => Effect.Effect<void, ProviderDriverError>; |
| 77 | /** Invalidate T3-owned discovery caches before an explicit provider refresh. */ | |
| 78 | readonly invalidateCaches?: Effect.Effect<void>; | |
| 77 | 79 | /** |
| 78 | 80 | * Redeem one banked rate-limit reset credit on the signed-in account, then |
| 79 | 81 | * re-probe so the snapshot reflects the cleared windows. Account-level, |
apps/server/src/server.test.ts+103-1| ... | ... | @@ -132,6 +132,7 @@ import { OrchestrationEventStoreLive } from "./persistence/Layers/OrchestrationE |
| 132 | 132 | import { OrchestrationEventStore } from "./persistence/Services/OrchestrationEventStore.ts"; |
| 133 | 133 | import { PersistenceSqlError } from "./persistence/Errors.ts"; |
| 134 | 134 | import * as ProviderRegistry from "./provider/Services/ProviderRegistry.ts"; |
| 135 | import * as ModelManifest from "./provider/ModelManifest.ts"; | |
| 135 | 136 | import * as ProviderService from "./provider/Services/ProviderService.ts"; |
| 136 | 137 | import { ProviderAuthService } from "./provider/Services/ProviderAuthService.ts"; |
| 137 | 138 | import { ProviderInstanceRegistry } from "./provider/Services/ProviderInstanceRegistry.ts"; |
| ... | ... | @@ -142,7 +143,10 @@ import { |
| 142 | 143 | import type { ProviderInstance } from "./provider/ProviderDriver.ts"; |
| 143 | 144 | import * as ProviderSessionDirectory from "./provider/Services/ProviderSessionDirectory.ts"; |
| 144 | 145 | import { ProviderAdapterRequestError } from "./provider/Errors.ts"; |
| 145 | import { makeManualOnlyProviderMaintenanceCapabilities } from "./provider/providerMaintenance.ts"; | |
| 146 | import { | |
| 147 | makeManualOnlyProviderMaintenanceCapabilities, | |
| 148 | ProviderVersionCache, | |
| 149 | } from "./provider/providerMaintenance.ts"; | |
| 146 | 150 | import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts"; |
| 147 | 151 | import * as ServerRuntimeStartup from "./serverRuntimeStartup.ts"; |
| 148 | 152 | import * as ServiceLauncherClient from "./cloud/serviceLauncherClient.ts"; |
| ... | ... | @@ -516,6 +520,7 @@ const buildAppUnderTest = (options?: { |
| 516 | 520 | keybindings?: Partial<Keybindings.Keybindings["Service"]>; |
| 517 | 521 | environmentTheme?: Partial<EnvironmentTheme.EnvironmentThemeService["Service"]>; |
| 518 | 522 | providerRegistry?: Partial<ProviderRegistry.ProviderRegistry["Service"]>; |
| 523 | modelManifest?: Partial<ModelManifest.ModelManifest["Service"]>; | |
| 519 | 524 | usageLimitSources?: Partial<UsageLimitSources.UsageLimitSources["Service"]>; |
| 520 | 525 | providerService?: Partial<ProviderService.ProviderService["Service"]>; |
| 521 | 526 | providerAuth?: Partial<ProviderAuthService["Service"]>; |
| ... | ... | @@ -790,6 +795,10 @@ const buildAppUnderTest = (options?: { |
| 790 | 795 | ), |
| 791 | 796 | Layer.provide( |
| 792 | 797 | Layer.mergeAll( |
| 798 | Layer.mock(ModelManifest.ModelManifest)({ | |
| 799 | forceRefresh: Effect.succeed(ModelManifest.BUNDLED_MODEL_MANIFEST), | |
| 800 | ...options?.layers?.modelManifest, | |
| 801 | }), | |
| 793 | 802 | Layer.mock(ProviderRegistry.ProviderRegistry)({ |
| 794 | 803 | getProviders: Effect.succeed([]), |
| 795 | 804 | refresh: () => Effect.succeed([]), |
| ... | ... | @@ -6337,6 +6346,99 @@ it.layer(NodeServices.layer)("server router seam", (it) => { |
| 6337 | 6346 | }).pipe(Effect.provide(NodeHttpServer.layerTest)), |
| 6338 | 6347 | ); |
| 6339 | 6348 | |
| 6349 | for (const mode of ["all", "targeted", "background"] as const) { | |
| 6350 | it.effect(`provider refresh invalidates T3 caches before probing (${mode})`, () => { | |
| 6351 | const driver = ProviderDriverKind.make("codex"); | |
| 6352 | const instanceIds = [ProviderInstanceId.make("codex"), ProviderInstanceId.make("codex_work")]; | |
| 6353 | const packageNames = ["@example/personal", "@example/work"]; | |
| 6354 | const versionCache = new Map( | |
| 6355 | packageNames.map((name) => [ | |
| 6356 | name, | |
| 6357 | { | |
| 6358 | expiresAt: Number.MAX_SAFE_INTEGER, | |
| 6359 | version: "1.0.0", | |
| 6360 | }, | |
| 6361 | ]), | |
| 6362 | ); | |
| 6363 | const invalidated: string[] = []; | |
| 6364 | const freshMaintenance: string[] = []; | |
| 6365 | let manifestRefreshed = false; | |
| 6366 | let probed = false; | |
| 6367 | const instances = instanceIds.map( | |
| 6368 | (instanceId, index) => | |
| 6369 | ({ | |
| 6370 | instanceId, | |
| 6371 | driverKind: driver, | |
| 6372 | continuationIdentity: { driverKind: driver, continuationKey: instanceId }, | |
| 6373 | displayName: undefined, | |
| 6374 | enabled: true, | |
| 6375 | invalidateCaches: Effect.sync(() => { | |
| 6376 | invalidated.push(instanceId); | |
| 6377 | }), | |
| 6378 | snapshot: { | |
| 6379 | resolveMaintenance: (options) => | |
| 6380 | Effect.sync(() => { | |
| 6381 | assert.isTrue(options?.fresh); | |
| 6382 | freshMaintenance.push(instanceId); | |
| 6383 | return makeManualOnlyProviderMaintenanceCapabilities({ | |
| 6384 | provider: driver, | |
| 6385 | packageName: packageNames[index]!, | |
| 6386 | }); | |
| 6387 | }), | |
| 6388 | getSnapshot: Effect.never, | |
| 6389 | refresh: Effect.never, | |
| 6390 | streamChanges: Stream.empty, | |
| 6391 | applyUsageLimits: () => Effect.void, | |
| 6392 | }, | |
| 6393 | adapter: {} as ProviderInstance["adapter"], | |
| 6394 | textGeneration: {} as ProviderInstance["textGeneration"], | |
| 6395 | }) satisfies ProviderInstance, | |
| 6396 | ); | |
| 6397 | const expected = | |
| 6398 | mode === "background" ? [] : mode === "targeted" ? [instanceIds[1]!] : instanceIds; | |
| 6399 | const probe = Effect.sync(() => { | |
| 6400 | probed = true; | |
| 6401 | assert.equal(manifestRefreshed, mode !== "background"); | |
| 6402 | assert.deepEqual(invalidated.toSorted(), expected.toSorted()); | |
| 6403 | assert.deepEqual(freshMaintenance.toSorted(), expected.toSorted()); | |
| 6404 | for (let index = 0; index < instanceIds.length; index++) { | |
| 6405 | assert.equal( | |
| 6406 | versionCache.has(packageNames[index]!), | |
| 6407 | !expected.includes(instanceIds[index]!), | |
| 6408 | ); | |
| 6409 | } | |
| 6410 | return []; | |
| 6411 | }); | |
| 6412 | return Effect.gen(function* () { | |
| 6413 | yield* buildAppUnderTest({ | |
| 6414 | layers: { | |
| 6415 | modelManifest: { | |
| 6416 | forceRefresh: Effect.sync(() => { | |
| 6417 | manifestRefreshed = true; | |
| 6418 | return ModelManifest.BUNDLED_MODEL_MANIFEST; | |
| 6419 | }), | |
| 6420 | }, | |
| 6421 | providerInstanceRegistry: { listInstances: Effect.succeed(instances) }, | |
| 6422 | providerRegistry: { refresh: () => probe, refreshInstance: () => probe }, | |
| 6423 | }, | |
| 6424 | }); | |
| 6425 | const wsUrl = yield* getWsServerUrl("/ws"); | |
| 6426 | yield* Effect.scoped( | |
| 6427 | withWsRpcClient(wsUrl, (client) => | |
| 6428 | client[WS_METHODS.serverRefreshProviders]({ | |
| 6429 | ...(mode === "targeted" ? { instanceId: instanceIds[1]! } : {}), | |
| 6430 | ...(mode !== "background" ? { refreshModels: true } : {}), | |
| 6431 | }), | |
| 6432 | ), | |
| 6433 | ); | |
| 6434 | assert.isTrue(probed); | |
| 6435 | }).pipe( | |
| 6436 | Effect.provideService(ProviderVersionCache, versionCache), | |
| 6437 | Effect.provide(NodeHttpServer.layerTest), | |
| 6438 | ); | |
| 6439 | }); | |
| 6440 | } | |
| 6441 | ||
| 6340 | 6442 | it.effect("serves config on reconnect without starting provider probes", () => |
| 6341 | 6443 | Effect.gen(function* () { |
| 6342 | 6444 | const refresh = vi.fn(() => Effect.never); |
apps/server/src/ws.ts+26| ... | ... | @@ -109,6 +109,8 @@ import { |
| 109 | 109 | observeRpcStreamEffect as instrumentRpcStreamEffect, |
| 110 | 110 | } from "./observability/RpcInstrumentation.ts"; |
| 111 | 111 | import * as ProviderRegistry from "./provider/Services/ProviderRegistry.ts"; |
| 112 | import * as ModelManifest from "./provider/ModelManifest.ts"; | |
| 113 | import * as ProviderMaintenance from "./provider/providerMaintenance.ts"; | |
| 112 | 114 | import * as ProviderService from "./provider/Services/ProviderService.ts"; |
| 113 | 115 | import * as ProviderSessionDirectory from "./provider/Services/ProviderSessionDirectory.ts"; |
| 114 | 116 | import * as ProviderMaintenanceRunner from "./provider/providerMaintenanceRunner.ts"; |
| ... | ... | @@ -561,6 +563,8 @@ const makeWsRpcLayer = ( |
| 561 | 563 | yield* Effect.context<Effect.Services<ReturnType<typeof remoteSshDeviceHosts>>>(); |
| 562 | 564 | const portDiscovery = yield* PortScanner.PortDiscovery; |
| 563 | 565 | const providerRegistry = yield* ProviderRegistry.ProviderRegistry; |
| 566 | const modelManifest = yield* ModelManifest.ModelManifest; | |
| 567 | const providerVersionCache = yield* ProviderMaintenance.ProviderVersionCache; | |
| 564 | 568 | const providerService = yield* ProviderService.ProviderService; |
| 565 | 569 | const providerSessionDirectory = yield* ProviderSessionDirectory.ProviderSessionDirectory; |
| 566 | 570 | const providerMaintenanceRunner = yield* ProviderMaintenanceRunner.ProviderMaintenanceRunner; |
| ... | ... | @@ -2361,6 +2365,28 @@ const makeWsRpcLayer = ( |
| 2361 | 2365 | observeRpcEffect( |
| 2362 | 2366 | WS_METHODS.serverRefreshProviders, |
| 2363 | 2367 | Effect.gen(function* () { |
| 2368 | // Only explicit catalog refreshes bypass T3's caches. Workspace | |
| 2369 | // discovery and background status checks retain their timers. | |
| 2370 | if (input.refreshModels) { | |
| 2371 | yield* modelManifest.forceRefresh; | |
| 2372 | const instances = yield* providerInstances.listInstances; | |
| 2373 | yield* Effect.forEach( | |
| 2374 | instances.filter( | |
| 2375 | (instance) => | |
| 2376 | input.instanceId === undefined || input.instanceId === instance.instanceId, | |
| 2377 | ), | |
| 2378 | (instance) => | |
| 2379 | Effect.gen(function* () { | |
| 2380 | yield* instance.invalidateCaches ?? Effect.void; | |
| 2381 | const maintenance = yield* instance.snapshot.resolveMaintenance({ | |
| 2382 | fresh: true, | |
| 2383 | }); | |
| 2384 | if (maintenance.packageName) | |
| 2385 | providerVersionCache.delete(maintenance.packageName); | |
| 2386 | }), | |
| 2387 | { concurrency: "unbounded", discard: true }, | |
| 2388 | ); | |
| 2389 | } | |
| 2364 | 2390 | // An untargeted refresh is "re-read everything's status", which |
| 2365 | 2391 | // includes quota from configured usage-limit sources. Awaited, |
| 2366 | 2392 | // not forked: the RPC scope closes on return and would |
packages/contracts/src/rpc.ts+2-1| ... | ... | @@ -476,7 +476,8 @@ const WsServerRefreshProvidersRpc = Rpc.make(WS_METHODS.serverRefreshProviders, |
| 476 | 476 | */ |
| 477 | 477 | instanceId: Schema.optional(ProviderInstanceId), |
| 478 | 478 | cwd: Schema.optional(TrimmedNonEmptyString), |
| 479 | /** Explicit user request. Background status refreshes must not open agent sessions. */ | |
| 479 | /** Explicit user request: bypass T3-owned caches and rediscover models. | |
| 480 | * Background status refreshes must not open agent sessions. */ | |
| 480 | 481 | refreshModels: Schema.optional(Schema.Boolean), |
| 481 | 482 | }), |
| 482 | 483 | success: ServerProviderUpdatedPayload, |