Skip to content

Make Comet local-first with optional multi-device sync - #30

Open
jsgrrchg wants to merge 19 commits into
zeronsh:mainfrom
jsgrrchg:local-first-optional-sync
Open

Make Comet local-first with optional multi-device sync#30
jsgrrchg wants to merge 19 commits into
zeronsh:mainfrom
jsgrrchg:local-first-optional-sync

Conversation

@jsgrrchg

@jsgrrchg jsgrrchg commented Aug 8, 2026

Copy link
Copy Markdown

I tested this extensively on macOS, but I haven't been able to test it on Linux or Windows since I don't currently have environments set up for those platforms.

Please review this carefully, as these changes affect important parts of the app. I've also extended the E2E tests to improve macOS coverage.

Feel free to request any changes. I'll keep an eye out for feedback and address anything that comes up.

Closes #3

-- The following is AI-- :

Summary

  • Make a clean Comet installation fully usable without an account or network connection.
  • Turn multi-device sync into an explicit opt-in while keeping local and synced workspace data isolated.
  • Harden authentication and runtime transitions so changing accounts never swaps storage or transports underneath a running engine.

What changed

Local-first engine profiles

  • Introduce immutable Local, Synced, and Development workspace scopes, resolve the active profile once at startup, and expose it through the new EngineInfo RPC.
  • Store local snapshots, run journals, and uploads under profiles/local/ with a stable installation-local identity.
  • Preserve the existing orgs/{org_id}/{user_id} layout for authenticated profiles, so current synced installations reopen their existing data in place.
  • Keep Edge rooms, relays, and uploads disabled for local profiles. Development mode also stays offline unless an explicit bearer is configured.
  • Keep device-scoped resources such as repositories, worktrees, agent credentials, settings, and device identity available across profiles without exposing transcripts or attachments from another profile.

Authentication and lifecycle

  • Make comet headless and the Linux installer start immediately in local-only mode; signing in is no longer required to run the daemon.
  • Make comet login and comet logout select the profile for the next engine start, while refusing to mutate credentials behind a running engine.
  • Report the live runtime scope in comet status, with signed-out local mode treated as healthy.
  • Add graceful engine shutdown over RPC and SIGTERM so desktop/daemon transitions drain state and release IPC before a replacement runtime starts.
  • Fence canceled OAuth exchanges so an in-flight callback cannot restore credentials after sign-out, and back off transient offline refresh failures without treating them as revocation.
  • Publish device and local-profile identities atomically and recover an empty legacy device identity safely.

Desktop experience

  • Replace the signed-out account gate with a ready local workspace and an optional Enable sync action.
  • Keep the current local workspace open throughout sign-in and require a restart before entering the synced profile; local sessions are never uploaded implicitly.
  • Coordinate pending sync/restart state across viewports, block every synced viewport after sign-out, and wait for a remote daemon to stop before quitting for a profile change.
  • Treat older daemons without EngineInfo as synced so they retain the previous authentication gate instead of being mistaken for local runtimes.

Tests and documentation

  • Add integration coverage for signed-out startup, offline transport behavior, profile persistence and isolation, attachment jails, legacy synced-layout reuse, auth changes during a local runtime, and graceful daemon shutdown.
  • Update the two-device smoke driver to materialize transcript reset/delta frames and make Worker process-group cleanup work on macOS and Linux.
  • Document local-first installation, optional sync, profile storage boundaries, privacy guarantees, and the intentionally deferred cross-profile import/switching work.

Upgrade behavior

  • A clean installation starts in the local profile without contacting the Edge service.
  • An installation with a valid saved WorkOS session continues to start in its existing synced profile and storage layout.
  • Signing in does not upload, import, link, or delete local sessions or attachments.
  • Signing out returns to the same local profile on the next start; switching profiles always requires an engine restart.

Testing

  • cargo test --workspace --features comet-sync/mock-server -- --skip repos_round_trip_add_branches_worktrees --skip falls_back_when_interactive_attempt_hangs
  • cargo test -p comet-rpc --example e2e_driver
  • cargo clippy --workspace --all-targets --features comet-sync/mock-server
  • scripts/e2e-smoke.sh
  • git diff --check upstream/main...HEAD

The changes introduced by this branch pass Clippy. The workspace Clippy command exits successfully with only pre-existing warnings outside the modified hunks: collapsible_if in crates/update; four cloned_ref_to_slice_refs warnings in crates/doc; needless_return, two obfuscated_if_else warnings, and one unused test variable in crates/engine; and, in crates/ui, seven unexpected_cfgs warnings from the macOS Objective-C macros plus manual_is_multiple_of, collapsible_if, question_mark, unnecessary_sort_by, four doc_lazy_continuation warnings, three type_complexity warnings, too_many_arguments, and len_without_is_empty. Running Clippy with -D warnings therefore stops at the first unchanged collapsible_if warning in crates/update/src/lib.rs. The workspace test command skips two unchanged baseline tests on macOS: one compares /var/... with its canonical /private/var/... path, and one is an existing interactive-shell timeout flake. cargo fmt --all -- --check currently reports only pre-existing formatting drift in unchanged crates/sync/src/room.rs, which is identical to upstream/main.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@jsgrrchg

jsgrrchg commented Aug 9, 2026

Copy link
Copy Markdown
Author

I'll resolve conflicts now

@wingleeio wingleeio left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review findings and Linux validation

Reviewed head 9a775fc890f4f592fbaca4c3355ade546074c94d.

Findings

  1. Local-only still contacts Edge. assemble_runtime starts the updater regardless of WorkspaceScope or the computed online flag. After the initial delay, the updater calls fetch_latest, which requests the release manifest and fallback version file. This conflicts with the clean-install “without contacting Edge” behavior described by the PR. Please gate update checks on online scope or explicit update consent.

  2. Synced-profile attachment isolation is not account-scoped. All Synced and Development profiles still use {data_dir}/uploads, and the attachment jail trusts the entire configured upload root. Switching accounts therefore leaves known attachment paths, staging IDs, and commit filenames accessible or collidable across profiles. Please use account-scoped upload roots and define a migration/read-only fallback for the legacy cache.

  3. Engine assembly failures can leave the UI falsely Ready. The embedded engine is assembled in a detached task, but attach_engine marks the connection Ready immediately. If store or journal assembly later enters DeferredEngineState::Failed, the standing watches log the subscription failure and return. A corrupt or inaccessible local store can therefore show an empty ready application with no error/retry path. Please propagate deferred failure into ConnectionStatus::Failed, or await local assembly before publishing Ready.

  4. The systemd crash-loop limit was removed while Restart=on-failure remains. Both the generated unit and installer unit now restart every five seconds without the previous explicit rate limit. Permanent errors such as a corrupt store, invalid environment, or bind failure will churn indefinitely. Please restore an explicit start limit.

Device identity locking

The short device-identity lock is useful because identity resolution and runtime assembly can race before the lifetime engine lock. A kernel-owned file lock also recovers automatically after a crash.

The raw unsafe { libc::flock(...) } call is not needed on the workspace's rolling stable Rust toolchain. Safe cross-platform std::fs::File::{lock, try_lock, unlock} has been stable since Rust 1.89. The blocking identity lock can use file.lock()?; the lifetime instance lock can use try_lock() while preserving its retry and error handling.

Validation

  • Workspace test suite with comet-sync/mock-server: passed, with the two documented baseline tests skipped
  • cargo test -p comet-rpc --example e2e_driver: passed
  • Workspace Clippy across all targets: passed with existing warnings
  • git diff --check: passed
  • Headed Linux runtime: clean local boot, local workspace operation, restart persistence, optional-sync UI, login UI, and Devices settings all rendered successfully
  • cargo fmt --all -- --check: the PR-touched failure is import ordering in crates/ui/src/shell.rs; the other reported files are outside this PR

UI captures

Clean local start:

Clean local start

Local account menu with the optional sync action:

Optional sync menu

Enable Sync browser handoff:

Enable Sync dialog

Login gate:

Login view

Local Devices settings:

Local Devices settings

@wingleeio

Copy link
Copy Markdown
Contributor

thanks a lot for this PR, i did a cursory read and had codex go through this on a linux machine

nit: i think enable sync should be above settings

@SinaKhalili can you take a look as well please

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.

Allow users to run it locally with optional WorkOS integration

2 participants