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
27 changes: 26 additions & 1 deletion apps/server/src/persistence/Layers/OrchestrationEventStore.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
CommandId,
DEFAULT_PROVIDER_KIND,
EventId,
IsoDateTime,
NonNegativeInt,
Expand All @@ -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";
Expand All @@ -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<T>(row: T): T {
const r = row as Record<string, unknown>;
const payload = r.payload;
if (typeof payload !== "object" || payload === null) return row;
const p = payload as Record<string, unknown>;
let patched: Record<string, unknown> | null = null;
for (const key of ["modelSelection", "defaultModelSelection"] as const) {
const ms = p[key] as Record<string, unknown> | 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);

Expand Down Expand Up @@ -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"),
),
Expand Down
24 changes: 15 additions & 9 deletions apps/server/src/provider/Layers/ProviderSessionDirectory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProviderKind, ProviderSessionDirectoryPersistenceError> {
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<string, unknown> {
Expand Down
4 changes: 4 additions & 0 deletions packages/contracts/src/orchestration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading