Persist provider-aware model selections#1371
Conversation
- Replace model-only fields with provider/model selections across orchestration - Add projection schema and migration updates for provider-backed snapshots - Update server and web tests to use the new selection shape
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Model picker clears reasoning effort and fast mode
- Preserved existing composerDraft.modelSelection.options in onProviderModelSelect when the provider remains the same, preventing reasoning effort and fast mode settings from being silently cleared on model switch.
Or push these changes by commenting:
@cursor push 569dd7bcab
Preview (569dd7bcab)
diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -3135,10 +3135,14 @@
return;
}
const resolvedModel = resolveAppModelSelection(provider, customModelsByProvider, model);
+ const existingOptions = composerDraft.modelSelection?.options;
+ const preserveOptions =
+ existingOptions !== undefined && composerDraft.modelSelection?.provider === provider;
const nextModelSelection: ModelSelection = {
provider,
model: resolvedModel,
- };
+ ...(preserveOptions ? { options: existingOptions } : {}),
+ } as ModelSelection;
setComposerDraftModelSelection(activeThread.id, nextModelSelection);
setStickyComposerModelSelection(nextModelSelection);
scheduleComposerFocus();
@@ -3150,6 +3154,7 @@
setComposerDraftModelSelection,
setStickyComposerModelSelection,
customModelsByProvider,
+ composerDraft.modelSelection,
],
);
const setPromptFromTraits = useCallback(
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
Outdated
Show resolved
Hide resolved
- Keep draft model options when switching models within the same provider - Decode SQL errors correctly in projection snapshot model selection - Default missing sticky provider to codex during draft migration
- Preserve default provider model selections in the draft state - Update composer draft store expectations for sticky/provider-specific models
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 4 total unresolved issues (including 2 from previous reviews).
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Runtime-mode-set handler may trigger spurious session restarts
- Removed the fallback
?? toProviderModelOptions(thread.modelSelection)so that when no cached model options exist,cachedModelOptionsremainsundefinedand themodelOptionskey is omitted from the options passed toensureSessionForThread, preventing the spurious restart.
- Removed the fallback
- ✅ Fixed: Unreachable fallback chain in plan implementation model selection
- Simplified
nextThreadModelSelectionto assignselectedModelSelectiondirectly, removing the unreachable??fallback chain sinceselectedModelSelectionis always a non-nullModelSelectionfromuseMemo.
- Simplified
Or push these changes by commenting:
@cursor push 14070d6c45
Preview (14070d6c45)
diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
--- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
+++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
@@ -705,9 +705,7 @@
return;
}
const cachedProviderOptions = threadProviderOptions.get(event.payload.threadId);
- const cachedModelOptions =
- threadModelOptions.get(event.payload.threadId) ??
- toProviderModelOptions(thread.modelSelection);
+ const cachedModelOptions = threadModelOptions.get(event.payload.threadId);
yield* ensureSessionForThread(event.payload.threadId, event.occurredAt, {
...(cachedProviderOptions !== undefined
? { providerOptions: cachedProviderOptions }
diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -3035,12 +3035,7 @@
text: implementationPrompt,
});
const nextThreadTitle = truncateTitle(buildPlanImplementationThreadTitle(planMarkdown));
- const nextThreadModelSelection: ModelSelection = selectedModelSelection ??
- activeThread.modelSelection ??
- activeProject.defaultModelSelection ?? {
- provider: "codex",
- model: DEFAULT_MODEL_BY_PROVIDER.codex,
- };
+ const nextThreadModelSelection: ModelSelection = selectedModelSelection;
sendInFlightRef.current = true;
beginSendPhase("sending-turn");|
Bugbot Autofix prepared fixes for both issues found in the latest run.
Or push these changes by commenting: Preview (e0f5c02e35)diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
--- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
+++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
@@ -16,6 +16,7 @@
} from "@t3tools/contracts";
import { Cache, Cause, Duration, Effect, Layer, Option, Schema, Stream } from "effect";
import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";
+import { toProviderModelOptions } from "@t3tools/shared/model";
import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts";
import { GitCore } from "../../git/Services/GitCore.ts";
@@ -81,17 +82,6 @@
right: ProviderModelOptions | undefined,
): boolean => JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
-function toProviderModelOptions(
- modelSelection: ModelSelection | undefined,
-): ProviderModelOptions | undefined {
- if (!modelSelection?.options) {
- return undefined;
- }
- return modelSelection.provider === "codex"
- ? { codex: modelSelection.options }
- : { claudeAgent: modelSelection.options };
-}
-
function isUnknownPendingApprovalRequestError(cause: Cause.Cause<ProviderServiceError>): boolean {
const error = Cause.squash(cause);
if (Schema.is(ProviderAdapterRequestError)(error)) {
diff --git a/apps/server/src/persistence/Layers/ProjectionProjects.ts b/apps/server/src/persistence/Layers/ProjectionProjects.ts
--- a/apps/server/src/persistence/Layers/ProjectionProjects.ts
+++ b/apps/server/src/persistence/Layers/ProjectionProjects.ts
@@ -77,7 +77,7 @@
${row.workspaceRoot},
${row.defaultModelSelection?.provider ?? null},
${row.defaultModelSelection?.model ?? null},
- ${JSON.stringify(row.defaultModelSelection?.options ?? null)},
+ ${row.defaultModelSelection?.options != null ? JSON.stringify(row.defaultModelSelection.options) : null},
${JSON.stringify(row.scripts)},
${row.createdAt},
${row.updatedAt},
diff --git a/apps/server/src/persistence/Layers/ProjectionThreads.ts b/apps/server/src/persistence/Layers/ProjectionThreads.ts
--- a/apps/server/src/persistence/Layers/ProjectionThreads.ts
+++ b/apps/server/src/persistence/Layers/ProjectionThreads.ts
@@ -89,7 +89,7 @@
${row.title},
${row.modelSelection.provider},
${row.modelSelection.model},
- ${JSON.stringify(row.modelSelection.options ?? null)},
+ ${row.modelSelection.options != null ? JSON.stringify(row.modelSelection.options) : null},
${row.runtimeMode},
${row.interactionMode},
${row.branch},
diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -28,6 +28,7 @@
getDefaultModel,
normalizeModelSlug,
resolveModelSlugForProvider,
+ toProviderModelOptions,
} from "@t3tools/shared/model";
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
@@ -211,16 +212,6 @@
return provider === "codex" ? modelOptions?.codex : modelOptions?.claudeAgent;
}
-function toProviderModelOptions(
- modelSelection: ModelSelection | null | undefined,
-): ProviderModelOptions | undefined {
- if (!modelSelection?.options) {
- return undefined;
- }
- return modelSelection.provider === "codex"
- ? { codex: modelSelection.options }
- : { claudeAgent: modelSelection.options };
-}
const COMPOSER_PATH_QUERY_DEBOUNCE_MS = 120;
const SCRIPT_TERMINAL_COLS = 120;
const SCRIPT_TERMINAL_ROWS = 30;
diff --git a/packages/shared/src/model.ts b/packages/shared/src/model.ts
--- a/packages/shared/src/model.ts
+++ b/packages/shared/src/model.ts
@@ -10,7 +10,9 @@
type ClaudeCodeEffort,
type CodexModelOptions,
type CodexReasoningEffort,
+ type ModelSelection,
type ModelSlug,
+ type ProviderModelOptions,
type ProviderReasoningEffort,
type ProviderKind,
} from "@t3tools/contracts";
@@ -266,4 +268,15 @@
return `Ultrathink:\n${trimmed}`;
}
+export function toProviderModelOptions(
+ modelSelection: ModelSelection | null | undefined,
+): ProviderModelOptions | undefined {
+ if (!modelSelection?.options) {
+ return undefined;
+ }
+ return modelSelection.provider === "codex"
+ ? { codex: modelSelection.options }
+ : { claudeAgent: modelSelection.options };
+}
+
export { CLAUDE_CODE_EFFORT_OPTIONS, CODEX_REASONING_EFFORT_OPTIONS }; |
- Keep sticky selection in sync when provider options change - Add regression test for creating the initial sticky snapshot
- Store missing model options as SQL NULL in project and thread projections - Stop rehydrating derived model options when restarting provider sessions - Share model option conversion logic in `packages/shared`
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Redundant double call to
extractModelSelectionOptions- Stored the result of extractModelSelectionOptions in a local variable and reused it for both the truthiness check and the value assignment.
- ✅ Fixed: Model options always injected on session restart fallback
- Changed the fallback to only derive model options from requestedModelSelection (explicit user request) rather than from the thread's persisted modelSelection, preventing silent injection during runtime-mode-triggered restarts.
Or push these changes by commenting:
@cursor push 73c8d110db
Preview (73c8d110db)
diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
--- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
+++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
@@ -249,7 +249,10 @@
const desiredModelSelection = requestedModelSelection ?? thread.modelSelection;
const desiredModel = desiredModelSelection.model;
const desiredModelOptions =
- options?.modelOptions ?? toProviderModelOptions(desiredModelSelection);
+ options?.modelOptions ??
+ (requestedModelSelection !== undefined
+ ? toProviderModelOptions(requestedModelSelection)
+ : undefined);
const effectiveCwd = resolveThreadWorkspaceCwd({
thread,
projects: readModel.projects,
diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -643,21 +643,14 @@
);
const selectedPromptEffort = composerProviderState.promptEffort;
const selectedModelOptionsForDispatch = composerProviderState.modelOptionsForDispatch;
- const selectedModelSelection = useMemo<ModelSelection>(
- () => ({
+ const selectedModelSelection = useMemo<ModelSelection>(() => {
+ const options = extractModelSelectionOptions(selectedProvider, selectedModelOptionsForDispatch);
+ return {
provider: selectedProvider,
model: selectedModel,
- ...(extractModelSelectionOptions(selectedProvider, selectedModelOptionsForDispatch)
- ? {
- options: extractModelSelectionOptions(
- selectedProvider,
- selectedModelOptionsForDispatch,
- ),
- }
- : {}),
- }),
- [selectedModel, selectedModelOptionsForDispatch, selectedProvider],
- );
+ ...(options ? { options } : {}),
+ };
+ }, [selectedModel, selectedModelOptionsForDispatch, selectedProvider]);
const providerOptionsForDispatch = useMemo(() => getProviderStartOptions(settings), [settings]);
const selectedModelForPicker = selectedModel;
const modelOptionsByProvider = useMemo(- Add migration for legacy provider/model payloads - Cover project, thread, and orchestration event records
apps/server/src/persistence/Migrations/016_CanonicalizeModelSelections.ts
Show resolved
Hide resolved
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Dead
providerparameter instartProviderSessionclosure- Removed the unused
readonly provider?: ProviderKindfield from the startProviderSession closure's input type, since the body only uses the outerpreferredProvidervariable.
- Removed the unused
Or push these changes by commenting:
@cursor push e7ae3f40f4
Preview (e7ae3f40f4)
diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
--- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
+++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
@@ -260,10 +260,7 @@
.listSessions()
.pipe(Effect.map((sessions) => sessions.find((session) => session.threadId === threadId)));
- const startProviderSession = (input?: {
- readonly resumeCursor?: unknown;
- readonly provider?: ProviderKind;
- }) =>
+ const startProviderSession = (input?: { readonly resumeCursor?: unknown }) =>
providerService.startSession(threadId, {
threadId,
...(preferredProvider ? { provider: preferredProvider } : {}),- Replace provider-specific model options with typed modelSelection - Persist and replay selected provider, model, and options end to end - Update adapter, service, and chat composer tests for the new shape
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Broader comparison causes unnecessary claudeAgent session restarts
- Changed sameModelSelectionOptions to compare only left?.options and right?.options instead of the full ModelSelection, so model-name-only changes no longer trigger unnecessary session restarts for claudeAgent.
Or push these changes by commenting:
@cursor push de46c2f9bd
Preview (de46c2f9bd)
diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
--- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
+++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
@@ -78,7 +78,7 @@
const sameModelSelectionOptions = (
left: ModelSelection | undefined,
right: ModelSelection | undefined,
-): boolean => JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
+): boolean => JSON.stringify(left?.options ?? null) === JSON.stringify(right?.options ?? null);
function isUnknownPendingApprovalRequestError(cause: Cause.Cause<ProviderServiceError>): boolean {
const error = Cause.squash(cause);- Switch project and thread fixtures to provider-scoped model selection - Keep sidebar logic tests aligned with the new Codex provider model shape
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: sendTurn overwrites runtimePayload losing persisted model selection
- Fixed by reading the existing binding's runtimePayload and spreading it as the base object before applying sendTurn-specific fields, so previously persisted fields like cwd, model, and providerOptions are preserved.
Or push these changes by commenting:
@cursor push 2c97a21b07
Preview (2c97a21b07)
diff --git a/apps/server/src/provider/Layers/ProviderService.ts b/apps/server/src/provider/Layers/ProviderService.ts
--- a/apps/server/src/provider/Layers/ProviderService.ts
+++ b/apps/server/src/provider/Layers/ProviderService.ts
@@ -368,12 +368,20 @@
allowRecovery: true,
});
const turn = yield* routed.adapter.sendTurn(input);
+ const existingBinding = Option.getOrUndefined(yield* directory.getBinding(input.threadId));
+ const existingPayload =
+ existingBinding?.runtimePayload &&
+ typeof existingBinding.runtimePayload === "object" &&
+ !Array.isArray(existingBinding.runtimePayload)
+ ? existingBinding.runtimePayload
+ : {};
yield* directory.upsert({
threadId: input.threadId,
provider: routed.adapter.provider,
status: "running",
...(turn.resumeCursor !== undefined ? { resumeCursor: turn.resumeCursor } : {}),
runtimePayload: {
+ ...existingPayload,
...(input.modelSelection !== undefined ? { modelSelection: input.modelSelection } : {}),
activeTurnId: turn.turnId,
lastRuntimeEvent: "provider.sendTurn",There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Log message always shows "undefined" in production
- Conditionally formatted the log message to show the migration range only when toMigrationInclusive is defined, otherwise logging a generic "Running migrations..." message.
Or push these changes by commenting:
@cursor push 919971ae24
Preview (919971ae24)
diff --git a/apps/server/src/persistence/Migrations.ts b/apps/server/src/persistence/Migrations.ts
--- a/apps/server/src/persistence/Migrations.ts
+++ b/apps/server/src/persistence/Migrations.ts
@@ -90,7 +90,11 @@
*/
export const runMigrations = ({ toMigrationInclusive }: RunMigrationsOptions = {}) =>
Effect.gen(function* () {
- yield* Effect.log(`Running migrations 1 through ${toMigrationInclusive}...`);
+ yield* Effect.log(
+ toMigrationInclusive !== undefined
+ ? `Running migrations 1 through ${toMigrationInclusive}...`
+ : "Running migrations...",
+ );
const executedMigrations = yield* run({ loader: makeMigrationLoader(toMigrationInclusive) });
yield* Effect.log("Migrations ran successfully");
return executedMigrations;- keep the active Codex model across turns when in-session switching is unavailable - adjust reactor coverage for unsupported session model switching
- Store Codex and Claude composer traits separately - Keep sticky and draft model options in sync across provider switches - Update registry and picker tests for provider-scoped options
apps/web/src/components/chat/CompactComposerControlsMenu.browser.tsx
Outdated
Show resolved
Hide resolved
- keep default-state overrides in model selection updates - retain provider model options instead of clearing them when values normalize
apps/server/src/persistence/Layers/ProjectionRepositories.test.ts
Outdated
Show resolved
Hide resolved
apps/server/src/persistence/Migrations/016_CanonicalizeModelSelections.ts
Outdated
Show resolved
Hide resolved
- Persist model options as provider-specific JSON - Handle legacy fallback defaults during migration - Update composer and picker tests for split selections
- Log when all migrations run - Preserve project default model selection in sidebar test helper
| stickyModelSelection, | ||
| nextStickyModelOptions, | ||
| ); | ||
| const stickyActiveProvider = normalizeProviderKind(candidate.stickyProvider) ?? null; |
There was a problem hiding this comment.
🟡 Medium src/composerDraftStore.ts:975
In migratePersistedComposerDraftStoreState, stickyActiveProvider is derived only from candidate.stickyProvider (line 975), ignoring the provider in successfully migrated stickyModelSelection. For v2 persisted data that has stickyModelSelection (with an embedded provider like claudeAgent) but no separate stickyProvider field, the migration will incorrectly set stickyActiveProvider to null instead of using stickyModelSelection.provider. The fix should be: const stickyActiveProvider = normalizeProviderKind(candidate.stickyProvider) ?? stickyModelSelection?.provider ?? null;
| const stickyActiveProvider = normalizeProviderKind(candidate.stickyProvider) ?? null; | |
| const stickyActiveProvider = normalizeProviderKind(candidate.stickyProvider) ?? stickyModelSelection?.provider ?? null; |
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/web/src/composerDraftStore.ts around line 975:
In `migratePersistedComposerDraftStoreState`, `stickyActiveProvider` is derived only from `candidate.stickyProvider` (line 975), ignoring the provider in successfully migrated `stickyModelSelection`. For v2 persisted data that has `stickyModelSelection` (with an embedded provider like `claudeAgent`) but no separate `stickyProvider` field, the migration will incorrectly set `stickyActiveProvider` to `null` instead of using `stickyModelSelection.provider`. The fix should be: `const stickyActiveProvider = normalizeProviderKind(candidate.stickyProvider) ?? stickyModelSelection?.provider ?? null;`
Evidence trail:
apps/web/src/composerDraftStore.ts line 975: `const stickyActiveProvider = normalizeProviderKind(candidate.stickyProvider) ?? null;`
apps/web/src/composerDraftStore.ts lines 958-967: Shows `normalizedStickyModelSelection` and `stickyModelSelection` being created with provider from `candidate.stickyModelSelection`
apps/web/src/composerDraftStore.ts lines 497-528: `normalizeModelSelection` uses `candidate?.provider ?? legacy?.provider`, meaning it extracts provider from `stickyModelSelection.provider` first
apps/web/src/composerDraftStore.ts lines 116-130: Shows `LegacyStickyModelFields` has `stickyProvider` (optional) and `LegacyV2StoreFields` has `stickyModelSelection` (optional) as separate fields, confirming v2 data can have embedded provider without separate `stickyProvider`
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Duplicate DB row schemas risk future inconsistency
- Exported the DB row schemas from ProjectionProjects.ts and ProjectionThreads.ts and reused them in ProjectionSnapshotQuery.ts, eliminating the duplicated mapFields definitions.
Or push these changes by commenting:
@cursor push 896a4631c9
Preview (896a4631c9)
diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
--- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
+++ b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
@@ -6,7 +6,6 @@
OrchestrationCheckpointFile,
OrchestrationProposedPlanId,
OrchestrationReadModel,
- ProjectScript,
ThreadId,
TurnId,
type OrchestrationCheckpointSummary,
@@ -17,7 +16,6 @@
type OrchestrationSession,
type OrchestrationThread,
type OrchestrationThreadActivity,
- ModelSelection,
} from "@t3tools/contracts";
import { Effect, Layer, Schema, Struct } from "effect";
import * as SqlClient from "effect/unstable/sql/SqlClient";
@@ -29,8 +27,9 @@
toPersistenceSqlError,
type ProjectionRepositoryError,
} from "../../persistence/Errors.ts";
+import { ProjectionProjectDbRow } from "../../persistence/Layers/ProjectionProjects.ts";
+import { ProjectionThreadDbRow } from "../../persistence/Layers/ProjectionThreads.ts";
import { ProjectionCheckpoint } from "../../persistence/Services/ProjectionCheckpoints.ts";
-import { ProjectionProject } from "../../persistence/Services/ProjectionProjects.ts";
import { ProjectionState } from "../../persistence/Services/ProjectionState.ts";
import { ProjectionThreadActivity } from "../../persistence/Services/ProjectionThreadActivities.ts";
import { ProjectionThreadMessage } from "../../persistence/Services/ProjectionThreadMessages.ts";
@@ -44,12 +43,7 @@
} from "../Services/ProjectionSnapshotQuery.ts";
const decodeReadModel = Schema.decodeUnknownEffect(OrchestrationReadModel);
-const ProjectionProjectDbRowSchema = ProjectionProject.mapFields(
- Struct.assign({
- defaultModelSelection: Schema.NullOr(Schema.fromJsonString(ModelSelection)),
- scripts: Schema.fromJsonString(Schema.Array(ProjectScript)),
- }),
-);
+const ProjectionProjectDbRowSchema = ProjectionProjectDbRow;
const ProjectionThreadMessageDbRowSchema = ProjectionThreadMessage.mapFields(
Struct.assign({
isStreaming: Schema.Number,
@@ -57,11 +51,7 @@
}),
);
const ProjectionThreadProposedPlanDbRowSchema = ProjectionThreadProposedPlan;
-const ProjectionThreadDbRowSchema = ProjectionThread.mapFields(
- Struct.assign({
- modelSelection: Schema.fromJsonString(ModelSelection),
- }),
-);
+const ProjectionThreadDbRowSchema = ProjectionThreadDbRow;
const ProjectionThreadActivityDbRowSchema = ProjectionThreadActivity.mapFields(
Struct.assign({
payload: Schema.fromJsonString(Schema.Unknown),
diff --git a/apps/server/src/persistence/Layers/ProjectionProjects.ts b/apps/server/src/persistence/Layers/ProjectionProjects.ts
--- a/apps/server/src/persistence/Layers/ProjectionProjects.ts
+++ b/apps/server/src/persistence/Layers/ProjectionProjects.ts
@@ -12,7 +12,7 @@
type ProjectionProjectRepositoryShape,
} from "../Services/ProjectionProjects.ts";
-const ProjectionProjectDbRow = ProjectionProject.mapFields(
+export const ProjectionProjectDbRow = ProjectionProject.mapFields(
Struct.assign({
defaultModelSelection: Schema.NullOr(Schema.fromJsonString(ModelSelection)),
scripts: Schema.fromJsonString(Schema.Array(ProjectScript)),
diff --git a/apps/server/src/persistence/Layers/ProjectionThreads.ts b/apps/server/src/persistence/Layers/ProjectionThreads.ts
--- a/apps/server/src/persistence/Layers/ProjectionThreads.ts
+++ b/apps/server/src/persistence/Layers/ProjectionThreads.ts
@@ -13,7 +13,7 @@
} from "../Services/ProjectionThreads.ts";
import { ModelSelection } from "@t3tools/contracts";
-const ProjectionThreadDbRow = ProjectionThread.mapFields(
+export const ProjectionThreadDbRow = ProjectionThread.mapFields(
Struct.assign({
modelSelection: Schema.fromJsonString(ModelSelection),
}),| Struct.assign({ | ||
| modelSelection: Schema.fromJsonString(ModelSelection), | ||
| }), | ||
| ); |
There was a problem hiding this comment.
Duplicate DB row schemas risk future inconsistency
Medium Severity
The DB-row decode schemas for projects and threads (ProjectionProjectDbRowSchema/ProjectionProjectDbRow and ProjectionThreadDbRowSchema/ProjectionThreadDbRow) are defined identically in both ProjectionSnapshotQuery.ts and their respective repository files (ProjectionProjects.ts, ProjectionThreads.ts). Each independently applies mapFields with the same Schema.fromJsonString(ModelSelection) transform. If one definition changes without updating the other, the two read paths will silently decode differently, risking data corruption or runtime errors.
Additional Locations (2)
Port ultrathink effort-switching fix to new unified TraitsPicker component after upstream PR pingdotgg#1371 merged ClaudeTraitsPicker into TraitsPicker.
…ings architecture Major upstream changes merged: - Server-authoritative settings (pingdotgg#1421): appSettings.ts deleted, settings moved to serverSettings.ts + contracts/settings.ts + useSettings hook - Provider-aware model selections (pingdotgg#1371): model→modelSelection refactor, new composerDraftStore, unified TraitsPicker, migration 016 - Context window usage UI (pingdotgg#1351): ContextWindowMeter component - Claude as TextGenerationProvider (pingdotgg#1323): git text generation via Claude - Bootstrap FD for security (pingdotgg#1398): sensitive config sent over file descriptor - Sidebar recency sorting (pingdotgg#1372): projects/threads sorted by recency - ProviderHealth replaced by ProviderRegistry + per-provider services - Word wrapping, terminal history preservation, various fixes Krabby-specific adaptations: - Migration 016 renumbered to 024 (Krabby 016-023 preserved) - 20+ Krabby settings ported to new ClientSettingsSchema - KrabbyProviderOptions schema for chrome/enterprise/budget fields - All @t3tools→@kRaBby, t3code→krabbycode, service keys t3/→krabby/ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>



Note
High Risk
Introduces a breaking schema change from flat
modelfields to provider-awareModelSelectionacross commands/events/projections and applies an irreversible DB migration that rewrites existingorchestration_eventspayload JSON and drops legacy columns. Also changes provider session restart/switching logic, so regressions could surface as failed session starts or unexpected model/option behavior at runtime.Overview
Replaces flat model strings with provider-aware
ModelSelectionacross orchestration. Project defaults and thread metadata now persistdefaultModelSelection/modelSelection(including provider-scoped options) instead ofdefaultModel/model, and turn start requests carrymodelSelectionend-to-end.Updates persistence and projections to store/read model selections as JSON. Projection repositories and snapshot queries switch to
*_selection_jsoncolumns, and a new migration016_CanonicalizeModelSelectionsbackfills projection rows, rewrites legacy event payloads (defaultModel*,model*) into the canonical shape, then drops the olddefault_model/modelcolumns.Adjusts provider execution to use
modelSelectionand capability-based behavior.ProviderCommandReactornow enforces provider binding viamodelSelection, caches per-thread selections, preserves the active session model when in-session switching is unsupported, and adapters (CodexAdapter,ClaudeAdapter) map options frommodelSelectionwhile improving Claude token-usage tracking (use progress events for context usage; treat result totals as processed tokens).Written by Cursor Bugbot for commit 3701c8b. This will update automatically on new commits. Configure here.
Note
Replace flat model strings with provider-aware
ModelSelectionobjects across threads, projects, and turn commandsmodel: stringand separateprovider/modelOptionsfields with a unifiedModelSelectionobject ({ provider, model, options? }) in orchestration contracts, projections, persistence, provider adapters, and the web composer store.default_model_selection_jsonandmodel_selection_jsoncolumns, rewrite legacy event payloads, and drop the olddefault_model/modelcolumns.MODEL_CAPABILITIES_INDEX, replacing provider-wide effort/flag helpers with a data-drivengetModelCapabilitieslookup used for effort validation, fast mode, thinking toggle, and prompt-injected effort levels.stickyModel/stickyModelOptionswithstickyModelSelectionByProviderandstickyActiveProvider, and bumpsCOMPOSER_DRAFT_STORAGE_VERSIONto 3 with full migration from v1/v2 shapes.REASONING_EFFORT_OPTIONS_BY_PROVIDERandDEFAULT_REASONING_EFFORT_BY_PROVIDERare removed from the contracts package; any consumer importing them will fail to load.Macroscope summarized 3701c8b.