Skip to content

Relay: coordination as a Raft group, and joined computers can lead - #1950

Merged
AbirAbbas merged 103 commits into
devfrom
abir/raft
Oct 26, 2026
Merged

AbirAbbas merged 103 commits into
devfrom
abir/raft

Conversation

@AbirAbbas

@AbirAbbas AbirAbbas commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

What this changes

Until now one relay process was the single writer for each identity's chat records. That process decided who holds a chat, accepted every save and could not be replaced. This PR turns the relay into a small Raft group per identity, so coordination no longer depends on one writer:

  • Three voters by default. On the hosted relay these are three Durable Object anchors. codeaf relay --store runs three local anchors. A change counts only after a majority commits it, and ownership reads are fresh majority reads. Losing one voter is fine. Losing two refuses coordination and keeps saved chats; it never creates a writable minority.
  • Joined computers are members. An open, paired computer keeps a durable learner copy. Learners never add a vote, so pairing more machines or adding workers does not change the quorum.
  • A joined computer can lead. codeaf cluster status shows committed membership and the current leader. codeaf cluster lead swaps one anchor's vote to this computer through joint consensus, transfers leadership to it and returns an elected read receipt. The relay then forwards requests to that leader.
  • Fallback. If the leading computer sleeps, dies or switches sync off, the two remaining anchors elect a leader and restore the third anchor. The target is about 65 seconds in healthy conditions. A returning computer stays a learner and never takes leadership back by itself; it leads again only on another explicit request.
  • Workers. A delegated worker gets an expiring grant for selected chats, plus conditional assignment claims in the log. It cannot approve pairing, read the identity vault, rotate keys, enroll devices or lead.
  • Quiet when idle. Once every voter applies the idle barrier, a caught-up open home makes no HTTP requests, peer frames or client attempts, and the anchors schedule no recurring alarms. Presence sweeps run only while a viewer has the events stream open.

This is the coordination layer for the scenario where a phone leads and serverless dev boxes do the work. Both are follow-ups and neither is built here: there is no phone app yet, and no dev-box provisioning or headless worker loop. The manual says so plainly.

How it is built

  • Core: go.etcd.io/raft/v3 v3.7.0 RawNode, inside a deterministic wrapper (internal/raftcore). It is vendored under third_party/raft with a single patch: per-node election randomness, so replays are exact.
  • One core, two runtimes. The same Go core runs natively in codeaf relay and as a WASI reactor inside the hosted Worker's RaftMemberDO. A small JS shim supplies storage, alarms and transport. The hosted bundle is 2.47 MiB compressed, against Cloudflare's 3 MiB limit.
  • Shared rules. Directory rules and records live in one Go state machine (internal/clusterstate), and both runtimes apply the same logic.
  • Older clients still work through a v1 adapter. They pair, list, continue and take back. Requests up to the old 1 MiB HTTP limit are staged and committed atomically, so nothing is truncated. One request is refused: a blind force of a live chat without its observed holder. That gets the existing sentence "another device continued this chat first".
  • Upgrade. On first use, the existing directory database is frozen and imported into the three anchors, and the source is kept. The upgrade is one-way: an older relay started on an upgraded store refuses to start, with codeaf: directory upgraded; use a current relay build.

Numbers

  • Election fallback in the seeded simulator: p50 25 s, max 35 s. Ranked election windows keep split votes rare.
  • Idle open home over ten minutes, two real homes: 0 HTTP requests, 0 frames and 0 client attempts. This holds as a learner, as leader and after a fallback, on both relays.
  • bin/codeaf grows by 3,379,200 bytes, mostly the protobuf code upstream Raft needs. The size cap moves by exactly that measured weight, and PERF.md records it.
  • Self-hosted directory content now has an 8 MiB default allowance per identity. Larger imports keep their measured size.

Known limits

  • Three voters tolerate one unavailable member. Local anchors share one host. Hosted anchors share one provider and relay path.
  • Phone leadership needs the phone app. Dev-box workers need provisioning and a run loop. Both are next.
  • On the native relay, a brand-new identity's first device registration sometimes misses the client's 20 s deadline under heavy disk load. Its first write sets up three local anchors. A retry pairs in about a second, and normal pairing takes 1–3 s. Under the same load, dev failed this in 1 of 4 attempts and this branch in 3 of 4. I will file it as a follow-up.
  • An open home's watch socket keeps the relay's platform ping/pong for advisory presence, at most 122 frames per socket per 10 minutes. HTTP requests, client attempts, application frames and anchor alarms stay at zero.
  • Private task copies (opt-in), frame hints through the Raft relay, and two-computer fixes #1944 adds frame hints to directory records. After this lands, those hints have to be carried through the Raft command and snapshot path: the v1 adapter decodes strictly, so an unknown field is refused, not ignored. I will do that follow-up.

How it was checked

  • Deterministic simulator with a Porcupine linearizability checker, run over 2,000 seeds. It injects crash cuts, partitions, delayed or duplicated delivery and leader loss.
  • Native and hosted journey tests, run under local workerd, cover pairing, continue, take back, quit, sync off, revoke, joining as learner, leading, forwarding, fallback and the one-way upgrade.
  • A whole-branch adversarial review found eleven issues; all are fixed, each with a regression test.
  • Final full verification on the merged-with-dev head:
    • raftsim and clusterstate at 2,000 seeds;
    • make pr-ready, and the touched legs on CI;
    • internal/syncsetup in 590 s;
    • the relay, directory, pair, cellsync, remote and cluster packages;
    • the hosted suite (237/237) and every hosted Raft test file, with 0 skips;
    • the size, notice, law and manual gates.
  • Two-computer journey with the built binary, on both the native and the hosted relay under local workerd. It covers pairing, continue, take back, cluster status, cluster lead, the leader stopping (anchors coordinate after 40 s native and 46 s hosted, with no hand-back when it returns), revoke and a fresh install.
  • Regression found and fixed during that check: a joined home had shown a dead computer as online until its 90 s lease lapsed, so taking a chat back after the holder died without saying goodbye took up to 80 s; on dev it takes 3 s. Home presence follows the relay watch again, as on dev, and leases decide only ownership. Take-back now takes 3–4 s on both relays, and the C31 tests pin it at under 30 s.

Owner steps after merge

The hosted relay is not deployed by this PR. Its deployment configuration needs the RAFT_MEMBERS binding, the v3-raft-members migration and the new worker entry, all declared in relay/hosted/wrangler.toml. Deploy to staging first and run the two-computer journey there before production.

🤖 Generated with Claude Code

AbirAbbas and others added 30 commits October 26, 2026 07:03
Keep legacy lease decisions and JSON behavior intact while making the shared records usable without HTTP or SQLite. Define a strict typed v2 boundary with exact uint64 and sealed-byte round trips across Go and JavaScript before consensus code depends on it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Give the fault simulator an ownership, vault and revocation model that cannot inherit defects from production apply functions. Keep candidate history states independent and pin observed-take and displaced-write behavior before the history checker is added.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Native and WASI members must use the same reviewed election seam and protobuf wire implementation. Retain upstream source and notices, record the narrow patch and source hashes, and expose only these dependency directories through the existing ignore rule.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Wrap RawNode in a seeded single-threaded input and effect contract so hosts cannot release dependent messages or authority reads before persistence and application. Bound read contexts, reject peer-injected control messages, stop on storage or schema failures, and pin election replay with focused quorum and transfer smoke tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… Go core.

Keep consensus synchronous behind owned binary buffers and explicit durability completions. Bound frames and handles, audit the WASI runtime imports, and exercise real lease rules over immutable paged rows without adding a production writer.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…on it.

Compare native and wasm effects through real commit and ReadIndex exchanges, restart disk-backed workerd storage, and measure the complete package and a grandfathered-size fixture. Run consensus and bulk memory phases serially so the proof fits the shared laptop budget without changing the Worker workload.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Keep each member in its own FULL-synchronous SQLite WAL store so votes, conflicting suffixes and application results survive restart. Refuse corrupt authoritative state and commit rows, dedup results, configuration and the applied index together.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Cut subprocesses at every transaction boundary and observe actual WAL flushes before acknowledgment. Exercise production cores with three real stores so leader loss, joint configuration and dropped application completions recover without losing committed results.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Stream complete application snapshots, retain two checked generations and atomically install their state before acknowledging Raft persistence. Compact only covered applied history so a disconnected learner catches up without pinning the log.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Preserve ABI-1 effects while adding checkpoint completion and maintenance queries. Pin the snapshot schema and source hash in the Worker bundle so native and hosted adapters share the same format.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Keep timed-out operations as unknown outcomes and retain session results and tombstones so the oracle can detect stale reads, double takes and duplicate execution without sharing production rules.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…etwork faults.

Drive explicit write, flush, application and reply boundaries through seeded virtual-time schedules, preserve portable snapshots across recovery and compare the disk model with native SQLite. Keep replayable counterexamples and require broken fence, read and flush controls to fail.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…aigns.

A lost proposal can still be uncommitted after draining messages, and randomized elections can split votes beyond a fixed 25-tick wait. Preserve exact replay regressions, check each election timeout separately, and run 500 seeds by default with an explicit larger-soak count.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… displace voter traffic.

Bind relay exchanges to committed identity and member generations, preserve signed opaque bytes across reconnects, and reserve bounded inbox capacity for voters and snapshots. Publish a shared native and JavaScript wire vector.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…production host assembly.

Mount the peer handler through the relay host boundary and drive three durable native members through election, committed application, ReadIndex, restart and absent-learner progress without inbound member listeners.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Expose bounded protobuf inspection, streaming hashes and portable snapshot pointers so hosted storage can persist consensus without a second application writer. Exercise the same protocol through native SQLite proof peers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Serialize each actor across durability waits, authenticate bounded peer inboxes and stream retained snapshots through the shared reactor. Add the local compiled-module entry and independent object binding while leaving the directory cutover to a later change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Drive signed mixed-runtime commits, quorum reads, both snapshot directions and joint-state restarts in local workerd. Bound admission, retained state and co-resident memory before the directory adapter uses these actors.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…achine.

Verify committed enrollment and signed requests at application, retain exact sequence results, and expose bounded dependency evaluation through the same native and WASI ABI.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cation.

Adapt every Client verb to the consensus boundary, assemble immutable native read-sets and atomic write-sets, and prove the public directory and HTTP doors with real stores and the independent ownership model.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Keep JavaScript limited to dependency bytes and durable writes, expose the migration-ready facade, and prove signed hosted decisions with native ABI parity while preserving the earlier workerd mechanism fixture.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e delivering grants.

Keep effect signing separate from personal authority, bind approvals to authenticated request keys, and retain exact outbox outcomes across crashes. Preserve original v1 upload ordering while requiring evidence for v2 publication.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Reserve each public request for one cluster, commit its device authorization before exposing the sealed grant, and repair interrupted uploads and delivery acknowledgements without deleting content.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…story.

Keep the source irrevocably gated, preserve its complete rows and watch evidence, and resume the same three-voter genesis after interruption so old and new writers cannot overlap.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Drain admitted legacy effects before freezing, fence incomplete member activation, and compare the hosted genesis with native storage so eviction and retries retain the original authority and content.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…uorum.

Keep admission separate from applied configuration, retain every retired node ID, and complete authorized joins after the requester sleeps. Replay configuration at each log position so follower batching cannot change policy. Prove bounded churn, snapshot catch-up, exact retries and restart with the production core and native stores.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ts cannot restore retired nodes.

Page registry policy through the shared Go reactor and use the applied join barrier before reporting readiness. Preserve the source-provisioned three-anchor bootstrap, isolate the old mechanism seam in its test fixture, and exercise real workerd restarts with a dead learner. Document enrollment limits and refusals with the implementation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…back.

Use committed owner intents and explicit joint consensus for personal leadership, require a fresh quorum read for completion and retain it to prevent automatic regain. Rank randomized election windows from the immutable provisioned anchor manifest while preserving quorum and vote rules and exact versioned replay.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ors.

Recover the committed anchor order through registry and import snapshots in both runtimes. Serialize readiness and control effects, recheck current authorization, persist original request retries and document the actual commands, refusals and fallback limits.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Retain every original split-vote seed and all joint crash cases, measure every seeded conditional acquire and preserve old exact replay. Exercise native process death, WASI promotion, snapshot restoration and minority refusal under the existing laptop resource ceilings.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AbirAbbas and others added 20 commits October 26, 2026 07:03
Use complete local HTTP replies and a smaller inspector client, omit unused services, and bound background compilation caches. Reap completed native hosts while keeping every hosted actor and the whole-run combined memory measurement.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…pages.

The coordination pages add enough text to the chat manual that "is there a
newer version" fell out of its answer's top results, behind a short Windows
handoff section that mentions a newer engine. The update section now says the
asker's words in its body as well as in its heading, so the question reaches
the section that answers it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The relay now keeps its directory in a replicated log that the native relay,
the hosted relay and a joined computer share. The architecture guide names
the packages a contributor starts from for that work, beside the remote
session row, and points at the relay guide and the manual page that explain
the behaviour people see.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…es remain.

A large request from an older client is staged as raw body pages beside the
join requests in the same namespace. When such a request is cut short, its
pages wait for the next request to replace them, and the maintenance turn that
looks for undelivered join decisions decoded every row it scanned, failed on
the raw pages and skipped the rest of its idle work. It now reads only join
requests, as the delivery replay already does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The row probe's summary and the hosted smoke test's assertion message now say
what they observe about snapshots instead of naming internal planning steps.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Preserve complete older records with lossless encoding and retain their original sources. Reserve bounded initialization and retry bookkeeping so full directories can finish upgrading and persist capacity refusals without stopping committed work. Share the storage accounting between native and hosted relays while keeping content and protocol limits unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sleeping groups free their reservations so other households can activate. Busy groups keep their reservations and admission pressure returns a retryable refusal. The next operation recovers durable followers and establishes a fresh majority.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Freeze the retained tables and install durable write guards in one transaction so older relay code cannot acknowledge divergent changes. Keep the frozen source readable for import and recovery, and document the older relay refusal.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Hosted requests retain the configured prefix with the original signed URI so every member interprets the same operation when a joined computer leads. Directory changes, enrollment and large rotation requests use that exact prefix. Native creates and publishes treat an empty frame array like an omitted or null plan while retaining the original actor and operation binding.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Finished, cancelled and reassigned work frees claim retry records so later assignments and personal leadership requests retain session capacity. Retained assignment receipts keep retries idempotent and prevent old requests from acquiring work again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sharing the existing cycle avoids one replicated renewal per presence ping. Each bounded batch still requires fresh evidence for the exact holder and fence, and receipts remain available only after durable commitment.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The coordination core now adds 3,379,200 bytes to the stripped linux/amd64
binary: 75,755,785 bytes on the current dev base and 79,134,985 with the core
linked. The size cap moves by the same amount, from dev's 75,811,848 to
79,191,048, so the build keeps the 56,063 bytes of headroom the base has.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The entry lists what a reader of dev now believes wrongly. Problems that
existed only while this feature was being built never reached dev, so their
lines go, and the upgrade line now says what an operator sees when an older
relay is started on an upgraded store.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The entry was renamed for this pull request, but its pr field still said 0,
which the change-entry check rejects.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…and.

A hosted relay under a prefix commits that prefix beside the signed URI, and
members now route the proof by it. The maximum-command proof captured its
request on the client without the prefix, so its size probe could no longer
translate the request at all. It now carries the request's prefix, measuring
exactly the command the relay commits; the boundary and delivery assertions
are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…pen.

A joined home's Home took presence from its local learner, which counted any
computer holding an unexpired chat lease as online. A computer that stopped
without goodbye therefore stayed online until its 90-second lease lapsed, and
the other computer could not offer to continue its chat for over a minute.

Home now takes online and offline only from the relay watch, with the usual
fifteen-second offline debounce, while versions, device news and directory
reads still come from the local learner. Lease expiry stays the authority for
ownership decisions and never for the display.

New tests stop the holding process without goodbye and require the
offer within 30 seconds while the lease is still valid, on the native relay
and the hosted Worker. The open-home idle tests now allow Home's watch socket
its platform ping and pong, bounded per ten minutes from the ping interval,
and still require zero HTTP requests, client attempts, peer and forward
frames, recurring alarms and presence-driven coordination wakes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The manual and the change entry described a joined Home with no remote
presence viewer. Home keeps its relay watch with ten-second pings, so a
stopped computer appears offline within 30 seconds and its chat can be
continued elsewhere.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The holder runs in a re-executed test process, which did not mark the cell
engine available the way the test's other homes are prepared. On a computer
without an installed engine binary the holder could not open its home and
exited at once, so the proof depended on the machine it ran on.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Linking the client tests can take minutes on a busy disk. Doing it inside
the timed test let the link consume the proof's own limit, as the other
hosted client launchers already avoid by linking first.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@AbirAbbas
AbirAbbas marked this pull request as ready for review October 26, 2026 11:22
@AbirAbbas
AbirAbbas merged commit 808c90c into dev Oct 26, 2026
8 checks passed
@AbirAbbas
AbirAbbas deleted the abir/raft branch October 26, 2026 11:22
AbirAbbas added a commit that referenced this pull request Oct 26, 2026
…urrow v0.3.4

Brings the workspace-ownership branch up to dev cf282dd, which adds the
Raft relay (#1950), the two-computer fixes (#1967), #1972, #1974 and #1976.

- directory record types are now clusterstate aliases, so frame hints move
  into the replicated state machine: FrameHint and the frame_hints field on
  Cell, CellInit and Publish, bounded deterministically in the shared rules,
  dropped from listings and kept on single-cell reads. Signed v1 publishes
  with hints are accepted; unknown fields are still refused; existing
  canonical bytes are unchanged and no schema or protocol version moves.
- every take path records the send lease, including the workspace-set and
  identity-adopting restores; an already-held chat keeps its hold when the
  record fails.
- a removed computer cannot take through the folder-mapping road either, and
  unsent words and the pending send survive the root chooser.
- the seal refuses a folder holding home before taking the checkpoint lock.
- furrow pin follows dev at v0.3.4, which contains v0.3.2's non-mutating
  SQLite capture.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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