feat(stage-ui): sync user providers to a cloud replica - #2471
Conversation
⏳ Approval required for deploying to Cloudflare Workers (Preview) for stage-web.
Hey, maintainers, kindly take some time to review and approve this deployment when you are available. Thank you! 🙏 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 58c84efeb6
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cca910de54
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| catch { | ||
| return | ||
| } |
There was a problem hiding this comment.
Retry failed replica writes without another mutation
If a PUT fails once because of a transient network error, this catch returns while the provider remains dirty. No state changes, so the watcher does not schedule another attempt. The cloud replica stays stale until another user mutation or login occurs; a later login can also discard the edit through the merge path. Schedule a bounded retry or retry when connectivity returns.
AGENTS.md reference: AGENTS.md:L216-L219
Useful? React with 👍 / 👎.
| if (hasLocalDelete) { | ||
| const remoteIsNewerLive = !remoteRow.deletedAt && remoteTime > localTime | ||
| const remoteTombstoneWins = !!remoteRow.deletedAt && remoteTime >= localTime | ||
| if (remoteIsNewerLive) | ||
| live[id] = copyRemoteLive(remoteRow) | ||
| else if (!remoteTombstoneWins) | ||
| pendingDeletes[id] = localDeleteAt! |
There was a problem hiding this comment.
Avoid comparing client deletion clocks with server clocks
If an offline device clock lags the server, a local deletion can have an earlier timestamp than the unchanged remote row. This branch then restores the remote provider and removes the pending tombstone, so the user's deletion never reaches the server. Use a causal replica revision or another server-derived value instead of comparing client and server wall clocks.
AGENTS.md reference: AGENTS.md:L210-L218
Useful? React with 👍 / 👎.
cca910d to
0041221
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0041221278
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Login pulls the replica, merges by instance id and server write time, then uploads dirty rows. Official authentication providers stay local. Co-authored-by: Cursor <cursoragent@cursor.com>
Settings pages write config fields in place, which skipped the replica push that only ran from named actions. Co-authored-by: Cursor <cursoragent@cursor.com>
…oud replica Equal timestamps apply the cloud row, and only configured providers upsert. Local deletes keep a pending tombstone until the replica acknowledges them. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
0041221 to
6c3f242
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6c3f242f93
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| syncInFlight = (async () => { | ||
| try { | ||
| const remote = await service.listRemote(client) | ||
| applyMerged(mergeProviderSync(snapshotLocal(), remote)) |
There was a problem hiding this comment.
Invalidate provider clients after applying remote configuration
When login pulls newer cloud configuration after getProviderInstance() cached a client, this merge replaces only the persisted row. The cache keeps the old credentials, and future inference calls return that client until explicit disposal or reload. Invalidate cached instances whose configuration changed before the sync hook completes.
AGENTS.md reference: AGENTS.md:L211-L218
Useful? React with 👍 / 👎.
| const isValid = await validateProvider(providerId) | ||
| providerConfigStore.setProviderStatus(providerId, isValid ? 'configured' : 'invalid') | ||
| } | ||
| initializeProviderRuntimeState(providerId) |
There was a problem hiding this comment.
Validate generated provider IDs after a replica pull
When a new device pulls a provider whose ID came from nanoid(), this loop never visits it because it iterates only definition IDs in providerMetadata. applyMerged() sets remote-only rows to unconfigured, so synced speech and transcription providers stay unavailable until the user opens each settings page. Iterate the projected provider instance IDs as well.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a0c55b6f44
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (syncInFlight) { | ||
| await syncInFlight | ||
| return syncProviders() |
There was a problem hiding this comment.
Share the in-flight sync instead of restarting it
When authentication in two Electron renderers triggers this synchronized action while the first request is pending, each waiting invocation recursively starts another complete sync after the shared promise resolves. With multiple windows, one login therefore causes serial provider GET, push, and after-sync validation passes for every caller; those validations can also make external provider requests. Return the existing in-flight promise without calling syncProviders() again so duplicate starts converge without repeated side effects.
AGENTS.md reference: AGENTS.md:L232-L237
Useful? React with 👍 / 👎.
Description
User provider configs sync as a local-first cloud replica after login. There is no settings toggle. This matches chat sync.
The local store is the primary copy. The server holds encrypted rows. On login the client pulls the replica, merges by instance id and server write time, then uploads dirty rows after a debounce.
A successful GET applies the cloud row when its write time is the same or newer.
replicaUpdatedAtis the last successful upload, not a local edit clock.Only a provider this device has marked configured is upserted. Incomplete edits do not overwrite a working replica. Status is not uploaded. After a pull, the provider store validates locally.
Official providers with
configuredBy === 'authentication'do not sync.GET /api/v1/providersreturns live rows and tombstones.PUT /api/v1/providers/:idupserts with the client instance id.DELETE /api/v1/providers/:idwrites a tombstone. A missing local live row is not a delete.removeProviderwrites a pending tombstone and uploads it after the debounce.Migration
0023dropssystem_provider_configsand the olduser_provider_configstable, then creates the new replica table. The v2 providers UI never shipped, so those tables have no production data.Test plan
pnpm -F @proj-airi/stage-ui exec vitest run src/stores/providers/config.test.ts src/stores/providers/merge.test.tspnpm exec vitest run server/apps/api/src/routes/providers/route.test.ts server/apps/api/src/services/domain/providers.test.ts server/apps/api/src/services/domain/user-deletion/tests/service-deletion.test.tsdeleted_at.Visual changes
No appearance change. The shipped
/settings/providerspages still talk to the local store. Sync runs after login in the background.