Skip to content

feat(stage-ui): sync user providers to a cloud replica - #2471

Draft
lulu0119 wants to merge 6 commits into
moeru-ai:mainfrom
lulu0119:lulu/feat/provider-cloud-sync
Draft

feat(stage-ui): sync user providers to a cloud replica#2471
lulu0119 wants to merge 6 commits into
moeru-ai:mainfrom
lulu0119:lulu/feat/provider-cloud-sync

Conversation

@lulu0119

@lulu0119 lulu0119 commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Draft / blocked: Conflicts with #2486 wipe-on-sign-out. #2486 calls deleteAllData()resetProviders() while still authenticated; on this PR resetProviders tombstones cloud replicas via removeProvider + push, so “clear device data” can wipe remote provider configs too. Need a local-only wipe path (or sign out before cloud deletes) before merging.

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. replicaUpdatedAt is 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/providers returns live rows and tombstones. PUT /api/v1/providers/:id upserts with the client instance id. DELETE /api/v1/providers/:id writes a tombstone. A missing local live row is not a delete. removeProvider writes a pending tombstone and uploads it after the debounce.

Migration 0023 drops system_provider_configs and the old user_provider_configs table, 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.ts
  • pnpm 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.ts
  • Sign in on two windows. Configure OpenRouter until it is green. Confirm the other window has the key after login or hard refresh.
  • Clear the API key or leave the provider unconfigured. Confirm the other window still has the working replica.
  • Hard refresh while signed in. Confirm the cloud row is applied and the provider becomes green after local validation.
  • Delete OpenRouter with the Consciousness trash control. Confirm a DELETE is sent. Hard refresh. Confirm the row does not come back.
  • Sign out, delete OpenRouter with the trash control, then sign in. Confirm the pending tombstone survives the live GET and then uploads. Confirm a later GET returns deleted_at.
  • Open the deleted provider settings page. Confirm a local default skeleton does not upsert until the provider is configured again.
  • Confirm official authentication providers do not upload.

Visual changes

No appearance change. The shipped /settings/providers pages still talk to the local store. Sync runs after login in the background.

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

⏳ Approval required for deploying to Cloudflare Workers (Preview) for stage-web.

Name Link
🔭 Waiting for approval For maintainers, approve here

Hey, maintainers, kindly take some time to review and approve this deployment when you are available. Thank you! 🙏

@lulu0119
lulu0119 marked this pull request as draft September 5, 2026 13:44

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread server/apps/api/drizzle/0023_premium_frog_thor.sql
Comment thread packages/stage-ui/src/stores/providers/config.ts
Comment thread packages/stage-ui/src/stores/providers/config.ts
Comment thread server/apps/api/src/services/domain/providers.ts
@lulu0119
lulu0119 marked this pull request as ready for review September 6, 2026 13:42

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread packages/stage-ui/src/stores/providers/merge.ts
Comment thread server/apps/api/src/services/domain/providers.ts
Comment on lines +339 to +341
catch {
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +68 to +74
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!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment thread packages/stage-ui/src/stores/providers/config.ts
@lulu0119
lulu0119 force-pushed the lulu/feat/provider-cloud-sync branch from cca910d to 0041221 Compare September 7, 2026 01:32

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread packages/stage-ui/src/stores/providers/config.ts
Comment thread packages/stage-ui/src/stores/providers/config.ts Outdated
lulu0119 and others added 5 commits September 7, 2026 16:34
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>
@lulu0119
lulu0119 force-pushed the lulu/feat/provider-cloud-sync branch from 0041221 to 6c3f242 Compare September 7, 2026 09:29

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment on lines +292 to +294
if (syncInFlight) {
await syncInFlight
return syncProviders()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant