Skip to content

Two-computer relay: fast take-back, moved task pages, honest saving, and six rough edges - #1990

Merged
AbirAbbas merged 28 commits into
devfrom
fix/two-computer-stable
Oct 27, 2026
Merged

AbirAbbas merged 28 commits into
devfrom
fix/two-computer-stable

Conversation

@AbirAbbas

@AbirAbbas AbirAbbas commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes everything the real two-computer relay tests found after #1944 (a PC under WSL and a Mac on a LAN relay, with real models), plus what three more real rounds on this branch found along the way.

Closes #1982, closes #1984, closes #1985, closes #1986.

What changes for a person

Moving a chat between computers

  • Taking a chat back after the other computer was killed reaches its first acquire in about a second. Before, it took 6–16 s. The take inspects only the incoming metadata before the lease, and restores once, after it.
  • A copied chat can move back and forth any number of times. A detached inspection timeline no longer blocks the second take with "timeline is attached to another folder", and a home already in that state repairs itself.
  • /quit waits for the final publish and release before the process exits, so the other computer's bare launch lands on the chat. A long wait says saving this chat before quitting….
  • Only a live hold sends without asking the relay. A window that has been told another computer continued the chat raises the continue card on the very next Enter, so it can never fork the chat. The drive follows the directory feed and drops its hold the moment it is displaced.
  • A hold left behind by a killed process is reconciled with the relay on start, so the killed computer can take its chat back on the first try.
  • A removed computer keeps working in its own chats: new chats, and chats it held at removal. It refuses only chats another computer took.

Words and drafts

  • Words typed while /new opens stay with the new chat, and survive the terminal closing.
  • Words sent through a continue card are consumed. They never come back on a later take, and a take never leaves a stray chat.
  • A failed take keeps the words once, in the box they were typed in.
  • A card shows only its own chat's words and project, and Esc returns to the window that raised it. Drafts are keyed per chat, even for copied chats.

