Repository navigation
Continue a chat from another computer every time, and let doctor repair sync (#2070) - #2078
Merged
Merged
Conversation
When the engine answers that its store writer is still busy, the error is classified once at the engine boundary into StoreBusyError, which unwraps to ErrStoreBusy and names the holding program and pid where the OS can tell. The home's own saving process is never named as the holder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A kept branch copy no longer stands as its own row: the main row counts its copies and says whether discarding one loses anything. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ir sync Every ending of a take clears the running mark, so a second and third continue in one window work; a press during a take says so on the card; a failure is shown on the card with retry or in the chat. A busy engine store is named with its holder. Home keys rows by chat id. Under --host the window no longer wires this computer's sync into a session running on the other one, so continue here is not offered there. codeaf doctor reports a busy store, unfinished continues, duplicate rows and lapsed holds; --fix releases this computer's unfinished continues and retires branch copies whose turns are all in the main chat. The manual quotes each new sentence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The narrow spelling of a row with folded copies was the state plus the whole short copies sentence, too wide for the sessions column, so the painter dropped it and the row lost host-a offline. It now keeps the state and counts the copies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 30, 2026
AbirAbbas
added a commit
that referenced
this pull request
Oct 31, 2026
…OS test reds, provenance on private apply (#2086) * release: expand an empty test argument list safely under macOS bash 3.2 The release-surface step ran go test "${test_args[@]}" under set -u. With no known-red ledger the array is empty, and /bin/bash 3.2 on macOS treats that as an unbound variable, so the prologue test failed on a Mac. The conditional expansion behaves the same on bash 5 and is safe on 3.2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(syncsetup): give hand-built Sync fixtures their own home Two fixtures built Sync{} with an empty Home. A refusal retires the send hold, which joined the empty Home and wrote v3/sync/holds/<hash>.lock into the package's source directory on every run. They now use t.TempDir(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: engine fixtures record a real conversation and wait for async branches These tests need a furrow engine. GitHub's full tests stage none, so they skip there, and they failed on every machine that has one (a Mac after make build, or CODEAF_FURROW on Linux): - the scripted drive chat wrote files but never a reply, and a chat with no conversation is deliberately not published, so its turns never became durable; each scripted turn now writes a reply to the transcript - the large-home startup test drained a warm capture that has no turn, so nothing was sealed; it now completes one recorded call first - a refused drive branches asynchronously, so the counts and two-home tests wait for the branch row instead of reading it once - the superseded-drive test keeps A offline until its next publish, as the scenario states, and waits for the kept-work notice too Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Never write a new identity into a codeaf home that was deleted A codeaf engine that was still saving after it was asked to stop could find its home deleted and treat that as a first run: identity.EnsureSolo made the folder again and wrote a new identity.json, device.json and identity-solo, and the vault capture then wrote the sync ledgers beside them. The Mac tester saw exactly that 8-22 s after removing a test home. Each codeaf process now records the home it starts in (home.Witness). The identity writers, the vault open, the stats recorder and the send-hold writer refuse with "codeaf home was removed or replaced" once that folder is gone or is a different folder, and they never re-create a recorded root. A first run, where no home existed at start, creates it exactly as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * A stopping engine ends at once when its home is deleted An engine asked to stop closes every conversation before it exits, and for a synced chat that can take tens of seconds while the relay is unreachable. If the home was removed meanwhile, everything it still wrote went into a folder that no longer existed. While shutting down, the host now watches its home; if it is removed or replaced, the engine exits within about a tenth of a second instead of saving into it. With the home intact the stop does what it did before, in the same order and with the same limits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Bound how long a busy engine store spends naming its holder #2078 named the process holding a busy engine store. On macOS it asked lsof about every lock file it found, 500 ms each with no cap, so a failing seal could add seconds. One shared budget now covers the whole lookup, and it stops at the first busy holder; when the budget runs out the message is the plain busy sentence. Requests that are not busy never inspect anything. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * A chat made with chat --once always reaches your other computers runChatV3Once returned before the interactive road registered its process cleanup, so a paired --once never closed its sync drive. Publishing uploads a chat's frames before it registers the chat with the relay, and a --once that exited in between left the uploads with no record pointing at them: the other computer never listed the chat, and doctor said nothing (Hello saw it in 1 of 48 runs, about 1 s after pairing). --once now always registers drive cleanup; with no drive it returns at once, and a paired chat gets its final send and release inside the same limit the interactive road uses, with one plain stderr sentence if that fails. A paired drive records a small local publication marker, so codeaf doctor can report a chat this computer started that the relay does not list, and doctor --fix registers it through the ordinary road, create-only, never over another computer's record and never for a chat put away meanwhile. A never-paired computer writes no marker and makes no relay request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * manual: --once waits for your other computers, doctor finds a chat that did not arrive, a deleted home is refused Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: spell the Option chords the way the product does on macOS macOS names the Option key opt, so Home's continue offer shows opt+c and the Memory switch shows opt+u. Two tests hard-coded alt+. The Memory test now builds its expected line through the app's own chord spelling, and the takeback test waits for the offer's question and checks a continue chord is drawn beside it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * changes: entry for #2086 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(syncsetup): make the quiet solo home in a fresh folder The quiet case removed a paired machine's folder and created a solo identity at the same path in the same process. codeaf now refuses to create an identity in a home it saw and that was then deleted, so the case uses a fresh folder, which is what a never-paired home is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(session): task-copy fixtures that hold on macOS 42 internal/session subtests failed on macOS and passed on Linux: - On macOS a new file takes its folder's group, so the Apply fixtures' files often carried a group other than the process's and the portable-metadata check refused them during setup, before the test's own behaviour ran. The success fixtures now give their files the process's uid and gid, resolve temp-path aliases first, and clear extended attributes. The product refusal stays: private Apply stages replacements with the process's group, so a foreign group cannot be carried. A new setgid-directory test pins that refusal on Linux. - Bare mktemp on macOS ignores TMPDIR, so five scratch probes now use mktemp "$TMPDIR/codeaf-runtime.XXXXXX". - Two path checks compared a canonical path with a raw temp path; both sides are canonicalized now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Revert "manual: --once waits for your other computers, doctor finds a chat that did not arrive, a deleted home is refused" This reverts commit f28b0f4. * Revert "A chat made with chat --once always reaches your other computers" This reverts commit f9bb2e7. * manual: a deleted home is refused, and a stopping engine exits when its home is removed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * changes: #2086 no longer carries the --once change Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Private apply tolerates macOS's own com.apple.provenance attribute macOS 15 stamps com.apple.provenance on every file a process tree started from some apps creates, and the owner cannot remove it. Private apply refused any extended attribute, so on such a Mac it refused every changed file. That one attribute is now tolerated on macOS: the replacements apply writes come from the same process tree and are stamped the same way. Every other attribute, quarantine included, is still refused, and Linux is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(session): Apply fixtures leave macOS provenance in place Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
AbirAbbas
added a commit
that referenced
this pull request
Oct 31, 2026
… slow relay says so The public-network test found four ways continuing a chat between two computers went wrong: - D6: a bare start continued the newest chat another computer let go of, and while that move ran every explicit pick on Home answered "a chat is already continuing here" and the older chat opened. An automatic move now gives way: a confirmed different chat waits with the holder the person saw, the automatic move settles and its unused hold is released with a fresh bounded context, then the picked chat is taken and opened. Confirming the arriving chat joins its move. With no input a bare start behaves as before. - D3: a lost-race refusal skipped #2078's retry card and only said "another device continued this chat first" in the footer, and a holder whose own writer reopened after a move (raising its ownership number) was blamed as another device. Every refusal now keeps the card and the draft; a fresh reading tells the same holder reopening (the card asks again with what it now shows) from a real different device. - D5: Home gave up on a slow relay after 5 s, and the first message after a successful move was left in the box under Home's timeout sentence. Home's listing now waits up to 25 s, says "waiting for <host>; still checking" at 5 s and keeps its rows; a real outage still shows at once. A send right after this window's own move waits up to 30 s for its writer to start, then sends once; a send that cannot leave says the message was kept and names the relay. - D7: each continue writes one chat.log line with the time spent in each phase, so a slow empty-folder move can be read instead of guessed. Consent still binds holder and ownership number, and nothing in internal/clusterstate, the wire or stored formats changed. The manual pages say what the code now does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AbirAbbas
added a commit
that referenced
this pull request
Oct 31, 2026
… slow relay says so The public-network test found four ways continuing a chat between two computers went wrong: - D6: a bare start continued the newest chat another computer let go of, and while that move ran every explicit pick on Home answered "a chat is already continuing here" and the older chat opened. An automatic move now gives way: a confirmed different chat waits with the holder the person saw, the automatic move settles and its unused hold is released with a fresh bounded context, then the picked chat is taken and opened. Confirming the arriving chat joins its move. With no input a bare start behaves as before. - D3: a lost-race refusal skipped #2078's retry card and only said "another device continued this chat first" in the footer, and a holder whose own writer reopened after a move (raising its ownership number) was blamed as another device. Every refusal now keeps the card and the draft; a fresh reading tells the same holder reopening (the card asks again with what it now shows) from a real different device. - D5: Home gave up on a slow relay after 5 s, and the first message after a successful move was left in the box under Home's timeout sentence. Home's listing now waits up to 25 s, says "waiting for <host>; still checking" at 5 s and keeps its rows; a real outage still shows at once. A send right after this window's own move waits up to 30 s for its writer to start, then sends once; a send that cannot leave says the message was kept and names the relay. - D7: each continue writes one chat.log line with the time spent in each phase, so a slow empty-folder move can be read instead of guessed. Consent still binds holder and ownership number, and nothing in internal/clusterstate, the wire or stored formats changed. The manual pages say what the code now does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AbirAbbas
added a commit
that referenced
this pull request
Oct 31, 2026
… slow relay says so The public-network test found four ways continuing a chat between two computers went wrong: - D6: a bare start continued the newest chat another computer let go of, and while that move ran every explicit pick on Home answered "a chat is already continuing here" and the older chat opened. An automatic move now gives way: a confirmed different chat waits with the holder the person saw, the automatic move settles and its unused hold is released with a fresh bounded context, then the picked chat is taken and opened. Confirming the arriving chat joins its move. With no input a bare start behaves as before. - D3: a lost-race refusal skipped #2078's retry card and only said "another device continued this chat first" in the footer, and a holder whose own writer reopened after a move (raising its ownership number) was blamed as another device. Every refusal now keeps the card and the draft; a fresh reading tells the same holder reopening (the card asks again with what it now shows) from a real different device. - D5: Home gave up on a slow relay after 5 s, and the first message after a successful move was left in the box under Home's timeout sentence. Home's listing now waits up to 25 s, says "waiting for <host>; still checking" at 5 s and keeps its rows; a real outage still shows at once. A send right after this window's own move waits up to 30 s for its writer to start, then sends once; a send that cannot leave says the message was kept and names the relay. - D7: each continue writes one chat.log line with the time spent in each phase, so a slow empty-folder move can be read instead of guessed. Consent still binds holder and ownership number, and nothing in internal/clusterstate, the wire or stored formats changed. The manual pages say what the code now does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AbirAbbas
added a commit
that referenced
this pull request
Oct 31, 2026
…n a slow relay; trust-proxy for every relay limit (#2089) * relay: trust X-Forwarded-For only with --trust-proxy, for every limit A self-hosted codeaf relay took the first X-Forwarded-For address as the caller's network for its registration and dial limits whenever a request carried one, so a relay exposed directly to the internet let any caller mint a fresh allowance by naming its own address. --trust-proxy already existed but reached only the pairing limits. relay.Server now counts the socket peer unless it is handed a peer policy, and relayserve hands the pipe the same socket-or-forwarded policy the pairing mailbox and link pairing use. A relay behind a proxy must now pass --trust-proxy, or every caller shares the proxy's allowance; the flag help, docs/relay.md and the relay manual page say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * sync: a chat this computer never held primes from its frame plan SyncEngine.Holds read the store-wide published.<ledger> file, and new chats share one store, so after any earlier exchange with a relay every new chat looked warm and Fetcher.prime skipped its frame plan. A large chat then came over one serial object batch after another instead of its frames, eight at a time; on the hosted relay each batch costs seconds. Holds now asks whether this computer holds history for this chat (its adoption record or a recorded turn), and a return landing in a fresh staging folder asks about the chat's existing folder, so a warm return still fetches only its delta. The completeness loop is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * continue: the picked chat opens, a refused continue keeps its card, a slow relay says so The public-network test found four ways continuing a chat between two computers went wrong: - D6: a bare start continued the newest chat another computer let go of, and while that move ran every explicit pick on Home answered "a chat is already continuing here" and the older chat opened. An automatic move now gives way: a confirmed different chat waits with the holder the person saw, the automatic move settles and its unused hold is released with a fresh bounded context, then the picked chat is taken and opened. Confirming the arriving chat joins its move. With no input a bare start behaves as before. - D3: a lost-race refusal skipped #2078's retry card and only said "another device continued this chat first" in the footer, and a holder whose own writer reopened after a move (raising its ownership number) was blamed as another device. Every refusal now keeps the card and the draft; a fresh reading tells the same holder reopening (the card asks again with what it now shows) from a real different device. - D5: Home gave up on a slow relay after 5 s, and the first message after a successful move was left in the box under Home's timeout sentence. Home's listing now waits up to 25 s, says "waiting for <host>; still checking" at 5 s and keeps its rows; a real outage still shows at once. A send right after this window's own move waits up to 30 s for its writer to start, then sends once; a send that cannot leave says the message was kept and names the relay. - D7: each continue writes one chat.log line with the time spent in each phase, so a slow empty-folder move can be read instead of guessed. Consent still binds holder and ownership number, and nothing in internal/clusterstate, the wire or stored formats changed. The manual pages say what the code now does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * changes: entry for #2089, what continuing between computers now does Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * continue: review fixes — leave a shared view open, bound an abandoned move From the review of the continue fixes: - An abandoned automatic arrival no longer closes its conversation; it leaves through the path a departing window takes, so another window on the same session keeps working and the engine releases the chat only when the last window goes. AlreadyHere skips cleanup. - An abandoned automatic move is cancelled the way Escape used to cancel it if it has not answered within two minutes, so a waiting pick always runs. A second Escape navigates again instead of being swallowed. - Opening another chat's card no longer abandons the automatic arrival; only confirming it, or the resume chord, does. - The already-here recovery retries silently once, then keeps the card. - A send's own reading keeps Home's outstanding listing progress and host. - A writer wait ends at once on displacement, with the existing words, instead of waiting 30 s and blaming the relay. - The manual says what Escape does to an automatic continue; the release retry reuses cellsync.ReleaseRetryEvery. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * tui3: read the stale listing before the take, as the test intends askChats now carries its five-second progress tick in a batch, so a.askChats()() returned the batch rather than the answer, and the test ran the "old" read only after the take, against the new holder. It now takes the listing out of the batch and reads it first, which is the in-flight answer the test was written about; what it asserts is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * sync: a continue waits for another window's vault save instead of failing D4 in the two-network re-measure: 2 of 5 picks on the dev relay failed with "could not continue this chat here because the chat could not be put in this folder", and chat.log said "handoff: resource temporarily unavailable" with the hooks phase at 5 ms. A continue's last hook pulls the vault under state-sync.lock, taken without waiting, while every running chat's drive pushes the vault under the same lock; over a slow relay that push holds it for seconds, so the pull failed at once and the chat was handed back. The pick fix made this more frequent: picks that used to be refused as busy now reach the take while the bare start's engines are still saving. The continue's pull now waits up to 30 s for the lock, then fails as before. Background exchanges still skip when the lock is busy, because the holder is already carrying the same state. TestAContinueWaitsForAnotherWindowsVaultExchange holds the lock from a second open file through the move's restore; without the wait it fails with the exact error L3 saw. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <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.
Fixes #2070. Closes the rest of #2072 (A, D, F, G, I, L and M landed in #2075).
What changes
a chat is already continuing here; wait for it to open). A failure is shown on the card with1 retry/2 leave it there, or in the chat, never only on Home's say line after the card has closed.… the engine store is busy (held by <command>, pid <pid>)) and offers retry. The error is classified once at the engine boundary intocellstore.StoreBusyError(errors.Is(err, cellstore.ErrStoreBusy)), and the home's own saving process is never named as the holder.--hostis consistent. Undercodeaf chat --host B, the window no longer wires this computer's sync identity and takeover into a session that runs on B.continue hereis not offered there, and a chat B holds never shows as held elsewhere. The remote wire has no take verb, so this is the option the issue allowed for.codeaf doctorfinds it;codeaf doctor --fixrepairs what is safe. Doctor reports a busy store and its holder, an unfinished continue this computer started, duplicate rows for one chat, and this computer's lapsed holds.--fixreleases this computer's unfinished continues and retires branch copies whose turns are all in the main chat, with onefixed:line each. It never touches another computer's live hold and never removes a turn that exists only in what it would delete. Without--fixit changes nothing. A lock file whose process has ended is already free, so doctor neither reports nor deletes one.chat_test.go.Validation contract → tests
TestContinueThreeRemoteChatsInOneWindowShowsContinuingAndFullHistory,TestHomeTakingClearedOnEveryEndingAndBusyPressExplainsOnCard,TestEveryFailedTakeShowsItsSentenceTestBusyStoreNamedWithHolderAndOffersRetry,TestStoreBusyReportsHolderWithinSeconds,TestStoreBusyIgnoresHomesOwnDaemon--host:TestHostedSurfaceNeverOffersContinueOrShowsHeldElsewhereTestOneChatOneRowKeyedByIDAndCollapsedBranchCopies,internal/chatlistcollapse tests,TestC5OnlineContinueOpensTheArrivedCopy(now also asserts the local and sync rows merge)TestDoctorFindsAndRepairsSyncAndEngineStoreSafely(real relay, two paired homes, a foreign process holding the store, report-only mutates nothing,--fixkeeps every unique turn)TestEverySentenceOfTheDeviceScreensIsOnItsPageProof
Hand test with the real binary (
bin/codeaf): two homes paired through the card over a local self-hosted relay, with a scripted model endpoint. B wrote three chats and closed. On A, each of the three continued in turn from Home in one window: each showedcontinuing here…, opened with its message and reply, and Home then listed each chat once.codeaf doctoron A then reported nothing, andcodeaf doctor --fixchanged nothing and left every transcript byte-identical.Focused runs on the branch:
internal/tui3(continue, take, held, home, machines, wormhole, repository-read sets),cmd/codeaf(Doctor|Host|Surface),internal/cellstore,internal/chatlist,internal/manual, andmake test-laws. The touched-package suites run in CI.Five
internal/syncsetupreal-engine tests (TestDriveSideSupersededStopsTools,TestDriveSidePublishesOverTheDefaultDaemon,TestTwoHomesL12DeathBranches,TestTwoHomesTakeARunningChatAtOnce,TestPublicTakeRestoresEveryWorkspaceSetRoot) fail when the engine binary is staged locally. They fail identically on unmodifieddev(f128ec3), and CI skips them because it does not stage the engine. That is tracked separately.🤖 Generated with Claude Code