Repository navigation
syncsetup: a window displaced by a newer hold on the same computer lets go at once - #2034
Merged
Merged
Conversation
AbirAbbas
added a commit
that referenced
this pull request
Oct 29, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts go at once When another window or process on this computer takes a chat over at a higher fence, the displaced drive went to available() without retiring its send receipt or waking renewal readers, so it kept reporting the hold and kept its hold file until a heartbeat (TestWatchDisplacementRetiresTheDrive HoldImmediately/higher_local_fence). The two same-device displacement paths (becomeViewer and ConfirmWork) now retire this drive's own receipt conditionally, preserving a newer local writer's, before available() notifies. available() wakes renewal readers on a state change before OnViewer. A lapse that may renew the same fence keeps its receipt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…her hold Review found two gaps in the first commit. A refusal that arrives after this window reacquired the chat could delete the receipt of the hold it now drives, and a replacement fence that had already expired or been released reached available() through the expiry branches and left the displaced receipt behind. retireSendLease now takes the fresh relay reading and keeps the receipt only while that reading names this device at the receipt's own unreleased fence (a lapse this drive may renew). The expiry branches of becomeViewer, CheckLease and ConfirmWork retire before available() too. Without a fresh reading, becomeViewer retires only for a batcher branch, which follows a refusal of this drive's fence under the flush lock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…re the reading Review round 2 found three more gaps. A reacquire that recorded its receipt between a displacing reading and retirement could lose it, because retirement read recordedFence afterwards. A released lease of an unchanged fence can still be renewed by a heartbeat (clusterstate.Heartbeat checks device and fence only), so retiring its receipt could strand a window that resumes. And the same-device admission branch now waited on the receipt lock before its work gate closed. CheckLease, ConfirmWork and becomeViewer snapshot the receipt before they read the relay. availableAfter closes the gate, removes only that snapshot (conditionally, so newer receipts survive) unless the reading still names this device at the snapshot's fence whatever its expiry, and then tells readers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…holds The receipt lock (lockChat(id, "")) is a file lock, so a door that held it while retiring the displaced receipt would wait on itself. Each of becomeViewer, ConfirmWork and CheckLease must finish, and nothing on its call chain may hold the lock when it announces. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AbirAbbas
force-pushed
the
fix/drive-available-retires-displaced-hold
branch
from
October 29, 2026 19:28
ca74246 to
e967bc8
Compare
Windows may release a closed handle's lock late, so an immediate non-blocking probe there could fail after correct cleanup. The five-second deadline still proves no door waits on itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 29, 2026
Merged
AbirAbbas
marked this pull request as ready for review
October 29, 2026 20:10
This was referenced Oct 29, 2026
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.
Why
Two windows or processes on one computer can share a codeaf home, for example the desktop and a terminal. When one of them takes a chat over at a higher fence, the other window is displaced. The displaced drive went to
available()and stayed there: it kept reporting the hold inRenewalStatusand kept its send receipt (v3/sync/holds/<hash>) until a heartbeat came round.Drive.available(internal/syncsetup/confirm_work.go) neither retired the receipt nor woke renewal readers. Both same-device routes into it have this gap:becomeViewer(reached from the ownership watch'sCheckLease) andConfirmWork.Found on a Mac (macOS 15.6, Go 1.26.5, furrow staged). There,
TestWatchDisplacementRetiresTheDriveHoldImmediately/higher_local_fencefails on every run with "watch displacement left the drive held until a heartbeat" after 5.03 s. The cause is pure logic, not darwin code. The test only looks Linux-green because Linux CI stages no furrow, so it skips there.What changed
availableAfter(held, reading)replaces the plain path wherever a fresh relay reading is in hand: the expiry and same-device branches ofbecomeViewer,CheckLeaseandConfirmWork. It works in three steps:becomeViewer's other-device path already used.CheckLease,ConfirmWorkandbecomeViewersnapshot{directoryID, recordedFence}before they read. Removal goes through the existing conditionalforgetSendLease, so a newer local writer's receipt survives, and so does a receipt this window reacquired meanwhile.renewalChanged()runs, thenOnViewer, and only on a change.clusterstate.Heartbeatrenews an unchanged fence even after a release, so the window can resume. The receipt is retired when the reading names another device or a higher fence, even if that newer fence has since expired or been released.renewIdleLease) and a failed receipt write after a reacquire still go through plainavailable(), which keeps the receipt.Composition with #2031 (merged as 1c4ec8a; this branch is rebased onto it):
lockSendLeasetolockChat(id, suffix)insiderecordSendLease/forgetSendLease, and added a.publicationlock indriving()andPublishCopy.forgetSendLease, whose meaning pair: merge a computer's own chats when it joins, instead of replacing them #2031 kept. It touches neither the lock helpers nordriving(), and the rebase had no conflicts.lockChat(id, "")(the receipt lock) is taken only insiderecordSendLeaseandforgetSendLease. Both are leaf file operations that call nothing back, so no door reachesforgetSendLeasewhile already holding the lock.driving → reconcileSendLease), so there is no cycle.How it was checked (this Mac)
internal/syncsetup/displaced_same_device_test.go(no staged furrow, no build tags, so Linux CI runs them):becomeViewer,ConfirmWorkandCheckLease(own receipt, and a newer writer's receipt);-race, and pass with furrow unstaged.TestWatchDisplacementRetiresTheDriveHoldImmediately:higher_local_fenceFAIL (5.03 s) on app/v3, PASS (0.03 s) here;another_holderstill passes.go vet ./internal/syncsetuppasses on darwin andGOOS=linux;gofmtis clean../internal/syncsetuppackage, the receipt consumers' tests in tui3 and cmd/codeaf, the Mac full-tree gate and the dispatched CI runs are reported below.Codex adversarial review, three rounds:
Round 1. Fixed: a stale refusal could delete a reacquired receipt, and an expired or released replacement bypassed retirement.
Round 2. Fixed: retirement read
recordedFenceafter the reading; a released same fence was retired although it can resume; admission blocked on the receipt lock before closing the gate.Round 3. Rebutted: the reviewer kept one major, that the initiating
ConfirmWorkcall can still wait without bound if another process is suspended while holding the receipt lock. The gate does close first, so waiting admissions refuse within their budget. The same blocking lock is already taken synchronously on the same admission paths:ConfirmWork's success path (recordSendLease,confirm_work.go:53);CheckLease→becomeViewerother-device path (forgetSendLease,drive.go:433on app/v3);Every holder covers only a read plus rename/unlink. Making this one branch asynchronous would let an "available" notice arrive after a later resume. A bounded lock belongs to the helper pair: merge a computer's own chats when it joins, instead of replacing them #2031 renames, so it should be a separate change for every caller.
Pre-existing, not touched here (follow-up):
becomeViewer's other-device path keeps a stales.Byhint and retiresrecordedFencewithout a snapshot. A delayed refusal naming another device can therefore delete a receipt that this window reacquired. The round-3 review confirmed this branch neither adds nor widens that path.After the rebase onto #2031
New test
TestDisplacementRetiresWithoutHoldingTheReceiptLock:becomeViewer,ConfirmWorkandCheckLeasethrough a same-computer displacement against a 5 s deadline. It probes the receipt lock non-blocking at the moment each door announces the change.-race. IfConfirmWorkis made to takelockChat(id, "")before retiring, it fails at 5 s.Results on the rebased head:
TestWatchDisplacementRetiresTheDriveHoldImmediately/higher_local_fence): passes ×3.go vet ./internal/syncsetup: darwin,GOOS=linuxandGOOS=windows../internal/syncsetupplus every package that imports it (cmd/codeaf,internal/tui3,internal/enginehost,internal/raftdirectory), on the branch and on app/v3 1c4ec8a, compared by test name.higher_local_fenceis fixed and nothing new fails.TestR9R23PersonalLearnerRefreshesColdRouterGeneration,TestR26R27C30OpenLearnerIdleAndStateWake; ×5 reruns on each tree).TestSeniorDevShellResolvesBareAndCrewModelWords. It is a temp-dir cleanup race ("directory not empty") and passes on this Mac on both trees.TestR7PersonalLeaderFallbackAndNoAutomaticRegain/unpaired. That test also fails in the base's own full check, and it passes on this Mac on both trees.Codex review, round 4, on the rebased head:
driving(),PublishCopyand pair-merge copy is sound.ConfirmWorkcan wait on another process's receipt lock. The rebuttal stands: app/v3'sConfirmWorkresume path already takes the same blocking lock synchronously, underd.mu(recordSendLease). A bounded lock helper for every caller is a separate change.Manual verification, real processes on this Mac (app/v3 1c4ec8a vs this branch, both built with the shipped sidecar recipe):
codeaf app --home A, the engine process the desktop runs, driven over its wire.v3/sync/holds/<hash>stays until the process quits. One run still had it after 130 s, through four heartbeat ticks.🤖 Generated with Claude Code