diff --git a/apps/server/src/persistence/Layers/OrchestrationEventStore.ts b/apps/server/src/persistence/Layers/OrchestrationEventStore.ts index 4d81cf5e8d..e91ad5a719 100644 --- a/apps/server/src/persistence/Layers/OrchestrationEventStore.ts +++ b/apps/server/src/persistence/Layers/OrchestrationEventStore.ts @@ -1,5 +1,6 @@ import { CommandId, + DEFAULT_PROVIDER_KIND, EventId, IsoDateTime, NonNegativeInt, @@ -10,6 +11,7 @@ import { OrchestrationEventType, ProjectId, ThreadId, + isSupportedProvider, } from "@t3tools/contracts"; import * as SqlClient from "effect/unstable/sql/SqlClient"; import * as SqlSchema from "effect/unstable/sql/SqlSchema"; @@ -26,6 +28,29 @@ import { } from "../Services/OrchestrationEventStore.ts"; const decodeEvent = Schema.decodeUnknownEffect(OrchestrationEvent); + +/** + * Coerce unsupported provider names in `modelSelection` / + * `defaultModelSelection` payloads to the default provider so the strict + * `OrchestrationEvent` schema can always decode persisted events. This + * keeps the read path tolerant of legacy or future provider data without + * widening the canonical types. + */ +function coerceUnsupportedProviders(row: T): T { + const r = row as Record; + const payload = r.payload; + if (typeof payload !== "object" || payload === null) return row; + const p = payload as Record; + let patched: Record | null = null; + for (const key of ["modelSelection", "defaultModelSelection"] as const) { + const ms = p[key] as Record | null | undefined; + if (ms && typeof ms.provider === "string" && !isSupportedProvider(ms.provider)) { + patched ??= { ...p }; + patched[key] = { provider: DEFAULT_PROVIDER_KIND, model: ms.model }; + } + } + return patched ? ({ ...r, payload: patched } as T) : row; +} const UnknownFromJsonString = Schema.fromJsonString(Schema.Unknown); const EventMetadataFromJsonString = Schema.fromJsonString(OrchestrationEventMetadata); @@ -230,7 +255,7 @@ const makeEventStore = Effect.gen(function* () { ), Effect.flatMap((rows) => Effect.forEach(rows, (row) => - decodeEvent(row).pipe( + decodeEvent(coerceUnsupportedProviders(row)).pipe( Effect.mapError( toPersistenceDecodeError("OrchestrationEventStore.readFromSequence:rowToEvent"), ), diff --git a/apps/server/src/provider/Layers/ProviderSessionDirectory.ts b/apps/server/src/provider/Layers/ProviderSessionDirectory.ts index 961c63d696..47094ec389 100644 --- a/apps/server/src/provider/Layers/ProviderSessionDirectory.ts +++ b/apps/server/src/provider/Layers/ProviderSessionDirectory.ts @@ -18,19 +18,25 @@ function toPersistenceError(operation: string) { }); } +/** + * Accept any non-empty provider string. + * Legacy or future providers are passed through so the read path never fails; + * unsupported providers are simply cast — the caller is responsible for + * checking `isSupportedProvider` before dispatching adapter operations. + */ function decodeProviderKind( providerName: string, - operation: string, + _operation: string, ): Effect.Effect { - if (providerName === "codex" || providerName === "claudeAgent") { - return Effect.succeed(providerName); + if (providerName.trim().length === 0) { + return Effect.fail( + new ProviderSessionDirectoryPersistenceError({ + operation: _operation, + detail: `Provider name must be a non-empty string.`, + }), + ); } - return Effect.fail( - new ProviderSessionDirectoryPersistenceError({ - operation, - detail: `Unknown persisted provider '${providerName}'.`, - }), - ); + return Effect.succeed(providerName as ProviderKind); } function isRecord(value: unknown): value is Record { diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index 31631666e2..959a427eb6 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -29,6 +29,10 @@ export const ORCHESTRATION_WS_CHANNELS = { export const ProviderKind = Schema.Literals(["codex", "claudeAgent"]); export type ProviderKind = typeof ProviderKind.Type; + +export function isSupportedProvider(provider: string): provider is ProviderKind { + return provider === "codex" || provider === "claudeAgent"; +} export const ProviderApprovalPolicy = Schema.Literals([ "untrusted", "on-failure",