Skip to content
Open
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
2 changes: 1 addition & 1 deletion apps/server/src/provider/Drivers/ClaudeDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
haveSettingsChanged: () => false,
initialSnapshot: (settings) =>
makePendingClaudeProvider(settings).pipe(Effect.map(stampIdentity)),
checkProvider,
checkProvider: () => checkProvider,
enrichSnapshot: ({ snapshot, publishSnapshot }) =>
enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities).pipe(
Effect.provideService(HttpClient.HttpClient, httpClient),
Expand Down
15 changes: 11 additions & 4 deletions apps/server/src/provider/Drivers/CodexDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const httpClient = yield* HttpClient.HttpClient;
const eventLoggers = yield* ProviderEventLoggers;
const serverConfig = yield* ServerConfig;
const processEnv = mergeProviderInstanceEnvironment(environment);
const homeLayout = yield* resolveCodexHomeLayout(config);
const continuationIdentity = codexContinuationIdentity(homeLayout);
Expand Down Expand Up @@ -159,10 +160,16 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {
// in as instance rebuilds from the registry rather than in-place
// updates. Pre-provide `ChildProcessSpawner` so the check fits
// `makeManagedServerProvider.checkProvider`'s `R = never`.
const checkProvider = checkCodexProviderStatus(effectiveConfig, undefined, processEnv).pipe(
Effect.map(stampIdentity),
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
);
const checkProvider = (refreshInput?: { readonly cwd?: string | undefined }) =>
checkCodexProviderStatus(
effectiveConfig,
undefined,
processEnv,
refreshInput?.cwd ?? serverConfig.cwd,
).pipe(
Effect.map(stampIdentity),
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
);
const snapshot = yield* makeManagedServerProvider<CodexSettings>({
maintenanceCapabilities,
getSettings: Effect.succeed(effectiveConfig),
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/provider/Drivers/CursorDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {
haveSettingsChanged: () => false,
initialSnapshot: (settings) =>
buildInitialCursorProviderSnapshot(settings).pipe(Effect.map(stampIdentity)),
checkProvider,
checkProvider: () => checkProvider,
// Model catalog and capabilities come exclusively from Cursor's
// list_available_models extension method during provider checks.
enrichSnapshot: ({ settings, snapshot: currentSnapshot, publishSnapshot }) =>
Expand Down
11 changes: 6 additions & 5 deletions apps/server/src/provider/Drivers/OpenCodeDriver.ts
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,12 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv>
});
const textGeneration = yield* makeOpenCodeTextGeneration(effectiveConfig, processEnv);

const checkProvider = checkOpenCodeProviderStatus(
effectiveConfig,
serverConfig.cwd,
processEnv,
).pipe(Effect.map(stampIdentity), Effect.provideService(OpenCodeRuntime, openCodeRuntime));
const checkProvider = (refreshInput?: { readonly cwd?: string | undefined }) =>
checkOpenCodeProviderStatus(
effectiveConfig,
refreshInput?.cwd ?? serverConfig.cwd,
processEnv,
).pipe(Effect.map(stampIdentity), Effect.provideService(OpenCodeRuntime, openCodeRuntime));

const snapshot = yield* makeManagedServerProvider<OpenCodeSettings>({
maintenanceCapabilities,
Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/provider/Layers/CodexProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu
ChildProcessSpawner.ChildProcessSpawner | Scope.Scope
> = probeCodexAppServerProvider,
environment: NodeJS.ProcessEnv = process.env,
cwd: string = process.cwd(),
): Effect.fn.Return<
ServerProviderDraft,
ServerSettingsError,
Expand Down Expand Up @@ -446,7 +447,7 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu
const probeResult = yield* probe({
binaryPath: codexSettings.binaryPath,
homePath: codexSettings.homePath,
cwd: process.cwd(),
cwd,
customModels: codexSettings.customModels,
environment,
}).pipe(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ const makeFakeInstance = (
packageName: null,
}),
getSnapshot: Effect.succeed({} as unknown as ServerProvider),
refresh: Effect.succeed({} as unknown as ServerProvider),
refresh: () => Effect.succeed({} as unknown as ServerProvider),
streamChanges: Stream.empty,
},
adapter,
Expand Down
99 changes: 96 additions & 3 deletions apps/server/src/provider/Layers/ProviderRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { readProviderStatusCache, resolveProviderStatusCachePath } from "../prov
import type { ProviderInstance } from "../ProviderDriver.ts";
import { ProviderInstanceRegistry } from "../Services/ProviderInstanceRegistry.ts";
import { ProviderRegistry } from "../Services/ProviderRegistry.ts";
import type { ProviderSnapshotRefreshInput } from "../Services/ServerProvider.ts";
import { makeManualOnlyProviderMaintenanceCapabilities } from "../providerMaintenance.ts";
const decodeServerSettings = Schema.decodeSync(ServerSettings);
const encodeServerSettings = Schema.encodeSync(ServerSettings);
Expand Down Expand Up @@ -368,6 +369,23 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
}),
);

it.effect("passes the supplied cwd to the app-server probe", () =>
Effect.gen(function* () {
let capturedCwd: string | null = null;
yield* checkCodexProviderStatus(
defaultCodexSettings,
(input) => {
capturedCwd = input.cwd;
return Effect.succeed(makeCodexProbeSnapshot());
},
process.env,
"/workspace/project",
);

assert.strictEqual(capturedCwd, "/workspace/project");
}),
);

it.effect(
"returns ready with unknown auth when app-server does not require OpenAI auth",
() =>
Expand Down Expand Up @@ -709,7 +727,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
packageName: null,
}),
getSnapshot: Effect.succeed(initialProvider),
refresh: Effect.succeed(refreshedProvider),
refresh: () => Effect.succeed(refreshedProvider),
streamChanges: Stream.fromPubSub(changes),
},
adapter: {} as ProviderInstance["adapter"],
Expand Down Expand Up @@ -803,7 +821,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
packageName: null,
}),
getSnapshot: Effect.succeed(cachedProvider),
refresh: Effect.die(new Error("simulated refresh failure")),
refresh: () => Effect.die(new Error("simulated refresh failure")),
streamChanges: Stream.empty,
},
adapter: {} as ProviderInstance["adapter"],
Expand Down Expand Up @@ -845,6 +863,81 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
}),
);

it.effect("forwards manual refresh input to the targeted provider instance", () =>
Effect.gen(function* () {
const codexDriver = ProviderDriverKind.make("codex");
const codexInstanceId = ProviderInstanceId.make("codex");
const refreshInputs = yield* Ref.make<Array<string | undefined>>([]);
const provider = {
instanceId: codexInstanceId,
driver: codexDriver,
status: "ready",
enabled: true,
installed: true,
auth: { status: "authenticated" },
checkedAt: "2026-04-29T10:00:00.000Z",
version: "1.0.0",
models: [],
slashCommands: [],
skills: [],
} as const satisfies ServerProvider;
const instance = {
instanceId: codexInstanceId,
driverKind: codexDriver,
continuationIdentity: {
driverKind: codexDriver,
continuationKey: "codex:instance:codex",
},
displayName: undefined,
enabled: true,
snapshot: {
maintenanceCapabilities: makeManualOnlyProviderMaintenanceCapabilities({
provider: codexDriver,
packageName: null,
}),
getSnapshot: Effect.succeed(provider),
refresh: (input?: ProviderSnapshotRefreshInput) =>
Ref.update(refreshInputs, (inputs) => [...inputs, input?.cwd]).pipe(
Effect.as(provider),
),
streamChanges: Stream.empty,
},
adapter: {} as ProviderInstance["adapter"],
textGeneration: {} as ProviderInstance["textGeneration"],
} satisfies ProviderInstance;
const instanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry, {
getInstance: (instanceId) =>
Effect.succeed(instanceId === codexInstanceId ? instance : undefined),
listInstances: Effect.succeed([instance]),
listUnavailable: Effect.succeed([]),
streamChanges: Stream.empty,
subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) =>
PubSub.subscribe(pubsub),
),
});
const scope = yield* Scope.make();
yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
const runtimeServices = yield* Layer.build(
ProviderRegistryLive.pipe(
Layer.provideMerge(instanceRegistryLayer),
Layer.provideMerge(
ServerConfig.layerTest(process.cwd(), {
prefix: "t3-provider-registry-refresh-input-",
}),
),
Layer.provideMerge(NodeServices.layer),
),
).pipe(Scope.provide(scope));

yield* Effect.gen(function* () {
const registry = yield* ProviderRegistry;
yield* registry.refreshInstance(codexInstanceId, { cwd: "/workspace/project" });

assert.strictEqual((yield* Ref.get(refreshInputs)).at(-1), "/workspace/project");
}).pipe(Effect.provide(runtimeServices));
}),
);

it.effect("keeps consuming registry changes after one sync fails", () =>
Effect.gen(function* () {
const codexDriver = ProviderDriverKind.make("codex");
Expand Down Expand Up @@ -892,7 +985,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
packageName: null,
}),
getSnapshot: Effect.succeed(provider),
refresh: Effect.succeed(provider),
refresh: () => Effect.succeed(provider),
streamChanges: Stream.empty,
},
adapter: {} as ProviderInstance["adapter"],
Expand Down
40 changes: 24 additions & 16 deletions apps/server/src/provider/Layers/ProviderRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import * as Semaphore from "effect/Semaphore";
import { ServerConfig } from "../../config.ts";
import { ProviderInstanceRegistry } from "../Services/ProviderInstanceRegistry.ts";
import { ProviderRegistry, type ProviderRegistryShape } from "../Services/ProviderRegistry.ts";
import type { ProviderSnapshotRefreshInput } from "../Services/ServerProvider.ts";
import {
hydrateCachedProvider,
isCachedProviderCorrelated,
Expand Down Expand Up @@ -429,27 +430,33 @@ export const ProviderRegistryLive = Layer.effect(

const refreshOneSource = Effect.fn("refreshOneSource")(function* (
providerSource: ProviderSnapshotSource,
input?: ProviderSnapshotRefreshInput,
) {
return yield* providerSource.refresh.pipe(
Effect.flatMap((nextProvider) =>
correlateSnapshotWithSource(providerSource, nextProvider).pipe(
Effect.flatMap(syncProvider),
return yield* providerSource
.refresh(input)
.pipe(
Effect.flatMap((nextProvider) =>
correlateSnapshotWithSource(providerSource, nextProvider).pipe(
Effect.flatMap(syncProvider),
),
),
),
);
);
});

const refreshAll = Effect.fn("refreshAll")(function* () {
const refreshAll = Effect.fn("refreshAll")(function* (input?: ProviderSnapshotRefreshInput) {
const sources = yield* getLiveSources;
return yield* Effect.forEach(sources, (source) => refreshOneSource(source), {
return yield* Effect.forEach(sources, (source) => refreshOneSource(source, input), {
concurrency: "unbounded",
discard: true,
}).pipe(Effect.andThen(Ref.get(providersRef)));
});

const refresh = Effect.fn("refresh")(function* (provider?: ProviderDriverKind) {
const refresh = Effect.fn("refresh")(function* (
provider?: ProviderDriverKind,
input?: ProviderSnapshotRefreshInput,
) {
if (provider === undefined) {
return yield* refreshAll();
return yield* refreshAll(input);
}
// Kind-scoped refreshes target the default instance for that driver.
const defaultInstanceId = defaultInstanceIdForDriver(provider);
Expand All @@ -460,18 +467,19 @@ export const ProviderRegistryLive = Layer.effect(
if (!providerSource) {
return yield* Ref.get(providersRef);
}
return yield* refreshOneSource(providerSource);
return yield* refreshOneSource(providerSource, input);
});

const refreshInstance = Effect.fn("refreshInstance")(function* (
instanceId: ProviderInstanceId,
input?: ProviderSnapshotRefreshInput,
) {
const sources = yield* getLiveSources;
const providerSource = sources.find((candidate) => candidate.instanceId === instanceId);
if (!providerSource) {
return yield* Ref.get(providersRef);
}
return yield* refreshOneSource(providerSource);
return yield* refreshOneSource(providerSource, input);
});

const getProviderMaintenanceCapabilitiesForInstance = Effect.fn(
Expand Down Expand Up @@ -681,10 +689,10 @@ export const ProviderRegistryLive = Layer.effect(

return {
getProviders: Ref.get(providersRef),
refresh: (provider?: ProviderDriverKind) =>
refresh(provider).pipe(Effect.catchCause(recoverRefreshFailure)),
refreshInstance: (instanceId: ProviderInstanceId) =>
refreshInstance(instanceId).pipe(Effect.catchCause(recoverRefreshFailure)),
refresh: (provider?: ProviderDriverKind, input?: ProviderSnapshotRefreshInput) =>
refresh(provider, input).pipe(Effect.catchCause(recoverRefreshFailure)),
refreshInstance: (instanceId: ProviderInstanceId, input?: ProviderSnapshotRefreshInput) =>
refreshInstance(instanceId, input).pipe(Effect.catchCause(recoverRefreshFailure)),
getProviderMaintenanceCapabilitiesForInstance,
setProviderMaintenanceActionState,
get streamChanges() {
Expand Down
7 changes: 6 additions & 1 deletion apps/server/src/provider/Services/ProviderRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import * as Context from "effect/Context";
import type * as Effect from "effect/Effect";
import type * as Stream from "effect/Stream";
import type { ProviderMaintenanceCapabilities } from "../providerMaintenance.ts";
import type { ProviderSnapshotRefreshInput } from "./ServerProvider.ts";

export type ProviderMaintenanceActionKind = "update";

Expand All @@ -36,7 +37,10 @@ export interface ProviderRegistryShape {
*
* @deprecated prefer `refreshInstance` for new call sites.
*/
readonly refresh: (provider?: ProviderDriverKind) => Effect.Effect<ReadonlyArray<ServerProvider>>;
readonly refresh: (
provider?: ProviderDriverKind,
input?: ProviderSnapshotRefreshInput,
) => Effect.Effect<ReadonlyArray<ServerProvider>>;

/**
* Refresh the specific configured instance. Returns the updated snapshot
Expand All @@ -46,6 +50,7 @@ export interface ProviderRegistryShape {
*/
readonly refreshInstance: (
instanceId: ProviderInstanceId,
input?: ProviderSnapshotRefreshInput,
) => Effect.Effect<ReadonlyArray<ServerProvider>>;

/**
Expand Down
6 changes: 5 additions & 1 deletion apps/server/src/provider/Services/ServerProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ import type * as Effect from "effect/Effect";
import type * as Stream from "effect/Stream";
import type { ProviderMaintenanceCapabilities } from "../providerMaintenance.ts";

export interface ProviderSnapshotRefreshInput {
readonly cwd?: string | undefined;
}

export interface ServerProviderShape {
readonly maintenanceCapabilities: ProviderMaintenanceCapabilities;
readonly getSnapshot: Effect.Effect<ServerProvider>;
readonly refresh: Effect.Effect<ServerProvider>;
readonly refresh: (input?: ProviderSnapshotRefreshInput) => Effect.Effect<ServerProvider>;
readonly streamChanges: Stream.Stream<ServerProvider>;
}
Loading
Loading