Saving tells the truth

  • sealing works again appears only once this chat's own turns are sealed and published again, and it reaches an idle chat.
  • A stuck store gets a plain sentence and a bounded, proven restart.
  • A final save that fails prints one sentence after the screen is restored, naming where the unsaved turns are kept.
  • Closing a chat never freezes the window. A late close failure is reported in the window that is open.
  • The embedded furrow is 0.3.5 (fix: an exiting daemon removes only its own socket (0.3.5) furrow#22): an exiting daemon removes only its own socket.

Tasks

  • A finished task that moved to another computer opens read-only, with its steps and worker transcript. This includes a home below a symlinked folder, such as macOS's /tmp.

Rough edges (#1986), plus plain sentences

  • A moved copy keeps its label after this computer is removed.
  • Home never offers to continue this computer's own chat.
  • The first Enter makes a bounded fresh read and never claims the relay is unreachable while it answers. A deadline says did not answer in time.
  • The computer holding a chat says once that it is not syncing during an outage, and says when syncing is back.
  • codeaf cluster status, devices and pairing say plain sentences when the relay is down or slow, and exit non-zero.

Proof

  • Every item has contract tests that fail without the fix. The native take, quit, seal and ping-pong tests run against the released furrow 0.3.5.
  • CI is green on the head, and the full structural-law run passes.
  • Three real two-computer rounds ran on this branch, PC + Mac, on real models:
    • round 8 found the regressions that later commits fix;
    • round 9 passed take-back after a kill, a six-move copied-chat ping-pong, card and draft handling, the removed computer's own chats, the outage notices and devices offline;
    • round 10 passed the displaced-window race in both directions (card within 80 ms, no turn, no fork), the moved task page with its steps and transcript, card Esc, and the coordinating-window first Enter. It found no blockers.

Left as follow-ups:

  • each publish is now followed by one extra lease read;
  • an engine stopped with TERM can start a new storage process;
  • a repeating knowledge-capture log line.

🤖 Generated with Claude Code

AbirAbbas and others added 28 commits October 26, 2026 22:28
Fetch the incoming objects, inspect only the workspace metadata in a scratch
timeline, and leave an ordinary chat's one whole-tree restore until after its
lease is acquired. Chats that carry a workspace plan, or an older root set,
still run their full admission before the lease, and a refusal leaves the
folder and the lease untouched. The holder also reads a retirement request
without waiting for its presence heartbeat, so a displaced session lets go at
once. Continuation phases can now be observed, and the first acquire after a
killed holder lands well inside two seconds on a loopback relay.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When a chat arrives without its plan database, its finished tasks fill their
rows and pages from the settled run receipt, the transferred trajectory and
the local worker history. The page offers no task verbs, says the task has
finished when asked to act, and never restarts a settled run. The sending
computer's page is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The sealing notice says saving works again only after this chat's own
transcript is sealed and, for a chat that syncs, published. A store whose lock
is held but whose socket is gone is diagnosed in bounded time with a plain
sentence, never retried into another writer, and restarted only when the
process is proven to be this client's own storage child for that exact socket.
Close errors now name where the unsaved turns are kept.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When the relay cannot be reached, cluster status and the leadership commands
now use the same plain network sentence as devices, with no Go error text, and
still exit non-zero. The sentence is shared rather than copied, and a removed
device's own sentence still takes precedence.

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

- Keys and pastes typed while /new opens belong to the new chat; Esc gives
  them back to the chat you were in, unsent, and says so.
- A moved copy keeps its project label after this computer is removed.
- Home never offers to continue this computer's own chat.
- The first Enter in a copy held elsewhere asks a fresh, bounded ownership
  read instead of trusting a hint, so a reachable relay no longer reads as
  "other machines unreachable".
- Closing a chat and quitting run the engine work off the update loop with a
  bound, and keys pressed meanwhile cannot send another chat's draft.
- A final save that fails is said once on the terminal after the screen is
  restored, in one plain sentence.
- The surface carries a once-per-outage "not syncing" notice behind an
  optional renewal-status interface.

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

furrow 0.3.5 records its socket's device and inode at bind time and removes
the path at shutdown only while it still names that socket, so a daemon that
outlived its client can no longer unlink the socket of the one that replaced it.

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

- The batcher keeps the first unreachable renewal or publish for the same
  hold and clears it on success; a displaced or removed hold reports none.
  That reading rides the engine host's facts as an optional field, so the
  surface's once-per-outage "not syncing" notice has a real source and an
  older peer simply reads as unknown.
- Saving replies carry an optional transcript location. The host joins the
  agent's close with its final publication before it replies, and a detach is
  acknowledged only after the release, so a quit no longer exits while the
  engine is still saving and publishing.
- The screen restores after its bounded wait while the launcher keeps the
  links open for the saving reply, then prints one plain sentence naming the
  transcript. A second ctrl+c exits at once even when cleanup is stuck.

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

The sealing watch now tracks this conversation's own turn boundary, so a later
bookkeeping append cannot hide a turn that was saved, and a turn that has just
started cannot borrow the previous one's saved head. Publication is required
only where the drive will actually publish, so an unused startup chat that
recovers locally clears. An idle hosted chat's notice crosses the host's
notice frames, so "sealing works again" reaches the window even when no turn
is running. A clean release also removes the matching local send hold.

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

- A chat this computer holds, or one never shared, sends from its local hold
  and fence with no relay request; a copy held elsewhere makes one bounded
  read, and an established outage is not paid for again on every send.
- A reply that arrives after the draft or chat changed says why the words are
  still unsent instead of dropping them silently.
- Any quit, including the terminal closing, folds the words typed while /new
  opens into the saved draft first.
- A close that finishes late reports its failure in the chat now open, naming
  the chat it closed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
After the screen is restored the launcher waits for the final publish and
release, says "saving this chat before quitting…" when that takes more than a
moment, and stops at the existing save ceiling with the plain sentence. A real
quit through the engine host is proven to release the chat, so the other
computer's bare launch moves it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A deadline now says the sync service did not answer in time; only a refused
connection, a missing route or a DNS failure says it cannot be reached. A
joined computer's status reads the relay's answer without reopening the local
member store, and the whole command is bounded by one ceiling.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The roster fixture now expects the read-only historical page a completed
transferred task carries, keeping its recorded-model and journal checks. The
services page names services in its title so a search for the word reaches
it.

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

The cluster command's formatting lives in clusterCommand again, with the
bounded runner split out and its goroutine guarded, so a panic there cannot
take the terminal down. The final-save and live-drive critical sections each
move into a helper that unlocks from a defer.

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

- The send shortcut that skips the relay now needs the agent's live held
  flag, not just a hold file on disk. A window another computer took asks
  before sending again, and a hold left by a process that died can never
  grant a send.
- A writer that became a viewer cannot record a new hold, and an explicitly
  displaced writer with no rows keeps its words unsent.
- On start and at a take, the saved hold is reconciled with the relay's record
  and retired when its device, fence or expiry is stale, so a computer that
  was killed and taken over takes the chat back on the first try from Home
  or from its own copy's card.
- The card names the relay's real holder and last saved turn, and every take
  that falls to the catch-all sentence logs its real cause.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The directory client keeps the typed cause of an HTTP failure, so a deadline
is no longer called a network fault, and the terminal and in-chat pairing
doors say the shared did-not-answer-in-time sentence instead of raw Go error
text. Cancelled is reserved for a person actually cancelling.

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

With the relay unreachable or slow, the command printed the unavailable
sentence and then a false "nothing is paired yet", and exited 0. It now
prints only the plain unreachable or deadline sentence and exits non-zero,
as cluster status does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The run history resolves a moved finished worker's transcript from the
task store when the plan identity never arrived, so the page can show it.

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

- A card take consumes the words it sends: they are gone from every draft
  store once sent, a later take or restart opens an empty box, and the take
  opens exactly one conversation with no stray copy of the words.
- A failed take, a cancelled card or a cancelled folder picker keeps the
  words once, in the box they were typed in.
- The card never names this computer as the holder of its own chat, a
  landing never says it moved from this computer, and a kept branch counts
  the conversation turns that were actually unpublished.
- A removed computer sends in a chat that was never shared and in the chat it
  held at removal, with no relay request; it refuses only a chat another
  computer took, with the removal sentence.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The pre-acquire inspection named its private timeline after the chat alone,
so once a copy was filed into its project bucket the next take's staging
folder no longer matched the binding the detached inspection timeline kept,
and furrow refused every later take with "timeline is attached to another
folder". The inspection timeline is now named from its scratch folder, so a
chat can ping-pong between computers any number of times, and a home already
holding the old binding takes the chat without any repair step.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The historical row now carries the receipt's conversation and run, so the
Tasks door opens the read-only historical page whichever of the index and the
roster arrives first, instead of keeping the summary card. The page also opens
the work fold, so the worker's own reply and the recorded steps are on screen.

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 window that has drawn "… continued this chat" could still send for a few
seconds, because its drive kept believing it held the chat until its own next
heartbeat failed; the turn ran, the publish was refused and the chat forked.
Now the surface latches a send block as soon as a newer listing row or the
displaced notice arrives, so the next Enter raises the continue card, and the
drive follows the directory feed and retires its hold the moment another
computer acquires the chat. A held chat with no displacement news still sends
at once with no relay request.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The task history reader refused a symlink anywhere above the conversation,
so on a computer whose home sits below an aliased folder (macOS's /tmp is
/private/tmp) a moved task's worker transcript and steps could never be read,
and its page showed only the summary. The trusted conversation location is now
resolved first, while the task's own state, history, worker and trajectory
files still refuse any redirect.

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

- Draft storage is keyed per chat, so two copied chats from different
  projects on one computer never share a draft, and a chat keeps its draft
  through a move into its copy folder.
- A continue card shows only the words of the window that raised it and that
  chat's own project; a card opened from a Home row shows that chat's draft or
  nothing.
- Esc ("leave it there") on a card raised from a chat window goes back to that
  window with the words still in its box.
- cluster status reads under one five-second ceiling, and Home says other
  machines did not answer in time on a deadline instead of calling them
  unreachable.

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

A read that only hit its deadline no longer reads as other machines being
unreachable: the window says they did not answer in time, keeps the words,
and the next Enter makes a fresh read instead of replaying the cached failure.

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

The displaced-window block set from a listing row or the displaced notice now
yields to the reading a send makes itself: when that newer reading shows this
computer holds the chat, the send goes ahead. A block a take decided, or a
failed take, still never lifts that way.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@AbirAbbas
AbirAbbas merged commit 644a38e into dev Oct 27, 2026
9 checks passed
@AbirAbbas
AbirAbbas deleted the fix/two-computer-stable branch October 27, 2026 07:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment