Make native Take control transfer ownership - #1240
Merged
Merged
Conversation
h0x91b
force-pushed
the
fix/dev3-native-take-control-transfer
branch
from
August 3, 2026 12:07
3902651 to
914d101
Compare
The host granted its single writer lease to the first client across every dev3 app process, and `claim` only ever took a vacant slot, so a viewer in any other instance was permanently read-only: Take control reached the host and came back refused. Add a third ownership action, `takeover`, which swaps the writer pointer in one synchronous turn and returns the displaced client so the host can tell it authoritatively that it is now an observer, before confirming the winner. `claim` keeps its non-stealing semantics, so attaching, ensureWriter and agent-prompt-native are unchanged. Old hosts drop the additive frame unparsed, so only a takeover-request timeout falls back to a plain claim; every other failure is surfaced as itself, and a refusal there becomes an actionable host-too-old state on the read-only strip. Canonical geometry follows the new owner: the host broadcasts the writer's grid to the other clients on resize, and a promoted viewer re-fits after layout has settled rather than measuring past the strip it just removed. Covered by a red-first regression that drives the production Take control frame from a second real OS process against one real native host.
h0x91b
force-pushed
the
fix/dev3-native-take-control-transfer
branch
from
August 4, 2026 02:03
914d101 to
5b1cf34
Compare
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.
Hi — Claude here, the AI assistant that wrote this branch. Description below is mine.
Summary
An explicit
Take controlin a native terminal could never transfer the writer lease when another dev3 window or app process held it — it reached the host, came back refused, and left the viewer permanently read-only.One branch caused it, not a race. The host grants a single writer lease to the first client across every app process (several dev3 processes share one
~/.dev3.0), andWriterOwnership.request(client, "claim")returnswriter-activewhenever the slot is taken. No action existed that could move a live lease, so the button was architecturally incapable of working. Reproduced deterministically in~/.dev3.0/logs/2026/08/2026-08-03.loglines 30978–31138: pid 74178 attaches to host 73645 as an observer, its attach-time claim is refused withanother client is already the writer, then fifteenTake control refusedlines follow while pid 80386 holds the lease.What changed
A third ownership action,
takeover, alongsideclaim/release:writer-ownership.ts—takeoverswaps the single writer pointer in one synchronous turn on the host's event loop and returns the displaced client.claimkeeps its exact non-stealing semantics, so ordinary attachment,ensureWriterandagent-prompt-nativeare unchanged.host.ts— sends the displaced client the unsolicited id-0 observer frame (the channel decision 191 built for the vacancy notice) before confirming the winner, so no turn exists in which two clients believe they own the PTY.pty-server.ts— only the explicit gesture usestakeover.OwnershipTimeoutErrorwithaction === "takeover"falls back to a plain claim (which still wins a vacant slot there); a disconnect, auth failure or host error frame is surfaced as itself, never relabelled. A claim refused on such a host becomes an actionablehost-too-oldstate on the read-only strip.Two further bugs surfaced while verifying, fixed here:
appliedCols/Rowswas stale and it kept telling its own viewers the old size — the writer's bytes would wrap at the wrong width. The host now broadcasts the canonical grid to the other clients on each writer resize, andnativePtyGeometryprefers the host's number over the local one. Geometry is owner-driven; attach and focus still never resize.TerminalViewnow re-fits once, after layout settles.The strip truncates, so the short label stays in the bar and the why-and-what-to-do sentence rides the
Take controlbutton's tooltip (focusable, unlike a span). The button stays clickable inhost-too-oldon purpose — the slot can still free up when the other window leaves, and disabling it would strand the one recovery path that works. Copy in en/ru/es. Rationale, rejected alternatives and risks:decisions/198-explicit-writer-takeover-across-processes.md.Evidence this fix is real
src/bun/__tests__/native-takeover-peer.tsis a second real OS process that reattaches viareattachNativeTaskSession, stands up its own pty-server and renderer socket, and sends the identicalclaimMessage()frameTerminalView.tsxsends — neverWriterOwnershipor the host protocol directly, and no tmux path. Against unmodifiedHEADit fails on seven targeted checks (lease not transferred, refusal reported, input never reached the shell, displaced process never told, host role not followed, exactly-once count0, geometry still100x30) and passes with this change. The guardrails stay green onHEADas well, so they are load-bearing rather than tautological: attaching from a second instance still observes, the same host/shell are reused, pids are unchanged, the tmux sentinel survives.Known gaps
~/.dev3.0, which exposed every real task — it surfaced another agent's completion-approval dialog inside the QA browser, one click from completing someone else's task. Aborted deliberately; nothing leaked. The three strip states and the button-to-frame path are covered in jsdom and the cross-process behaviour by the two-process e2e, but no human has yet watched it in a real window.