Skip to content

Two computers: continue a moved chat, clean leader quit, removed-device wording - #1967

Merged
AbirAbbas merged 23 commits into
devfrom
abir/two-computer-fixes
Oct 26, 2026
Merged

AbirAbbas merged 23 commits into
devfrom
abir/two-computer-fixes

Conversation

@AbirAbbas

@AbirAbbas AbirAbbas commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

A real two-computer test of dev after #1950, run on a Windows PC under WSL and a Mac against a self-hosted relay with a real model, found the bugs below. This PR fixes all of them. Moving a chat between computers now works with no workarounds, and both machines confirmed it over five rounds.

Fixes

  • A moved chat couldn't be continued on the second computer. This has been true since Wormhole: your codeaf follows you between computers (on by default) #1888. After bare codeaf took the chat, the window's chat listing never refreshed, because the startup batch dropped the answer to an ask whose single-flight guard was already set. The first send then asked "Continue this chat here?". Choosing continue here released the computer's own hold, and the window showed "another device continued this chat first". Now:
    • the listing follows the take;
    • the card appears only after a fresh read shows another holder;
    • continuing a chat this computer already holds keeps it as it is.
  • A send needs a hold. A window may send only while it holds the chat at the fence it recorded, or when the chat was never shared. After a failed take, the window says so in a plain sentence and keeps the typed words; it never sends on a copy it doesn't hold.
  • A git index lock could cancel the take. Background git reads now use --no-optional-locks. Home's repository read had created .git/index.lock during a take's in-place restore, and the restore cancelled.
  • A leading computer's clean quit lost its release. This came from Relay: coordination as a Raft group, and joined computers can lead #1950. The release used to commit after leadership was already gone, the relay returned 503, and the chat stayed held for about 90 s. A coordinating computer now commits its releases, hands coordination back to an anchor within 5 s, and stops. A temporary 503 on a release is retried until the close deadline.
  • A removed computer now gets the same "this device was removed" sentence from codeaf cluster status and codeaf cluster lead as from codeaf devices. It stops all sync traffic until it is paired again; before, it polled about every 2–5 s and got 401s. It also no longer offers to continue chats.
  • Smaller fixes:
    • no false "kept as N turns" after a take;
    • a window no longer announces its own take as another computer's;
    • the words typed for a chat travel with it, and another chat's draft stays where it was;
    • never-used chats don't appear on other computers' Home;
    • codeaf --version and codeaf help change nothing on disk.

Checks

  • Every fix has a regression test written from the observed sequence, and each test fails on dev. That includes a keyless loopback end-to-end test of a displaced take-back in both directions.
  • The real PC and Mac rounds (results kept in internal notes) covered:
    • pairing;
    • landing, and a first send with no card;
    • take-back after a kill: offer after about 16 s;
    • cluster lead: under 1 s;
    • a leader's clean quit: release 204, with an anchor coordinating again within about 2 s;
    • takes in both directions from displaced windows, with no release after the acquire and the turns published;
    • revoke, after which the removed device made 0 requests in 62 s.
  • Usage counts were off for every test run.

🤖 Generated with Claude Code

AbirAbbas and others added 7 commits October 26, 2026 12:45
A bare start that continued another computer's released chat dropped the
command batch Home returned, which carried the directory ask whose
single-flight guard it had already set. The guard then stayed set, so the
window kept the listing it read before the take: the first message opened
"Continue this chat here?" for a chat this computer held, and choosing
continue here retired this computer's own session and released its own
hold before the conditional take was refused.

Home's commands are now returned on every automatic continuation outcome.
A completed take marks the chat as held here at once and replaces a listing
that was asked before it. A send decides the continuation card from a fresh
ownership read rather than the cached listing, and that read no longer ends
an outstanding listing request. Continuing checks the fresh holder before
retiring anything: a chat this computer already holds stays open as it is,
and a different holder is refused before local work is touched.

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

A personal computer that led coordination ran its member inside the open
terminal and stopped it when the terminal exited. The engine's closing
release for the chat it held then reached a coordinator that was already
leaving, got 503, and was never tried again, so the chat read as held
offline until its lease ran out and coordination paused until the anchors
elected a new leader.

A coordinating terminal now waits, as it leaves its chats, for each idle
chat's closing release to finish, then transfers leadership to an anchor
and waits for an anchor to answer a fresh majority read before its member
stops. That hand-back is bounded by LeaderQuitWithin (5 seconds); if the
anchors cannot take over, the ordinary fallback applies. The anchors then
restore their own voters as fallback already does, and the computer that
quit does not get leadership back. A terminal that is not coordinating
leaves exactly as before. A closing release that meets a temporary
coordination refusal, including a bare 503, is retried within its existing
close deadline; a definitive refusal is never retried.

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

