Skip to content

Browser lifecycle: one Phase, attach instead of persisted ports, controller-owned launch - #775

Open
nedtwigg wants to merge 22 commits into
browser-quick-winsfrom
browser-lifecycle
Open

nedtwigg wants to merge 22 commits into
browser-quick-winsfrom
browser-lifecycle

Conversation

@nedtwigg

Copy link
Copy Markdown
Member

Reworks the browser Surface lifecycle around one explicit state machine, so a whole class of relaunch races can't recur, and a restored browser reopens the page it was on. Stacked on #774. Items B1–B3 from the browser-stack deep dive.

One Phase, one daemon gate (B1)

  • One Phase union replaces about a dozen flags. AgentBrowserSurfaceController now tracks idle / launching / attaching / live / parked / relaunching / ended / disposed, where it used to track relaunching, poppedOut, headedConnected, parked, connectionLost, liveStreamPort, recoveryGen, and so on. The connection and CDP observer exist only in live. The spec's Phase table lists every transition.
  • Every daemon command goes through one driver() that yields only in live. That covers chrome and Display modal actions, tabs, sync-to-pane, edit chords, captures, and get cdp-url. The rule "never query the daemon mid-relaunch" used to be enforced per call site, and header navigation, edit chords, and the context-menu reuse (which sent open into a pop-out gap every time) all bypassed it.
  • A request made outside live is kept, not dropped. A pending {url, headed} intent is applied on the next live. The context-menu reuse is one setRenderMode(mode, { url }) intent, reached by Surface id so an unmounted Door keeps it.
  • A headless stream drop is ended. A headed drop still pops back in.

attach instead of persisted stream ports (B2)

  • Both hosts gain an attach verb, on every layer: VS Code, Tauri/Rust, sidecar, and the dev harness.
    • agent-browser reads the daemon's state files and never spawns. Before, recovery ran stream status, which starts a fresh about:blank daemon, so after a reboot a pane never reopened its page.
    • Playwright connects through its registry.
    • Either provider relaunches at the page only when the browser is gone.
  • wsPort is no longer persisted. A dor handover passes the port straight to the controller, and every scrub is gone. Restored Playwright panes no longer spend about 6 s "Connecting…" and then show "ended" (their port was always stale after a restart). The stale-port recovery machinery is deleted.

The controller owns first launch (B3)

  • One launching phase replaces four Wall-side launch-and-bind copies: tool swap, render swap, context port, and tool serving.
  • Launch failure policy is stored on the Surface as launchFallback: close, embed, or restore. One Wall action applies it, so a pane restored mid-launch still gets its creator's policy.
  • Tool serving no longer waits for page load and launches in the Tool's own tool.<leafId> session.
  • Fixes:
    • A minimized cross-provider swap failure left the pane bound to a disposed controller.
    • A failed swap kept the old managed key on a new session, so dor ab --key then opened a second pane.
    • A transferred Workspace left its source window's controllers running.
  • One closeSessionOn primitive is behind every close. A launch into a named session waits for any close still in flight.

Specs: docs/specs/dor-browser.md (Phase table, gate, attach, launch, wsPort rule, host rows) and its rationale, plus dor-tool.md, dor-cli.md, and tiling-engine.md.

Testing

  • lib 3877, dor 189, vscode-ext 181, and standalone (tsc, 243 vitest, 210 node) all pass. cargo check passes.
  • Spec, loopback, and public-docs lints pass.
  • The Argos story runs pass in Chromium and WebKit (294/294 each).
  • 33 mutation checks each turn their test red.
  • All of this is tests only; no real agent-browser or Playwright was run.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe

nedtwigg and others added 19 commits September 23, 2026 21:16
The webview recovered a stale stream port through `stream status`, a CLI
verb that starts a daemon to answer: after a reboot a restored pane bound
to a fresh about:blank browser and never reopened the page it had, and a
query in the pop-out close/reopen gap raced a competing daemon.

`attach(session, { url, headed })` replaces the webview-facing stream
status on every host layer (shared hosts, VS Code message/router/adapter,
the Tauri command and sidecar case, the browser-dev harness):

- agent-browser reads `<session>.pid` / `<session>.stream` and probes the
  port, as `launch` already did. A daemon that is up but not streaming is
  left alone; only a gone one, for a caller naming the page it had, is
  relaunched there (headed for a pop-out, tracked for shutdown), through
  the relaunch path's generations and blank-tab sweep. Concurrent
  attaches of one session join.
- Playwright connects its viewer, relaunching at `url` only when the CLI
  registry lists no browser for the session (never one it merely cannot
  view), serialized with launches and closes. `dor pw`'s binding uses it
  with no page, so it never relaunches.

`open` also takes a caller-chosen session, for launches that must land in
a known one (a Tool's own, a restored swap).

The existing callers switch to page-less attach, so behavior is otherwise
unchanged here; the controller adopts the relaunch policy next.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
The rule that nothing may query the daemon during a relaunch's
close/reopen gap was enforced by a `relaunching` flag checked at some call
sites and not others: header back/forward/reload/URL edits, the Display
modal's device and custom viewport, tab clicks, sync-to-pane and the
Cmd-A/C/X edit channel all still reached the daemon mid-relaunch, and
the pane context menu's reuse deterministically sent `open <url>` into a
pop-out's gap. The lifecycle itself was an implicit state machine over a
dozen flags (`relaunching`, `poppedOut`, `headedConnected`, `parked`,
`liveStreamPort`, `recoveryGen`, `recoverySeq`, …).

`AgentBrowserSurfaceController` now holds one `Phase` union — idle,
unbound, attaching, live, parked, relaunching, ended, disposed — and the
stream connection and CDP observer exist exactly in `live`. Every daemon
command (chrome actions, screen actions, tabs, sync, `get cdp-url`, the
connection's tab selection, edit chords, screenshots) goes through one
`canDrive()` gate that runs only in `live`; a navigation asked for
meanwhile is kept as the one latest intent and runs once live.
`poppedOut`/`relaunching` in the view snapshot are projections, and the
snapshot carries the phase.

Stale-port recovery becomes the attaching step: a restored pane attaches
with its page and presentation (relaunching there if the daemon is
gone), an unpark attaches without a page (a daemon that ended while
hidden is not relaunched behind the user's back) and falls back to the
parked port, and a failed pop-out/pop-in comes back in the pane through
the same attach. The controller no longer writes `wsPort` into params.

`setRenderMode(mode, { url })` makes the context menu's reuse one
intent: the relaunch opens the port's page instead of a navigation
racing it.

The webview-global closed-session set goes: `closeBrowserSurface`
closes a Surface's session through its controller, which re-closes
after any relaunch or relaunching attach that lands later, and falls
back to the params for a Surface no controller holds. Wall kills, swaps
and Tool retirement use it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
`wsPort` was saved with every automated browser Surface, though it is
ephemeral: the Playwright one is the host's in-process viewer server,
always stale after a restart, so every restored Playwright pane spent
~6 s "Connecting…" before reading "ended"; an agent-browser one after a
reboot names nothing, or another process's listener.

The port stays in memory as the one-shot handover of the launch that
learned it (`dor ab`, a GUI launch) and is dropped at persistence — and
at hydration, so a blob saved before this change restores without it
too. A restored Surface attaches to find the live port.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
A Workspace transfer's commit released terminal Sessions but left every
browser controller alive in the source Window: a popped-out one, never
parked, kept streaming there, and closing its headed window auto-reverted
it from both Windows at once. The commit now disposes each member's
controller without closing its session, which the destination attaches
to from the persisted params.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
The same launch-and-bind was implemented four times in the Wall — the
Tool swap, the iframe/cross-provider swap (with `restorePrevious`), the
pane context menu's port launch, and Tool serving — each with its own
liveness predicate and failure rule, all racing the controller they
handed a session to. A session-less pane that was persisted, or whose
webview reloaded mid-launch, restored session-less forever; Tool serving
blocked on the page load through `agentBrowserCommand(['open', url])`;
a minimized cross-provider failure left the pane bound to a disposed
controller; and a failed swap kept the old managed `key` on a fresh gui
session, so `dor ab --key` opened a second pane.

First launch is now a controller phase. The Wall creates or replaces the
Surface with `{ renderMode, url, cwd, binaryPath }` and no session; the
controller opens the page (headed for a popout) in a new session, or in
`launchSession` when params name one, binds the session itself, and
closes whatever comes up if it was released meanwhile. A restored
session-less pane launches at its URL. The Wall hears the outcome
through `whenBrowserLaunched(id)` and applies its one rule per origin:

- a failed Display-modal swap gives the previous renderer back in place,
  visible or minimized — the embed, or the previous provider reopened in
  its own session once that session's close has landed, keeping its key;
- a failed Tool swap falls back to the embed;
- a failed context-menu or new-tab launch takes its pane with it.

Tool serving writes the destination with `launchSession` — the session
it had, or the Tool's own — and the controller's `open` navigates a live
daemon or starts one, never waiting for the page. The panel re-acquires
its controller when the provider changes under one id.

Deletes `bindLaunch`, `boundBrowserParams`, `eagerSurfaceExists`, most of
`restorePrevious`, `tool-browser-session.ts`, and `dor`'s session-less
ensure path.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
Promotes the lifecycle this stack built into docs/specs/dor-browser.md:
the Phase table with its transitions (replacing the flag-by-flag parking
and relaunch prose and the session-less-inert rule), the single daemon
gate, attach's page-or-no-page policy, the controller-owned launch and
`launchSession`, `closeBrowserSurface` replacing the closed-session mark,
"`wsPort` is never persisted", the context menu's one-intent reuse, the
in-place failed-swap restore, and the `agentBrowserAttach`/`agentBrowserOpen`
host rows. dor-tool.md's serving rules follow the controller-owned
launch; dor-cli.md and tiling-engine.md drop the eager-pane and
`wsPort`-refresh wording. Evidence moves to the rationale, keyed by
heading; the obsolete session-less and handover evidence goes.

The budget grows for the Phase table the brief asked for.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
agent-browser host:
- One `joinInFlight(session, kind, work)` replaces both `oneCapture` and
  the attach join, keeping eviction on a session's close or relaunch
  (`forgetInFlight`); an attach's cold start evicts only its captures.
- `acceptingStreamPort` serves both `daemonState` and the launch poll,
  and `killDaemon` waits on `processAlive`.
- `openArgs` builds every `open` argv and owns the headed-for-shutdown
  bookkeeping for open, attach, pop-out and pop-in.
- A cold start — a GUI open, or an attach relaunching a gone daemon —
  sweeps no blank tabs: only a relaunch's close+reopen leaves a stray
  one. Both go through one `coldLaunch`, which closes what came up when
  nothing publishes a port.

Playwright host:
- One result builder for the launch and attach answers.
- `attach` refreshes tab state only when a viewer is connected (a
  connecting one is sent the state anyway), and relaunches a session no
  registry entry names without a pointless `close` first.

The VS Code adapter's attach mapper now keeps `headed` and
`nativeIdentity`. agent-browser host tests share `useTempSocketDir` and
`closeServer`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
`command()` claimed to be the only way the controller reached a daemon,
while edit chords and the screenshot loop each checked the gate
themselves. `driver()` now yields `{ platform, session, binaryPath }`
only in `live`, and commands, edits and captures all take it from
there; the screenshot loop's deps shrink to one `capture(opts)` the
controller supplies.

Also:
- One close primitive, `closeSessionOn(provider, cwd, session,
  binaryPath)`, applying the binary gate, behind the controller's close,
  the params fallback and a late launch's close (which no longer builds
  a fake params object to recover the provider).
- The two echo guards (`session`, `renderMode`) are one map.
- Drops the view snapshot's `relaunching` projection and the unread
  `headed` on the relaunching phase.
- `surfaceProvider(renderMode)` replaces the three
  `automationProvider(...) ?? 'agent-browser'` fallbacks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
`wsPort` rode in the Surface's params though only `dor`'s handover
wrote it, so seven places had to clear or scrub it (three
`withoutStreamPort` sites plus the Tool branch, two Wall writes, two
in Tool serving), and a params diff could not see the same port handed
over twice — a pane that ended stayed ended when `dor ab open` brought
its browser back on the port it had.

`wsPort` leaves `BrowserPanelParams` and the controller's params.
`handOverBrowserPort(id, params, port)` gives the Surface's controller
the params the command just refreshed and then the port — acquired
first if no view has mounted it, so its first start streams at once.
The params go first, so a Playwright host-reported presentation or cwd
applies before the new stream. Every scrub is deleted; a blob saved
with a `wsPort` just carries a key nothing reads.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
A dropped stream only set `connectionLost` / `status.connected` while
the phase stayed `live`, so the gate stayed open: a URL-bar navigation
ran `open`, which started a daemon on a port the controller never
learned, and the panel read "ended" off three signals. Now:

- A stream whose browser went away — the socket failing three times, or
  a status saying so after one said it was connected — is Phase `ended`
  for a headless pane; a headed one seen connected still auto-reverts
  (`popIn`), one not yet seen is still opening. `connectionLost` is
  gone, and "ended" is only the phase.
- From `ended`, a navigation re-binds through attach (never a daemon
  command) and runs once live; a handed-over port, the same one again
  included, streams again.
- An unpark reconnects to the port it parked at at once, and asks the
  host (attach, no page) only if that connection fails — rather than
  waiting a host round trip on every show.
- The view snapshot projects `parked` and `attaching` to `live` for a
  headless pane, which still shows its last frame, so a hide/show no
  longer re-renders the view three times.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
A pop-out or pop-in requested while the Surface was still launching or
attaching was dropped, keeping only its URL; and the context menu's
reuse reached the controller through `getAgentBrowserScreenController`,
which exists only once a view has started it, so a request that raced a
restored Door's first mount lost both mode and URL.

- The controller keeps one pending `{ url, headed }` intent for a
  request made before it is bound (idle, launching, attaching) and
  applies it on the next `live`: a relaunch carrying the URL, or a
  navigation. One arriving mid-relaunch is still dropped — one relaunch
  at a time.
- `setRenderMode` is a controller method, with its offered modes fixed
  on first use; `requestBrowserRenderMode(id, params, mode, opts)`
  acquires the Surface's controller by id, and the context menu's reuse
  goes through it.
- Wall tests release browser controllers between tests: controllers
  outlive the Wall that mounted them, and reused ids met a previous
  test's controller.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
The creator of a browser Surface that launches its own browser writes what
happens when that launch fails as a param, `launchFallback`: close the pane,
fall a Tool back to its embed, or restore the renderer a swap replaced. The
controller reports a failed first launch through one Wall action,
`onBrowserLaunchFailed`, which reads it behind one liveness check, so a pane
restored mid-launch still gets its creator's policy.

A Tool's swap and its serving share one launch helper that opens the Tool's
own `tool.<leafId>` session. The controller looks up the binary a `dor ab`
surface resolved and remembers the one its launch ran, so no creation site
carries it. A launch into a named session waits out a close of that session
still in flight, and a launch released without a close leaves a named
session to whoever opens it next (a Workspace transfer's destination),
closing only one the host minted. Releasing a controller always settles its
launch waiter.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
A panel kept its controller for as long as its id and provider held, so
one released under it (a Tool retiring its browser while the pane stays)
left the panel feeding params to a dead controller and the next launch
never ran. A released controller is now replaced when the params next
change, never on the release itself: a kill releases it as the pane
starts to fade, where a new controller would outlive the Surface.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
"Never attaches while parked" and "does not attach again after the port it
streams from drops" are the controller's rules, pinned by its own tests;
the panel copies (and a comment about stale-port recovery that no longer
exists) only duplicated them through a mounted view.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
`connectionLost` and `livePortOpened` fed the stale-port recovery and the
panel's "ended" signal, both gone: the controller's Phase says whether a
stream ended. Nothing read them from the connection's snapshot.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
The Phase table now carries every transition the controller makes —
relaunch from `parked` and `ended`, a headless drop to `ended`, the unpark
fast path — with the handover, hidden-pane and rebind rules as its caption,
and drops the bullet that repeated its rows. New rules: pending pop-outs
before the browser is bound, `launchFallback`, a launch waiting out its
session's close, a released controller replaced on the next params, the
GUI launch's binary memo, and an abandoned launch closing only a minted
session. Stream ports are never params. "Closes what came up" and the
echo rule are stated once; dor-tool.md points at them instead of
paraphrasing. The budget grows with the transitions and the new rules.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
Since an unpark streams from its parked port before asking the host,
nothing passes `attach` a port to fall back to. A Tool's persisted params
drop `launchFallback` with the rest of its derived browser state; the
persistence test now carries one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
A mutation run found three rules no test held once the port handover and
the stale-launch rule changed shape: the session echo guard (a remounted
view feeding pre-launch params relaunched the browser), the panel's
provider key (the port handover acquired the controller itself, masking
it), and a stale launch's check against a session the Surface bound since,
which a minted session can never meet. The first two get tests; the third
is dropped.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
The comments still described a session and its port landing as one params
write; the port is handed over separately now.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 24, 2026

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 14dca06
Status: ✅  Deploy successful!
Preview URL: https://c709f14c.mouseterm.pages.dev
Branch Preview URL: https://browser-lifecycle.mouseterm.pages.dev

View logs

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feedback on a draft, not a merge verdict. Mark the PR ready when you want the full review.

The one finding is inline: navigating from ended can load the page twice. I found nothing else in the controller's Phase transitions, the host attach verbs, or the launchFallback path.

Comment thread lib/src/components/wall/agent-browser-surface-controller.ts
nedtwigg and others added 2 commits September 23, 2026 22:26
A navigation out of `ended` relaunched a gone daemon at that page through
`attach`, then ran `open` for it again on `live`, since the pending
navigation was still set: the page reloaded right after its first load,
dropping any state it set up and repeating a non-idempotent GET. The
controller cannot tell a relaunch from a found daemon on its own; both
answer with a port.

- Both hosts' `attach` answer `relaunched` when they started a browser at
  the caller's page (agent-browser for a gone daemon, Playwright for a
  session no registry entry lists), and the VS Code adapter keeps it.
- A launch, pop-out/pop-in relaunch, or relaunching attach opens the
  pending navigation's page itself (`launchUrl`), rather than the previous
  page followed by a navigation; one the host opened settles that
  navigation (`openedByHost`) before `live` could run it again. A page
  asked for with a relaunch supersedes one pending. An attach that only
  found a live daemon leaves the navigation to run on `live`, as before.

The drop test that pinned both loads now names the live-daemon case it
covers; new tests cover the relaunching attach, a launch out of a failed
launch, a relaunch to the page it is opening, and a pop-out from a parked
pane carrying the page asked for meanwhile.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
@nedtwigg
nedtwigg marked this pull request as ready for review September 24, 2026 05:36
@argos-ci

argos-ci Bot commented Sep 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Argos notifications ↗︎

Build Status Details Updated (UTC)
storybook-chromium (Inspect) ✅ No changes detected - Sep 24, 2026, 6:04 AM
storybook-webkit (Inspect) 👍 Approved by Ned Twigg 1 changed Sep 24, 2026, 6:04 AM
Deployment Status Branch Updated (UTC)
preview (Open) Ready browser-lifecycle Sep 24, 2026, 6:05 AM

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three lifecycle races are left in the new launch/park paths. Each one is inline:

  • A late close from an abandoned launch shuts the next launch into the same named session.
  • A Tool that is re-framed while its launch is in flight stays on the old URL.
  • An unpark sends daemon commands before its parked port is proven, and those commands can start a fresh daemon.

The spec's wsPort rule also misstates where dor pw's port comes from.

Comment thread lib/src/components/wall/agent-browser-surface-controller.ts Outdated
Comment thread lib/src/components/wall/agent-browser-surface-controller.ts
Comment thread lib/src/components/wall/agent-browser-surface-controller.ts
Comment thread docs/specs/dor-browser.md Outdated
- A Surface closed while its launch into a named session was in flight
  issued no close (no session bound yet) and registered nothing, so the
  next launch into the name opened it at once, and the abandoned open
  closed it when it landed. The same held for a closed Surface's pop-out,
  pop-in or relaunching attach: each closes its session again when it
  lands. The controller now tracks that work (`track`), and `close()`
  records the session's close as lasting until it has landed and closed
  again (`trackClose`), naming the launch's session (the `launching`
  phase carries it) when none is bound yet. A launch into the name waits
  for all of it (`closeLanded`), and one released while it waits opens
  nothing.
- A new `url` in params while `launching` (a Tool re-framed mid-launch,
  which no longer awaits the open) was dropped; it is now the pending
  navigation, run once live unless the host opened that very page.
- An unpark entered `live` at once, so sync-to-pane and a pending
  navigation reached the daemon before the parked port was proven; for a
  daemon gone while hidden, `set viewport` started a fresh about:blank
  daemon that the failed stream's attach then streamed. The gate
  (`driver`) now refuses while `resumed`, and what waited for `live` runs
  once the stream opens (`drivable`).
- The spec's port rule names where `dor pw`'s port comes from: the
  Playwright host's `attach`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe

This branch has not been deployed

No deployments
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.

2 participants