Skip to content

fix(THU-637): version-gate models reconcile + OTA + THU-645 lineup#1049

Merged
raivieiraadriano92 merged 21 commits into
mainfrom
raivieiraadriano92/thu-637-models-are-janky
Jul 7, 2026
Merged

fix(THU-637): version-gate models reconcile + OTA + THU-645 lineup#1049
raivieiraadriano92 merged 21 commits into
mainfrom
raivieiraadriano92/thu-637-models-are-janky

Conversation

@raivieiraadriano92

@raivieiraadriano92 raivieiraadriano92 commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes THU-637 (models flap) and lands THU-645 (new lineup) atomically

The two changes ship together because the version-gate introduced for THU-637 is the mechanism that makes the THU-645 lineup change safe against multi-device races.

The race — THU-637

models is a PowerSync-synced table, but the shipped defaults live in the client binary. Every launch, reconcileDefaults treats rows whose stored defaultHash matches their current content as "unedited" and rewrites them with the local bundle's defaults. When two devices (or the same device across app updates) ship different defaults, they ping-pong via sync every boot: a V2 device writes V2 rows → a V1 device reconciles them back to V1 → V2 upgrades again → ...

Even single-device: waitForInitialSync timing out (10 s cap) can drop reconcile back to local V1 while newer rows arrive post-reconcile — next launch downgrades.

The fix — per-table monotonic version gate

Ship defaultModelsVersion alongside defaultModels. Store the highest ever applied version in the already-synced settingsTable under defaults_version.models:

canOverwrite = pickedVersion > (storedVersion ?? -∞)
  • canOverwrite = false (my bundle is older than what's already applied on this account) → skip in-place updates AND skip cleanupRemovedDefaults.
  • canOverwrite = true → normal reconcile, bump stored version at the end only when at least one row was actually written.

Hash-based user-edit protection still runs alongside — user edits survive under both older-bundle and newer-bundle passes.

Only models (and paired model_profiles) are gated in this PR; other reconciled tables (modes, tasks, skills, settings) keep their current behavior via canOverwrite: true defaults. No new synced column — the version marker piggybacks on the existing settings sync, so no PowerSync two-PR flow.

Additional guards layered on top of the base gate:

  • Sync-incomplete pessimism — when `initialSyncCompleted=false` and the DB has any rows, cloud state is unknown; skip mutations for that boot.
  • Insert guard on canOverwrite — models can be legitimately retired under a newer version, so ghost-inserting them is unsafe; profiles opt into `insertMissing: true` so the 1:1 model↔profile invariant holds regardless of authority.
  • Marker only advances on writes — no false-positive "V2 applied" signal to peers whose rows haven't caught up.
  • Cleanup-soft-delete resurrect — undo a lagging pre-THU-637 client's cleanup soft-delete when the row's content still matches the shipped default's authoring hash. Discriminator is `defaultHash`: cleanup preserves it (row is resurrectable), user's `deleteModel` scrubs it via `clearNullableColumns` (deletion is permanent).

/config as OTA channel for defaults

Backend /config now returns:

defaults: {
  models: { version: N, data: SharedModel[] }
}

Frontend picks between server and bundle by max(version) with a payload-sanity guard (Number.isFinite(version) && Array.isArray(data) && data.length > 0).

  • Server ahead → OTA rollout, no client release needed.
  • Server behind/equal → bundle wins by strict > (no wasted swap).
  • Offline → cached /config via zustand/persist, or bundle on first-ever fresh install.
  • Rollback is monotonic: retract a bad server payload by shipping a higher version with reverted content.

The picker awaits the /config fetch that was already fired at boot Step 0, bounded by its 5 s internal timeout. On cache-hit boots (persisted /config present) it uses the cached value immediately and lets the fetch resolve in the background.

THU-645 — new model lineup

  • Kimi K2.6 retired (removed from defaultModels).
  • DeepSeek V4 Pro (Tinfoil confidential) → DeepSeek V4 Flash (Thunderbolt/Fireworks, non-confidential). Flash ships under a fresh id — reusing V4 Pro's id would flip isConfidential 1 → 0 on threads created encrypted, stranding them because the picker and send-guard enforce isEncrypted === isConfidential. Cleanup soft-deletes the Pro row instead, so encrypted threads bound to it surface as "model retired" rather than broken chats.
  • deepseek-v4-flash added to backend supportedModels, routed to Fireworks.
  • defaultModelsVersion bumped 1 → 2. Snapshot test pinned to { version: 2, hash: … }.

Race guard in action for THU-645 rollout

  • Combined-PR device applies V2 → syncs Flash + stored=2.
  • Pre-THU-637 device (still on prod main) syncs Flash — its unconditional cleanupRemovedDefaults doesn't recognize Flash's id, so it soft-deletes Flash. Cloud now has Flash deleted.
  • Any combined-PR device seeing the deletion: the resurrect branch un-deletes Flash (content still matches Flash's authoring hash → distinguishable from a user delete). Cloud converges back to Flash alive. Flap self-limits as pre-THU-637 devices update.

Without the resurrect branch, Flash would be permanently soft-deleted on any account touched by a lagging pre-THU-637 device — stored=2 plus deletedAt-included hashing would make every future reconcile treat the row as user-edited.

Version-bump discipline

A colocated snapshot test in shared/defaults/models.test.ts fails on any change to defaultModels without a matching bump to defaultModelsVersion. AGENTS.md documents the convention.

Also in this PR (ported from #1012)

  • fix(backend): keep pglite in-memory when DATABASE_URL is a connection string — prevents pglite tests from bootstrapping a spurious data dir when .env has DATABASE_URL=postgresql://….
  • fix(tauri): allow localhost:8080 in CSP connect-src — needed for the local dev backend on 8080.

Supersedes #1044

PR #1044 shipped the THU-645 lineup change against the pre-THU-637 branch. Its open comments — pre-THU-637 cleanup deleting Flash across the account, retired Tinfoil ids on user-edited rows, count-based profile tests — are addressed here (race by version-gate + resurrect guard; test brittleness by the new structural profile↔model check). PR #1044 can be closed as superseded.

Test plan

  • Frontend + backend bun tsc --noEmit: clean
  • bun run test:5x: 15,470 frontend + 440 shared, 0 fail across 5 iterations
  • Backend full suite: 830/830 pass
  • Frontend + backend bun run lint: 0 errors
  • Manual: wipe local Thunderbolt data → boot on this branch → confirm Flash present, defaults_version.models = 2 written
  • Manual: on an existing prod-main install (with Pro + Kimi rows), install this build → confirm Flash inserted, Pro + Kimi soft-deleted in place, existing user-edited rows preserved
  • Manual: server serves a higher defaults.models.version with a different content payload → confirm client adopts (OTA)
  • Manual: simulate the pre-THU-637 race — soft-delete Flash's row manually (leaving defaultHash intact), reboot → confirm resurrect fires

Follow-up

Same version-gate + /config treatment for modes / tasks / skills / settings — pure repetition of the pattern established here. In-app "model retired" hint for user-edited retired rows (ital0's #2 on 1044) also for follow-up.


Note

High Risk
Touches PowerSync-synced models reconciliation, multi-device convergence, and default model IDs/confidentiality—incorrect gating or OTA picking could wipe or flap user-visible models across accounts.

Overview
Stops multi-device models reconcile ping-pong (THU-637) by gating writes on a monotonic defaults_version.models in synced settings and only advancing that marker when models/profiles actually change. Reconcile now takes authority flags (canOverwrite, canResurrect, insertMissing), skips mutations when initial sync is incomplete but the DB already has rows, can resurrect cleanup-shaped soft-deletes, and keeps model↔profile pairing (including insertMissing profiles and dropping OTA-only models without bundled profiles).

OTA: Default models move to shared/defaults/models.ts with defaultModelsVersion (now 2) and a snapshot test; GET /config exposes defaults.models, the config store persists it, boot uses pickModelsDefaults (sanity checks + bundle floor) before reconcile.

Lineup (THU-645): Kimi retired; DeepSeek V4 Pro → V4 Flash on Thunderbolt/Fireworks under a new id (avoids flipping isConfidential on encrypted threads); deepseek-v4-flash added to inference routing; profiles/tests updated.

Also: shared hashValues, PGlite ignores postgres DATABASE_URL for in-memory tests, Tauri CSP allows localhost:8080, AGENTS.md documents version-bump discipline.

Reviewed by Cursor Bugbot for commit 107513d. Bugbot is set up for automated code reviews on this repo. Configure here.

@raivieiraadriano92 raivieiraadriano92 self-assigned this Jul 2, 2026
@raivieiraadriano92 raivieiraadriano92 added the bug Something isn't working label Jul 2, 2026
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

Semgrep Security Scan

No security issues found.

@raivieiraadriano92 raivieiraadriano92 changed the title Raivieiraadriano92/thu 637 models are janky fix(THU-637): version-gate models reconcile + OTA defaults via /config Jul 2, 2026
@raivieiraadriano92
raivieiraadriano92 marked this pull request as ready for review July 2, 2026 12:46
Comment thread src/lib/pick-defaults.ts
Comment thread src/lib/reconcile-defaults.ts
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

PR Metrics

Metric Value
Lines changed (prod code) +726 / -248
JS bundle size (gzipped) 🟢 781.6 KB → 782.1 KB (+549 B, +0.1%)
Test coverage 🟢 77.53% → 77.57% (+0.0%)
Performance (preview) Preview not ready — Render deploy may have timed out
Accessibility
Best Practices
SEO

Updated Mon, 06 Jul 2026 21:24:38 GMT · run #2202

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔭 thunder-deep-review (advisory)

Complements the other bots — surfaces only what they did not flag. Never approves, never requests changes, never gates merge.
head: 5d91ec025bcc · mode: single · deferred 3 item(s) already reported by other bots (best-effort dedup)

Comment thread src/lib/reconcile-defaults.ts Outdated
Comment thread src/lib/reconcile-defaults.ts

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔭 thunder-deep-review (advisory)

Complements the other bots — surfaces only what they did not flag. Never approves, never requests changes, never gates merge.
head: d450d47cf6bd · mode: deep · deferred 4 item(s) already reported by other bots (best-effort dedup)

Additional notes (couldn't anchor to a diff line)

  • src/lib/reconcile-defaults.ts — 🚫 Blocking — Editing a model-profile default silently won't propagate unless someone bumps the models version

Comment thread shared/defaults/models.test.ts
Comment thread src/lib/reconcile-defaults.ts Outdated
Comment thread src/hooks/use-app-initialization.ts
Comment thread src/lib/pick-defaults.ts Outdated
Comment thread backend/src/api/config.ts
Comment thread shared/defaults/models.ts
Comment thread src/lib/reconcile-defaults.ts Outdated
Comment thread shared/defaults/models.test.ts Outdated
Comment thread backend/src/db/client.ts
Comment thread src-tauri/tauri.conf.json
Comment thread src/lib/reconcile-defaults.ts
Comment thread src/lib/reconcile-defaults.ts
Comment thread src/lib/reconcile-defaults.ts Outdated
@raivieiraadriano92 raivieiraadriano92 changed the title fix(THU-637): version-gate models reconcile + OTA defaults via /config fix(THU-637): version-gate models reconcile + OTA + THU-645 lineup Jul 6, 2026
Comment thread src/lib/reconcile-defaults.ts
Comment thread src/lib/reconcile-defaults.ts
@raivieiraadriano92
raivieiraadriano92 force-pushed the raivieiraadriano92/thu-637-models-are-janky branch from 866d552 to 6175532 Compare July 6, 2026 15:03

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔭 thunder-deep-review (advisory)

Complements the other bots — surfaces only what they did not flag. Never approves, never requests changes, never gates merge.
head: 6175532ac300 · mode: deep · deferred 9 item(s) already reported by other bots (best-effort dedup)

Comment thread src/lib/reconcile-defaults.ts Outdated
Comment thread src/lib/reconcile-defaults.ts

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔭 thunder-deep-review (advisory)

Complements the other bots — surfaces only what they did not flag. Never approves, never requests changes, never gates merge.
head: be630ff76ebf · mode: deep · deferred 10 item(s) already reported by other bots (best-effort dedup)

Comment thread src/lib/reconcile-defaults.ts
Comment thread src/lib/reconcile-defaults.ts
Comment thread src/hooks/use-app-initialization.ts Outdated

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔭 thunder-deep-review (advisory)

Complements the other bots — surfaces only what they did not flag. Never approves, never requests changes, never gates merge.
head: a50c71628cf2 · mode: deep · deferred 10 item(s) already reported by other bots (best-effort dedup)

Comment thread shared/defaults/models.ts
Comment thread src/lib/reconcile-defaults.ts
Comment thread backend/src/db/client.ts Outdated
@raivieiraadriano92
raivieiraadriano92 force-pushed the raivieiraadriano92/thu-637-models-are-janky branch from a50c716 to 107513d Compare July 6, 2026 20:55
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Preview environment destroyed 🧹

Stack preview-pr-1049 and its Cloudflare subdomain have been cleaned up.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 107513d. Configure here.

Comment thread src/lib/reconcile-defaults.ts

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔭 thunder-deep-review (advisory)

Complements the other bots — surfaces only what they did not flag. Never approves, never requests changes, never gates merge.
head: 107513d0ee56 · mode: deep · deferred 11 item(s) already reported by other bots (best-effort dedup)

Comment thread src/lib/reconcile-defaults.ts Outdated
Comment thread src/lib/reconcile-defaults.ts
@raivieiraadriano92
raivieiraadriano92 merged commit e77cf09 into main Jul 7, 2026
28 checks passed
@raivieiraadriano92
raivieiraadriano92 deleted the raivieiraadriano92/thu-637-models-are-janky branch July 7, 2026 12:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants