Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e077cdc
refactor(engine): model local and synced workspace profiles
jsgrrchg Aug 8, 2026
98c2045
feat(engine): boot signed-out installations in local-only mode
jsgrrchg Aug 8, 2026
23e2fbe
feat(ui): replace the auth gate with a local sync opt-in
jsgrrchg Aug 8, 2026
810633a
feat(cli): make local-only the default daemon lifecycle
jsgrrchg Aug 8, 2026
1002f70
test(engine): lock down local and synced profile isolation
jsgrrchg Aug 8, 2026
f4fb129
docs: document local-first usage and optional sync
jsgrrchg Aug 8, 2026
484d983
fix(ui): block all synced viewports after sign-out
jsgrrchg Aug 8, 2026
f141942
fix(engine): require a bearer for development sync
jsgrrchg Aug 8, 2026
c37dafc
fix(ui): share pending sync restart across viewports
jsgrrchg Aug 8, 2026
c7df2f8
fix(engine): publish device identity atomically
jsgrrchg Aug 8, 2026
a824656
fix(ui): stop remote daemon before runtime changes
jsgrrchg Aug 8, 2026
cefcbb6
fix(auth): invalidate canceled OAuth exchanges
jsgrrchg Aug 8, 2026
4b985d8
fix(engine): recover truncated device identity
jsgrrchg Aug 8, 2026
5dee4fc
fix(ui): wait for remote engine shutdown
jsgrrchg Aug 8, 2026
d2d6bdb
fix(auth): avoid unsupported match guard
jsgrrchg Aug 8, 2026
15c006b
fix(ui): clear stale sync restart after revocation
jsgrrchg Aug 8, 2026
0c4f452
fix(auth): back off offline refresh failures
jsgrrchg Aug 8, 2026
296259b
fix(e2e): support macOS and transcript deltas
jsgrrchg Aug 8, 2026
9a775fc
Merge upstream main into local-first optional sync
jsgrrchg Aug 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 57 additions & 39 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ A ground-up native rewrite of [comet](../comet) — a multi-device controller fo
(Claude Code / Codex) — in Rust, with a gpui UI. Fresh app; no backwards compatibility required.

**Pillars (from the goal):**
- Sync is Loro CRDT docs (loro-mirror model) through Cloudflare Durable Objects.
- Optional sync uses Loro CRDT docs (loro-mirror model) through Cloudflare Durable Objects; the same docs persist locally when sync is disabled.
- Durable Objects stay **TypeScript** (decision + evidence: `docs/research/durable-objects-language.md`).
Everything device-side is Rust.
- Feature parity with comet **except token-usage display** (poor fit for CRDTs; excluded).
Expand All @@ -17,18 +17,14 @@ A ground-up native rewrite of [comet](../comet) — a multi-device controller fo

```
gpui UI ─ in-proc/localhost RPC ─ engine A ══ DeviceRoom DO relay ══ engine B ─ RPC ─ gpui UI
(edge Worker: auth, rooms, R2)
└── Loro sync ─ SessionRoom DO (per chat) ──────────┘
└── Workspace doc room (per org) ────────┘
optional edge Worker: auth, rooms, R2 │
└── optional Loro sync ─ SessionRoom DO (per chat) ──┘
└─ Workspace registry room ────┘
```

- **Engine = backend** (was `@comet/backend`): runs agents, owns auth, terminals, repos/worktrees,
diff sync, doc hosting. Pure Rust daemon, fully functional headless.
- **UI = viewport** (was Electron): gpui app rendering engine state. Talks the same typed RPC
whether the engine is in-process or a separate daemon. Organized around **spaces** — synced
(device, folder) pairs: the sidebar lists spaces plus a global attention-sorted Active list;
the main area shows the selected space's sessions as horizontal tabs (closing a tab archives);
new sessions are minted onto the space's device via relay-forwardable RPCs.
- **UI = viewport** (was Electron): gpui app rendering engine state. Talks the same typed RPC whether the engine is in-process or a separate daemon. Organized around **spaces** — (device, folder) pairs, local or synced according to the active profile: the sidebar lists spaces plus a global attention-sorted Active list; the main area shows the selected space's sessions as horizontal tabs (closing a tab archives); new sessions are minted onto the space's device via relay-forwardable RPCs.
- **Edge (TypeScript, ported from comet `apps/edge`)**: Worker + SessionRoom DO (per chat) +
DeviceRoom DO (per device) + R2 attachments + WorkOS JWKS auth. Absorbs the old `apps/server`
responsibilities (WorkOS code exchange/refresh, orgs) so **Postgres, the Hono server, and
Expand All @@ -42,14 +38,51 @@ Single binary `comet`:
port**. The embedded engine is not private: any other viewport can attach to the running app
without it first being restarted as a daemon. Binding is best-effort — if the port is taken the
window still opens, having lost only the ability to host peers.
- `comet headless` — engine only; prints sign-in URL on TTY (paste-code flow), serves IPC on
localhost + hosts its DeviceRoom for remote control. A VPS runs this; a laptop's UI drives it.
- `comet headless` — engine only. A clean installation immediately serves its local profile over localhost IPC; when a saved account selects the synced profile at startup and a bearer is available, it also hosts its DeviceRoom for remote control. A VPS can run this while a laptop's UI drives it.

### Local-first workspace profiles

Authentication and workspace selection are deliberately separate state machines:

- `AuthState` is live credential state: `SignedOut`, `NeedsOrganization`, or `SignedIn`. It may change after login, refresh, revocation, or logout.
- `WorkspaceScope` is the immutable storage and transport boundary captured once at engine startup: `Local`, `Synced`, or explicit `Development`.

The engine never re-resolves an open store because `AuthState` changed. This prevents a sign-in, token refresh, or revocation from silently swapping databases or attaching online transports to a runtime that started local-only.

| Startup condition | `WorkspaceScope` | Online transports |
| --- | --- | --- |
| WorkOS enabled, no parseable saved `session.json` | `Local` | Disabled |
| Parseable saved WorkOS session | `Synced` | Enabled when a bearer is available; organization onboarding completes before opening the store when needed |
| WorkOS disabled without a dev bearer | `Development` | Disabled |
| Explicit non-empty dev bearer | `Development` | Enabled |

`comet login` and `comet logout` operate on `session.json` while the engine is stopped. Login selects `Synced` for the next start; logout selects `Local` for the next start. The UI may update live authentication status, but the active `WorkspaceScope` still changes only after restart.

The resolved profile selects the session snapshots, registry snapshot, run journals, and attachment cache that may contain workspace data:

| Scope | Store and journals | Uploads |
| --- | --- | --- |
| `Local` | `{data_dir}/profiles/local/` | `{data_dir}/profiles/local/uploads/` |
| `Synced` | `{data_dir}/orgs/{org_id}/{user_id}/` | `{data_dir}/uploads/` |
| `Development` | `{data_dir}/orgs/{org_id}/{user_id}/` | `{data_dir}/uploads/` |

The synced and development roots intentionally preserve the historical cloud layout. Local identity lives in `{data_dir}/local-profile.json`; its UUID is stable across restarts and is not an account or development identity.

Device identity and machine resources remain device-scoped under the common data directory: `device-id`, repository registration, managed worktrees, agent credentials/accounts, and UI settings. They are available across profiles, but they do not contain or expose another profile's transcripts or attachments.

#### Privacy boundary and follow-ups

This first local-first change does not upload, import, link, or delete local sessions when a user signs in. Local attachments remain jailed under the local upload root and are not readable through the synced attachment cache. Returning to local-only mode reopens the same local identity and data.

The following product work is intentionally deferred:

1. Explicit session selection and copy between local and synced profiles, including attachment copying, provenance, and conflict behavior.
2. Browsing both scopes simultaneously or switching the visible scope without restarting the engine.
3. A supported self-hosted backend contract covering authentication modes, room APIs, authorization, persistence, and blob storage. Current endpoint and bearer overrides remain development/deployment seams, not a promised compatibility surface.

## 2. Data model — all Loro

Two doc kinds, one room protocol (loro-protocol over WebSocket, the same protocol the TS edge
already speaks; Rust side uses the official `loro-protocol`/`loro-websocket-client` crates or a
thin hand-rolled client over `loro` 1.13.x — verify interop early, M1 exit criterion):
Two persistent doc kinds. When sync is enabled they share one loro-protocol room protocol over WebSocket; local-only profiles persist the same docs without joining rooms:

1. **Session doc** (per chat) — the transcript + durable command queue. Schema is a Rust port of
`packages/session-doc` (same container names/shapes so the edge's tail materializer keeps
Expand All @@ -60,25 +93,11 @@ thin hand-rolled client over `loro` 1.13.x — verify interop early, M1 exit cri
run journal), tail/diff sidecars. Constants carried over (`STREAM_COMMIT_MS=120`,
`DO_FLUSH_MS=5s`, compaction at 8MB, retain 30d, tail 64).