The cluster control client read every answer other than 200, 404 and 409 as
a coordination outage, including the 401 a removed device receives. So on a
removed computer `codeaf cluster status` said the cluster could not
coordinate and told the person to reconnect voting computers, while
`codeaf devices` correctly said the device was removed.

A 401 carrying the directory's revoked code is now the removed-device error,
and `codeaf cluster status` and `codeaf cluster lead` explain it with the
same sentence `codeaf devices` uses, from one shared definition. Every other
refusal keeps its words.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hats, and keep version checks side-effect free

Four small findings from a two-computer run:

- Choosing continue here on the card a send raised replaced the displaced
  session with a fresh one and lost the words held behind the card. The
  arriving chat's box now receives the same draft, pasted text and
  attachments, unsent until the person presses enter again.
- Taking a chat back into a displaced window counted the retired
  session's closing save of unchanged bookkeeping as unsent turns, so the
  move said edits were kept and Home grew a branch row for turns the relay
  already had. A sealed turn is now kept as a branch only when the engine
  finds real local work in it, the same rule continuing already uses.
- Every window registers its empty new chat with the directory, and the
  other computer listed it as an untitled running chat. A record with no
  saved turn is no longer drawn on another computer's Home; it appears
  once its first turn is saved. Registration and ownership are unchanged.
- codeaf --version, help and manual requests ran the former-folder
  adoption and removed old scheduler timers before parsing anything, so a
  version check could move a state folder and recreate the compatibility
  link. Those informational requests now skip both; every real start keeps
  them.

The footer's fit-by-priority, the engine outliving its window, and /quit
closing the conversation in front are the documented behaviour and are
unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The entry is numbered when the pull request exists.

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

The new paragraphs about a send's ownership check and the words kept after
continue here lengthened the page's first section until "continue on
another computer" no longer reached the section that says where moving
works, and the clean-quit heading competed for the same words. They now
have a section of their own and the clean-quit heading says what it is
about. The version-request note on the help page drew "old version of the
file" away from the task pages; the starting page already says that
version, help and manual requests change nothing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@AbirAbbas
AbirAbbas marked this pull request as draft October 26, 2026 16:47
AbirAbbas and others added 16 commits October 26, 2026 12:55
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Taking a chat back into a window another computer had displaced reported
"your unsaved edits here were kept as 1 turn" and left a branch row on both
Homes, in both directions. A real reply appends pace, usage and call
accounting after its shared save, and retiring the displaced session seals
that tail. Both the take's unsaved-work check and the old drive's closing
publish counted it as new work.

Local work is now decided from the journal beyond the shared boundary:
accounting records are not work, while conversation turns, tool results,
unreadable records and workspace changes are still kept. The same decision
guards the unsaved-work check and the closing publish, so neither creates a
branch for what the relay already has.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A send in a displaced window raises Home with the continuation card. The
typed words then live in the conversation's stashed composer, not in the
input Home shows, and retiring the displaced session can clear them before
the moved chat opens. The moved chat opened with an empty box.

The conversation's composer is now taken when the confirmed take starts,
carried with the take's answer and restored into the arrived chat, even
when its transcript or session differs from the displaced one. Nothing is
sent until the person presses enter.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A window's launch chat saves a bookkeeping head at start, and that head
was published, so the other computer listed the never-used chat as
"untitled running on <device>". A directory record always has a head, so
the earlier filter on an empty head could never match.

The drive keeps saving startup bookkeeping locally but notes a head for
publication only once its saved journal holds a conversation message,
bounded to that save, so text typed later cannot publish an earlier empty
snapshot. A never-used chat therefore never reaches another computer's
Home; it appears with its first turn and keeps ownership, quit and continue
as before. A keyless end-to-end run covers the displaced take-back in both
directions and both Homes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`codeaf cluster status` and `codeaf cluster lead` start with a control
request whose handler answers an authentication refusal as a plain 403, so
the client still reported the generic coordination failure on a removed
computer.

When a cluster request fails without a typed cause, the command now checks
through the signed directory read whether this device was removed and, if
so, explains it with the `codeaf devices` sentence. An unexplained 403 is
never guessed to be a removal, and other failures keep their words.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
After a removal, the open window kept listing chats, sending presence and
heartbeats and running state and member rounds every few seconds, each
answered 401.

The sync HTTP gate now remembers the removed refusal, and so do directory
watches and a committed local removal. A marker bound to this device,
identity and relay shares that fact across windows and restarts, and
blocked requests are answered locally with the same removed refusal. State
and member rounds stand down and open members close. Pairing installs a new
device key, which lifts the marker, and an open Home resumes with the new
device. Local chats keep working throughout.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Home's repository band ran `git status` without `--no-optional-locks`, so
each refresh could create `.git/index.lock` and rewrite `.git/index`. When
that landed during a move's in-place restore, the restore saw `.git` change
and cancelled, and the take gave the chat back.

Every git command codeaf runs on its own to read a repository now passes
`--no-optional-locks`, so a background read never takes the index lock or
rewrites the index. Commands that deliberately write are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
After a failed take the chat was free, so the window's send door, which
only blocked a chat another computer held, let a turn run on a stale copy
that could never publish.

A send now goes ahead only when this window holds the chat at the fence it
recorded, or the chat was never shared. A free chat whose local head equals
the shared head is taken back quietly with a conditional take, then sent;
otherwise the continuation card or the view-only state appears and the
words stay in the box. A failed take marks the window's copy as one it may
not send on and says why in plain words, and a stale-fence refusal makes
the window view-only. When the ownership check cannot reach the other
computers, a chat this window holds keeps working and its turns wait to be
shared, and anything else stays unsent.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The draft captured when a take started was restored only on success, so a
lost race, a refused or unreachable take, or a failed in-place restore lost
the words typed behind the card. Every failure path now puts the draft back
in the box, unsent, and the manual says what a failed move leaves behind.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A removed computer's Home still offered "Continue <device>'s latest chat
here?" with its chord. Once this computer knows it was removed, Home drops
the left-off offer, the continue chord and the continue action on rows,
and keeps the removal sentence.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The device pages keep to the words people use for their computers; the
removal paragraph now says the computer stops asking the other computers
for chats, and that pairing can still reach the service.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The test drove the drive's heartbeat every 10 ms from the start, which
queued relay commits ahead of the member's first catch-up. On a slow disk
the member round waited minutes, and the request signed before that wait
was refused as clock skew. The heartbeat now keeps an ordinary pace until
the removal is learned and only then runs fast, so the observation still
spans many heartbeat intervals.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
After every take the window that took the chat said "<its own device>
continued this chat; this window now only shows it". The take's acquire is
followed by the arrived drive's own acquire, which raises the fence, and the
window compared the listing's fence with the hold it recorded at the take.

The window's recorded hold now follows its own drive's publishing receipt,
so a newer fence held by this computer for this window is still this
window's hold. The notice and the card come only from a hold by another
computer; a stale local receipt still cannot send. The second acquire stays:
it is the boundary that keeps two local drives from publishing under one
fence.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A take captured whatever the window's main box held and restored it into
the chat that arrived, so an unsent draft typed for another chat appeared
in the moved chat's box and was saved as its draft.

The arrived chat's box now holds only the words typed for it: the draft
held behind the card its own send raised, otherwise its own saved draft,
otherwise nothing. A draft typed for another chat stays with that chat,
including when a take fails.

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

Keeping each chat's draft with that chat had also stopped an ordinary
opening from adopting the draft a dead window left in its directory, which
is how the words follow the person and the directory. Adoption is restored.
Only a chat arriving through a take now brings just its own saved words and
never adopts another chat's draft.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The removal and empty-chat sections spoke of "another computer" and
"adding the computer again", which drew the question away from the section
with the pairing steps once that section's card text changed. They now say
"machine" and "your other devices", and the question reaches the steps
again.

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 19:46
@AbirAbbas
AbirAbbas merged commit 9e34209 into dev Oct 26, 2026
8 checks passed
@AbirAbbas
AbirAbbas deleted the abir/two-computer-fixes branch October 26, 2026 19:46
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>
@santoshkumarradha

Copy link
Copy Markdown
Member

@AbirAbbas heads-up: this PR looks like it broke 8 internal/syncsetup tests when furrow is built in. CI's touched legs run without the embedded engine, so they skip there and the PR went green.

Bisected on Spark (make furrow at each commit, then the same go test -run set):

dev commit syncsetup failures
4b03be9 (#1968) 0
08b922b (#1969) 0
9e34209 (#1967) 8
later commits on dev 8

Failing, all 30 s timeouts:

  • TestDriveSidePublishesTurns — drive_test.go:254: timed out waiting for the directory head to reach the chat's head
  • TestKeepsSnapshotIDs — rotate_ids_test.go:45: timed out waiting for the turns to be durable
  • TestDriveSidePublishesOverTheDefaultDaemon, TestDriveSideStartsAsViewerWhenHeld, TestDriveSideSupersededStopsTools, TestDriveSideUploadsTurnsASealedRunNeverSent, TestRotatedShowsWaitOnce, TestAChatMovedBackGetsADriveSideOfItsOwn / TestAChatTakenAndTakenBackGetsADriveSideOfItsOwn

Repro:

git checkout 9e3420981 && make furrow
go test -count=1 ./internal/syncsetup -run 'DriveSide|KeepsSnapshotIDs|RotatedShowsWaitOnce|AChatMovedBack|AChatTakenAndTakenBack'

(Run make furrow first in a fresh checkout, or the tests pick up whatever furrow is on PATH.)

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.

2 participants