The relay Worker (nucleic-edge at relay.nucleic.blakeslee.xyz) is deployed, so land the
formerly deploy-gated client side of the data path:
- NucleicProtocol/Sync/RelayTransport.swift: RelayAPI (one base URL for REST + WS,
membership -> connection token trade), RelayWebSocket (ordered sends, ping keepalive,
ping-confirmed connect), RelayFrameChannel (client leg, WireFraming inside WS binary,
presence fail-fast when the room has no host), RelayPresence.
- NucleicCore/Sync/RelayAccess.swift: X25519 PoP enrollment (RelayEnrollment), room
credential in the login Keychain (separate from the push credential), membership minting
with re-enroll-on-401.
- NucleicCore/Sync/RelayListener.swift: host SyncListener demuxing the room socket into
per-tag virtual FrameChannels via RelayEnvelope; presence-driven reaping; backoff redial;
injectable RelayRoomSocket seam for tests.
- Wire: additive HostMsg.relayMembership(WireRelayMembership) pushed after every hello
(SyncHost.register -> AppStore mint) so devices paired before the relay adopt it and the
~90-day token refreshes on each connect; the pairing QR also carries a bootstrap
membership so first contact can ride the relay. Old clients ignore the unknown tag.
- AppStore: .relay joins the listener composite behind the Connection-methods checkbox
(failure degrades to a status row), advertises relayRoomID in PeerAddresses, mints the
QR bootstrap in beginPairing.
- Desktop UI: the Nucleic Private Relay toggle is enabled (was "coming soon"); the
LAN-only banner offers it alongside Tailnet.
- iOS: relay is the last dial candidate in HostConnection pair + reconnect (10s handshake
watchdog); PairedHost persists relayRoomID/relayMembershipToken/relayURL; the
relayMembership push updates the registry in place; Settings shows Relay in Transports.
Tests: RelayTransportTests, RelayListenerTests, SyncHostTests relay push + QR bootstrap.
Full suite green (783 core + 113 protocol + 2 new); iOS simulator build succeeds. Live
smoke test against the deployed Worker passed end-to-end (PoP enroll, both token tiers,
two-socket frame round-trip through the Room DO with correct envelope tags).
Known limits: host revoke-on-unpair not wired (endpoint is admin-only); PeerClient
(Mac<->Mac) doesn't dial the relay yet.
Co-Authored-By: Claude Fable 5 <[email protected]>
The iOS [HostID: HostConnection] multiplexer: connect to all paired Macs at once, mirror the active
one, instant host switching, cross-host badge/Live Activity. Compile + demo-verified; the live
multi-connection path needs a two-Mac test (may be rolled back).
Resolves the RemoteStore.swift overlap with dev's Tailnet-node-name/auth-key change by taking the
multiplexer version (which moved the tailnet config into HostConnection); dev's authKey removal is
re-applied there.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
# Conflicts:
# ios/NucleicRemote/NucleicRemote/Models/RemoteStore.swift
Completes the multiplexer rewire begun in f335eb4. RemoteStore no longer runs an inline single
connection — it owns `connections: [HostID: HostConnection]` and connects every paired Mac at once,
so all your Macs are live simultaneously.
- reconnect() connects/reconnects all paired hosts (dropping since-unpaired ones); pair() adds a new
connection and makes it active while the others keep running; unpair() drops the active Mac and
switches to a remaining one.
- The flat @Published state (connectivity/sessions/dashboard/capabilities/…) mirrors the *active*
connection via callbacks, so switchHost() is now instant — every Mac is already connected, so it
just re-points at the target's live state (no reconnect).
- Aggregate concerns merge across hosts: the app-icon badge counts needs-you across ALL Macs, Live
Activity summarizes all live sessions, and an approval answered from a notification is broadcast to
every connection (the owner resolves it; the notification carries no hostID yet). Intents (send /
open / approvals) route to the active connection.
Removed ~450 lines of connection machine from RemoteStore (now in HostConnection); kept
friendlyTransportError/showError/dismissError. Demo bypasses connections (seeds state directly).
Compiles (iOS Simulator BUILD SUCCEEDED) and the demo host switcher is runtime-verified (screenshots:
switching Macs swaps the session list + tab badge). The LIVE multi-connection path — simultaneous
dials, reconnect/teardown, the shared embedded tailnet node — is compile-verified only; it needs two
real Macs to validate.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Begins the simultaneous multiplexer. HostConnection.swift is the per-host connection engine lifted
out of RemoteStore: one instance owns a single Mac's SyncClient, LAN→tailnet candidate chain,
reconnect backoff, tailnet-node lifecycle, and event stream, and keeps that Mac's projection
(connectivity, sessions, dashboard, capabilities, meshPeers). It talks back to its owner through a
Callbacks struct so aggregate concerns — the badge, Live Activity, notifications, and the single open
transcript — can be merged across hosts by the coordinator.
Not yet wired: RemoteStore still runs its own inline single-connection machine. The next step swaps
RemoteStore onto a `[HostID: HostConnection]` map (connect all paired Macs, aggregate their sessions,
route intents by host) — the behavior-critical part, to be verified with two real Macs. Compiles
(iOS Simulator BUILD SUCCEEDED); no behavior change yet (the engine is unused until the rewire).
Also: RemoteStore.friendlyTransportError made non-private so the engine shares it.
Co-Authored-By: Claude Opus 4.8 <[email protected]>