Skip to content

An idle chat on a paired computer can hand its hold to the relay (opt-in idle handoff) - #2287

Merged
AbirAbbas merged 934 commits into
devfrom
feat/idle-chat-releases-to-relay
Nov 14, 2026
Merged

AbirAbbas merged 934 commits into
devfrom
feat/idle-chat-releases-to-relay

Conversation

@AbirAbbas

@AbirAbbas AbirAbbas commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Ported from app/v3 onto dev after app/v3 landed (#2561). This branch keeps the original commits in its history; the last commit is the port onto today's dev.

Off by default

The new behaviour sits behind one setting, idle handoff (idle_handoff in Settings → General, or CODEAF_IDLE_HANDOFF=1). It is off by default. With it unset, a paired computer behaves exactly as on dev today:

  • an open idle chat keeps renewing its computer's hold;
  • a released remote chat still asks Continue here;
  • agents.open never acquires a shared chat on its own.

With it on (and hosted sync storage allowed):

  • an idle chat in a paired set releases its hold to the relay once everything it saved has been uploaded;
  • a free shared chat opens and acquires on terminal Home and through agents.open (can_open) without a Continue-here step.

A live hold or a lapsed hold still asks in both cases.

What stays on by default

These change dev's behaviour only where dev was wrong:

  • Refused sends keep their words. A send or ! command the engine refuses puts the words back in the box.
  • Refused owed replies keep their question. Opening a chat whose owed reply is refused keeps the question and shows only the refusal.
  • Offline sends say why. An offline send names the unreachable sync service and keeps the words.
  • A chat whose own hold lapsed in an outage sends again. On dev its window refused every Enter until restart. Now the first Enter takes the chat back at a new fence, with no card, when no other computer holds it.
  • Take conflicts name the right computer. A desktop take conflict caused by a newer hold from the same computer now says that computer picked the chat up again.

Three defects from the hand tests, fixed here

  • P9-X.1. A ! shell line now runs the same ownership check as a plain message. If it is refused, the line stays in the box and one line explains why.
  • P9-X.2. After orphaned turns are kept in a branch, Enter on the original chat reopens the original chat. The branch stays a kept copy. Every refused Enter prints one line.
  • P9-X.3. A turn running when its own hold lapses reacquires the hold in place (fence + 1) when no other computer holds the chat. It clears the outage notice and keeps working on the original chat, with no copy branch.

#2381's behaviour on dev is intact. ConfirmWork still retires a noticed expired hold, and a recovering batcher still heartbeats before it publishes. #2643's owed-upload wrap is kept. Dev's storage-off rules (#2509, #2536) gate both new behaviours.

Existing deployments

  • Protocol. appwire gains one field, can_open, which is omitempty. An older desktop client ignores it. The desktop companion is not part of this change.
  • Defaults. Default behaviour is unchanged apart from the corrections listed above. The change entry's invalidates: lines each say whether they need idle handoff on.
  • Deploy and rollback. Nothing deploys from this change beyond the usual dev channel build. To roll back, revert this squash commit on dev. The setting has no effect on a build that does not know it.

Verification

Every pass below was rerun by the driver after the last edit, not taken from the porting run.

  • Real terminal. The six real-terminal tests (cmd/codeaf/idle_*terminal*_test.go) pass twice, with the setting switched on by the tests themselves and nothing exported, as CI runs them.
  • Default off. TestIdleRelease_DefaultOff_* (syncsetup), TestIdleOpen_DefaultOff_* (tui3) and TestIdleHandoff_DefaultOff_* (cmd/codeaf) prove that unset behaves like dev.
  • P9-X fixes. Each has a regression test: TestIdleOpen_P9X1_*, TestIdleRelease_P9X2_*, TestIdleOpen_P9X2_* and TestIdleRelease_P9X3_*.
  • sync: a late save after a noticed expiry is kept as a copy #2381 and cellsync: a recovering heartbeat that fails is an owed upload #2643 intact. TestLateSealOnExpiredUnacquiredHoldDoesNotRenewLease, TestAppRelayOutage* and the TestL4Hosted* / TestHosted* quit tests pass unchanged.
  • Packages. internal/syncsetup passes in full, as do internal/cellsync, internal/config (see below), internal/manual, internal/chatlist and internal/appwire. make test-quick passes.
  • Attributed by name. Every test that failed in a local make pr-ready was rerun by name on this branch and on dev 6a5f28744. Each one now either fails on dev too or passes on both.

The driver's reruns also found three defects in the port, all fixed here:

  • R9 matrix cleanup race. One new matrix cell returned before the drive's background notice step finished, so its temporary folder was still in use (3 failures in 40). The test now waits for that notice, and the cell passes 100 of 100.
  • Settings search. The new row's hint and alias said "paired", so a settings search for "pair" landed on General instead of Devices. The row now says "your other computers".
  • Storage-off sends. With hosted storage off, the new pre-send check waited for an uploading writer that never starts, and the words disappeared. Separately, a changed saved head continued without asking. Both now behave as on dev unless the setting is on, which restores four dev tests in storage_off_lifetime_test.go.

🤖 Generated with Claude Code

AbirAbbas and others added 30 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 <[email protected]>
…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 <[email protected]>
…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 <[email protected]>
…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 <[email protected]>
…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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
…2283)

* test: reproduce lost job ownership and duplicated call receipts

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: account for durable job ownership and journal call identity

Competing manager opens remain owned after Talk selects another journal. Recognize the saved owner for live budget admission and spending. Match receipt rebuilds to the journal identity as well as the cell identity.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test: cover factory identity collisions and seeded session names

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: keep factory budget ownership and seeded identities precise

Validate durable ownership against the candidate store. A local worker mirror must share the owning floor manager path before taking an extra manager charge. Seed journal headers and titles from the session folder metadata in either layout.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test: racing asks make one manager

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory: one manager per job, with a lock across create and join

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: one manager per job

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* changelog: name the pull request

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory: the mint lock wraps the work, so no closer is handed out

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test: reproduce foreign manager charging a local budget

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: bind manager budget ownership to the issue and creation time

Old ownership marks keep their root, job and repository matching.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test: reproduce duplicate aliases hiding a new receipt

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: count saved calls once across ledger session aliases

Match call facts across aliases, retaining repeated calls under a single alias and leaving historical rows untouched.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test: reproduce unrelated mint blocked by recipe network read

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test: reproduce remote Talk reusing an identical local row

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: read recipes before locking manager minting

Keep the held-manager fast path and recheck after locking. Network reads no longer hold the floor-wide mint lock.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: keep remote Talk on the worker mirror

GitHub issues supply Created (github/source.go issueItem); Store.Create preserves it and api transport carries it. Independent factories can therefore share ID, Repo, Num and Created. The remote door must use its mirror; owned runners still consult their floor. Remote manager deduplication remains unsupported.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test: verify receipt refresh through the exported handoff seam

Assert historical ledger bytes remain unchanged and repeated refresh imports no saved call twice.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test: distinguish saved calls by time and row identity

Exercise exported Refresh with separate equal-cost calls, microdollar rounding, and repeated ledger IDs. Normalize Go formatting.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test: cover both owner identity fields and legacy marks

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: match saved calls against distinct ledger row identities

Match each alias to saved calls in order, using receipt precision for cost and allowing live usage to precede its seal. Count repeated row IDs once and append only missing calls, preserving historical ledger bytes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: preserve legacy aggregate call identity and finish formatting

Record the legacy prefix assumption for calls sharing a seal. Final review retains no blocking findings.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test: a remote job's conversation works in the floor's folder

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* factory: a remote job's conversation keeps the floor's folder

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* cellindex: count a chat's held rows by its fuller id, so nothing is booked again

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test: rows under both ids are one prefix, as the rebuild writes them

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
PR #2099 integrated these changes as a single-parent commit. Preserve its app/v3 behavior while recording the 49 dev commits as merged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep later app/v3 work, preserving the true dev merge and the published #2099 behavior choices.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Retain app/v3 solo-chat pairing, ordinary effort repair and opt-in team sharing. Adapt newer team sharing safeguards and replacement receipts to SQLite and durable pairing recovery.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Preserve opt-in sharing, adapt SQLite fixtures and first-sharing backups, and carry replacement receipts through adoption retries.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
* test: expose unpublished evidence when memory resume stalls

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* scratch: distinguish memory snapshot timing from interrupted passes

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* scratch: complete memory admission trace import

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test: hold resumed memory while the last stage finishes

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test: isolate a successful resumed pass from job-end cancellation

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test: observe resume after trailing journal capture

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test: reproduce trailing evidence through public history admission

Remove scratch traces after identifying the two later manager timeline sources. Classify fixture requests by their provider role so brief errands do not count as memory.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test: wait for resumed memory citations instead of future job evidence

The owned pass publishes its fixed input before the runner writes final timeline messages. Those later sources can raise profile-wide Pending again without losing retained evidence. Freeze the expected sources before releasing the resumed turn and wait for their published citations.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test: require trailing pending evidence in the resume regression

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* changelog: name the pull request

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
* Turn host pressure off in the E13 B2 recovery test

TestAppBackgroundE13B2QueuedTaskAcquiresAndSettlesAtStartup waited 30 s for a
queued task to settle, but the admission governor holds a queued task while the
host's load is at or over 1.5 per core or memory is under the floor. Under a full
test run the host stays busy and the task stays queued, so the wait ran out.

The governor is doing its job. The test now turns both ceilings off through the
settings a person uses. The wait and every assertion are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Add the change note for the E13 B2 test

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
…tches (#2196)

* app tests: a fake lane holds an event until it is watched (red)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* app tests: a fake's lane holds what is put on it until the backend watches

The backend publishes an agent as live before it starts watching its lanes, so
a test's first raise or lane could read a nil channel and block for ever, which
hung all of cmd/codeaf until the timeout. Events put before the watch are now
held and handed over in order when the lane opens.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* changes: entry for the held fake lane (#2196)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
…st (#2181)

* tests: R2 and R3 wait for the startup publication instead of chats.list

#2031 moved the startup pass that publishes existing chats out of chats.list
into a background loop, and TestR1_6InventoryDoesNotWaitForPublication pins
that chats.list never waits for it. R2 and R3 still called ListChats to join
that pass, so with the real engine they read the directory before anything
was published. They now wait for the startup pass, and R3's restart observes
one whole settled publication round before checking that nothing is released
again.

* docs: change entry for this pull request
…chat (#2182)

* tests: damage the teams store, not a teams.json, before continuing a chat

#2066 moved teams into an SQLite store. TestContinuingAChatKeepsItsLease-
DespiteDamagedTeamsAndLaterHeals still damaged teams by overwriting the bytes
at teams.Path and later wrote them back, which no longer breaks the store, so
the take never met a failed rebind and never said so. It now uses
teamstest.Break and its restore, as the other teams tests did in #2099.

* docs: change entry for this pull request
…o answer (#2184)

* tests: keeping an unsent turn on an ownership check needs the store to answer

TestOwnershipCheckThatFindsAnotherHolderKeepsUnsentTurns kept the store
blocked through CheckLease, but keeping the turn uploads it as a branch, and
Batcher.lost documents that a failed branch stays stale for the next flush to
retry. The check could never keep the turn, so the test failed on its premise.
It now refuses only A's heartbeats (quiet) and lets the store answer before
the check, so only the check can learn of the move and the kept-turn notice
is still required by the time it returns.

* docs: change entry for this pull request
)

* syncsetup: run the takeover proof's sessions with routing off

TestTasklessTakePreservesConversationWorkspaceIdentity counted every request to
its fake router and failed when one arrived. Its own two session.New calls start
the lane-sheet beat, whose first act is GET /models/<model>/endpoints, racing
the immediate Close. When the beat won, the test failed with "taskless takeover
unexpectedly called a provider". Every caught request was that GET, with no
body, made while the test was in session.New or Close; none during Take.

The sessions now run with RoutingOff, which refuses the beat, so any request
still fails the test and means something new called the router.

* changelog: takeover proof routing off

* changelog: shorter title
* Run the clock while a learner waits for a state write

TestR26R27C30OpenLearnerIdleAndStateWake froze its fake clock after the idle
count, then waited for learner a to apply the next write. A learner's transport
lane holds two messages. When it is full, the message carrying the new commit is
dropped and raft counts it as sent, so only the next heartbeat delivers it. With
no tick, no heartbeat came, and the wait ran out at 30 s.

Advance the clock at its real rate from the write on. The idle count and every
assertion are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Add the change note for the learner wait clock

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
* Check pairing copy around the random check words

TestM1M4M8M9M10AppPair banned engine words by substring across every visible
line, including the three random check words. The word list holds "ledger" and
"cello", which contains "cell", so about 2.3% of pairings failed the check.

Remove the pairing's own words from the line before the ban check. All other
copy is still checked.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Add the change note for the pairing copy test

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Team sharing stays an explicit opt-in on app/v3. Two lines carried from dev still described it as on by default.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Seed unrepaired rows through teamstest and verify persisted repairs with a non-repairing snapshot. Enable sharing explicitly before checking deferred pairing reads after Settings saves and completed turns. Both product promises already work; preserve the default-off preference.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
AbirAbbas and others added 9 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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
…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 <[email protected]>
…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 <[email protected]>
…s, publishes or resumes in place

ConfirmWork and the terminal's work check now share one decision over a fresh
directory reading. A hold this drive still validly holds sends on its fence.
Once a lapse or an outside release is observed, the old fence is retired: it no
longer renews or publishes, so a later heartbeat cannot revive a writer that
has lost its permission. Such a drive refuses work, as 93d4b7f requires, and
the chat continues only through a conditional take into a fresh drive. A
window that released its own hold still reacquires in place at a new fence.
Another holder, or any newer fence, still needs consent.

The take road's already-here shortcut now needs this computer's matching send
receipt, so a retired writer takes the ordinary conditional road, carrying its
unpublished turns. A table-driven test covers every reachable combination of
the relay's view, the drive's state, the service's answer, the turn and
unpublished local turns, with impossible cells named.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…, and a refused send keeps its words

A window whose own hold lapsed, or was released from outside, sends its first
Enter through a conditional take into a fresh writer, carrying the draft, with
no card and no trip to Home. The card appears only when another computer holds
the chat or has taken it. A send the engine refused used to leave the message
box empty, because the box is cleared before the asynchronous submit; the
refused words now go back ahead of any newer draft, on the plain and
attachment roads.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…again, and recovery order has one outcome

A retired fence refused every drain, so a call that was already running when
the hold expired could neither publish nor go to a kept branch, which
87c7f49's late-seal orders require. Retirement now stops renewal beats and
new work only; already sealed turns take the ordinary publish or resolve road,
where the relay fences them: an unchanged expired fence may renew through that
late publish, and a newer holder's fence sends them to a kept branch.

The state matrix gains a call in flight when the hold expires, and the order of
events after an outage, each cell with one expected outcome: a renewal that
reaches the unchanged hold first keeps the fence and uploads local turns; an
ownership check that sees the expiry first keeps pending work on a branch; a
work check that sees it first lets sealed work drain without reopening work.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…d move or a stale held status

A failed move from Home to another chat marked the current chat blocked as
well, and that mark survived the outage, so both Enters replayed the cached
reading and nothing was sent after recovery. A stale writer status that still
claimed the retired hold hid the recovery check the same way. Enter now checks
again whenever no local hold is admitted, a blocked own fence recovers through
the conditional reopen, and the box keeps its words until the send succeeds.
Admission also waits for a late writer-status update.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
AbirAbbas and others added 6 commits November 13, 2026 22:27
…-in)

Port of #2287 onto dev. The new behaviours sit behind one setting,
`idle handoff` (CODEAF_IDLE_HANDOFF), which is off by default: with it
unset a paired computer behaves exactly as before, idle chats keep
renewing their hold and a released remote chat still needs Continue here.

With it on, an idle chat in a paired set releases its hold to the relay
after its upload, and a free shared chat opens and acquires on Home and
through agents.open. The defect corrections from the PR stay on by
default: refused words go back into the box, a refused owed reply keeps
its question, an offline send names the unreachable service.

Also fixes the three defects the owner's hand tests found on the PR:
a `!` shell line now takes the same ownership check as a plain message;
Enter on a chat whose orphaned turns were kept in a branch reopens the
original chat; a running turn whose own hold lapsed reacquires it in
place when no other computer holds the chat.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
In the other-holder cells with unsaved turns, the matrix calls ConfirmWork
a second time after draining the late seal. That call starts the drive's
background forget-then-notice step again, and the subtest returned before it
finished, so TempDir cleanup raced its hold-file lock (3 failures in 40).
The cell now waits for that step's final notice, as the consent and
unreachable branches already do.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Its hint and an alias said "paired", so typing "pair" in settings landed on
General instead of Devices (TestSettingsDevicesSearchFindsTheTabAcrossQueries).
The row now says "your other computers" instead.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… on dev

Two places in the port's send check diverged from dev with the setting off:

- A storage-off chat has no uploading writer, so the new take-back road
  waited the full take-back time for one and then refused. The words left
  the box with nothing said. A local, unheld reading now skips that road,
  as dev did.
- An own expired hold whose saved head differs from this window's
  continued in place without asking. That widening now needs idle handoff
  on or the reopen road. Otherwise Continue here asks, as on dev.

Restores four dev tests in cmd/codeaf/storage_off_lifetime_test.go.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@AbirAbbas AbirAbbas changed the title An idle chat on a paired computer hands its hold to the relay, and opening it elsewhere takes it An idle chat on a paired computer can hand its hold to the relay (opt-in idle handoff) Nov 14, 2026
@AbirAbbas
AbirAbbas changed the base branch from app/v3 to dev November 14, 2026 15:19
@AbirAbbas
AbirAbbas marked this pull request as ready for review November 14, 2026 15:19
@AbirAbbas
AbirAbbas merged commit 473b159 into dev Nov 14, 2026
1 check passed
@AbirAbbas
AbirAbbas deleted the feat/idle-chat-releases-to-relay branch November 14, 2026 15:19
AbirAbbas added a commit that referenced this pull request Nov 14, 2026
…in (#2644)

#2287 added a SubmitBash method to the shared switchAgent test fake. It shadowed fakeAgent.SubmitBash, which records the line, so TestUserBashHomeRunsInTheSelectedProject failed on dev. The method is removed; no product code changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.

3 participants