Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6f3bd89
Add provider version advisories
justsomelegs Apr 22, 2026
c0c89e5
more
justsomelegs Apr 22, 2026
9121fc8
Improve provider update advisories
justsomelegs Apr 22, 2026
cc1206d
Refine provider update advisories
justsomelegs Apr 23, 2026
9d32e68
Refine provider update toast and pill flow
justsomelegs Apr 23, 2026
597b8b5
dev path removal
justsomelegs Apr 23, 2026
06ead24
unknown version case
justsomelegs Apr 23, 2026
2db6e4a
update success copy
justsomelegs Apr 23, 2026
289a64f
Fix provider registry test capabilities after rebase
justsomelegs Apr 23, 2026
d34bde0
Fix settings browser tests after smooth scroll
justsomelegs Apr 23, 2026
149a5c1
Fix provider advisory rebase fallout
justsomelegs Apr 30, 2026
e75b6e6
Make provider updates binary-resolution aware
justsomelegs Apr 30, 2026
6824618
Fix shared provider update lock race
justsomelegs Apr 30, 2026
8b10d0e
Release provider updates on unsupported lock keys
justsomelegs Apr 30, 2026
80fb17f
Avoid stale queued state on unsupported update locks
justsomelegs Apr 30, 2026
1179cdc
Fix post-rebase import cleanup
justsomelegs May 4, 2026
5591cfc
Improve provider update notification UX
justsomelegs May 4, 2026
58eb8f1
Fix provider update edge cases
justsomelegs May 4, 2026
c6ed36f
Merge branch 'main' into feature/provider-update-advisories
juliusmarminge May 4, 2026
1589e67
Add provider update advisories and lifecycle detection
juliusmarminge May 4, 2026
544cddd
Add per-instance provider update advisories
juliusmarminge May 4, 2026
a41d4a9
Refactor provider update advisories to lifecycle helpers
juliusmarminge May 4, 2026
5d2613b
Rename provider lifecycle advisory test fixtures
juliusmarminge May 4, 2026
190e611
Rename provider version lifecycle to maintenance capabilities
juliusmarminge May 4, 2026
57c7071
Scope provider update advisories by instance
juliusmarminge May 4, 2026
21f9fb5
Fix provider update instance notification tracking
justsomelegs May 5, 2026
ad4a577
Merge branch 'main' into feature/provider-update-advisories
justsomelegs May 5, 2026
59275e5
Refactor provider updates into maintenance runner
juliusmarminge May 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/desktop/src/clientPersistence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const clientSettings: ClientSettings = {
autoOpenPlanSidebar: false,
confirmThreadArchive: true,
confirmThreadDelete: false,
dismissedProviderUpdateNotificationKeys: [],
diffIgnoreWhitespace: true,
diffWordWrap: true,
favorites: [],
Expand Down
36 changes: 36 additions & 0 deletions apps/server/src/provider/Drivers/ClaudeDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,39 @@ import {
} from "../ProviderDriver.ts";
import type { ServerProviderDraft } from "../providerSnapshot.ts";
import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
import {
enrichProviderSnapshotWithVersionAdvisory,
makePackageManagedProviderMaintenanceResolver,
normalizeCommandPath,
resolveProviderMaintenanceCapabilitiesEffect,
} from "../providerMaintenance.ts";
import { makeClaudeCapabilitiesCacheKey, makeClaudeContinuationGroupKey } from "./ClaudeHome.ts";

const DRIVER_KIND = ProviderDriverKind.make("claudeAgent");
const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5);
const CAPABILITIES_PROBE_TTL = Duration.minutes(5);

function isClaudeNativeCommandPath(commandPath: string): boolean {
const normalized = normalizeCommandPath(commandPath);
return (
normalized.endsWith("/.local/bin/claude") ||
normalized.endsWith("/.local/bin/claude.exe") ||
normalized.includes("/.local/share/claude/")
);
}

const UPDATE = makePackageManagedProviderMaintenanceResolver({
provider: DRIVER_KIND,
npmPackageName: "@anthropic-ai/claude-code",
homebrewFormula: "claude-code",
nativeUpdate: {
executable: "claude",
args: ["update"],
lockKey: "claude-native",
isCommandPath: isClaudeNativeCommandPath,
},
});

export type ClaudeDriverEnv =
| ChildProcessSpawner.ChildProcessSpawner
| FileSystem.FileSystem
Expand Down Expand Up @@ -82,6 +109,10 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
instanceId,
});
const effectiveConfig = { ...config, enabled } satisfies ClaudeSettings;
const maintenanceCapabilities = yield* resolveProviderMaintenanceCapabilitiesEffect(UPDATE, {
binaryPath: effectiveConfig.binaryPath,
env: processEnv,
});
const continuationGroupKey = yield* makeClaudeContinuationGroupKey(effectiveConfig);
const stampIdentity = withInstanceIdentity({
instanceId,
Expand Down Expand Up @@ -121,11 +152,16 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
);

const snapshot = yield* makeManagedServerProvider<ClaudeSettings>({
maintenanceCapabilities,
getSettings: Effect.succeed(effectiveConfig),
streamSettings: Stream.never,
haveSettingsChanged: () => false,
initialSnapshot: (settings) => stampIdentity(makePendingClaudeProvider(settings)),
checkProvider,
enrichSnapshot: ({ snapshot, publishSnapshot }) =>
enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities).pipe(
Effect.flatMap((enrichedSnapshot) => publishSnapshot(enrichedSnapshot)),
),
refreshInterval: SNAPSHOT_REFRESH_INTERVAL,
}).pipe(
Effect.mapError(
Expand Down
20 changes: 20 additions & 0 deletions apps/server/src/provider/Drivers/CodexDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
import type { ProviderDriver, ProviderInstance } from "../ProviderDriver.ts";
import type { ServerProviderDraft } from "../providerSnapshot.ts";
import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
import {
enrichProviderSnapshotWithVersionAdvisory,
makePackageManagedProviderMaintenanceResolver,
resolveProviderMaintenanceCapabilitiesEffect,
} from "../providerMaintenance.ts";
import {
codexContinuationIdentity,
materializeCodexShadowHome,
Expand All @@ -43,6 +48,12 @@ import {

const DRIVER_KIND = ProviderDriverKind.make("codex");
const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5);
const UPDATE = makePackageManagedProviderMaintenanceResolver({
provider: DRIVER_KIND,
npmPackageName: "@openai/codex",
homebrewFormula: "codex",
nativeUpdate: null,
});

/**
* Services the driver needs to materialize an instance. Surfaced as the
Expand Down Expand Up @@ -115,6 +126,10 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {
enabled,
homePath: homeLayout.effectiveHomePath ?? "",
} satisfies CodexSettings;
const maintenanceCapabilities = yield* resolveProviderMaintenanceCapabilitiesEffect(UPDATE, {
binaryPath: effectiveConfig.binaryPath,
env: processEnv,
});

// `makeCodexAdapter` and `makeCodexTextGeneration` have `never` error
// channels at construction time — their failure modes are all on the
Expand All @@ -138,11 +153,16 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
);
const snapshot = yield* makeManagedServerProvider<CodexSettings>({
maintenanceCapabilities,
getSettings: Effect.succeed(effectiveConfig),
streamSettings: Stream.never,
haveSettingsChanged: () => false,
initialSnapshot: (settings) => stampIdentity(makePendingCodexProvider(settings)),
checkProvider,
enrichSnapshot: ({ snapshot, publishSnapshot }) =>
enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities).pipe(
Effect.flatMap((enrichedSnapshot) => publishSnapshot(enrichedSnapshot)),
),
refreshInterval: SNAPSHOT_REFRESH_INTERVAL,
}).pipe(
Effect.mapError(
Expand Down
19 changes: 19 additions & 0 deletions apps/server/src/provider/Drivers/CursorDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,23 @@ import {
} from "../ProviderDriver.ts";
import type { ServerProviderDraft } from "../providerSnapshot.ts";
import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
import {
makeProviderMaintenanceCapabilities,
makeStaticProviderMaintenanceResolver,
resolveProviderMaintenanceCapabilitiesEffect,
} from "../providerMaintenance.ts";

const DRIVER_KIND = ProviderDriverKind.make("cursor");
const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5);
const UPDATE = makeStaticProviderMaintenanceResolver(
makeProviderMaintenanceCapabilities({
provider: DRIVER_KIND,
packageName: null,
updateExecutable: "agent",
updateArgs: ["update"],
updateLockKey: "cursor-agent",
}),
);

export type CursorDriverEnv =
| ChildProcessSpawner.ChildProcessSpawner
Expand Down Expand Up @@ -87,6 +101,10 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {
continuationGroupKey: continuationIdentity.continuationKey,
});
const effectiveConfig = { ...config, enabled } satisfies CursorSettings;
const maintenanceCapabilities = yield* resolveProviderMaintenanceCapabilitiesEffect(UPDATE, {
binaryPath: effectiveConfig.binaryPath,
env: processEnv,
});

const adapter = yield* makeCursorAdapter(effectiveConfig, {
environment: processEnv,
Expand All @@ -103,6 +121,7 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {
);

const snapshot = yield* makeManagedServerProvider<CursorSettings>({
maintenanceCapabilities,
getSettings: Effect.succeed(effectiveConfig),
streamSettings: Stream.never,
haveSettingsChanged: () => false,
Expand Down
35 changes: 35 additions & 0 deletions apps/server/src/provider/Drivers/OpenCodeDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,36 @@ import {
} from "../ProviderDriver.ts";
import type { ServerProviderDraft } from "../providerSnapshot.ts";
import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
import {
enrichProviderSnapshotWithVersionAdvisory,
makePackageManagedProviderMaintenanceResolver,
normalizeCommandPath,
resolveProviderMaintenanceCapabilitiesEffect,
} from "../providerMaintenance.ts";

const DRIVER_KIND = ProviderDriverKind.make("opencode");
const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5);

function isOpenCodeNativeCommandPath(commandPath: string): boolean {
const normalized = normalizeCommandPath(commandPath);
return (
normalized.endsWith("/.opencode/bin/opencode") ||
normalized.endsWith("/.opencode/bin/opencode.exe")
);
}

const UPDATE = makePackageManagedProviderMaintenanceResolver({
provider: DRIVER_KIND,
npmPackageName: "opencode-ai",
homebrewFormula: "anomalyco/tap/opencode",
nativeUpdate: {
executable: "opencode",
args: ["upgrade"],
lockKey: "opencode-native",
isCommandPath: isOpenCodeNativeCommandPath,
},
});

export type OpenCodeDriverEnv =
| ChildProcessSpawner.ChildProcessSpawner
| FileSystem.FileSystem
Expand Down Expand Up @@ -87,6 +113,10 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv>
continuationGroupKey: continuationIdentity.continuationKey,
});
const effectiveConfig = { ...config, enabled } satisfies OpenCodeSettings;
const maintenanceCapabilities = yield* resolveProviderMaintenanceCapabilitiesEffect(UPDATE, {
binaryPath: effectiveConfig.binaryPath,
env: processEnv,
});

const adapter = yield* makeOpenCodeAdapter(effectiveConfig, {
instanceId,
Expand All @@ -102,11 +132,16 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv>
).pipe(Effect.map(stampIdentity), Effect.provideService(OpenCodeRuntime, openCodeRuntime));

const snapshot = yield* makeManagedServerProvider<OpenCodeSettings>({
maintenanceCapabilities,
getSettings: Effect.succeed(effectiveConfig),
streamSettings: Stream.never,
haveSettingsChanged: () => false,
initialSnapshot: (settings) => stampIdentity(makePendingOpenCodeProvider(settings)),
checkProvider,
enrichSnapshot: ({ snapshot, publishSnapshot }) =>
enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities).pipe(
Effect.flatMap((enrichedSnapshot) => publishSnapshot(enrichedSnapshot)),
),
refreshInterval: SNAPSHOT_REFRESH_INTERVAL,
}).pipe(
Effect.mapError(
Expand Down
1 change: 0 additions & 1 deletion apps/server/src/provider/Layers/CodexProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import type {
import { ServerSettingsError } from "@t3tools/contracts";

import { createModelCapabilities } from "@t3tools/shared/model";

import { buildServerProvider, type ServerProviderDraft } from "../providerSnapshot.ts";
import { expandHomePath } from "../../pathExpansion.ts";
import { scopedSafeTeardown } from "./scopedSafeTeardown.ts";
Expand Down
70 changes: 45 additions & 25 deletions apps/server/src/provider/Layers/CursorProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
type CommandResult,
type ServerProviderDraft,
} from "../providerSnapshot.ts";
import { enrichProviderSnapshotWithVersionAdvisory } from "../providerMaintenance.ts";
import { AcpSessionRuntime } from "../acp/AcpSessionRuntime.ts";

const PROVIDER = ProviderDriverKind.make("cursor");
Expand Down Expand Up @@ -1222,36 +1223,55 @@ export const enrichCursorSnapshot = (input: {
const { settings, snapshot, publishSnapshot } = input;
const stampIdentity = input.stampIdentity ?? ((value) => value);

if (
!settings.enabled ||
snapshot.auth.status === "unauthenticated" ||
!hasUncapturedCursorModels(snapshot)
) {
return Effect.void;
}
const enrichVersionAdvisory = enrichProviderSnapshotWithVersionAdvisory(snapshot).pipe(
Comment thread
cursor[bot] marked this conversation as resolved.
Effect.flatMap((enrichedSnapshot) =>
publishSnapshot(stampIdentity(enrichedSnapshot)).pipe(Effect.as(enrichedSnapshot)),
),
Effect.catchCause((cause) =>
Effect.logWarning("Cursor version advisory enrichment failed", {
cause: Cause.pretty(cause),
}).pipe(Effect.as(snapshot)),
),
);

return discoverCursorModelCapabilitiesViaAcp(settings, snapshot.models, input.environment).pipe(
Effect.flatMap((discoveredModels) => {
if (discoveredModels.length === 0) {
return enrichVersionAdvisory.pipe(
Effect.flatMap((baseSnapshot) => {
if (
!settings.enabled ||
baseSnapshot.auth.status === "unauthenticated" ||
!hasUncapturedCursorModels(baseSnapshot)
) {
return Effect.void;
}
return publishSnapshot(
stampIdentity({
...snapshot,
models: providerModelsFromSettings(
discoveredModels,
PROVIDER,
settings.customModels,
EMPTY_CAPABILITIES,
),

return discoverCursorModelCapabilitiesViaAcp(
settings,
baseSnapshot.models,
input.environment,
).pipe(
Effect.flatMap((discoveredModels) => {
if (discoveredModels.length === 0) {
return Effect.void;
}
return publishSnapshot(
stampIdentity({
...baseSnapshot,
models: providerModelsFromSettings(
discoveredModels,
PROVIDER,
settings.customModels,
EMPTY_CAPABILITIES,
),
}),
);
}),
Effect.catchCause((cause) =>
Effect.logWarning("Cursor ACP background capability enrichment failed", {
models: baseSnapshot.models.map((model) => model.slug),
cause: Cause.pretty(cause),
}).pipe(Effect.asVoid),
),
);
}),
Effect.catchCause((cause) =>
Effect.logWarning("Cursor ACP background capability enrichment failed", {
models: snapshot.models.map((model) => model.slug),
cause: Cause.pretty(cause),
}).pipe(Effect.asVoid),
),
);
};
1 change: 0 additions & 1 deletion apps/server/src/provider/Layers/OpenCodeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
import { Cause, Data, Effect } from "effect";

import { createModelCapabilities } from "@t3tools/shared/model";

import {
buildServerProvider,
nonEmptyTrimmed,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { OpenCodeAdapterShape } from "../Services/OpenCodeAdapter.ts";
import { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts";
import { ProviderInstanceRegistry } from "../Services/ProviderInstanceRegistry.ts";
import type { ProviderInstance } from "../ProviderDriver.ts";
import { makeManualOnlyProviderMaintenanceCapabilities } from "../providerMaintenance.ts";
import type { TextGenerationShape } from "../../textGeneration/TextGeneration.ts";
import { ProviderAdapterRegistryLive } from "./ProviderAdapterRegistry.ts";
import * as NodeServices from "@effect/platform-node/NodeServices";
Expand Down Expand Up @@ -111,6 +112,10 @@ const makeFakeInstance = (
displayName: undefined,
enabled: true,
snapshot: {
maintenanceCapabilities: makeManualOnlyProviderMaintenanceCapabilities({
provider: driverKind,
packageName: null,
}),
getSnapshot: Effect.succeed({} as unknown as ServerProvider),
refresh: Effect.succeed({} as unknown as ServerProvider),
streamChanges: Stream.empty,
Expand Down
Loading
Loading