2. **Workspace doc** (per org — NEW; replaces comet's residual entity sync) — **spaces**
registry (id, deviceId, path, name?, gitDetected, checkoutId — a space is a synced
device+folder pair, the app's unit of organization; the owning device's SpacesSync stamps git
presence so branch pickers / the diff sidebar gate on a synced bool, no RPC), chats index
(id, deviceId, title, archived, cwd, branch, checkoutId, spaceId, lastSeenAt,
lastMessagePreview/At, config), devices registry (id, name, platform, lastSeenAt), session
status rows (Working indicator; staleness-checked client-side so a crashed backend never shows
eternal "Working"), checkout-diff summary pointers. `lastSeenAt` is the synced LWW seen marker
behind the "completed (unseen)" indicator. Lives in its own DO room (same SessionRoom DO
class, doc id `ws2/{orgId}` — the `2` is the spaces-overhaul destructive break), with presence
via Loro `EphemeralStore` (replaces the 15s heartbeat writes). Writer discipline: each device
writes only its own device/session/chat rows and the git stamps of spaces it owns;
creates/renames/archives/seen-marks are LWW map sets from any device. `deleteSpace` cascades:
the space row and every chat/session row in it tombstone in one commit.

*Why a workspace doc and not N tiny docs:* the sidebar needs one subscription for the whole
list (grouping, resort animations, unseen markers); one doc = one room connection + one mirror.
Volume is tiny (index rows, no transcripts), so oplog growth is negligible and daily compaction
applies anyway.
2. **Workspace registry doc** (per profile) — the `registry1` snapshot stores spaces (id, deviceId, path, name?, gitDetected, checkoutId), the chats index (id, deviceId, title, archived, cwd, branch, checkoutId, spaceId, lastSeenAt, lastMessagePreview/At, config), devices, session-status rows, and checkout-diff summary pointers. A space is a device+folder pair in the active profile; the owning device's `SpacesSync` stamps git presence so branch pickers and the diff sidebar can gate without another RPC. Local scope keeps the registry entirely in its profile store. Synced and development scopes join `/registry/{orgId}/ws`, backed by the private per-user room `reg1/{orgId}/{userId}`; rows are never visible to every member of an organization.

Writer discipline: each device writes its own device and session-status rows, rows for chats it hosts, and git stamps for spaces it owns. Creates, renames, archives, and seen marks are LWW sets accepted from any device. `deleteSpace` tombstones the space and every chat/session row in it in one commit. Presence uses ephemeral room frames rather than durable heartbeat writes.

*Why one registry and not N tiny docs:* the sidebar needs one subscription for the whole list (grouping, resort animations, unseen markers). Its rows contain indexes rather than transcripts, so one local snapshot and, when enabled, one room connection remain bounded and cheap.

3. **Mirror layer** (`comet-doc` crate) — Rust equivalent of loro-mirror: typed structs for the
schema, **incremental** application of `doc.subscribe` diffs into cached state (no full
Expand All @@ -104,7 +123,7 @@ comet-native/
# entities, RPC envelopes (serde; ndjson framing);
# `view` = the pure derivations both frontends share
# (sort orders, staleness gating, grouping, boot gate)
doc/ comet-doc # session-doc + workspace-doc schemas, mirror layer,
doc/ comet-doc # session-doc + workspace-registry schemas, mirror layer,
# parts fold, continuations, command ledger, sidecars
sync/ comet-sync # loro room client (join/VV backfill/fragments/backoff),
# ephemeral presence, DocsStore (SQLite snapshots +
Expand Down Expand Up @@ -196,7 +215,7 @@ Direct ports of comet behaviors (spec: feature-inventory §3):
`codex exec --json`; model/reasoning/option catalogs ported from `packages/harness`.
- **Repos/diffs**: git2 or `git` subprocess (subprocess — matches comet, avoids libgit2 edge
cases); worktrees under `~/.comet-native/worktrees`; fs watchers (`notify`) + 2min repair; diff
capture (patch + numstat + untracked, 3MiB cap, sha256) → workspace doc summary + DO diff
capture (patch + numstat + untracked, 3MiB cap, sha256) → workspace registry summary + DO diff
sidecar.
- **Agent accounts**: credential-slot swap (macOS Keychain via `security-framework`, files
elsewhere), plan labels, usage probes, paste-code/browser-poll OAuth flows.
Expand All @@ -208,8 +227,7 @@ Direct ports of comet behaviors (spec: feature-inventory §3):
Port `comet/apps/edge` nearly verbatim (it is already Loro-native and smoke-tested: session room
w/ hibernation + two-level compaction + daily alarm backups, device room byte relay + nudges +
sidecar slots, R2 attachments, JWKS auth). Additions:
1. Workspace-doc rooms (`ws/{orgId}`) — same DO class, org-membership authz instead of
claim-on-first-join.
1. Private per-user registry rooms (`/registry/{orgId}/ws` → `reg1/{orgId}/{userId}`) with authenticated row sync and ephemeral device presence.
2. `/auth/*` routes absorbed from `apps/server` (WorkOS API key in Worker secret).
3. Drop `/seed` migration path and legacy sync anything (fresh app).
Hibernation hygiene: no idle timers (flush timer only while dirty), auto-response ping/pong —
Expand All @@ -220,7 +238,7 @@ per `docs/research/durable-objects-language.md`.
- **Excluded**: token-usage display (profile heatmap, lifetime stats, per-message token columns,
`WatchUsage`). Rate-limit meters on agent accounts are *kept* (separate concern; probed from
CLIs, not CRDT-synced).
- **Changed**: Postgres entity sync/server → workspace doc + edge; Electron/React/mugen → gpui with
- **Changed**: Postgres entity sync/server → workspace registry + edge; Electron/React/mugen → gpui with
ported techniques; Node harness SDKs → subprocess protocols; WebRTC → device-room relay (comet
had already made this move); mobile app → out of scope for this repo.
- **Kept verbatim**: session-doc schema shape + constants, command ledger rules, edge DO design,
Expand All @@ -240,7 +258,7 @@ Status legend: ✅ shipped · 🟡 shipped with named gaps (see `docs/PARITY.md`
- ✅ **M3 UI core** — shell (sidebar/panes/header), transcript (virtualized, markdown, streaming,
stick-to-bottom), composer (send/steer/stop, question panel); local chat fully usable headed.
- ✅ **M4 Multi-device** — device-room host/client virtual sockets, remote device control, workspace
doc entity sync, WorkOS auth + org gate, presence. Proven live by `scripts/e2e-smoke.sh`:
registry sync, WorkOS auth + org gate, presence. Proven live by `scripts/e2e-smoke.sh`:
two headless engines against a real edge — B queues a run into the chat doc, the durable
nudge wakes host A, A executes (mock harness), transcript + session status sync back to B.
- 🟡 **M5 Full surface** — terminals, diff pane, repo/branch/folder pickers + worktrees,
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 31 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,51 @@
# Comet

Control your coding agents (Claude Code, Codex, Grok, Hermes, Pi) from any of
your devices.
Control your coding agents (Claude Code, Codex, Grok, Hermes, Pi) locally by default, with optional multi-device sync.

![Comet running a Claude Code session](docs/screenshot.png)

Every device runs a small engine that keeps your sessions in sync: start an
agent on one machine, follow and drive it from another. Install the engine as
a daemon on an always-on machine (a VPS, a spare box) and your agents keep
working after you close your laptop.
Every device runs a small engine that stores sessions on that device. A new installation starts in local-only mode without an account or a network connection.

## Install the daemon (Linux)
## Install and run locally (Linux)

```bash
curl -fsSL https://comet.zeron.sh/install.sh | sh
comet login # sign in (paste a code, done)
systemctl --user start comet-native
comet status
```

No configuration needed. Day-to-day:
The installer starts the daemon immediately and keeps it running across reboots. No sign-in or sync configuration is required.

Day-to-day:

```bash
comet status # signed in? engine running?
comet status # local/synced mode and engine status
comet update # update to the latest release
comet daemon start|stop|restart|status
```

On macOS: build `comet` from source, then `comet daemon install` (launchd).
## Optional multi-device sync

Sign in only when you want to open your account's synced workspace. Authentication changes the profile selected by the next engine start, so stop the daemon before changing it:

```bash
comet daemon stop
comet login
comet daemon start
```

You can then start an agent on one synced device and follow or drive it from another. An always-on machine such as a VPS can keep those agents working after you close your laptop.

Signing in does not upload, move, or import existing local sessions. Local sessions and their attachments remain under the local profile and reappear when you return to local-only mode:

```bash
comet daemon stop
comet logout
comet daemon start
```

`comet login` and `comet logout` refuse to modify credentials while an engine owns the data directory. The desktop app follows the same next-restart profile boundary.

On macOS: use the desktop release, or build `comet` from source and run `comet daemon install` to install the launchd service.

---

Expand Down
Loading