From ade8819452be3509083b5647f65fb3512bd5434a Mon Sep 17 00:00:00 2001 From: fixfon <12512757+fixfon@users.noreply.github.com> Date: Sat, 24 Oct 2026 07:55:24 +0300 Subject: [PATCH 1/2] fix(server): stale provider update results clear once the provider is up to date A finished update that reported "unchanged" or "failed" stayed attached to the provider until the server restarted, even after the install was fixed and a refresh showed the provider current. A refreshed snapshot that reports the provider current now retires those results; queued, running and succeeded results, and any provider still behind or not yet checked, are left alone. --- .../src/provider/ProviderRegistry.test.ts | 149 ++++++++++++++++++ apps/server/src/provider/ProviderRegistry.ts | 22 +++ 2 files changed, 171 insertions(+) diff --git a/apps/server/src/provider/ProviderRegistry.test.ts b/apps/server/src/provider/ProviderRegistry.test.ts index 4aeaddff8f4d5..2c2059ef4adf5 100644 --- a/apps/server/src/provider/ProviderRegistry.test.ts +++ b/apps/server/src/provider/ProviderRegistry.test.ts @@ -30,6 +30,8 @@ import { ServerSettingsError, ServerProvider, type ServerProviderSlashCommand, + type ServerProviderUpdateState, + type ServerProviderVersionAdvisoryStatus, type ServerSettings as ContractServerSettings, } from "@t3tools/contracts"; import { HttpClient, HttpClientResponse } from "effect/http"; @@ -305,6 +307,85 @@ const awaitPersistedProvider = ( Effect.forkScoped, ); +const codexUpdateSnapshot = (advisory: ServerProviderVersionAdvisoryStatus) => + ({ + instanceId: ProviderInstanceId.make("codex"), + driver: ProviderDriverKind.make("codex"), + status: "ready", + enabled: true, + installed: true, + auth: { status: "authenticated" }, + checkedAt: "2026-06-10T00:00:00.000Z", + version: "1.0.0", + models: [], + slashCommands: [], + skills: [], + versionAdvisory: { + status: advisory, + currentVersion: "1.0.0", + latestVersion: advisory === "unknown" ? null : "2.0.0", + updateCommand: null, + canUpdate: true, + checkedAt: "2026-06-10T00:00:00.000Z", + message: null, + }, + }) as const satisfies ServerProvider; + +const makeUpdateState = (status: ServerProviderUpdateState["status"]) => + ({ + status, + startedAt: "2026-06-10T00:00:00.000Z", + finishedAt: status === "queued" || status === "running" ? null : "2026-06-10T00:01:00.000Z", + message: `update ${status}`, + output: null, + }) satisfies ServerProviderUpdateState; + +// One Codex instance whose probe returns whatever `snapshot` currently holds. +const buildRegistryOverSnapshot = Effect.fn("buildRegistryOverSnapshot")(function* ( + snapshot: Ref.Ref, +) { + const driver = ProviderDriverKind.make("codex"); + const instance = { + instanceId: ProviderInstanceId.make("codex"), + driverKind: driver, + continuationIdentity: { driverKind: driver, continuationKey: "codex:instance:codex" }, + displayName: undefined, + enabled: true, + snapshot: { + resolveMaintenance: () => + Effect.succeed( + makeManualOnlyProviderMaintenanceCapabilities({ provider: driver, packageName: null }), + ), + getSnapshot: Ref.get(snapshot), + refresh: Ref.get(snapshot), + streamChanges: Stream.empty, + applyUsageLimits: () => Effect.void, + }, + orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"], + textGeneration: {} as ProviderInstance["textGeneration"], + } satisfies ProviderInstance; + const layerInstanceRegistry = Layer.succeed(ProviderInstanceRegistry.ProviderInstanceRegistry, { + getInstance: (instanceId) => + Effect.succeed(instanceId === instance.instanceId ? instance : undefined), + listInstances: Effect.succeed([instance]), + listUnavailable: Effect.succeed([]), + streamChanges: Stream.empty, + subscribeChanges: Effect.flatMap(PubSub.unbounded(), PubSub.subscribe), + }); + const scope = yield* Scope.make(); + yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void)); + return yield* Layer.build( + ProviderRegistry.layer.pipe( + Layer.provideMerge(layerInstanceRegistry), + Layer.provideMerge( + ServerConfig.layerTest(process.cwd(), { prefix: "t3-provider-registry-update-state-" }), + ), + Layer.provideMerge(layerBackgroundPolicyAlwaysRun), + Layer.provideMerge(NodeServices.layer), + ), + ).pipe(Scope.provide(scope)); +}); + const layerTestNodeServices = Layer.mergeAll( ProviderLatestVersions.layer, NodeServices.layer, @@ -1808,6 +1889,74 @@ it.layer( }), ); + describe("update results across refreshes", () => { + const instanceId = ProviderInstanceId.make("codex"); + + it.effect.each([ + { status: "unchanged", advisory: "current", retired: true }, + { status: "failed", advisory: "current", retired: true }, + { status: "unchanged", advisory: "behind_latest", retired: false }, + { status: "failed", advisory: "behind_latest", retired: false }, + // The probe publishes `unknown` until the latest-version check lands. + { status: "unchanged", advisory: "unknown", retired: false }, + { status: "queued", advisory: "current", retired: false }, + { status: "running", advisory: "current", retired: false }, + ] as const)( + "a $status update result is retired: $retired when a refresh reports the provider $advisory", + ({ status, advisory, retired }) => + Effect.gen(function* () { + const snapshot = yield* Ref.make(codexUpdateSnapshot("behind_latest")); + const runtimeServices = yield* buildRegistryOverSnapshot(snapshot); + + yield* Effect.gen(function* () { + const registry = yield* ProviderRegistry.ProviderRegistry; + const state = makeUpdateState(status); + yield* registry.setProviderMaintenanceActionState({ + instanceId, + action: "update", + state, + }); + yield* Ref.set(snapshot, codexUpdateSnapshot(advisory)); + + const [refreshed] = yield* registry.refresh(ProviderDriverKind.make("codex")); + + expect(refreshed?.updateState).toEqual(retired ? undefined : state); + }).pipe(Effect.provide(runtimeServices)); + }), + ); + + it.effect("does not bring a retired result back, and a later update still reports", () => + Effect.gen(function* () { + const snapshot = yield* Ref.make(codexUpdateSnapshot("behind_latest")); + const runtimeServices = yield* buildRegistryOverSnapshot(snapshot); + + yield* Effect.gen(function* () { + const registry = yield* ProviderRegistry.ProviderRegistry; + const codex = ProviderDriverKind.make("codex"); + yield* registry.setProviderMaintenanceActionState({ + instanceId, + action: "update", + state: makeUpdateState("unchanged"), + }); + yield* Ref.set(snapshot, codexUpdateSnapshot("current")); + yield* registry.refresh(codex); + + yield* Ref.set(snapshot, codexUpdateSnapshot("unknown")); + const [reprobed] = yield* registry.refresh(codex); + expect(reprobed?.updateState).toBeUndefined(); + + const running = makeUpdateState("running"); + const [updating] = yield* registry.setProviderMaintenanceActionState({ + instanceId, + action: "update", + state: running, + }); + expect(updating?.updateState).toEqual(running); + }).pipe(Effect.provide(runtimeServices)); + }), + ); + }); + it.effect("re-probes when settings change the codex binaryPath", () => Effect.gen(function* () { const firstMissing = `t3code_codex_first_`; diff --git a/apps/server/src/provider/ProviderRegistry.ts b/apps/server/src/provider/ProviderRegistry.ts index 97da44aa93ed6..37e8db664a16a 100644 --- a/apps/server/src/provider/ProviderRegistry.ts +++ b/apps/server/src/provider/ProviderRegistry.ts @@ -470,6 +470,27 @@ export const layer = Layer.effect( }; }); + // Runs on source snapshots only. `setProviderMaintenanceActionState` re-applies the stored + // snapshot, whose advisory may already read `current`, and must not discard the result the + // runner is recording. A probe publishes `unknown` until the latest-version check lands, so + // only `current` shows the provider is up to date. + const retireStaleUpdateState = Effect.fn("retireStaleUpdateState")(function* ( + provider: ServerProvider, + ) { + if (provider.versionAdvisory?.status !== "current") { + return; + } + yield* Ref.update(maintenanceActionStatesRef, (previous) => { + const status = previous.get(provider.instanceId)?.update?.status; + if (status !== "unchanged" && status !== "failed") { + return previous; + } + const next = new Map(previous); + next.delete(provider.instanceId); + return next; + }); + }); + const upsertProviders = Effect.fn("upsertProviders")(function* ( nextProviders: ReadonlyArray, options?: { @@ -539,6 +560,7 @@ export const layer = Layer.effect( readonly publish?: boolean; }, ) { + yield* retireStaleUpdateState(provider); const providers = yield* upsertProviders([provider], options); // Reclassify the current read model after fetching. Never republish the // probe captured before the fetch: a newer health result may have landed. From 4e4abb7fe694238c4606f285291ae5313df84629 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 26 Oct 2026 13:56:54 +0000 Subject: [PATCH 2/2] fix(server): preserve targeted provider install failures on refresh --- .../src/provider/ProviderRegistry.test.ts | 26 ++++++++- apps/server/src/provider/ProviderRegistry.ts | 56 ++++++++----------- .../providerMaintenanceRunner.test.ts | 7 +++ .../src/provider/providerMaintenanceRunner.ts | 1 + 4 files changed, 56 insertions(+), 34 deletions(-) diff --git a/apps/server/src/provider/ProviderRegistry.test.ts b/apps/server/src/provider/ProviderRegistry.test.ts index 2c46902259909..849e9a0db9cdd 100644 --- a/apps/server/src/provider/ProviderRegistry.test.ts +++ b/apps/server/src/provider/ProviderRegistry.test.ts @@ -323,7 +323,7 @@ const codexUpdateSnapshot = (advisory: ServerProviderVersionAdvisoryStatus) => versionAdvisory: { status: advisory, currentVersion: "1.0.0", - latestVersion: advisory === "unknown" ? null : "2.0.0", + latestVersion: advisory === "unknown" ? null : advisory === "current" ? "1.0.0" : "2.0.0", updateCommand: null, canUpdate: true, checkedAt: "2026-06-10T00:00:00.000Z", @@ -2021,6 +2021,8 @@ it.layer( { status: "unchanged", advisory: "unknown", retired: false }, { status: "queued", advisory: "current", retired: false }, { status: "running", advisory: "current", retired: false }, + { status: "succeeded", advisory: "current", retired: false }, + { status: "failed", advisory: "unknown", retired: false }, ] as const)( "a $status update result is retired: $retired when a refresh reports the provider $advisory", ({ status, advisory, retired }) => @@ -2045,6 +2047,28 @@ it.layer( }), ); + it.effect.each(["failed", "unchanged"] as const)( + "keeps a %s recommended-version install after a current refresh", + (status) => + Effect.gen(function* () { + const snapshot = yield* Ref.make(codexUpdateSnapshot("current")); + const runtimeServices = yield* buildRegistryOverSnapshot(snapshot); + yield* Effect.gen(function* () { + const registry = yield* ProviderRegistry.ProviderRegistry; + const state = makeUpdateState(status); + const input = { + instanceId, + action: "update" as const, + state, + targetVersion: "0.9.0", + }; + yield* registry.setProviderMaintenanceActionState(input); + const [refreshed] = yield* registry.refresh(ProviderDriverKind.make("codex")); + expect(refreshed?.updateState).toEqual(state); + }).pipe(Effect.provide(runtimeServices)); + }), + ); + it.effect("does not bring a retired result back, and a later update still reports", () => Effect.gen(function* () { const snapshot = yield* Ref.make(codexUpdateSnapshot("behind_latest")); diff --git a/apps/server/src/provider/ProviderRegistry.ts b/apps/server/src/provider/ProviderRegistry.ts index 187dcf7ce371b..13ca8d778d192 100644 --- a/apps/server/src/provider/ProviderRegistry.ts +++ b/apps/server/src/provider/ProviderRegistry.ts @@ -83,6 +83,7 @@ export class ProviderRegistry extends Context.Service< readonly instanceId: ProviderInstanceId; readonly action: ProviderMaintenanceActionKind; readonly state: ServerProviderUpdateState | null; + readonly targetVersion?: string | undefined; }) => Effect.Effect>; readonly streamChanges: Stream.Stream>; @@ -415,7 +416,10 @@ export const layer = Layer.effect( ReadonlyMap>> >(new Map()); const maintenanceActionStatesRef = yield* Ref.make< - ReadonlyMap + ReadonlyMap< + ProviderInstanceId, + { readonly update?: ServerProviderUpdateState; readonly targetVersion?: string | undefined } + > >(new Map()); const liveSubsRef = yield* Ref.make>( @@ -459,27 +463,6 @@ export const layer = Layer.effect( }; }); - // Runs on source snapshots only. `setProviderMaintenanceActionState` re-applies the stored - // snapshot, whose advisory may already read `current`, and must not discard the result the - // runner is recording. A probe publishes `unknown` until the latest-version check lands, so - // only `current` shows the provider is up to date. - const retireStaleUpdateState = Effect.fn("retireStaleUpdateState")(function* ( - provider: ServerProvider, - ) { - if (provider.versionAdvisory?.status !== "current") { - return; - } - yield* Ref.update(maintenanceActionStatesRef, (previous) => { - const status = previous.get(provider.instanceId)?.update?.status; - if (status !== "unchanged" && status !== "failed") { - return previous; - } - const next = new Map(previous); - next.delete(provider.instanceId); - return next; - }); - }); - const upsertProviders = Effect.fn("upsertProviders")(function* ( nextProviders: ReadonlyArray, options?: { @@ -549,7 +532,21 @@ export const layer = Layer.effect( readonly publish?: boolean; }, ) { - yield* retireStaleUpdateState(provider); + // Latest-version status cannot resolve a failed recommended-version install. + if (provider.versionAdvisory?.status === "current") { + yield* Ref.update(maintenanceActionStatesRef, (previous) => { + const action = previous.get(provider.instanceId); + if ( + action?.targetVersion !== undefined || + (action?.update?.status !== "unchanged" && action?.update?.status !== "failed") + ) { + return previous; + } + const next = new Map(previous); + next.delete(provider.instanceId); + return next; + }); + } const providers = yield* upsertProviders([provider], options); // Reclassify the current model after fetch; republishing the old probe could overwrite newer health results. if (!(yield* Ref.getAndSet(compatibilityRefreshRunning, true))) { @@ -567,21 +564,14 @@ export const layer = Layer.effect( readonly instanceId: ProviderInstanceId; readonly action: "update"; readonly state: ServerProviderUpdateState | null; + readonly targetVersion?: string | undefined; }) { yield* Ref.update(maintenanceActionStatesRef, (previous) => { - const previousActions = previous.get(input.instanceId); - const nextActions = { ...previousActions }; - if (input.state === null || input.state.status === "idle") { - delete nextActions[input.action]; - } else { - nextActions[input.action] = input.state; - } - const next = new Map(previous); - if (Object.keys(nextActions).length === 0) { + if (input.state === null || input.state.status === "idle") { next.delete(input.instanceId); } else { - next.set(input.instanceId, nextActions); + next.set(input.instanceId, { update: input.state, targetVersion: input.targetVersion }); } return next; }); diff --git a/apps/server/src/provider/providerMaintenanceRunner.test.ts b/apps/server/src/provider/providerMaintenanceRunner.test.ts index 75c592b7811a1..2f108907f9647 100644 --- a/apps/server/src/provider/providerMaintenanceRunner.test.ts +++ b/apps/server/src/provider/providerMaintenanceRunner.test.ts @@ -698,6 +698,7 @@ it.effect.each([ "verifies the $mode OpenCode update using the refreshed installation", ({ mode, version, installation, status }) => { const calls: string[] = []; + const targets: Array = []; const capabilities = lifecycleFor(OPENCODE_DRIVER); const pinnedCapabilities = { ...capabilities, @@ -722,6 +723,10 @@ it.effect.each([ const updater = yield* makeTestRunner( { ...registry, + setProviderMaintenanceActionState: (input) => { + targets.push(input.targetVersion); + return registry.setProviderMaintenanceActionState(input); + }, getProviderMaintenanceCapabilitiesForInstance: () => Effect.succeed(pinnedCapabilities), refreshAfterUpdate: () => Effect.gen(function* () { @@ -752,6 +757,8 @@ it.effect.each([ assert.deepStrictEqual(calls, [ `npm install -g opencode-ai@${mode === "default" ? "latest" : "1.14.20"}`, ]); + assert.ok(targets.length > 0); + assert.ok(targets.every((target) => target === (mode === "default" ? undefined : "1.14.20"))); const provider = result.providers[0]; assert.ok(provider); assert.strictEqual(provider.updateState?.status, status); diff --git a/apps/server/src/provider/providerMaintenanceRunner.ts b/apps/server/src/provider/providerMaintenanceRunner.ts index 9dddf23579d8a..f49ee393e44b7 100644 --- a/apps/server/src/provider/providerMaintenanceRunner.ts +++ b/apps/server/src/provider/providerMaintenanceRunner.ts @@ -391,6 +391,7 @@ export const make = Effect.fn("ProviderMaintenanceRunner.make")(function* () { instanceId, action: "update", state, + targetVersion, }); const setQueuedState = setUpdateState( makeUpdateState({