Repository navigation
app: pair computers, list their chats and continue them here, with the two-computer fixes - #2011
Merged
Merged
Conversation
The CLI's chat opening, pairing mailbox/grant and Continuer construction read the process home. Each now has a variant that takes the home folder, and the CLI calls it with home.Dir(), so the app backend can build the same objects for its own home instead of copying them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
New app-wire doors behind three hello features: - devices.pair: pair.start/get/answer/cancel and pair.changed. One pairing at a time in all four modes the terminal has (offer a code, use a code, make a link, approve a link), with the terminal's own sentences. A computer that already has chats is asked before its own chats are replaced; a join while an agent works is refused as busy; a join that replaces this computer's identity ends with restart. - chats.take: chats.list plus chat.changed/chat.removed from one shared Sync.Follow follower (no polling), each row naming the computer it lives on, presence when known, and the local agent when this computer has it. Older home-folder chats stay local-only (#2002). agents.take continues a chat here through the CLI's Continuer, refuses a stale seen with conflict/holder_changed, and returns the local agent; the other computer's open agent gets agent.changed with here:false. Location gains cell, seen, can_take and takeover_line. - sync.relay: Sync.relay reports the relay address and where it came from (environment, paired, default, none) or off. Tests cover the desktop contract (E-P1..E-P4, E-C1, E-C2, E-T1, E-T2, E-Q1, E-R1) with two homes and a loopback relay, including the real app entry point in two processes taking a chat, refusing the old holder, and saving on quit so it can be taken straight back. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PROTOCOL.md lists the new methods, events, types and hello features; the chat manual's desktop-sync page says how Relay, pairing, chats on other computers and Continue here work in the desktop; and a change entry records the additions. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The app reused the terminal's sentences: an unpaired home said "pair from a terminal with `codeaf pair`" and sync off said "set CODEAF_SYNC_URL". The desktop now pairs and sets its relay itself, so app mode says "Only this computer is here so far." and "Sync is off on this computer." (devices.list, sync.get, devices.remove, agents.take). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…vents From the first live two-computer run (Windows desktop and a Mac engine through one relay): - Chats saved before pairing now reach the other computer: at app start and after a pairing, each local cell chat not yet in the relay's directory is published once through the terminal's publish door (PublishCopy), without opening it or running a turn. Older home-folder chats keep #2002's rule and stay local. - chat.changed is sent only for rows that changed. Rows carried a relative save age, so every directory version looked like a change; they now carry the saved date and time. - Pairing speaks to a desktop person: sentence case, "computer", no command lines, flags or y/n buttons (Pair / Don't pair, Replace this computer's chats / Not now). Option ids are unchanged; the terminal's copy in internal/pair is untouched. - A chat open on another computer reads "open on <name>", not "running": chatlist's running means a live lease, not a turn. - A failed Continue here says "This chat could not be moved to this computer." with the cause in the error detail. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Exercises the take's fetch into a staging folder and its swap into place with the native store, so the Windows rename failure seen in the live run can be reproduced with a Windows test binary. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A preference restore from a paired computer now emits one settings.changed with the full settings, as a local settings.set does. Identical computer lists no longer repeat devices.changed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A model the catalog marks as non-reasoning gets no reasoning parameter. An unlisted model's specific thinking refusal is repaired once without reasoning and remembered for the process; that 400 is permanent, so the app no longer keeps the failed turn to run again later. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
After an install fails and the taker releases its lease, a computer without the chat in place no longer reads it as here; it offers Continue here and names the last holder. The source's next send re-acquires the free lease at a higher fence when its saved head still matches, instead of refusing held_elsewhere. Fences never roll back. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Listing, recovery and lazy admission could open a copied chat under copies/<id>.taking and watch it before the taker renamed it into place, which on Windows fails the rename with Access is denied. 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 native relay client's wrapper hid ReleaseQuiet, so the fallback sent an ordinary release that no longer matched the signed quiet mutation. The relay refused it with 401 and the startup publish left a stray lease behind. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A published cell now records when its sealed head was saved, so a chat published later still says when its last turn ended. Relays advertise the optional field with Codeaf-Saved-Turn-Times: 1; an older relay still accepts the publish without it, and an unknown time is omitted, never replaced by the publish time. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A closed chat no longer reads "<name> offline" while its computer is online; offline needs presence to say so, and unknown presence is omitted rather than sent as false. saved_at is the saved turn's time and the takeover line carries no absolute UTC time. chats.list never emits, and a row is sent again only when its content changed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An incoming model slot is applied only when this computer's resolved provider connections can call it (a key, a sign-in or a local endpoint). A skipped choice keeps the receiver's own value, is remembered so it is not echoed back as a newer edit, and does not advance the slot's clock; a later choice made after the provider is set up still travels. Effort and spending limits keep syncing as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Startup heartbeats a hold it still owns instead of acquiring it again, a take ends at the fence it returns, and a watch reconnect renews an unchanged idle hold rather than releasing and re-acquiring it. A take whose seen fence was bumped by the same holder with the same saved head is no longer a holder_changed conflict; a different holder or a new head still is. Relays advertise this with Codeaf-Head-Takes: 1, and older relays keep exact-fence checks. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Last-seen times only move forward, fresh presence survives a watch reconnect, and presence queries use the same reconnect grace as watch events. A computer reads offline only when its presence actually lapsed; lost observation becomes unknown after the existing 25-second window. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The computer list is emitted once complete and then only when its wire value changes, from every emitter. A watch reconnect that changes no chat row sends no chat.changed, and a sync round whose only incoming model choice was skipped sends no settings.changed. 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 shared store daemon keeps a directory watch on a tree it materialized. A take renames its staging tree into place next, which Windows refuses while the tree is watched (Access is denied). Guarded materialization now runs a one-shot engine and waits for it to exit; Follow binds the final root after the swap. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Follow and the dirty/local-work plans started the engine inside the chat's workspace. A taken chat filed under its origin folder can pass Windows' 260-character limit for a working directory, and a recorded workspace can be the other computer's path. The engine now starts in the local store and receives the workspace with --repo. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Restoring a chat writes receipts under its .cell folder, and those paths can pass 260 characters. Furrow does not canonicalize this argument itself, so Windows now receives it in \\?\ form. Other platforms are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ake-back Real encrypted fetch, staging-watch check, swap, Follow, Dirty/Seal and a take-back that restores receipts past 260 characters. The app round-trip test can keep its state on failure (CODEAF_E2_KEEP_FAILED=1) for native runs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
If this computer's lease lapsed only because the relay was unreachable, it renews it when the relay answers again (same fence when nobody else acquired and the relay's head is an ancestor of the local one) and publishes the work saved meanwhile. An unreachable renewal keeps the chat writable and its publisher alive; startup publishing retries every 5–60 seconds instead of giving up. A computer never names itself as the one that continued a chat elsewhere. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e local default Before the first turn on this computer, a chat whose model has no usable provider here switches to this computer's default model and says so once. Going back to the original computer keeps working. With no usable model at all, the turn fails with a plain setup reason instead of a provider 400. The provider check is the one model sync already uses, moved to config. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Two homes on a loopback relay that stops past the lease TTL and restarts on the same address; a competing take during the outage; startup recovery; and disjoint providers for the model fallback. 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 ten-minute idle sweep retired an open chat's conversation, and the next read (skills.list, agents.open) reopened it and acquired again, moving the fence every ten and a half minutes. A chat the person has open now keeps its hold with heartbeats until it is closed; background-only conversations still retire. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A test built Sync without Home, so its refusal wrote a hold lock relative to the package folder. The fixture now uses a temp home, and an empty home is an error before any read, mkdir or lock. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… from offline Presence lives in relay memory. Right after a relay (re)starts, every paired computer that has not reconnected yet looked offline, so a peer that never stopped read "<name> offline" until its first heartbeat reached the new relay. For one default presence window after presence memory starts, both the native and hosted relays omit a computer they have not observed (unknown). An observed connection that actually lapses still reads offline, and a computer that stays away past the window reads offline as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A take whose chat used a model this computer can't run, with no usable local default either, acquired the lease and installed the chat, then returned an error. The chat had moved, but the desktop showed a failure and a stale row. Now the take completes and returns the local agent, and the row's local id is reconciled in the same change. The first send fails locally with a plain setup sentence and no provider call (naming waits too). Once a usable default is set, the next send uses it and explains the switch once. Take errors with a plain, actionable reason carry it in the visible message; the detail keeps the technical cause without repeated "engine:" labels. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…y restart Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… chose The guarded materialize always replaced the engine's transport with a fresh Spawn, so a caller's own transport (a test's fake, an isolated runner) was dropped and a carried furrow became mandatory. Only the shared daemon is now swapped for a one-shot process, keeping its socket, so the daemon still never holds a watch on a staging tree; any other transport is used as given. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A model choice now travels only to a computer that can use its provider, so the receiving home needs the same fake provider key as the sender for the C30 preference to arrive. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The repository laws require each goroutine in the guarded tree to run under guard.Go and each Lock to be released by a defer directly beneath it, so an absorbed panic can neither take the surface down nor leave a lock held. The relay work added two bare goroutines (take and existing-chat cancellation) and critical sections in the chat inventory, pairing, take, devices, sync and live-drive code that unlocked by hand. Each section now sits in a small helper that defers its unlock. Lock order, what each section covers, and what stays outside it (waits, network calls, emission) are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A job that ended instantly could leave its process record saying "running" while the chat's task checkpoint recorded it as finished. Every engine start, and every idle sweep that closed the chat, found that record, opened the chat to recover it and so took its relay lease, then released it again 10.5 minutes later: the fence moved every cycle with nobody using the chat. The recovery check now treats a running record as finished when the checkpoint holds the ending of the same run (same start and call), and it only reads, so a chat held by another computer is never written to. When the chat is next opened for real work, recovery also writes that ending into the process record. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The conversation's model and its main/fallback pair lived only in the local summary, outside the sealed tree, so taking a chat back onto a computer that still had its own record of it restored the model from before the other computer changed it. The choice now rides in the sealed session truth, and a completed take refreshes the local record from what was installed; the usual rule then applies (keep it if usable here, else the local default with one line). Older cells keep reading their summary until their next save. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Switching a continued chat to this computer's model recorded the generic "Main model changed from X to Y." and then the specific reason, and the first request could add "Using main model Y." too. The switch now records only the specific sentence through the same conversation model rules; a separate fallback change keeps its own line. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ith it Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The store's SQLite URI was built as file://C:%5C…, which SQLite reads as an invalid authority, so a native relay on Windows never opened its group's store and every directory request answered 503. Drive paths now become file:///C:/…. Directory fsync goes through fsreplace.SyncDir, which skips the flush Windows cannot do on a read-only directory handle; regular files still sync before rename. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every 503 path now logs its underlying cause through the relay's log, once per cause per minute; responses are unchanged. Native provision and transport persistence use the Windows-safe directory sync. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A computer wrote its own device record only when pairing finished and when a chat's sync drive started, so one failed write left it invisible to its other computers until a chat synced. The engine now publishes its record whenever it starts paired, after pairing, after a rename and when the relay answers again, retrying with backoff until it lands and never rewriting an identical record. devices.list keeps a readable roster when the advisory presence read fails, instead of calling the relay unreachable. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The offer tells the other computer to choose Pair a computer, then Enter a code from the other computer. A successful pairing no longer warns that the name could not be shared yet (the engine retries it). The approval names the other computer with its platform, e.g. "Abir (Windows)", and older peers that send no platform keep working. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…relay The device pages keep their vocabulary law (no relay, lease or node), and the rename line quoted on naming-a-device said "when they can reach the relay". It now says "once this computer is back online". Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…h first Device publication opens the shared directory watch at startup, so the chat inventory could join a watch that had already heard its version and presence. A new subscriber only hears later frames, so the first online rows could stay cached and never reach the desktop. The inventory now reconciles an already connected watch once when it subscribes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #2009 (merge of dev into app/v3). Once that merges, this PR's base becomes
app/v3.Summary
Gives the desktop app (
codeaf app) what it needs to set up the relay, pair computers, list chats on your other computers, and Continue here. Then fixes what a live two-computer test turned up: a Windows desktop and a Maccodeaf appsyncing through one relay, over many rounds.New app wire (features
devices.pair,chats.take,sync.relay)pair.start/get/answer/cancel, with desktop wording and labelled choices;devices.*;chats.list(a versioned inventory withLocationandChatRow);agents.take(Continue here with aseenconsent);sync.get.pair.changed,chat.changed,chat.removed,devices.changed,sync.changed,settings.changed(now also sent for synced settings).docs/design/app-v3/PROTOCOL.mdandinternal/manual/chat/desktop-sync.mddescribe both.Fixes from the live test
Chats and rows
false.saved_atis the saved turn's time, not the publish time. The takeover line has no absolute timestamp; the desktop formats time locally.chat.changed/devices.changedare sent only for a real change: never fromchats.list, never for a reconnect that changed nothing, never as a partial list.Ownership
either, is taken; its first send then says plainly to set up a model, without calling a provider.
ending its checkpoint already holds; that check only reads.
Relay outages
presence window (native and hosted relays).
Settings and models
settings.changed).back no longer reverts it to the model this computer remembered.
Windows: Continue here onto Windows. Three fixes:
Verified with native Windows test runs and live.
Relay protocol (backward compatible)
Codeaf-Saved-Turn-Times: 1. A cell can carry the saved-turn time. Older relays still accept a publish without it.Codeaf-Head-Takes: 1. A head-guarded take accepts a newer fence by the same holder when the head is unchanged. Older relays keep exact-fence checks.relay starts, unobserved computers are omitted (unknown) for one presence window.
relayserve) and the hosted relay (relay/hosted) both implement these.Verification
failures). The full check fails the same tests as
app/v3runs, exceptTestR25ArchivedPreconditionalClient, whichbuilds an archived client offline and fails on a module the runner's cache lacks (
golang.org/x/sys@v0.6.0underGOPROXY=off); this branch changes no module and adds no module import to that client's packages. Failures thebranch had introduced (two tests, two repo laws) are fixed in later commits.
Runs on the last commit: PR gate 40697998349 (re-run once for a temp-dir cleanup race that passes 30/30 locally),
Full check 40698002131. This branch's PRs into a
merge/branch trigger no checks, so these were dispatched by hand.Follow-ups (not in this PR)
copy-<encoded origin>, so a very deep Windows home or project can still pass 260 characters for git/shell working directories. Typical homes stay well under. Options: shorter, hash-based copy folder names; or a long-path-aware manifest withLongPathsEnabled, though that still doesn't let git or a shell start in a long working directory.ChatRow.crewis not filled yet.🤖 Generated with Claude Code