-
Notifications
You must be signed in to change notification settings - Fork 320
fix(THU-637): version-gate models reconcile + OTA + THU-645 lineup #1049
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
raivieiraadriano92
merged 21 commits into
main
from
raivieiraadriano92/thu-637-models-are-janky
Jul 7, 2026
Merged
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
a6422ef
refactor: move models defaults to shared
raivieiraadriano92 3773a5a
feat: version and snapshot-lock models defaults
raivieiraadriano92 f8ffa07
fix: version-gate models reconcile against downgrades
raivieiraadriano92 bc5d026
test: cover THU-637 downgrade regression
raivieiraadriano92 d9bb8e6
feat(backend): serve models defaults via /config
raivieiraadriano92 e36ce58
feat: prefer server models defaults when newer
raivieiraadriano92 6534157
fix: inline version upsert to avoid nested transaction
raivieiraadriano92 876ad4e
fix(backend): keep pglite in-memory when DATABASE_URL is a connection…
raivieiraadriano92 11dfdda
fix(tauri): allow localhost:8080 in CSP connect-src
raivieiraadriano92 1b4d4ad
fix: address bugbot findings — empty OTA guard, version-key upsert, p…
raivieiraadriano92 da9f006
fix: harden model reconcile against sync races
raivieiraadriano92 63dfaa3
chore: harden shared/ boundary — extract hashValues, drop apiKey, enf…
raivieiraadriano92 4a277a1
refactor: options object for reconcileDefaultsForTable
raivieiraadriano92 e28fefd
fix(ci): pick up shared subdirs in test globs, camelCase constants
raivieiraadriano92 b8f0746
chore: cache short-circuit, stricter picker guard, pglite fallback warn
raivieiraadriano92 cb7b426
fix: address bugbot round 2 — marker advance, sync guard, profile ins…
raivieiraadriano92 7675cdd
feat: land THU-645 model lineup update + resurrect race guard
raivieiraadriano92 a8bf8dc
fix: gate resurrect and profile-cleanup on sync completion
raivieiraadriano92 4bcb402
fix: filter OTA models without a bundled profile from reconcile
raivieiraadriano92 107513d
fix: reject disjoint OTA payloads in pickModelsDefaults
raivieiraadriano92 d97d297
fix(THU-637): address open PR review blockers
raivieiraadriano92 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| /* This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
|
||
| import { describe, expect, test } from 'bun:test' | ||
| import { defaultModels, defaultModelsVersion, hashModel } from './models' | ||
|
|
||
| /** | ||
| * Snapshot pinning the shipped defaults to their declared version. When you | ||
| * change any default model (add/remove/edit/reorder), this test fails. | ||
| * | ||
| * Fix it in this order: | ||
| * 1. Bump `defaultModelsVersion` in `shared/defaults/models.ts`. | ||
| * 2. Update `expected` below to match the actual values from the failure. | ||
| * | ||
| * The version is the ordering signal reconcile uses to decide who owns the | ||
| * newest defaults across devices (THU-637). Changing defaults without bumping | ||
| * the version breaks that ordering silently. | ||
| */ | ||
| const computeSnapshotHash = () => | ||
| defaultModels.map((model, index) => `${index}:${model.id}:${hashModel(model)}`).join('|') | ||
|
|
||
| const expected = { | ||
| version: 2, | ||
| hash: '0:019af08a-c27b-7074-8aac-95315d1ef3fd:-1vf2pk|1:019f227e-d640-727d-ba12-d51bd7d0a3d6:bvaax2|2:019e7580-2b0e-719c-a43f-d2b56e7f31b4:-g7x2jr', | ||
| } | ||
|
|
||
| describe('defaultModels version snapshot', () => { | ||
|
raivieiraadriano92 marked this conversation as resolved.
|
||
| test('version and content are in sync — read the file header if this fails', () => { | ||
| expect({ | ||
| version: defaultModelsVersion, | ||
| hash: computeSnapshotHash(), | ||
| }).toEqual(expected) | ||
| }) | ||
| }) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| /* This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
|
||
| import { hashValues } from '@shared/lib/hash' | ||
|
|
||
| /** | ||
| * Shape of a shipped model default. Structurally a subset of the frontend | ||
| * `Model` type (`src/types.ts`) — `SharedModel` intentionally omits `apiKey`, | ||
| * which is a runtime concern (populated by the DAL from a LEFT JOIN with the | ||
| * local-only `models_secrets` table) and must never traverse the wire. That | ||
| * omission also makes the public `/config` endpoint structurally incapable of | ||
| * leaking an API key even if a future server-shipped payload got sloppy. | ||
| * | ||
| * A compile-time assignability check in `src/types.ts` guards against silent | ||
| * drift when the frontend `Model` gains new required fields. | ||
| */ | ||
| export type SharedModel = { | ||
|
raivieiraadriano92 marked this conversation as resolved.
|
||
| id: string | ||
| provider: 'openai' | 'custom' | 'openrouter' | 'thunderbolt' | 'anthropic' | 'tinfoil' | ||
| name: string | ||
| model: string | ||
| url: string | null | ||
| isSystem: number | null | ||
| enabled: number | ||
| toolUsage: number | ||
| isConfidential: number | ||
| startWithReasoning: number | ||
| supportsParallelToolCalls: number | ||
| contextWindow: number | null | ||
| deletedAt: string | null | ||
| defaultHash: string | null | ||
| vendor: string | null | ||
| description: string | null | ||
| userId: string | null | ||
| } | ||
|
|
||
| /** | ||
| * Compute hash of user-editable fields for a model. | ||
| * Includes deletedAt to treat soft-delete as a user configuration choice. | ||
| */ | ||
| export const hashModel = (model: SharedModel): string => { | ||
| return hashValues([ | ||
| model.name, | ||
| model.provider, | ||
| model.model, | ||
| model.url, | ||
| model.isSystem, | ||
| model.enabled, | ||
| model.toolUsage, | ||
| model.isConfidential, | ||
| model.startWithReasoning, | ||
| model.supportsParallelToolCalls, | ||
| model.contextWindow, | ||
| model.deletedAt, | ||
| ]) | ||
| } | ||
|
|
||
| /** | ||
| * Default system models shipped with the application. | ||
| * These are upserted on app start and serve as the baseline for diff comparisons. | ||
| * Each model is exported individually so it can be referenced by automations. | ||
| */ | ||
|
|
||
| /** | ||
| * Opus 4.8 reuses the row id originally assigned to Sonnet 4.5 (and inherited by 4.7). | ||
| * Reconciliation upgrades unmodified rows in place; edited rows survive. | ||
| */ | ||
| export const defaultModelOpus48: SharedModel = { | ||
| id: '019af08a-c27b-7074-8aac-95315d1ef3fd', | ||
| name: 'Opus 4.8', | ||
| provider: 'thunderbolt', | ||
| model: 'opus-4.8', | ||
| isSystem: 1, | ||
| enabled: 1, | ||
| isConfidential: 0, | ||
| contextWindow: 200000, | ||
| toolUsage: 1, | ||
| startWithReasoning: 0, | ||
| supportsParallelToolCalls: 1, | ||
| deletedAt: null, | ||
| url: null, | ||
| defaultHash: null, | ||
| vendor: 'anthropic', | ||
| description: 'Top-tier Anthropic reasoning', | ||
| userId: null, | ||
| } | ||
|
|
||
| /** | ||
| * Flash ships under a fresh id — not the retired V4 Pro id. Reusing Pro's id | ||
| * would flip `isConfidential` 1 → 0 on threads that were created encrypted | ||
| * (`isEncrypted` mirrors the model's `isConfidential` at creation), stranding | ||
| * them because the model picker and send guard both enforce | ||
| * `isEncrypted === isConfidential`. The retired Pro row is instead | ||
| * soft-deleted by `cleanupRemovedDefaults`, so encrypted threads bound to it | ||
|
raivieiraadriano92 marked this conversation as resolved.
|
||
| * surface as "model retired" rather than broken chats. | ||
| * | ||
| * The reconciler's `frozenFields: ['isConfidential', 'provider']` guard | ||
| * enforces the same invariant from the OTA side — an OTA payload that ships | ||
| * an existing id with `isConfidential` flipped is silently ignored on those | ||
| * two columns. New values for either field must ship under a fresh id. | ||
| */ | ||
| export const defaultModelDeepseekV4Flash: SharedModel = { | ||
| id: '019f227e-d640-727d-ba12-d51bd7d0a3d6', | ||
| name: 'DeepSeek V4 Flash', | ||
| provider: 'thunderbolt', | ||
| model: 'deepseek-v4-flash', | ||
| isSystem: 1, | ||
| enabled: 1, | ||
| isConfidential: 0, | ||
| contextWindow: 131072, | ||
| toolUsage: 1, | ||
| startWithReasoning: 0, | ||
| supportsParallelToolCalls: 0, | ||
| deletedAt: null, | ||
| url: null, | ||
| defaultHash: null, | ||
| vendor: 'deepseek', | ||
| description: 'Fast DeepSeek reasoning', | ||
| userId: null, | ||
| } | ||
|
|
||
| export const defaultModelGlm52: SharedModel = { | ||
| id: '019e7580-2b0e-719c-a43f-d2b56e7f31b4', | ||
| name: 'GLM 5.2', | ||
| provider: 'tinfoil', | ||
| model: 'glm-5-2', | ||
| isSystem: 1, | ||
| enabled: 1, | ||
| isConfidential: 1, | ||
| contextWindow: 131072, | ||
| toolUsage: 1, | ||
| startWithReasoning: 0, | ||
| supportsParallelToolCalls: 0, | ||
| deletedAt: null, | ||
| url: null, | ||
| defaultHash: null, | ||
| vendor: 'zhipu', | ||
| description: 'Confidential chat via Tinfoil', | ||
| userId: null, | ||
| } | ||
|
|
||
| /** | ||
| * Array of all default models for iteration. Order = display order in the | ||
| * "Provided" group of the model picker. Reorder freely — but bump | ||
| * `defaultModelsVersion` when you do. | ||
| * | ||
| * Retired between V1 and V2: `defaultModelDeepseekV4Pro` (superseded by | ||
| * Flash under a fresh id) and `defaultModelKimiK26` (dropped). Their rows are | ||
| * soft-deleted by `cleanupRemovedDefaults` on next reconcile; unedited copies | ||
| * disappear cleanly, user-edited copies survive but point at retired ids and | ||
| * will surface upstream errors when used. | ||
| */ | ||
| export const defaultModels: ReadonlyArray<SharedModel> = [ | ||
| defaultModelOpus48, | ||
| defaultModelDeepseekV4Flash, | ||
| defaultModelGlm52, | ||
| ] as const | ||
|
|
||
| /** | ||
| * Monotonic version of the shipped defaults. Bump every time `defaultModels` | ||
| * changes in any way. The reconciler uses this as the ordering signal to | ||
| * decide which device's defaults win in a multi-device sync group (THU-637): | ||
| * a device only overwrites existing rows when its picked defaults version is | ||
| * strictly newer than the highest ever applied on this account. | ||
| * | ||
| * The paired snapshot test in `models.test.ts` fails on any change to this | ||
| * file's defaults without a matching version bump. | ||
| */ | ||
| export const defaultModelsVersion = 2 | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.