Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions apps/server/src/provider/Layers/ClaudeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ import {
} from "../providerSnapshot.ts";
import { compareCliVersions } from "../cliVersion.ts";
import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
import {
enrichProviderSnapshotWithVersionAdvisory,
getProviderVersionLifecycle,
} from "../providerVersionLifecycle.ts";
import { ClaudeProvider } from "../Services/ClaudeProvider.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { ServerSettingsError } from "@marcode/contracts";
Expand Down Expand Up @@ -914,6 +918,7 @@ export const ClaudeProviderLive = Layer.effect(
);

return yield* makeManagedServerProvider<ClaudeSettings>({
versionLifecycle: getProviderVersionLifecycle(PROVIDER),
getSettings: serverSettings.getSettings.pipe(
Effect.map((settings) => settings.providers.claudeAgent),
Effect.orDie,
Expand All @@ -924,6 +929,14 @@ export const ClaudeProviderLive = Layer.effect(
haveSettingsChanged: (previous, next) => !Equal.equals(previous, next),
initialSnapshot: makePendingClaudeProvider,
checkProvider,
enrichSnapshot: ({ snapshot, publishSnapshot }) =>
Effect.promise(() => enrichProviderSnapshotWithVersionAdvisory(snapshot)).pipe(
Effect.flatMap((enrichedSnapshot) =>
Equal.equals(enrichedSnapshot, snapshot)
? Effect.void
: publishSnapshot(enrichedSnapshot),
),
),
});
}),
);
13 changes: 13 additions & 0 deletions apps/server/src/provider/Layers/CodexProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ import { ServerSettingsError } from "@marcode/contracts";
import { createModelCapabilities } from "@marcode/shared/model";

import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
import {
enrichProviderSnapshotWithVersionAdvisory,
getProviderVersionLifecycle,
} from "../providerVersionLifecycle.ts";
import { buildServerProvider } from "../providerSnapshot.ts";
import { CodexProvider } from "../Services/CodexProvider.ts";
import { expandHomePath } from "../../pathExpansion.ts";
Expand Down Expand Up @@ -503,6 +507,7 @@ export const CodexProviderLive = Layer.effect(
);

return yield* makeManagedServerProvider<CodexSettings>({
versionLifecycle: getProviderVersionLifecycle(PROVIDER),
getSettings: serverSettings.getSettings.pipe(
Effect.map((settings) => settings.providers.codex),
Effect.orDie,
Expand All @@ -513,6 +518,14 @@ export const CodexProviderLive = Layer.effect(
haveSettingsChanged: (previous, next) => !Equal.equals(previous, next),
initialSnapshot: makePendingCodexProvider,
checkProvider,
enrichSnapshot: ({ snapshot, publishSnapshot }) =>
Effect.promise(() => enrichProviderSnapshotWithVersionAdvisory(snapshot)).pipe(
Effect.flatMap((enrichedSnapshot) =>
Equal.equals(enrichedSnapshot, snapshot)
? Effect.void
: publishSnapshot(enrichedSnapshot),
),
),
refreshInterval: Duration.minutes(5),
});
}),
Expand Down
76 changes: 51 additions & 25 deletions apps/server/src/provider/Layers/CursorProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ import {
type CommandResult,
} from "../providerSnapshot.ts";
import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
import {
enrichProviderSnapshotWithVersionAdvisory,
getProviderVersionLifecycle,
} from "../providerVersionLifecycle.ts";
import { CursorProvider } from "../Services/CursorProvider.ts";
import { AcpSessionRuntime } from "../acp/AcpSessionRuntime.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
Expand Down Expand Up @@ -1169,6 +1173,7 @@ export const CursorProviderLive = Layer.effect(
);

return yield* makeManagedServerProvider<CursorSettings>({
versionLifecycle: getProviderVersionLifecycle(PROVIDER),
getSettings: serverSettings.getSettings.pipe(
Effect.map((settings) => settings.providers.cursor),
Effect.orDie,
Expand All @@ -1180,37 +1185,58 @@ export const CursorProviderLive = Layer.effect(
initialSnapshot: buildInitialCursorProviderSnapshot,
checkProvider,
enrichSnapshot: ({ settings, snapshot, publishSnapshot }) => {
if (
!settings.enabled ||
snapshot.auth.status === "unauthenticated" ||
!snapshot.models.some((model) => !model.isCustom && !hasCursorModelCapabilities(model))
) {
return Effect.void;
}
const enrichVersionAdvisory = Effect.promise(() =>
enrichProviderSnapshotWithVersionAdvisory(snapshot),
).pipe(
Effect.flatMap((enrichedSnapshot) =>
Equal.equals(enrichedSnapshot, snapshot)
? Effect.succeed(enrichedSnapshot)
: publishSnapshot(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).pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
Effect.flatMap((discoveredModels) => {
if (discoveredModels.length === 0) {
return enrichVersionAdvisory.pipe(
Effect.flatMap((baseSnapshot) => {
if (
!settings.enabled ||
baseSnapshot.auth.status === "unauthenticated" ||
!baseSnapshot.models.some(
(model) => !model.isCustom && !hasCursorModelCapabilities(model),
)
) {
return Effect.void;
}

return publishSnapshot({
...snapshot,
models: providerModelsFromSettings(
discoveredModels,
PROVIDER,
settings.customModels,
EMPTY_CAPABILITIES,
return discoverCursorModelCapabilitiesViaAcp(settings, baseSnapshot.models).pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
Effect.flatMap((discoveredModels) => {
if (discoveredModels.length === 0) {
return Effect.void;
}

return publishSnapshot({
...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),
),
);
},
refreshInterval: CURSOR_REFRESH_INTERVAL,
Expand Down
13 changes: 13 additions & 0 deletions apps/server/src/provider/Layers/OpenCodeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import { createModelCapabilities } from "@marcode/shared/model";
import { ServerConfig } from "../../config.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
import {
enrichProviderSnapshotWithVersionAdvisory,
getProviderVersionLifecycle,
} from "../providerVersionLifecycle.ts";
import {
buildServerProvider,
nonEmptyTrimmed,
Expand Down Expand Up @@ -486,6 +490,7 @@ export const OpenCodeProviderLive = Layer.effect(
);

return yield* makeManagedServerProvider<OpenCodeSettings>({
versionLifecycle: getProviderVersionLifecycle(PROVIDER),
getSettings: getProviderSettings.pipe(Effect.orDie),
streamSettings: serverSettings.streamChanges.pipe(
Stream.map((settings) => settings.providers.opencode),
Expand All @@ -500,6 +505,14 @@ export const OpenCodeProviderLive = Layer.effect(
}),
),
),
enrichSnapshot: ({ snapshot, publishSnapshot }) =>
Effect.promise(() => enrichProviderSnapshotWithVersionAdvisory(snapshot)).pipe(
Effect.flatMap((enrichedSnapshot) =>
Equal.equals(enrichedSnapshot, snapshot)
? Effect.void
: publishSnapshot(enrichedSnapshot),
),
),
});
}),
);
64 changes: 64 additions & 0 deletions apps/server/src/provider/Layers/ProviderRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import { checkClaudeProviderStatus, parseClaudeAuthStatusFromOutput } from "./Cl
import {
haveProvidersChanged,
mergeProviderSnapshot,
mergeProviderSnapshots,
ProviderRegistryLive,
selectProvidersByKind,
} from "./ProviderRegistry.ts";
import { ServerConfig } from "../../config.ts";
import { ServerSettingsService, type ServerSettingsShape } from "../../serverSettings.ts";
Expand Down Expand Up @@ -427,6 +429,68 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest()))(
]);
});

it("persists merged provider snapshots for the providers that were refreshed", () => {
const previousProviders = [
{
provider: "cursor",
status: "ready",
enabled: true,
installed: true,
auth: { status: "authenticated" },
checkedAt: "2026-04-14T00:00:00.000Z",
version: "2026.04.09-f2b0fcd",
models: [
{
slug: "claude-opus-4-6",
name: "Opus 4.6",
isCustom: false,
capabilities: createModelCapabilities({
optionDescriptors: [
selectDescriptor("reasoning", "Reasoning", [
{ id: "high", label: "High", isDefault: true },
]),
booleanDescriptor("fastMode", "Fast Mode"),
booleanDescriptor("thinking", "Thinking"),
],
}),
},
],
slashCommands: [],
skills: [],
},
{
provider: "codex",
status: "ready",
enabled: true,
installed: true,
auth: { status: "authenticated" },
checkedAt: "2026-04-14T00:00:00.000Z",
version: "1.0.0",
models: [],
slashCommands: [],
skills: [],
},
] as const satisfies ReadonlyArray<ServerProvider>;
const refreshedCursor = {
...previousProviders[0],
checkedAt: "2026-04-14T00:01:00.000Z",
models: [],
} satisfies ServerProvider;

const mergedProviders = mergeProviderSnapshots(previousProviders, [refreshedCursor]);
const persistedProviders = selectProvidersByKind(
mergedProviders,
new Set<ServerProvider["provider"]>(["cursor"]),
);

assert.deepStrictEqual(persistedProviders, [
{
...refreshedCursor,
models: [...previousProviders[0].models],
},
]);
});

it.effect("probes enabled providers in the background during registry startup", () =>
Effect.gen(function* () {
let spawnCount = 0;
Expand Down
Loading
Loading