Skip to content

An idle chat on a paired computer hands its hold to the relay, and opening it elsewhere takes it - #2287

Draft
AbirAbbas wants to merge 48 commits into
app/v3from
feat/idle-chat-releases-to-relay
Draft

AbirAbbas wants to merge 48 commits into
app/v3from
feat/idle-chat-releases-to-relay

Conversation

@AbirAbbas

@AbirAbbas AbirAbbas commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

An idle chat on a paired computer now hands its hold to the sync service. Opening it on another paired computer takes it over without the Continue-here step, even when the first computer is off. A running chat still asks, so you can watch it without taking it.

Base is app/v3. The desktop engine and the take path this builds on live on app/v3. When app/v3 lands on dev, this change goes with it, and if it is still open then, the PR re-points to dev. The branch is up to date with app/v3 a687c92, merged rather than rebased. Before it merges, it is also tested in a local merge on the app/v3 → dev landing candidate. That merge is not part of this PR.

Desktop half: Agent-Field/codeaf-desk#102 opens a can_open row directly.

What changes

  • Release when idle. A chat on a computer in a paired set (two or more computers on one identity) lets go of its hold about 2 seconds after it goes idle, once everything is on the sync service: turns, settings saved after the last turn, workspace and vault.
    • Work keeps the hold exactly as today: a running turn, queued or steering work, a task that is starting or parked, jobs, title and memory writers, and unsent turns.
    • Release also waits until the capture is complete and nothing is unsaved, since app/v3 saves the work folder at pauses (Save the work folder at pauses, not after every tool batch #2356).
  • Take back in place. New work in the window that released (a send, queue, steer, retry, task or ! shell) takes the chat back before any write or model call.
    • It acquires a new fence, conditionally and with the saved head unchanged. There is no restore and no setup question, and local files such as node_modules stay.
    • If another computer took the chat, the work gets today's held-elsewhere refusal and nothing is written.
  • Open a free chat directly. Chat rows carry an optional can_open.
    • It is set only for paired homes, and only from the directory's own lease expiry.
    • Opening such a chat in the terminal, or through agents.open with its id, takes it with a non-forcing conditional acquire, checked again at acquisition.
    • A live hold keeps the Continue-here choice. Rebuildable files are not carried, as today.
  • Copy folder name (pre-existing, now frequent). A chat moving back and forth no longer nests its copy-folder name until file name too long. Names are kept and capped at 240 bytes with a stable hash.

How this fits app/v3's ownership rules (93d4b7f)

app/v3's 93d4b7f ("Restore dev ownership expiry and exact-fence consent", part of the #2291 dev merge) replaced the earlier app/v3 behaviour, where an idle expired hold was renewed with the same fence by heartbeat. Its rules:

  • an expired hold admits nothing until the person continues the chat;
  • reopening takes a new fence;
  • a take needs the exact fence it saw.

This PR keeps those rules for every hold but one:

  • Only a window that released its own hold (through the idle path above, after certifying its saved state) may take the chat back, and only when new work arrives in that window.
  • It does so by conditionally acquiring a new fence, with the saved head unchanged and nothing pending, before admitting anything. It never heartbeats or reuses the released fence, even if a stray heartbeat or a lost release reply left that lease live.
  • A hold that merely lapsed, or was released from outside its window, takes app/v3's path unchanged: it is available, and the person continues it.

93d4b7f's own regression tests pass on this branch:

  • TestReleasedSameFenceRetiresReceiptUntilContinuation
  • TestAnExpiredHoldCannotAuthorizeTools
  • TestIdleExpiredHoldRequiresContinuation
  • TestStartupAcquiresNewHold
  • TestIdleWatchReconnectAndProcessReacquisition
  • TestRelayOutageO4FailedIdleRenewalKeepsWorkLocal
  • TestAvailableAfterALapseRetiresTheReceipt
  • TestConditionalAcquireRejectsNewerFenceWithSameHead
  • TestTakeF2SameHolderSameHeadFenceChange
  • TestHostedTakeBackReachesAcquireWithoutSharedUploadsOrReleaseDelay

TestIdleRelease_93d4_SelfReleasedRetakeIsANewFenceNeverARevival is built from the first of them. It checks that:

  • a self-release closes the gate and sends no heartbeat at the released fence;
  • an outside heartbeat there does not reopen it;
  • new work takes a strictly newer fence;
  • a chat another device took is refused;
  • a lapsed or outside-released drive behaves exactly as 93d4b7f requires.

Nothing changes for anyone else

  • Opt-in by pairing. A home with sync off, or a single device, makes no new requests and keeps the same renewal cadence. A test counts every request type over a full 30 s renewal period.
  • Old builds keep working. An old holder never releases, so a new computer shows Continue here as before. An old reader sees a released chat the way it sees a lapsed hold today. can_open is optional and omitempty. The lease rules, wire, directory protocol and replicated records are unchanged.

Validation contract

  • C1. A paired, idle chat with its turns uploaded is released within a few seconds; the directory shows no live holder.
  • C2. A running turn, queued work, a running, starting or parked task or job, or unsent turns keep the hold, renewed as today.
  • C3. New work in the released window, when nobody else took the chat, retakes it at a new fence before any write or model call, with no prompt.
  • C4. If another computer took it, new work gets the held-elsewhere refusal and nothing is written locally.
  • C5. Nothing is lost. Turns, journal, workspace (minus rebuildable files), memory and settings are on the sync service before the release, and a failed upload keeps the hold.
  • C6. Opening a released chat on another paired computer pulls and takes it with no Continue-here prompt; its state matches the last save. A hold that merely lapsed (for example during a long outage, possibly mid-turn) still asks Continue here, as on app/v3.
  • C7. Opening a chat another computer holds behaves as today: you can watch, and Continue here stays the person's choice.
  • C8. C6 works while the first computer is stopped.
  • C9. Rebuildable files are not part of the hand-off.
  • C10. Unpaired and single-device homes behave exactly as before.
  • C11. Old builds on either side keep working.
  • C12. A send racing an open ends with exactly one owner under today's fencing.

Each item maps to TestIdleRelease_C* / TestIdleOpen_C* tests in cellsync, cellstore, session, syncsetup, chatlist, handoff, tui3 and cmd/codeaf.

Verification

  • CI: the PR gate is green on every pushed head. Full check is green except TestAServedFactoryStartsAGoAheadsJobWithNoAppAttached, which is pre-existing on app/v3 and fixed on the landing candidate. Other reds seen along the way are pre-existing on base or proved to be flakes: TestMemoryLateReceiptReachesFactoryHeaderAfterQuit (A factory memory receipt that lands after Quit is missing from the ledger or the header (TestMemoryLateReceiptReachesFactoryHeaderAfterQuit fails ~1 in 3) #2566), TestFactoryRetryActionsDoNotReportStopped and the factory memory pause tests fail on base too, and a loop-speed timing law matched base in interleaved runs.

  • Mac two-home hand test (macOS arm64), throwaway homes, local sync service and stub model, base app/v3 a687c92 against this branch:

    • C1: released 4.2–4.7 s after idle.
    • C2/C7: a running chat is never taken; B gets the Continue card.
    • C3: retakes in place at a new fence before the reply.
    • C4: held-elsewhere refusal; nothing written.
    • C5/C6/C9: no prompt, and history, files, memory and a setting changed after the last turn all arrive. Inside a Git work folder, an ignored node_modules stays behind. Outside Git it travels, the same as on base, because the install-folder rule needs a lockfile that travels.
    • C8: works with the first computer stopped.
    • C10: unpaired and single-device homes match base.
    • C11: interoperates with dev's build as the old device.
    • C12: one owner in every race.
    • Six alternating hops kept every journal exact.
  • Found and fixed during the hand tests:

    • After a sync outage, a chat taken back from Home could not send until restart. Drive.RenewalStatus reported a self-released writer as never installed. It now keeps its period with holding false. Covered by TestIdleRelease_R4_RA_…; the Mac reran 15 of 15 sends and Linux 5 of 5.
    • A stray heartbeat at the released fence could have let a self-released window work under that fence. It now always takes a new one (TestIdleRelease_93d4_…).
    • The desktop's take now says <computer> picked this chat up again when that same computer took its own chat back, instead of blaming another device.
    • After a restart, the first Home open of a chat another computer had released stayed a viewer. Home decided from its display rows before they had loaded. It now reads ownership when the display can't answer, and then takes the free chat (TestIdleOpen_R5_…). The Mac measured 0.7–0.8 s to hold.
    • Opening your own saved chat never waits on, or fails on, the sync service. That read is bounded to 2 s and only made when needed; offline, Enter opens the local chat as before, and a remote row says the service is unreachable (TestIdleOpen_R5b_…, TestIdleOpen_R5c_…).
    • Opening a chat that owes no turn no longer prints a false "asking again". The resume door now checks for an owed turn before asking the work gate (TestR6_…). This also removes the same noise on app/v3 for chats held elsewhere.
    • A lapsed hold whose holder may have been working is never taken silently. It keeps the Continue-here card; only a released hold opens directly (TestIdleOpen_R6_…). TestIdleRelease_R6_LapsedHolderIsFencedAfterAnOpenElsewhere proves the fencing: the expired holder admits nothing, the open takes a new fence, an old-fence publish is refused, and local turns are kept.
    • The first Enter after the sync service returns sends, as on app/v3. It no longer replays a cached refusal (TestIdleOpen_R7_…).
    • A chat reopened after its writer's close failed offline gets a fresh writer instead of staying view-only (TestIdleRelease_R7_…; the same path exists on dev, A chat reopened after its writer's close failed stays 'this window only shows the chat' until restart #2581).
  • Performance (Mac, 10 open chats, 3 runs each, equal homes):

    • steady idle CPU is 0.35–0.41% on this branch against 0.34–0.37% on base;
    • wakeups are 16–22/s against 21–28/s;
    • RSS and open files match base within noise, and the sync service sees no idle HTTP traffic;
    • taking a held chat is unchanged;
    • opening a released chat takes 0.8–1.0 s;
    • each release costs about 0.07 CPU-seconds once.
  • Focused tests after the merge:

    • the contract tests;
    • app/v3's newer sync tests;
    • 93d4b7f's regression tests;
    • the full internal/manual package;
    • make test-quick.
  • Landing candidate. In a local merge with land/app-v3, with dev's hosted sync storage opt-in (Hosted relay storage is off until an account is approved #2509):

    • an idle chat whose account cannot store keeps its hold and adds no requests;
    • nothing unstored is ever offered as openable;
    • storage-off Continue keeps working.

    That needs three dev-only guards, which go into the landing merge rather than this PR.

  • Pending before merge:

    • Mac re-check of the R7 fixes on this head.
    • Core flows rerun with short paths: the first run's socket paths exceeded 103 bytes, so its reds don't count; the hand checks for tools, tasks, settings, pairing with --at and moving a running chat passed.
    • A rerun of the F-6.k2 double-outage sequence on the landing candidate once it carries Fix what the pre-release hand test of dev found #2567.

Evidence that a take never admits a stale publish (Mac, local sync service, chat taken while its holder was mid-turn): the relay log shows the holder's last publish and beat at fence 1 (10:10:46.919/.931), then the taker's forced acquire expecting fence 1 → fence 2 (10:10:48.561), a conditional acquire → fence 3, its publish at fence 3 and its release. There is no entry from the old holder after the acquire, and no 409 or 5xx. The taker's continue fetched the holder's last result before acquiring.

Issues found along the way and filed: #2563 (the stub answers codeaf's helpers note), #2564 (the arrival brief swallows typing), #2569 (approval waits for the 10-minute permission refresh), #2574 (offline Enter on a remote row gives no answer), #2579 (copy-folder names grow per move on app/v3 and the landing candidate; fixed here), #2580 (a cached refusal replayed after recovery, on other roads), #2581 (a reopened chat after a failed close; fixed here). Full-check flakes seen: TestSteersLandInTheOrderTheyWereSent passes 5/5 on head and base (recorded on #1749).

Also noted: on first launch, codeaf chat continues another computer's latest released chat automatically. That is base behaviour, seen on dev's build too.

Not in this PR

  • An acquire-receipt protocol for reconciling a lost acquire reply. It would change the replicated lease state, so it belongs to the sync-service owner. An uncertain take-back here stays honest and retryable instead.

🤖 Generated with Claude Code

AbirAbbas and others added 27 commits November 3, 2026 00:21
…is on the relay

When a chat on a computer that shares its identity with another goes idle,
its saved state (turns, settings, workspace, vault) is certified and uploaded
outside the admission lock, then the hold is released in a short bounded step.
Running, queued, starting or parked work keeps the hold. New work takes the
hold back in place, or is refused if another computer took the chat. A home
with one device makes no new requests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t asking

A chat whose hold was released or has lapsed is marked openable in the list,
only for paired homes and only from the directory's own clock. Opening it
fetches and takes it with a non-forcing conditional acquire, checked again at
acquisition, so a holder that renews during the fetch keeps the chat. A live
hold still offers Continue here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a free chat directly

An ordinary message in the window that released the chat reacquires the hold
through the work gate: no restore, no setup question, and local files such as
node_modules stay. A Home row for a free chat pulls the newest saved head even
when an older viewer of it is open here. Remote pre-send checks honour the
caller's deadline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eleased chat back

Rows carry an optional can_open. agents.open with a shared chat's id takes a
free chat without the Continue-here step. Sends, queues, steers, retries and
actions in a window that released its chat take the hold back first; app
queues and actions count as work, so they keep the hold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t moves

The copy bucket was "copy-" plus the origin's full encoded path, so a chat
moving back and forth nested the name and failed with 'file name too long'
on the third hop. A chat that already has a local root keeps it, an origin
that is itself a copy reuses its own name, and longer names are capped at 240
bytes with a stable hash. Existing short names are unchanged. This predates
the idle hand-off, which makes repeated moves common. (The filing code moved
to internal/syncsetup/copy_filing.go in the previous commits.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Describe idle release, opening a free chat directly, take-back in the same
window, and that a running chat still asks. Add the change entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lock-defer law keeps an absorbed panic from becoming a deadlock. The
open-row lookup and the live drive's idle checks now read under a helper that
unlocks from a defer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ver undoes a take-back

The terminal seals through the warm native daemon, whose capture plan is
metadata-only, so the saved-state proof refused it and the hold was renewed
forever. Certification now asks for a complete standalone plan; ordinary
sealing stays on the daemon. A lost optimistic comparison or a competing local
vault round retries at the quiet cadence. Ownership notices carry a version,
so a release notice delivered after a newer acquisition is dropped. A held
writer whose fresh ownership read fails keeps working offline, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only the idle-release vault step uses the non-blocking lock. An ordinary
SyncState waits under its own context, as on the base.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A nested project under a received copy keeps its own bucket, and the long-name
hash is taken from the original path rather than the lossy encoded one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d first

Adds the real-terminal test: openV3Agent and runSurface with the default
native daemon, an on-disk relay and two homes, asserting release, in-place
take-back and that effort and memory set after the last turn reach the other
computer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wnership door

Effort, model choices, approval posture, places, task effort and a remembered
note retake the hold or get the held-elsewhere refusal before they are saved,
so a save is never acknowledged where it cannot reach the relay. Unpaired and
unreleased windows keep their old path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ars a stale release snapshot

A canceled or expired Close budget skipped the base cleanup and left an
ordinary writer running; Close now keeps the idle-join error and always
runs the capture, stop, cancel and join it ran before. A zero send receipt
from the release is discarded once a reacquire has cleared the viewer, so a
send after take-back is no longer refused as "available to continue here".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ld first

Rename, rewind, task rooms, deletes and merges, plan steering, autonomy,
names, anchors, standing tree and remembered notes enter the same
released-window door before they write, so none is acknowledged where it
cannot reach the relay. Every other window returns to its old path before
any network read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Beyond 20,000 paths, 50,000 entries or 64 MiB of chat files the proof
fails and the chat keeps its hold, as before this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s install lock

The drive is published through an atomic pointer, so a window that never
released no longer waits on catch-up disk work before its own check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… an unknown outcome is honest

The take-back acquire ran under the 750 ms admission read and could time
out after the relay had committed it; the window then refused its own send
as "available to continue here". It now has a bounded 15 s budget, held
admission keeps 750 ms, and an unknown outcome keeps the draft with a
retryable line. A hold this computer cannot attribute to a window is never
adopted or offered back to itself; it lapses and a retry takes the chat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings the idle hand-off up to app/v3 a687c92, resolving 13 files.

app/v3's 93d4b7f restored dev's ownership rules: an expired hold admits
nothing, reopening acquires a new fence, and a take needs the exact fence.
The merge keeps those rules for every hold except one this window released
itself after its saved state reached the sync service. Only that window
may retake the chat when new work arrives in it. It does so by
conditionally acquiring a new fence, with the saved head unchanged, before
any write. It never heartbeats the released fence. A lapse that was not a
release keeps app/v3's behaviour. 93d4b7f's own regression tests pass
(TestReleasedSameFenceRetiresReceiptUntilContinuation,
TestAnExpiredHoldCannotAuthorizeTools, TestStartupAcquiresNewHold,
TestIdleExpiredHoldRequiresContinuation,
TestIdleWatchReconnectAndProcessReacquisition,
TestRelayOutageO4FailedIdleRenewalKeepsWorkLocal,
TestConditionalAcquireRejectsNewerFenceWithSameHead,
TestTakeF2SameHolderSameHeadFenceChange).

Other interactions resolved:
- Saves at pauses (#2356): release now also waits for the capture and
  for no unsaved turns.
- Chat rows (#2425, #2348): the new rows carry can_open beside working,
  notes, sharing and move.
- Beat state (#2432): a fresh fence resets it.
- Opening stamp (#2556): kept with the metadata-write hook.

TestIdleOpen_C6_D2 now finds the freed row at the fence the reopened
writer holds, because reopening takes a new fence on app/v3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eased one

app/v3's ownership rules say a heartbeat never revives retired work
permission. A window that released its own idle hold could still see its
lease live at the released fence, after a stray heartbeat or when the
release reply was lost. ConfirmWork used to treat that as held. A
self-released drive now always retakes through the conditional acquire,
which advances it to a new fence, so new work never runs under the fence
it gave up.

TestIdleRelease_93d4_SelfReleasedRetakeIsANewFenceNeverARevival is built
from 93d4b7f's TestReleasedSameFenceRetiresReceiptUntilContinuation. It
checks that a self-release closes the gate and sends no heartbeat at the
released fence, that an outside heartbeat there does not reopen it, and
that new work takes a strictly newer fence. It also checks that a chat
another device took is refused, and that a merely lapsed or
outside-released drive behaves exactly as 93d4b7f requires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…still asks

Every page that described Continue here or taking a chat over now says
what this branch does:
- A chat no computer holds opens directly on another paired computer,
  from terminal Home or through agents.open.
- A live hold still offers Move here or Continue here.
- An idle chat lets go about two seconds after it goes idle, once
  everything is on the sync service.
- New work in the same window takes it back.
- A hold that merely lapsed admits nothing until continued.

The merged "Home lists chats" section no longer repeats its held-on
paragraph. The change entry says sync service and states that app/v3's
ownership rules still hold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
agents.take reported every ownership conflict as "another device continued
this chat first". With idle release, the computer B saw holding a chat can
take its own chat back at a newer fence while B's take is on the way, and
B was then told a device that does not exist had taken it. The app now
reads the record again after the refusal. When the holder is still the
computer B saw, at a newer fence, the refusal says "<that computer> picked
this chat up again", the sentence the terminal already used, now one
constant for both. It keeps the conflict code and the holder_changed
detail. A different holder keeps the old refusal, and nothing is taken in
either case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AbirAbbas and others added 7 commits November 10, 2026 02:02
The "Continue <device>'s latest chat here?" rescue is for a computer that
is away. It stood when presence said the device was offline, or when the
chat's hold had lapsed. On app/v3 an online computer keeps renewing, so
the two rarely disagreed. Now an idle chat on a paired computer releases
its hold, and an online computer's idle chats read as lapsed. While the
change feed is up, only presence decides, so an online computer's chats
are never offered. Without the feed, a lapsed or released hold still
stands in for the device being away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…offer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…talled

After a sync outage, a chat taken back from Home could not send until
its engine restarted. The new writer idle-released before the first send,
and RenewalStatus then reported it the way it reports a writer that never
installed: no period, not holding. Home keeps the taken chat's startup
wait until that first send, so every Enter waited for an installation
that had already happened. It never reached the ownership read and the
conditional acquire that would take the chat back. A writer that released
its own hold now keeps reporting its period with holding false, so the
send runs the ordinary bounded ownership check and acquires a new fence.
Stopped, lost and displaced writers still report no active writer.

TestIdleRelease_R4_RA_TakenBackReleasedWriterRemainsInstalled runs the
real continuation path. A releases, B opens and releases, A takes the
chat back and releases before new work. The test checks the status, the
new fence, the transcript and both windows' gates, and it failed before
the fix. The shared idle fixture's seed message gets a valid timestamp,
so knowledge capture stops re-reading it during certification.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A move leaves out untracked files that Git ignores, such as node_modules
in a Git work folder. Outside a Git repository every file copies, subject
to the existing secret screening and exclusions, as it always has. The
pages said rebuildable folders were left out in general.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… hold"

This reverts 89871e2 and its manual and change-entry sentences. The
designed rule offers a computer's latest chat when the computer is offline
or has let go of the chat, and an idle-released chat has let go. The
copied-chat journeys (TestCopiedChatPingPongsThroughThreeTakes,
TestCopiedChatFromDeepWorkspaceMovesAndReturns,
TestCopiedChatTakeRepairsDetachedAdmissionBinding) rely on that offer,
with alt+c, while the other computer is online. A two-computer check
showed the offer behaves the same on app/v3, so there was no regression
to fix here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This reverts 2aa6cd7. The pages were already right. A move leaves out
an install folder only when the lockfile that rebuilds it travels too, Git
tracks nothing inside it, and .furrowpolicy does not already exclude it.
A folder with no lockfile always travels, and Git-ignored untracked
output stays behind (cellstore/rebuild.go, cellstore/ignored.go). The
rewrite claimed a lockfile alone does not exclude a folder, which is
false, and it broke the manual probes that ask why node_modules is
missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AbirAbbas and others added 5 commits November 10, 2026 07:05
After a restart, Home decided how to open this computer's own local copy
of a chat from its display rows alone. Those rows could be empty, or still
show this computer's old hold, until the directory follower caught up. A
chat another computer had since released then opened as a stale viewer
instead of being taken. Home now reads ownership through the send source
before opening a local copy, and a free shared row goes down the same
conditional free-open road as a remote row.

TestIdleOpen_R5_FirstOpenAfterRestartTakesFreeChat covers released and
lapsed holds, with and without a stale own-hold display, and failed in
all four cases before the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The restart fix read ownership through the directory before every Home
open of a local copy on a paired home. On a read error it refused to open
the chat at all, after up to 25 s, so a paired computer could not open its
own saved chats offline. The read now happens only when it can change the
outcome: when this window has no writer or viewer for the chat, and Home's
display has no row for it or still shows this computer's own stale hold.
It is bounded to 2 s, and any error or timeout opens the local chat
exactly as before. Unpaired and single-device homes never read.

Tests: an unreachable sync service still opens the local chat; a display
that already answers makes no read; an unpaired home never reads. All
three failed against the previous commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TestIdleOpen_R5_FirstOpenAfterRestart... contained the retired name as a
substring, which internal/namelaw rightly refuses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TestIdleOpen_R5b_LocalOpenWithUnreachableSyncOpensLocal,
TestIdleOpen_R5b_LocalOpenUsesDisplayWhenItAnswers and
TestIdleOpen_R5b_UnpairedNeverReads belong to dee1cd1; the file was not
staged with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
R5 sent this computer's own released chat down the free-open take, which
needs the sync service. Offline, that take neither answered nor said
anything, so Enter on one of your own saved chats did nothing. Now:
- While Home's machine reading is down or failing, a local line opens
  through the ordinary local door, as it did before.
- A free-open of a row that has a local copy falls back to it.
- A free-open take whose bounded first read (2 s) cannot reach the
  service also opens the local copy.
- A remote row offline gets the unreachable sentence, never silence.
Online, an own released chat still takes a new fence through the free road.

Tests cover: your own released chat opens locally when sync is down; a
remote row offline says why; online it still takes a new fence; a local
copy keeps the take-race refusal; the free take bounds its first read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AbirAbbas and others added 3 commits November 10, 2026 09:50
ResumeStoppedTurn asked the work gate before checking whether any turn was
owed, so a window whose admission refused printed "the reply stopped when
this conversation moved — asking again" and then the refusal, with nothing
to resume. That happened on app/v3 for a chat held elsewhere. With idle
release it also happened for your own released chat opened offline. The
owed turn is now checked first. A refused owed turn says the refusal once,
without the asking-again line, and stays owed for a later attempt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A hold that merely lapsed, for example during a long outage while its
computer was mid-turn, opened on another computer with no question. Base
asks Continue here in exactly that case, and the holder's turn quietly
forked. Now only an explicit idle release (expires 0) opens directly.
Every lapsed hold keeps the Continue-here card, because older holders
do not always say whether they were working. In a self-released window
with the sync service unreachable, a new send says the chat cannot be
taken back until the service answers, and keeps the words in the box.

TestIdleRelease_R6_LapsedHolderIsFencedAfterAnOpenElsewhere proves the
lapsed holder is fenced: once its lease has expired, A admits nothing new.
B's open takes a new fence. A's old-fence publish is refused, and B's head
is unchanged. A's local turns are kept, and its next send is refused as
held elsewhere.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nue here

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AbirAbbas and others added 5 commits November 10, 2026 11:10
… before

With idle release, a chat that went quiet before an outage has let go of
its hold. Sending in it needs one directory read, and that read replayed
Home's cached refusal until the next machine reading, for up to about
20 s after the service was back. The live-hold road never needed the
read. In a window with no live hold that is not displaced, Enter now makes
one fresh, 3-second read instead of replaying the cached failure. It
retakes at a new fence when the service answers, and refreshes Home's
reading. When the service is really down it prints the take-back line and
keeps the words. Live-held windows keep their offline admission unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reopening a chat whose old writer no longer held its hold closed that
writer first. When the close's final save failed, for example offline, the
old, stopped writer was handed back, and the window answered "this window
only shows the chat" until a restart. A fresh writer is now installed, and
the failed save stays in the final-save receipt. The same path exists on
dev (#2581); idle release made it common, because a self-released writer
counts as not holding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lock-defer law requires every critical section to unlock from a
defer, so an absorbed panic cannot leave the drive locked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the same fence

When the sync service refused the connection, an idle release had already
retired the send receipt, set the viewer to Available and stopped renewals,
so a chat this computer still held live at its fence could not send after a
short outage. A release that provably never reached the service (a failed
dial) is now fully undone: the receipt, viewer and renewals return and the
next heartbeat renews the same fence. An uncertain failure stays
self-released and never revives the old fence; the next send retakes it at a
new fence. Idle release makes one request and leaves retries to its own
backoff, so a refused dial is not hidden behind a retry deadline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nstead of asking

A window whose writer was not holding, while the fresh reading showed this
computer's own live lease at the window's fence, fell through to the
Continue here card about its own chat and never asked its writer. Such a row
now goes to the writer's own work check, which resumes the same fence or
conditionally retakes it. The card still appears when another computer holds
the chat or a newer fence from this computer does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant