See
docs/specs/glossary.mdfor Session / Surface / Pane / Door vocabulary. Owns the standalone-specific layer: the Tauri windows, the Rust ↔ sidecar bridge, the AppBar, persistence at the adapter boundary, shutdown ordering, logging, and the build/dev workflow. Defers the protocol it speaks — PTY lifecycle, message contracts, persisted-session types, adapter-agnostic invariants — todocs/specs/transport.md. Evidence and dead approaches: standalone.rationale.md.
Start at the runtime boundary involved, then follow its imports and dispatch:
| Entrypoint | Role |
|---|---|
standalone/src/main.tsx |
Webview bootstrap, adapter selection, and app composition. |
standalone/src/tauri-adapter.ts |
Shared frontend's Tauri command/event bridge. |
standalone/src-tauri/src/lib.rs |
Native app entry, sidecar supervision, and command registration. |
standalone/sidecar/main.js |
JSON-lines command dispatch into PTY and shared host modules. |
standalone/src/window-restore.ts |
Per-Workspace boot planning over one live-PTY list. |
standalone/src/quit.ts |
Webview quit orchestration and updater handoff. |
standalone/src-tauri/src/routing.rs |
Which window a sidecar event belongs to, and the label bookkeeping. |
standalone/src/workspace-move.ts |
Both halves of a Workspace moving between windows. |
Rust stays thin: it spawns and supervises the sidecar, bridges the webview to
it, and owns the OS-integration edges (window events, menu, file drop, dock icon,
logging) plus the session file store. All real logic runs in the Node sidecar, on
the same lib/src/host/ modules the VS Code host runs — build-sidecar-proxy.mjs
bundles them into the sidecar's .cjs copies, so the two hosts cannot drift.
Source of truth: standalone/src/main.tsx (bootstrap()).
setWindowLabel(await resolveWindowLabel())first: every window-keyed Rust command andisMainWindow()below read it.- Pick the platform:
BrowserSidecarAdapterwhenVITE_DORMOUSE_BROWSER_DEV_HOSTis set (§Standalone browser-dev harness), elseTauriAdapter. setPlatform(platform), thenawait platform.init()before the restore — init registers the listeners resume replay arrives on and hydrates the session cache (§Persistence).installPeerSurfaceResponder()afterinit(), never before (§Burrow service) — the responder seeds itself with astatuscommand that the adapter must already have listeners for (rationale).getAvailableShells()without awaiting, so its webview → Rust → sidecar round trip overlaps steps 6–7.- Tauri branch only:
initQuitFlow(platform),initWindowClose(platform)— the listener §Per-window close's ack watchdog waits on — andsetQuitConfirmGate(openQuitConfirm)(§Quit flow). initAlertStateReceiver(),restoreActiveTheme()(docs/specs/theme.md).seedShellStoreon the awaited shell list — restores the persisted selection (dormouse:selected-shell) and publishes it viasetDefaultShellOpts, the default-shell slot for split/spawn/restore (docs/specs/layout.md). Awaited: seeding must finish before the Wall mounts, so the first restored pane already spawns with that shell.- Tauri branch only:
bootFromTearOut(platform)andinitDropCaret(), thenrestoreWindowOrFresh(platform)for every window a tear-out did not answer — the per-Workspace boot (§Persistence) over the priority-based recovery fromdocs/specs/transport.md.armWorkspaceMoves()runs strictly after that restore, since arming drains whatever was dropped on this window while it booted and the restore installs the Workspace store wholesale (§Arrival queue). startUpdateCheck()inmainonly (docs/specs/auto-update.md) — the windowcapabilities/main-only.jsonscopesupdater:defaultto — then renderAppBar+AppwithmultiWorkspace— one Wall per Workspace (docs/specs/layout.md→ Workspaces) — andenableBurrow, the mount gate for the lazily-imported Burrow UI chunk (§Burrow service); the Burrow itself runs in the sidecar regardless.<ConnectedUpdateBanner />rides thebaseboardNoticeslot,<WorkspaceTeardownModalHost />thedialogHostslot; both go to the visible Workspace's Wall.
Must display fatal bootstrap errors with a reload action, for both Tauri and the browser harness, instead of leaving a blank window.
Source of truth: standalone/src-tauri/src/lib.rs (SidecarState, the
#[tauri::command] set, resolve_sidecar_path) and
standalone/sidecar/main.js (the dispatch table).
The sidecar speaks JSON-lines over stdio: commands in on stdin, events out on stdout. stdout is the protocol — sidecar diagnostics go to stderr, which Rust appends to the log file.
Webview → Rust is Tauri invokes; the #[tauri::command] set and TauriAdapter
own the exact command list, most of them thin sidecar forwarders. Two carve-outs
are not forwarded:
| Not forwarded | Handled | Why |
|---|---|---|
load_session / save_session |
Rust | the per-window session file is Rust's store (§Persistence) |
the clipboard readers (Windows only) |
Rust (clipboard_win.rs) |
native Win32 reads (docs/specs/mouse-and-clipboard.md §8.6) |
Request/response commands block on the sidecar's reply under a timeout.
OPEN_PORT_TIMEOUT_MS and OPEN_PORT_TIMEOUT_PER_ID_MS in lib.rs mirror the
constants in lib/src/lib/platform/types.ts (and standalone/sidecar/pty-core.js);
lib/src/lib/mirrored-constants.test.ts pins the copies together.
A blocking command must be async — #[tauri::command(async)] or a plain
#[tauri::command] over an async fn, which the guard below accepts equally.
Tauri runs a sync command on the main thread, where the recv_timeout inside
request_from_sidecar / request_from_sidecar_timeout stops the webview painting
for the whole round trip, up to AGENT_BROWSER_TIMEOUT (30s) (rationale). The
three clipboard readers included: their non-Windows branches round-trip through
the sidecar, and the declaration is per command, not per branch. A unit test in
lib.rs scans the source and fails on any command that reaches the blocking
helpers in neither form.
pty_graceful_kill (TauriAdapter.gracefulKillPtys) SIGTERMs the calling
window's live PTYs (§Windows) and awaits the sidecar's gracefulKillDone (echoing the request's
requestId; bounded at timeout + 1.5s). It resolves one 50 ms grace tick after
the last PTY exits — so ConPTY's late final flush still lands — or at the timeout
for SIGTERM-ignoring programs. Must forward final output during that grace
period; the sidecar retains no scrollback. The quit flow's graceful teardown
calls it (§Quit flow), pinned by standalone/sidecar/pty-core.test.js.
Sidecar events (pty:*, dor control requests, async results) are emitted to the
webview, where TauriAdapter converts dor control requests into the
dormouse:control-request CustomEvent that Wall handles
(docs/specs/dor-cli.md, Host Plumbing — including the sidecar env:
DORMOUSE_NODE, DORMOUSE_CLI_*, DORMOUSE_CONTROL_*).
resolve_sidecar_path strips Windows \\?\ verbatim prefixes from
resource_dir() once at the boundary so every derived path is plain
(docs/specs/dor-cli.md, Bundling And PATH).
The Burrow — relay socket, enrollment, ACL, pairing ceremony, remote-api v1
— runs in the sidecar, never the webview (docs/specs/relay.md → "Burrow
side", which owns that split and what the webview keeps): the same
BurrowService the VS Code extension host runs, bound by
lib/src/host/remote/sidecar-entry.ts and bundled to sidecar/burrow.cjs
with the relay-origin allowlist baked in (docs/specs/relay.md). Nothing the
webview says can widen access (docs/specs/remote-security-model.md).
State. Rust creates the app-data directory, locks it owner-only, and passes it
as DORMOUSE_STATE_DIR (§Persistence, "Rust file store"); FileBurrowStateStore
keeps enrollment and ACL there as one burrow.json, 0600 in a 0700
directory via temp-then-rename — one file, so a write is one atomic rename
(rationale). burrowToken is a bearer credential and never enters a webview
realm. Against the shared store contract (docs/specs/relay.md → "Burrow side"):
- Reads fail closed. Only
ENOENTand a read-but-unparseable file answer empty; the parse failure warns. Any other read error is neither answered nor memoized — the load rejects and takes the save behind it with it (rationale). A later read recovers. - The in-memory view advances only after the rename succeeds. Re-tightening a directory Rust already created is best-effort; failing the save over it would lose the Burrow instead.
persistentis declared, never inferred. With no state directory — Rust passes an empty value when it cannot create one — the fallback store still holds both values in memory, warns once, and reportspersistent: false. The browser dev harness is not this case: its per-run temp directory makes a dev enrollment live and die with the run.
The direct path. The sidecar is the one Burrow that answers a direct-offer
(docs/specs/remote-api.md → Transport → "Direct path"), over
node-datachannel's W3C polyfill. A sidecar package's transitive dependencies
do not ship — the Tauri bundle copies standalone/sidecar/node_modules and
nothing else — so the addon's platform package and detect-libc are declared in
standalone/sidecar/package.json directly — the addon's six platform packages
under optionalDependencies, since only one installs anywhere. Every entry
that manifest declares, under either key, stays external to burrow.cjs,
the external list being derived from those keys rather than listed beside
them. The build fails if the
manifest stops declaring the addon, and asserts from esbuild's metafile that
none of those packages was inlined: the addon resolves its .node
relative to its own __dirname, and inlining would move that out of the
installed package.
Source of truth: standalone/sidecar/package.json,
lib/src/host/remote/native-direct-peer.ts, assertNothingInlined in
standalone/scripts/build-sidecar-proxy.mjs.
The bridge. Webview → sidecar is one generic passthrough invoke,
burrow_command(payload), writing {"event":"burrow:command", "data":payload} to stdin for createSidecarHost. Sidecar →
webview is three ordinary stdout events — burrow:result, burrow:ask,
burrow:event — forwarded by Rust's generic handle.emit. The correlation
field is burrowRequestId, never requestId: Rust swallows any sidecar line whose
data.requestId matches a pending invoke (rationale). Everything above those
shapes is the shared link-client.ts (docs/specs/transport.md → Message
protocol).
Asks and answers. What the sidecar cannot know — a pane's name, its focus, its
xterm size — it asks over burrow:ask, and
lib/src/remote/burrow/peer-surfaces.ts answers as an ordinary answer command
naming the ask's own burrowRequestId. An ask collects one answer per window
and concatenates them: each window sees only its own Workspaces, so a directory
built from the first answer would omit every other window's panes. The
collector is keyed by which window answered, never by how many have — Rust
stamps the sending window's label on every burrow:command it forwards — so a
window answering twice can settle nothing and contributes its panes once. Rust
pushes the live window labels (burrow:windows) at setup and on every window
create and destroy, and dropping one settles the asks that window can no longer
answer; an ask in flight is only ever narrowed, since a window that opened
after it never received it. An ask naming a surfaceId goes to that Surface's
owner alone — attach and resize mutate the pane they reach, and fanned out
they ask every other window to resize one it does not hold — and Rust names the
window it delivered to (burrow:askDelivered) so the collector settles on that
one answer instead of spending the budget on windows the ask never reached. ASK_BUDGET_MS (1s) still bounds the whole fan-out,
and whatever did answer is still the best available snapshot.
An answer for an ask the bridge no longer holds invalidates the directory
rather than being dropped (docs/specs/remote-api.md → Directory).
The sidecar owns the parse, standalone's only one
(docs/specs/terminal-escapes.md → Parsing location, which owns the rules): a
pty-core data event reaches the webview as the pty:data,
terminal:semanticEvents and terminal:toolEvents the bridge emits, never
raw, and every attached Client and the app's AlertManager (§Alerts) read the
same parse. The webview pushes its
resolved terminal colours (pty_theme_colors → pty:themeColors) because this
process has no DOM; null before the first push falls a colour query through to
xterm.js, and a malformed push is ignored, never half-applied.
A remote sink must never break the local pipe. The tap sits inside
pty-core's event callback in main.js and is wrapped: a throw is logged to
stderr and every non-data pty:* event goes out either way. Inside the parse,
each sink is guarded, and so is the reply write ahead of them — a PTY that
died since the read throws — so nothing can cost the webview its pty:data.
Exit codes are
retained so a stream installed after surface resolution can replay liveness
before attach acknowledgement, and a spawn or an exit retires that PTY
generation's parser so a half-read sequence cannot splice onto the next one.
Source of truth: lib/src/host/remote/service.ts,
createSidecarSurfaceBridge / createSidecarHost in
lib/src/host/remote/sidecar-entry.ts, standalone/sidecar/main.js (the tap),
burrow_command / burrow_state_dir / pty_theme_colors in
standalone/src-tauri/src/lib.rs.
The sidecar runs the alerts' host role (docs/specs/alert.md), the same
createAlertHost VS Code's extension host runs, beside the PTYs and the parse
that feeds it: one AlertManager, every window a realm under its label
(rationale).
- Must offer every stdin line to
createSidecarHost'shandleCommandbeforemain.jsdispatches it: it owns the PTY commands the alerts must see — spawn, input, resize, kill, reap,pty:requestInit— every alert and Burrow command, the theme push. - Webview → sidecar is one passthrough,
alert_command(payload), and Rust stamps the invoking window's label on it aswindow, over any the payload claimed, exactly as onburrow_command. An unstampedalert:commandis ignored. Human input ridespty_write'suserInputinstead (docs/specs/alert.md→ Engagement). helloat adapter init ends the label's previous realm, a reload keeping the label; a label gone fromburrow:windowsends its realm too.- An await's answer carries
forWindow, the label that parked it, which Rust routes it to — never a Sessionid, which would route it to that Session's owner, nor arequestId, which Rust swallows (rationale). alert:speakcarries its Session'sid, which Rust routes it by.- Must re-send each listed Session's
alert:statebehind the answer topty:requestInit: a reloaded window and an arriving Workspace learn their rings and TODOs nowhere else. Asyncre-sends only the Sessions its window names, each to its owner, and both stores' snapshots withforWindow. - Rust passes a spawn's
options.alertthrough opaque (a_spawn_carries_its_persisted_alert_to_the_sidecar); the sidecar seeds from it (docs/specs/alert.md→ Public State) and strips it beforepty-core. pty-coreis the one source of helper status, reporting each spawn's validation and each successful promotion throughonHelper.
Source of truth: createSidecarHost in lib/src/host/remote/sidecar-entry.ts;
create in standalone/sidecar/pty-core.js; alert_command /
forward_stamped / stamped_message / pty_input_message /
pty_spawn_message in
standalone/src-tauri/src/lib.rs. Pinned by the sidecar host in
lib/src/host/remote/sidecar-entry.test.ts and
standalone/src/sidecar-adapters.test.ts.
On Windows the app carries two subsystem variants of the same node.exe,
because the sidecar and the dor CLI have opposite console requirements:
-
The sidecar must run under a GUI-subsystem node, or Win11's DefTerm handoff flashes a stray Windows Terminal window behind Dormouse (rationale).
build.rspatches the bundlednode.exeat build time (force_windows_gui_subsystem), and the sidecar's explicit piped stdio works fine under it. -
dormust run under a console-subsystem copy. A GUI-subsystem node does not attach to an inherited console, so it silently drops everythingdorprints inside a shell's ConPTY (rationale).start_sidecarderives the copy once (resolve_dor_node_path→ensure_console_subsystem_node, flipping the PE subsystem byte back, cached in app-local data and re-derived when the bundled node's size changes) and pointsDORMOUSE_NODEat it.doralways runs inside an existing pseudo-console, so that copy can never cause a stray window. -
Never leave the GUI node's directory on a pane's PATH.
cargo runputs it there for DLL resolution, so a barenodein a dev pane would get a console-less one and fail silently in both directions (rationale).start_sidecarpasses it asDORMOUSE_GUI_NODE_DIR; the sidecar strips it from each pane's PATH.
The byte-flip lives in standalone/src-tauri/src/pe_subsystem.rs, shared with
build.rs, so the load-bearing PE offsets are in one place; the mechanism is in
the comments at force_windows_gui_subsystem and resolve_dor_node_path.
Source of truth: withoutGuiNodeDir in standalone/sidecar/pty-core.js,
pinned by resolveSpawnConfig drops the GUI node directory from a pane PATH on win32 in standalone/sidecar/pty-core.test.js.
Source of truth: standalone/sidecar/main.js. Browser cleanup is pinned by standalone/sidecar/shutdown.test.js.
Shutdown (sidecar:shutdown message, stdin EOF, or SIGTERM) is idempotent and
ordered:
- Must await the browser host's cleanup under one 1.5s deadline
(
browserHost.close(), which closes Playwright's only once its lazily required host exists);docs/specs/dor-browser.mdowns the teardown contract. - Close the dor control socket.
host.dispose(): the alerts (§Alerts), then the Burrow service, dropping the relay socket and settling every outstanding ask so nothing waits on a webview that is going away.mgr.killAll()(all PTYs), thenprocess.exit(0).
A parent-PID watchdog polls every 2s and self-triggers shutdown if the Tauri
process disappears: stdin EOF is not always delivered when the host is
force-killed, and an orphaned sidecar keeps conpty.node/conpty.dll loaded and
blocks the NSIS installer (docs/specs/auto-update.md, Sidecar teardown on
Windows).
Burrow-side ordering: every quit trigger is driven through the webview quit
orchestrator (§Quit flow, which owns the teardown/install/exit sequence); Tauri's
RunEvent::Exit then runs shutdown_sidecar_and_wait as a final backstop
(harmless post-teardown — the PTY map is already empty, so killAll no-ops).
Source of truth: standalone/src/AppBar.tsx.
The AppBar is the draggable titlebar region, carrying left to right the
Workspace strip and — Windows/Linux only, since macOS gets native traffic
lights from titleBarStyle: "Overlay" and left padding instead — the window
controls (minimize / maximize / close via @tauri-apps/api/window, dimmed by
window-focus tracking). Neither a theme picker nor a shell picker belongs here:
both live in the Settings dialog at the bottom-right of the window
(docs/specs/theme.md).
The strip's gestures and appearance are docs/specs/layout.md → Workspace tabs;
its indicators are docs/specs/alert.md → Workspace union.
- Never put
data-tauri-drag-regionon a tab or anything inside one. Tauri matches that attribute on the event target alone, so a tab carrying it would drag the window instead of activating, renaming, or reordering. A dedicated spacer after the strip carries it, with a minimum width, so the window stays draggable at every tab count and the strip scrolls into what is left. onDragOutsideWindow/onDropOnOtherWindowcarry the drag past the strip's own edge (§Tear-out, and dragging between windows); the browser-dev harness supplies neither, because it has no windows.
Source of truth: WorkspaceStrip in lib/src/components/WorkspaceStrip.tsx;
createWorkspaceStripDrag in lib/src/components/workspace-strip-drag.ts.
Shell selection lives in the Settings dialog's Shell row
(lib/src/components/ShellPicker.tsx over lib/src/lib/shell-store.ts), hidden
when fewer than two shells were detected or when the host owns shell selection
itself (hostOwnsShells, VS Code). Picking one persists the choice in
localStorage under the shell's full identity, publishes it via
setDefaultShellOpts, and dispatches dormouse:new-terminal with
replaceUntouched: true, announce: true (docs/specs/layout.md → "Session
lifecycle and terminal registry", Shell selection replacement) — after dismissing
the dialog, so the replacement takes keyboard focus on the next animation frame.
Edge cases:
- A legacy path-only selection restores the first matching entry and gains the full identity on the next choice.
- Re-picking the visible fallback records that explicit choice without spawning a redundant terminal.
- Re-seeding an unchanged detected list is a no-op: it preserves an interactive
selection but also skips re-reading the persisted key (
seedShellStore's comment carries what that costs Storybook).
Source of truth: the .menu(...) builder in standalone/src-tauri/src/lib.rs.
The app replaces Tauri's default menu with a macOS-only App submenu (about /
services / hide / hide-others / quit) and a Window submenu (minimize / maximize /
close, plus a macOS-only fullscreen toggle). Must keep the fullscreen item —
it and its Ctrl+Cmd+F are the only exit from native fullscreen when AppKit does
not reveal the overlay title bar's traffic lights. No Edit submenu — its predefined Paste item binds Cmd+V natively and
would fire alongside the terminal's own DOM-level Cmd+V handling
(docs/specs/mouse-and-clipboard.md §8.2). macOS therefore delivers Cmd+C/X/V to
the webview as plain keydowns and WKWebView performs no native edit, in Dormouse's
own text fields too; JS supplies their clipboard
(docs/specs/mouse-and-clipboard.md §8.9). A new menu item must not claim a
chord the webview already handles.
Several windows, each with several Workspaces, over one sidecar
(docs/specs/glossary.md). The sidecar has no window concept, so Rust owns
the map from PTY to window and every stdout line passes through it.
The label is the window's persistence identity: main for the first window
(fixed in tauri.conf.json), ws-<n> for every later one, seeded above every
live label, every sessions/ws-*.json on disk, and retained arrival-journal
endpoints so a new window cannot claim a saved or pending identity. Journal-only
labels reserve numbers without opening windows (saved_windows and
a_retained_arrival_reserves_both_window_labels_without_opening_them in
standalone/src-tauri/src/lib.rs). standalone/scripts/tauri-conf.test.mjs pins the
label.
Every new window is cloned from app.windows[0] (window_config, then
WebviewWindowBuilder::from_config), so titleBarStyle, hiddenTitle,
dragDropEnabled, backgroundThrottling and the CSP carry across with no
second copy of any of them.
Never let a window's webview throttle or suspend in the background:
app.windows[0] sets "backgroundThrottling": "disabled" (macOS 14+; a no-op
on Windows and Linux; rationale). Pinned by every_window_disables_background_throttling in
standalone/src-tauri/src/lib.rs and standalone/scripts/tauri-conf.test.mjs.
Capabilities are split: default.json covers main and the ws-* glob,
and main-only.json scopes updater:default and core:app:allow-version to main,
which structurally enforces that the install runs in the window the walk tears
down last (docs/specs/auto-update.md). Custom commands need no capability
entry. standalone/scripts/tauri-conf.test.mjs pins both.
Rust holds the union of every window's Workspaces, since each webview's
store (lib/src/lib/workspace-store.ts) sees only its own. Each window reports
its list on every change, coalesced per microtask, and the union is broadcast as
dormouse://workspaces with a monotonic revision; a webview drops a snapshot
behind the one it holds.
- Must mint numbered ids only in Rust,
workspace-<n>off one counter, handed to a webview in blocks (workspace_reserve_ids) so a create mints synchronously. The refworkspace:<n>is the id's number, so it never renumbers and never collides across windows; an unused reservation is a gap, nothing more. - Must allow boot and creation when reservation fails, using opaque UUID
ids; log the failure. Must retain those opaque IDs and refs for their lifetime,
even after reservation recovers. Canonical refs follow
docs/specs/dor-cli.md→ "Handle Model" (lib/src/lib/workspace-store.test.ts). - Must seed the counter above every id named by a snapshot or retained arrival-journal record,
and above every id a window reports. Pinned by
an_unreadable_source_retains_the_record_without_changing_the_targetinstandalone/src-tauri/src/lib.rs. Never below 2:workspace-1is a bare Wall's only Workspace. - A
dorrequest naming a Workspace or Window routes to the window holding it (§Routing precedence). A target the registry cannot place — one no window reports, or a name two windows carry — falls through to the caller's window, which refuses a name duplicated there and otherwise resolves its own, so a local Workspace wins. A target routes as a number only when it reads asPOSITIONAL_WORKSPACE_REF(dor/src/protocol.ts);007and0are names (a_number_with_a_leading_zero_is_a_name). - Must log a rejected Workspace report and retry on the next store notification.
A stale failed attempt cannot invalidate a newer report’s cache. Pinned by
retries a failed report on the next store notification with the same final entriesanddoes not invalidate a newer same-shaped report when an older attempt failsinstandalone/src/workspace-registry.test.ts. - Must keep numbered and opaque refs consistent across Rust, the webview, and
the browser harness. Shared cases pin
refs_match_the_shared_host_grammarinstandalone/src-tauri/src/workspaces.rs,shares canonical numbered and opaque refs with Rust and the browser harnessinstandalone/src/workspace-registry.test.ts, andregistry seeds reservations above restored IDs and mirrors canonical workspace refsinstandalone/scripts/dev-agent-browser.test.mjs. Destroyedforgets the window's entries and broadcasts.
Source of truth: standalone/src-tauri/src/workspaces.rs;
saved_windows / workspace_reserve_ids / workspace_report / workspace_registry in
standalone/src-tauri/src/lib.rs; installWorkspaceRegistry in
standalone/src/workspace-registry.ts; installWorkspaceIdPool /
workspaceRefFor in lib/src/lib/workspace-store.ts. Pinned by the tests in
standalone/src-tauri/src/workspaces.rs and
an_explicit_target_routes_to_the_window_holding_it.
Showing an id is the source still consuming it until its transfer mark, otherwise its owner.
| Sidecar event | Key | Goes to |
|---|---|---|
pty:data, terminal:semanticEvents, terminal:toolEvents |
data.id |
the window showing it; after the mark, dropped until its replay, which carries the bytes and from which the receiving window re-derives the events (rationale) |
pty:exit |
data.id |
the window showing it, never suppressed |
pty:replay |
data.forWindow, then data.id |
the requesting window, including exited buffers; without an address, its owner; never suppressed |
pty:marked |
data.id |
the window showing it; the id then falls silent until its replay |
pty:list |
data.forWindow |
the window that asked |
alert:* |
data.forWindow, then data.id |
that window; else the window showing the Session; neither → every window |
dor:controlRequest |
params.workspace, params.window, data.surfaceId |
in that precedence: the window holding the named Workspace (§Workspace registry), the named window, the caller's Surface's owner; none → the focused window |
dor:controlCancel |
data.requestId |
the window its request went to; unknown → every window |
burrow:ask |
data.params.surfaceId |
its owner; a Surface with no PTY here, or an ask naming none, → every window (§Burrow service) |
| everything else | — | every window |
- Ownership is minted only in
pty_spawn, dropped bypty_killor the window going away, and reassigned by a transfer. Never dropped by an exit, nor does an exit end a transfer's marking phase: the exited pane is still shown, itsalert:statemust still reach it, and the sidecar marks an exited id like a live one (rationale;an_exited_pty_stays_owned_until_it_is_killed,an_exit_before_the_mark_leaves_the_transfer_to_finish). Minting also clears any suppression left under that id: no replay is coming for a fresh PTY.pty_killdrops the owner before the sidecar removes the Session's alert entry, so the removal's state reaches no window. - A PTY event no window owns is dropped, and the shell is reaped. An
unowned id is one whose window went away, and a broadcast would hand every
sibling state for a pane none of them shows.
Destroyedsendspty:reapfor whatever the departing window still owned — the close ack-timeout path killed nothing — and the sidecar removes those Sessions' alert entries and SIGTERMs them. The quit teardown'spty:gracefulKillremoves no entry: windows still show those PTYs (an_orphan_reap_removes_the_sessions_it_kills). - A
dorrequest naming a Surface no window owns is answered with an error —No Dormouse window owns surface '<id>'— never handed to a sibling, since acting on the wrong terminal is worse than failing. pty_request_init,pty_graceful_killandcapture_agent_recoverytarget the invoking window's own PTYs, and take no ids at all: a window tearing down must not interrupt or kill a sibling's terminals, and a set it could name is a set it could name wrong.pty_request_initfurther excludes what an arrival claims (§Arrival queue).- A
dorcancel follows its request: only the window handling it holds the subscription, watch or completion claim the cancel releases. Rust remembers which window took eachrequestIdand forgets it on the response. - Every webview listener names its own window. Tauri delivers an
emit_toevent to any listener registered with the defaultAnytarget, so a barelistenwould take every other window's traffic and make this whole table decoration (rationale).listenToWindowinstandalone/src/window-label.tsis the only caller of the event API, pinned bystandalone/scripts/window-listeners.test.mjs. - The focus order is the fallback owner for a
dorrequest naming no Surface, and the drag hit test's stand-in for a z-order the OS does not expose.
Source of truth: route / showing in standalone/src-tauri/src/routing.rs;
dispatch_sidecar_event / pty_reap_message in standalone/src-tauri/src/lib.rs.
Boot reopens every window sessions/ names, main first (already up from
the config) then each ws-<n> in numeric order, capped at
MAX_RESTORED_WINDOWS (8) with the excess logged and left on disk. An
unreadable snapshot still opens its window — the webview boots fresh, which is
a window the user can use rather than one they lost. main is focused last,
so it comes up in front. A session naming no main relaunches with a fresh,
empty main — the config creates it unconditionally — focused last, in front
of the restored ws-* windows.
Geometry is a sibling of the snapshot, sessions/<label>.geometry.json,
written through the same write_file_atomically and debounced past the flood a
window drag produces; main's box is re-applied to the window the config
created. No tauri-plugin-window-state (rationale).
The live box is cached from the window events themselves — Moved and
Resized carry it — and read from that cache by both the debounced write and
the cross-window drag hit test, which probes ~16 times a second. The platform is
asked only once per window at creation, and for the minimized and scale-factor
checks in the debounce flush.
- Never ask the platform anything while holding the rect cache. Off the main
thread
scale_factor()andis_minimized()park on the event loop, which the main thread may be driving while it waits insidewindow_at_cursorfor that same lock. The flush reads both first and hands the scale torefresh_rect, whose signature takes no window at all. - The flush slot is released in the same step as the drain. A
Movedlanding between the two was marked dirty with no thread left to write it — and that move is exactly a window's final position.
Source of truth: CachedRect / GeometryState / note_geometry /
restore_windows in standalone/src-tauri/src/lib.rs; the sequencing is pinned
by the_geometry_flush_slot_is_released_with_the_drain.
Must clear label-keyed ownership, registry, geometry, and close state in the
Destroyed arm, when Tauri has removed the window from webview_windows().
Must remove incoming arrivals from the reap before killing orphaned PTYs;
the source still holds those Sessions. Hand-backs run in a blocking worker that
reports completion on the main thread. Must defer every approved exit until all
such workers have completed, including exit requested through the quit walk
or Tauri/AppKit (every_approved_exit_path_checks_cleanup;
quit_waits_for_every_destroyed_window_handback in
standalone/src-tauri/src/quit_state.rs pins the counter). Must force an
approved exit after QUIT_PHASE_TIMEOUT_MS waiting for cleanup, logging the
timeout; a stalled disk operation or completion callback cannot trap the app
(stalled_cleanup_cannot_block_an_approved_exit_forever). The arm updates the
quit machine and sends the remaining live labels to the sidecar; the Burrow’s
ask collector must never wait for a window that cannot answer.
Source of truth: CleanupGate and WindowEvent::Destroyed in
standalone/src-tauri/src/lib.rs.
Closing a window with siblings alive ends that window alone; only the last
window's close is the quit. Rust prevents the close and emits
dormouse://window-close-requested; the webview acks (a ~2 s watchdog closes it
anyway if that listener is dead), asks about its own running work, archives
its own notes, removes its snapshot, kills the PTYs it owns, and calls back
close_window.
- A close is deliberate, so it archives and it removes the blob — geometry
and temp sibling included — and the next launch does not reopen the window
(
docs/specs/transport.md→ "The governing rule"). - It runs no agent-recovery capture: nothing is coming back.
- A cancelled close retires its watchdog's token and never reuses it: the
next close on that window is a fresh seq, so a watchdog still sleeping on the
cancelled one cannot destroy the window under the second dialog
(
a_cleared_close_never_hands_its_seq_to_the_next_request). - It confirms on a pending download as well as on running work. An approved,
downloaded update lives in this webview's memory, so closing the window throws
it away and nothing else can install it (
docs/specs/auto-update.md). - The snapshot is removed before the kill, and Rust refuses every later save
for that label, so a PTY exit's save cannot write it back. Both close paths
set that refusal — the webview's own
remove_window_session, andfinish_window_closefor the ack-timeout path, where the webview never ran at all. It is dropped when the webview is destroyed and can no longer save. close_windowis the one Rust half both endings share — a deliberate close and a window whose last Workspace moved away (§Transfer) — because what separates them is entirely what the webview did before calling it.- macOS keeps its rule: closing the last window quits.
The ack, confirm and archive gates are one shared flow with the quit
(createTeardownFlow in standalone/src/teardown-flow.ts); what differs is only
the step past them — a quit votes and waits its turn in the walk, a close tears
down at once.
Arbitration. They are two machines over one window, one dialog and one human, and a second flow is never refused in silence: an unsettled context parks its own flow and leaves its host waiting out a decision that cannot come.
| Arriving | Holder | Outcome |
|---|---|---|
| quit | a close still on its dialog | the close is cancelled (window_close_cancel); the quit takes over |
| quit | any committed flow | the quit acks and votes — this window is ending anyway, and a window that never votes holds the machine in Voting with no dialog to answer |
| close | a quit, in any state | refused at once with window_close_cancel; the window stays |
A committed flow that retreats and is then cancelled re-drives the quit it
took the vote for. archive-failed is a committed close asking a human about
notes it could not store, and declining there leaves the window standing with the
quit's own question never asked. The re-drive gates the intent once: it
re-enters the quit's request from inside the close's cancel, and the trigger
that caused it returns without gating again. A quit cancelled elsewhere
forgets what it deferred, so a later retreat cannot re-open a quit Rust has
abandoned.
A quit cancelled elsewhere drops only a quit's dialog, never this window's
own close question. The confirm store cancels any context it cannot open, as the
backstop. Both orderings are pinned by
standalone/src/teardown-arbiter.test.ts.
Source of truth: standalone/src/window-close.ts; request_window_close /
finish_window_close in standalone/src-tauri/src/lib.rs.
A Workspace moves between windows without ending anything. Nothing is archived and no process is killed: a move is not a closure.
The protocol and every failure path are §Arrival queue; what a move is:
- A window whose last Workspace left closes itself, with no confirmation, no archive and no kill: nothing ended.
- Must collapse the source only after
workspace-departedconfirms adoption, before committing its release and removing its tab or closing its Window. The pending guard spans the animation;workspace-move.test.tspins this order. Presentation isdocs/specs/layout.md→ Workspace motion. - A pane's helper Session travels with it. A helper is not a member Surface, so nothing else in the payload names it, and one left behind is a leaked shell plus a stray pane on the source's next reload. It rides directly after its source, which is what lets the target's resume re-parent it.
- An arrival whose PTYs never answer is refused, never cold-restored. A
timed-out collection is not a collection that found nothing: those shells are
still running, and restoring from the record would start a second set over
them (
docs/specs/transport.md→ "Reconnection"). - A Workspace that comes back must mount from the record it brought, never the plan it first booted with, or a fresh pane lands over the Sessions that just arrived.
Source of truth: prepareWorkspaceTransfer in
lib/src/components/wall/workspace-transfer.ts, standalone/src/workspace-move.ts,
transfer_workspace in standalone/src-tauri/src/lib.rs.
Tool transfer follows docs/specs/dor-tool.md → Persistence and hosts.
Alert state and alarm delivery follow docs/specs/alert.md → Live Workspace transfer.
A tear-out opens the window positioned so the dragged tab lands under the
cursor, at the source window's size. Its first flush writes
sessions/ws-<n>.json, and from there it is an ordinary restorable window.
Everything else is the transfer above.
An arrival is one transaction keyed by workspaceId, carrying the source's
{ workspaceId, workspace, notepad, terminalIds, allIds } — allIds naming every
member Surface, browser ones included, which is what the target hydrates notes
against — under Rust's own from / to. Rust holds the record from the
source's invoke until the target adopts the Workspace or dies, and every step
below reads that record rather than inferring itself from the suppression map.
- Source prepares the Workspace, touching nothing, and invokes
transfer_workspace/open_workspace_window. Must return preparation refusals as{ moved: false, reason }without changing ownership. OnOkit marks the Workspace transferring: the Wall stays mounted and the notes stay put, nothing is released, andgetWindowSnapshotomits it. - Rust reassigns
terminalIdsto the target, keeps routing their output to the source, and asks the sidecar to stamp apty:markedline per id; at that line the id's suppression begins, until its replay has been emitted to the target. The source serializes each buffer at its mark and invokestransfer_workspace_content, which attaches the content to the record and only then nudges the target withworkspace-arrivingcarrying nothing — or, for a tear-out, builds the new window, whose boot pulls a payload that is complete (docs/specs/transport.md→ "Transferring a Workspace"; rationale). An arrival without content is not drainable (an_arrival_is_drainable_only_once_its_content_landed). - Target drains with
take_arrivalsand, per arrival, arms its collector before callingadopt_ready(workspaceId)— the hop that removes the whole "arrived before armed" class of bug (rationale). Rust answerspty:requestInitwith that arrival's ids and no others;pty:listand eachpty:replayecho the collector's token. The target resumes over them, hydrates the notes and mounts the Workspace at the payload’s index, else the drop index. Never spawns or kills: the Sessions' alert state never left the sidecar, whose answer to thatpty:requestInitre-sends it (§Alerts;docs/specs/alert.md→ Live Workspace transfer). - Target adopted invokes
adopt_done(workspaceId). Rust retires the record, clears pending marks and suppression so live output routes to the target, and emitsworkspace-departedfor that Workspace alone to its own source. - Source commits on
workspace-departed: releases every Session (never kills one), drops the notes and the helper, closes the Workspace, and closes the window if it was the last one (§Transfer).
- Nothing is released before the target has adopted it. The target can refuse the arrival or close before taking it, and a Workspace released at the invoke had no Sessions and no window that owned them.
- A transferring Workspace is in no snapshot its source writes, and neither
end's teardown kills or interrupts its shells. They belong to the target by
ownership from the invoke, and the target's
pty_graceful_killandcapture_agent_recoveryexclude every id an arrival claims (boot_list_ids), since the source is still showing them. A quit or a close in the gap would otherwise persist the same Workspace in two windows, or kill it under the source. - Must await
adopt_donebefore installing a torn-out Window; refusal releases its resumed Sessions and notes and boots fresh. - A refused
adopt_doneunwinds the mount. TheARRIVAL_MAXwatchdog has already handed the shells back and the source kept the Workspace, so the target releases its Sessions (never kills them), drops the notes, and closes the Workspace rather than leaving it live and persisted in two windows. Must unwind from the received payload without preparing another move. - Must remove this window's unmounted semantic and Activity state when arrival
collection times out, and kill nothing: the source goes on showing those
Sessions. Source of truth:
discardArrivalinstandalone/src/workspace-move.ts. - A refused arrival hands the shells back. The target's
adopt_failed(aplanArrivaltimeout, a missing list, a mount error) and a targetDestroyedwith the arrival still queued both returnterminalIdsto the source unsuppressed (a_target_closing_mid_arrival_hands_its_shells_back), drop the record, and emitworkspace-arrival-failed; the source clears transferring and the Workspace is simply still there. With both ends gone the shells are reaped rather than left owned by a dead label. - Must change transfer ownership and source routing under one routing lock,
so output before the mark always reaches the source
(
transfer_ownership_and_source_routing_change_together). - Must reject a repeated move while that Workspace is in flight, preserving
the first attempt’s content and recovery state. Async continuations act only
on their own attempt (
keeps the first move recoverable when the same tab is dropped twiceinstandalone/src/workspace-move.test.ts). Must await the host's hand-back when source content capture fails, keeping the Workspace in flight until routing returns (recovers serialization failure through host hand-back before another move). - A hand-back replays what the marked ids missed. From an id's mark to the
hand-back every byte went to the target, or nowhere, so
hand_back_arrivalreturns each id the content marked to the source suppressed and asks the sidecar foroutputSince(mark)scoped to the source (requestIdhandback-<workspaceId>); that replay lifts the suppression and lands in the existing xterms (acceptHandBackReplay). Must record source cuts atpty:marked, retaining them through target replay and natural PTY exit until settlement, and carry replay ids in the failure event; content submission and the source invoke reply may both still be pending. Must discard cuts on explicit kill; an exited PTY is still owned (§Routing), so a hand-back returns it to the source like a live one (a_pty_exit_keeps_its_cut_until_the_arrival_settles). Must apply a handed-back PTY’s exit status after its replay, leaving its existing pane dead with no running command (settles the replayed command when a marked buffer belongs to an exited PTYinstandalone/src/tauri-adapter.test.ts). An id the sidecar never stamped goes straight back: a whole-buffer replay would paint it twice (a_hand_back_replays_only_the_marked_ids; rationale). planArrivalnever throws intobootstrap(). A refused sole arrival on the boot path renders a fresh one-pane Workspace, never a blank window.take_arrivalsdoes not consume. The record settles atadopt_done, so a webview that drains at boot and again when its listener is installed cannot lose a Workspace to a drain that happened too early; the webview dedupes by id.- A window with a snapshot boots as itself, mounting whatever was dropped on it mid-boot over the restore rather than instead of it.
AWAITING_REPLAY_MAXfails open only for suppressions no arrival claims. A cold boot slower than it would otherwise have a real arrival's shells unsilenced into a window that has not resumed them yet.- A boot's
pty_request_initexcludes every id an arrival claims. Ownership moves at the invoke, so those shells would otherwise be listed as top-level panes beside the Workspace about to mount them. begin_arrivalrecords the arrival insessions/arrivals.json— a JSON array of{ workspaceId, from, to, workspace, settled }, never an entry in either window's snapshot (rationale); the tombstone rules below readsettled. Must retain an adopted record until target and source snapshots both reflect the move, marking it settled atadopt_doneand checking after eachsave_sessionor source-window close (adoption_keeps_the_journal_until_both_snapshots_are_durable). Must reverse the durable destination on hand-back and retain the record until both snapshots reflect the return (a_hand_back_is_recovered_in_the_source_before_its_next_flush). Must tombstone settled arrivals into a deliberately closed Window until both snapshots omit them, including during boot recovery (closing_an_adopted_target_never_resurrects_either_copy). A record left at boot is merged into its recorded destination beforerestore_windows— a tear-out target gets a file holding just it, active; a source snapshot still naming the id loses it, an emptied one is removed — so the Workspace restores once, with fresh shells, and successful records are deleted; must retain failed records for retry and roll back the target if trimming the source fails. Must preserve a settled arrival’s newer target record during boot recovery (an_arrival_record_round_trips_until_it_is_forgotten,a_leftover_arrival_boots_into_an_existing_target_snapshot,a_leftover_arrival_boots_into_a_tear_out_targets_new_snapshot,a_leftover_arrival_leaves_a_source_snapshot_that_still_names_it,the_arrivals_file_is_gone_after_the_boot_merge).- Must run journal I/O and its lock waits off the main thread, including
transfer/settlement/close commands and destroyed-window cleanup
(
blocking_commands_run_off_the_main_thread). - An arrival unadopted after
ARRIVAL_MAXis handed back by a watchdog armed atbegin_arrival, retiring only the record it was armed for (queued_at): a target alive but wedged never reachesadopt_failedorDestroyed, and the source would otherwise stay transferring with its shells silent for good (an_expiry_retires_only_the_record_it_was_armed_for).
Source of truth: Arrival / sweep_awaiting / expire_arrival / boot_list_ids in
standalone/src-tauri/src/routing.rs; begin_arrival / adopt_ready /
adopt_done / adopt_failed / hand_back_arrival / record_arrival_on_disk /
forget_arrival_on_disk / restore_arrivals in
standalone/src-tauri/src/lib.rs; standalone/src/workspace-move.ts;
markWorkspaceTransferring in lib/src/lib/window-session-aggregator.ts.
Pinned by standalone/src/workspace-move.test.ts, the disk tests in
standalone/src-tauri/src/lib.rs, and the arrival tests in
standalone/src-tauri/src/routing.rs.
A pointer captured on a strip tab keeps delivering pointermove and
pointerup outside the window, so the gesture stays the webview's and the
host is only asked where the cursor is (rationale). Past the strip edge the host
throttles a window_at_cursor probe (~60 ms) and lights a drop caret in
whichever window is under it; the release transfers there, or tears out when
the cursor is over no window or over this window outside its own strip.
Among windows containing the cursor the most recently focused wins — the OS exposes no z-order — and the caret is what makes a wrong guess visible before the release. The target decides the drop index: it alone knows its own tabs.
- The throttle probes the trailing edge too: the leading one never sees where the pointer came to rest, which is the position the drop uses.
- A probe answering after the gesture is ignored, or it re-lights a caret in a window the drag has already left, where it would burn until the next one.
- The caret clears when the pointer comes back over its own strip, where the live reorder takes the gesture back.
- Never assume in-range pointer coordinates. A captured pointer keeps reporting client coordinates past the window's edges and negative rather than clamping (rationale).
Source of truth: window_at in standalone/src-tauri/src/routing.rs;
standalone/src/workspace-drag.ts; standalone/src/workspace-drop-caret.ts.
One PersistedWindow per window, restored on the next launch
(docs/specs/transport.md → "The governing rule", which owns the rule). The
webview owns the composition: each Workspace's Wall publishes its
PersistedSession to the Window aggregator, whose one debounced writer is
TauriAdapter.saveWindowState (docs/specs/transport.md → "Persisted session
types"). getWindowState is the boot reader, and it parses the blob once —
the store behind it is a boot-seeded cache and every later write comes through
saveWindowState. The bare-Session saveState / getState pair answers nothing
on either standalone adapter, because the stored blob is a Window and every
shared reader of getState wants a Session. Source of truth: windowStateSlot in
standalone/src/window-recovery.ts.
Boot restores per Workspace off one live-PTY list. restoreWindowOrFresh
seeds the aggregator, installs the Workspaces and the
writer, then runs one collectLivePtys and plans each Workspace from its own saved
record. Reload and relaunch are the same path with a different list: nothing wires
shutdown() to beforeunload, so a reload's PTYs are still there and partition by
saved pane id, while a relaunch's list is empty and every Workspace cold-restores
into fresh shells at its saved cwds.
- A live PTY no saved Workspace names goes to the active Workspace, which is the only one that can hold it — except a helper, which is never a persisted pane and so is always unnamed. A helper is routed to the Workspace holding its source, resolving parents across the whole live list before any slicing; one landing anywhere else is resumed as an ordinary pane and its stray id voids that Workspace's whole saved layout.
- A restore that throws degrades to a fresh Window and overwrites the blob. Installing the Workspaces is the step that can reject a stored blob outright, and a throw at boot would leave nothing rendered, on this launch and every later one.
- A fresh Window mints its first Workspace's id, rather than taking the lib's
DEFAULT_WORKSPACE_ID, which every window would otherwise start on: a second window opened after the first one closed would write a blob naming a Workspace id already live in another window's blob, and the next launch would meet the same id twice and refuse the whole restore. A bare Wall — one Window's whole application — keeps the default id, fromwrapSessionInWindow's default parameter inlib/src/lib/session-types.ts.
Source of truth: restoreWindowOrFresh / routeUnownedPtys in
standalone/src/window-restore.ts.
Every Workspace saving at the same moment costs one pty_get_cwds. A flush
fans out to every Wall at once, so both adapters put their cwd probe behind
coalesceCwds (standalone/src/coalesce-cwds.ts), which folds the calls arriving
in one microtask into a single invoke — the same batching getCwdsForPids already
does one layer down, extended across the callers.
A listing that spans terminals costs one pty_get_open_ports_many. Both
adapters carry it, and the sidecar answers every id from one process-table read
and one socket scan (getOpenPortsForPids) — the scans are synchronous on its
only event loop, so a dor list --ports across Workspaces must not multiply them
by its row count (docs/specs/dor-cli.md → "Current Implemented Commands").
Must follow docs/specs/transport.md → "Port scan deadlines" for both port commands.
The macOS and Windows socket scans run under
OPEN_PORT_TIMEOUT_MS + OPEN_PORT_TIMEOUT_PER_ID_MS × ids, and the command waits
that plus the process-table read's OPEN_PORT_TIMEOUT_MS and IPC margin — one terminal's cap
never bounds the whole Window (open_ports_many_timeout in
standalone/src-tauri/src/lib.rs). A macOS socket scan keeps the rows lsof
printed before a non-zero exit — a pid gone mid-batch would otherwise empty
every terminal's answer, as getCwdsForPids already guards.
Nothing is deleted at boot but orphaned session temp files
(docs/specs/transport.md → "Retiring the transcripts already on disk"). The
harness mirrors this answer (§Standalone browser-dev harness), in
localStorage and a per-run temp state directory.
Never back the session blob with WebKit localStorage — a WAL that grows
without bound (rationale). The blob rides the SessionKeyValueStore seam instead,
over the Rust-backed standalone/src/tauri-session-store.ts. Theme selection
still persists on localStorage (docs/specs/theme.md) — tiny and rarely
written.
Rust file store. save_session(window, state) / load_session(window)
(lib.rs) persist the blob as one atomic file per Tauri window,
<state root>/sessions/<label>.json:
- The label is sanitized so it cannot escape the directory.
- Temp-then-rename, so a crash cannot truncate the previous snapshot. The temp file is fsynced before the rename and, on unix only, the sessions directory after it (rationale).
- Window identity is implicit: each command keys by the invoking
tauri::Window'slabel(), so the frontend stays window-agnostic and every window (ws-2, …) persists to its own file rather than rewriting a sibling's. - No WAL to grow, and rewriting the same path bounds the on-disk size to one blob (rationale).
- The writer removes its own temp file on every error path, so only a crash can leave one behind.
- A per-window close removes the blob, its temp sibling and its geometry (§Per-window close); nothing else deletes a snapshot but the boot merge (§Arrival queue).
- Must sweep orphan session temp files once at boot in the active sessions
directory and, for debug builds, the legacy
<app_data_dir>/sessionsdirectory.SESSION_TEMP_SUFFIXis pinned against the writer bysession_temp_suffix_matches_what_the_writer_leaves. Transcript migration followsdocs/specs/transport.md→ "Retiring the transcripts already on disk".
Must use <app_data_dir>/dev as the debug state root and <app_data_dir> for
release builds (rationale). app_data_dir() follows the Tauri identifier;
pnpm dev:standalone already supplies a distinct per-worktree identifier
(§Build and development). The subtree also isolates session/recovery state when
raw Tauri dev bypasses that wrapper. Must keep the notepad archive and Burrow
state directly under that identifier’s app_data_dir.
Source of truth: state_root_from and sweep_session_roots in
standalone/src-tauri/src/lib.rs.
Rust passes the sidecar its two directories by environment, each created
owner-only first and each an empty string when it could not be:
DORMOUSE_STATE_DIR (the Burrow store, app_data_dir) and
DORMOUSE_RECOVERY_DIR (the recovery record, the state root — so a dev run's
record cannot reach the installed app). The browser-dev harness sets both to its
own per-run temp directory. Source of truth: recovery_state_dir in
standalone/src-tauri/src/lib.rs.
Must keep the notepad archive outside sessions/ and the debug subtree —
<app_data_dir>/notepad-archive-v1.json, its own compare-and-swap commands and
its own lifetime, so the session sweep never reaches it
(docs/specs/notepad.md -> "Standalone quit"). Both stores write through the one
write_file_atomically.
Must restrict the session store to the owner before any bytes are written
(docs/specs/security-local.md -> "Persisted state"; rationale).
restrict_to_owner sets 0700 on the directory and 0600 on the temp file
first, since the rename preserves its mode; on Windows, where a unix mode is a
silent no-op, it applies a protected single-entry DACL instead (mechanism in its
doc comment). burrow_state_dir locks the sidecar's state directory with the
same call and relies on it reaching a file that already existed, which
restrict_to_owner_leaves_one_owner_only_ace pins (rationale). Must abort a snapshot save if either permission change fails, preserving the previous snapshot. The state-directory call remains nonfatal and logs a WARNING naming the path. Pinned by session_permission_failures_preserve_previous_snapshot_without_writing_bytes and session_write_tightens_directory_and_existing_temp_file.
Boot + the synchronous-read constraint. getState() is synchronous —
cold-start restore reads it before React mounts — but a Tauri invoke is async, so
TauriSessionStore keeps an in-memory write-through cache: TauriAdapter.init()
hydrates it from load_session (§Boot sequence), getItem reads it
synchronously, setItem updates it and forwards to save_session asynchronously,
coalescing bursts to at most one in-flight write (latest value wins). Mirrors the
VS Code adapter's host-injected seed (docs/specs/vscode.md).
Dirty tracking is shared frontend behavior (docs/specs/layout.md → Session persistence).
Must skip an unchanged store write only when that value is queued or saved.
An idle failed write remains retryable even though the read cache already holds
its value; pinned by tauri-session-store.test.ts.
Source of truth: TauriSessionStore.setItem in standalone/src/tauri-session-store.ts.
Must await the store pipeline before exiting, under the quit timeout
(§Quit flow; rationale). drainSessionSaves awaits TauriSessionStore.drain(),
which resolves when the write pipeline goes idle, including after a rejected
write; failed writes are logged. With Session persistence disabled, the pipeline
is already idle. Drain is a completion barrier, not a guarantee of successful
disk persistence.
Must run shared capture and record ownership in the sidecar, under docs/compatible-agents.md. Rust bridges capture_agent_recovery and take_recovery_commands asynchronously. Must answer both through respondAsync, returning { error } on throws rather than stranding the invoke.
- Must store the record at
<state root>/recovery.json. - Must claim the saved Window's pane ids across all its Workspaces from
TauriAdapter.init(). Init starts the claim without awaiting it; boot awaitsrecoveryReadyonly on the branch that can cold-restore. - Must restrict capture to the closing Window's eligible PTYs, following Teardown ordering and Transfer.
- Never capture in the browser-dev harness; it claims records but reloads resume live PTYs.
Source of truth: pty:captureRecovery / recovery:take in standalone/sidecar/main.js; TauriAdapter in standalone/src/tauri-adapter.ts.
Source of truth: standalone/src-tauri/src/lib.rs (QuitState, request_quit,
the quit_ack / quit_progress / quit_cancel / quit_proceed commands, the CloseRequested /
ExitRequested arms) and standalone/src/quit.ts (the webview orchestrator).
Must intercept every quit trigger in Rust and run the webview teardown before exiting (rationale).
Every window votes before any window is torn down. A cancel in the last
window could otherwise not put back the windows already destroyed
(rationale). Rust asks them all, and only once they all agree walks them one
teardown at a time. Source of truth: QuitMachine in
standalone/src-tauri/src/quit_state.rs.
| Phase | What happens |
|---|---|
| Voting | every window acks, archives its own notes, asks about its own running work, and calls quit_vote — or quit_cancel, which tells every window and destroys nothing |
| Walking | the dormouse://quit-teardown event reaches one window at a time, main last; each hands on with quit_window_done, and the last one installs and calls quit_proceed |
- A cancel is refused once the walk starts: the first window is already gone.
- A window that leaves outside the flow is forgotten, so its vote is never
waited on and the walk advances past it. A flow that runs out of windows
exits rather than leaving a process with none, and so does a trigger that
finds none: parked in
Votingit would have no window to vote and refuse every later exit. - A quit keeps every window's snapshot on disk — that is what a relaunch restores from, and the whole difference from a per-window close. A Workspace in transfer is in no snapshot until its target publishes it, and neither end's teardown kills its shells (§Arrival queue). A quit mid-transfer restores it at most once: from the target once it has published it, or from a source handed it back because the target was destroyed first; a source torn down before its target adopts leaves it in no snapshot.
Every trigger funnels into request_quit(app):
| Arm | Fired by | Guard |
|---|---|---|
WindowEvent::CloseRequested |
the window close button | api.prevent_close() unless the quit is approved. Refused outright while the walk is running: a window taken out from under its own teardown leaves the walk emitting to a dead label. Only the last window's close is a quit; every other one is a per-window close (§Windows) |
RunEvent::ExitRequested |
a window-level exit request | api.prevent_exit() unless approved and cleared by the bounded cleanup gate (§What a window's Destroyed settles). The event's code is ignored |
| the app menu's Quit item | the menu, and its Cmd+Q accelerator |
a custom MenuItem, never PredefinedMenuItem::quit, whose event calls request_quit; muda wires the predefined one straight to AppKit's terminate: (macOS; rationale) |
applicationShouldTerminate: |
the Dock's Quit, osascript, logout, restart |
spliced onto tao's live delegate class at Ready, answering NSTerminateCancel and starting the flow; a terminate the OS re-sends after approval gets NSTerminateNow once cleared by the bounded cleanup gate (§What a window's Destroyed settles; macOS; rationale). The flow's own app.exit(0) never arrives here |
the quit_restart command |
the update notice's "Restart now", dor app restart |
request_quit with the restart intent (§Restart) |
Source of truth: standalone/src-tauri/src/macos_terminate.rs.
The ack / vote / progress / proceed / cancel protocol. request_quit clears
every window's acked, bumps seq, and broadcasts dormouse://quit-requested.
It must leave a walk in flight alone — a repeat
trigger fired mid-teardown must not send the machine back to voting, or the fresh
watchdog drops into the unbounded vote wait and stops bounding the teardown that
is running. It must keep every vote already cast: a committed window answers
the repeat with an ack alone, so clearing its vote would hold the machine in
voting with no dialog left to answer (a_repeat_trigger_while_voting_keeps_the_votes_already_cast).
Each window's orchestrator (registered by initQuitFlow, Tauri-only) responds:
- Always
quit_ackfirst (fire-and-catch), so phase 1 stands down even if the orchestrator then dedupes the event out. - Archive the notepads, bounded at 3 s, before the first
quit_progress— the last point at which a failure may still ask a question, since teardown may not (docs/specs/notepad.md-> "Standalone quit"). Failure or timeout leaves the quit pending: its dialog is another human decision, which phase 2 is unbounded for. quit_votewhen this window is ready — immediately on an all-idle quit, or after the user confirms and the archive gate passes. A vote is not a teardown: nothing anywhere may be destroyed until every window has agreed.quit_progresswhen its ownquit-teardownarrives, bumping aprogresscounter. Sent again at the install phase boundary.- The teardown (below), then
quit_window_done— orquit_proceedin the last window, which setsapprovedand callsapp.exit(0). - A confirmation-dialog cancel (below), or a Cancel on the archive-failure
dialog, calls
quit_cancel— bumpsseq, invalidating the live watchdog, tells every window to drop its dialog, and leaves the app running. Nothing else cancels: a Quit anyway must reach teardown with the watchdog still armed.
A cloned-AppHandle watchdog thread keeps quit bounded against a dead or
wedged webview, in three phases:
| Phase | State | Budget |
|---|---|---|
| 1 — ack | some window has not acked | ~2 s; a listener is dead ⇒ log and app.exit(0) |
| 2 — voting | acked, no window walking yet | none — a window may be parked on its confirmation dialog waiting on a human, who must never be force-quit out from under it. Only quit_proceed (approved) or quit_cancel/repeat-trigger (seq bump) ends the wait |
| 3 — walking | one window tearing down | per phase, ~14 s, refreshed by its quit_progress bumps and by the walk advancing to the next window, so each phase and each window gets its own budget; no progress for the budget ⇒ log and exit |
Approved exits from these watchdogs also pass the bounded cleanup gate
(§What a window’s Destroyed settles).
Phase 3's budget comfortably exceeds the webview's own teardown ceiling. Each
watchdog captures the seq it was spawned for, so a repeated quit trigger —
which bumps seq, spawns a fresh watchdog and re-emits — leaves the stale one to
exit without acting: the user's escape hatch if the webview acked then wedged.
Must use the Workspace typed-letter confirmation for window close and quit, naming every Workspace in the window, including hidden ones. The gate opens for running Sessions; an all-idle quit proceeds without a prompt. A quit's prompt says supported agent sessions resume when Dormouse reopens (§Agent recovery); a window close's never does, since it runs no capture. Window close also asks before discarding a pending download (§Per-window close).
- Must keep one letter per request across Workspace switches and repeat quit
triggers.
WorkspaceKillConfirmsupplies the key rule defined indocs/specs/layout.md→ Workspaces. - Must leave the prompt open when its running count reaches zero.
- Must cancel an unconfirmed request if Workspace membership changes, so a transfer cannot leave approval addressing a different set. Switching and reordering preserve it.
- Must clear competing Workspace close/move/rename prompts when opening the gate, preserving transfer guards. A full-window overlay covers the strip; its keyboard lease lasts through confirmation, archive failure, and teardown.
- Must exclude transfers from host teardown. Queue app quit while any arrival
is pending, and native window close while that window is an arrival endpoint;
automatically retry through normal confirmation once the relevant transfer
settles. Repeated requests coalesce; quit supersedes queued closes, and
cancellation or window destruction retires applicable queued requests.
Must refuse new transfers while app quit or either endpoint’s close is
queued, confirming, or tearing down. Both are decided under one lock
(
ArrivalQueue). Pinned bydeferred_quit_and_close_requests_wait_for_membership_then_run_onceinstandalone/src-tauri/src/quit_state.rs, andtransfers_cannot_change_membership_after_close_or_quit_confirmation_beginsandbegin_arrival_admits_under_the_arrivals_lock_before_queueinginstandalone/src-tauri/src/lib.rs. - Must collect votes before killing any window's Sessions. Confirmation consumes its callback once; a noninteractive full-window progress overlay remains through voting, archive, recovery, persistence, and teardown. All-idle requests acquire the same overlay and keyboard lease before archiving or voting.
- Must retain the separate note-loss decision on archive failure, with Cancel focused by default; process-kill approval never approves discarding notes.
Source of truth: openQuitConfirm in standalone/src/quit-confirm-store.ts;
WorkspaceTeardownModalHost in standalone/src/WorkspaceTeardownModal.tsx;
WorkspaceKillConfirm in lib/src/components/WorkspaceKillConfirm.tsx.
Pinned by holds an all-idle window through archiving and voting until another window cancels
in standalone/src/quit.test.ts,
holds one keyboard lease through commitment and releases it on matching dismissal
in standalone/src/quit-confirm-store.test.ts,
blocks the entire window during confirmation and while waiting for other votes
and retains the letter across host remount and names hidden workspaces in
standalone/src/WorkspaceTeardownModal.test.ts, and
ignores repeat Cmd+Q, other modified letters, and bare modifiers in
lib/src/components/WorkspaceKillConfirm.test.tsx.
runQuitTeardown. Every step is individually
bounded so a stall cannot wedge quit, and the whole is wrapped in a ceiling
derived from the sum of those bounds, never a literal — one below the sum
aborts the final save of a slow teardown instead of guarding a wedged one. The two
steps that reach the sidecar cost their own budget plus Rust's round-trip margin,
so both terms count (QUIT_TEARDOWN_CEILING_MS in standalone/src/quit.ts; pinned by
lib/src/lib/mirrored-constants.test.ts). The notepad
archive is not a step here: it runs ahead of quit_progress precisely because
teardown's rule below holds — no failing step prevents exit — and archiving must be
able to stop the quit (docs/specs/notepad.md -> "Standalone quit"):
captureAgentRecovery— this window's PTYs, and first, because an agent's resume invocation exists only between the interrupt and the kill and is the one thing here that cannot be reconstructed afterwards (§Agent recovery). A failed capture must not abort the steps behind it.requestSessionFlush— save while PTYs are alive, so CWDs are fresh.gracefulKillPtys— SIGTERM this window's PTYs, resolving early once all exit and their final output has had a grace tick to reach the webview (§Rust ↔ sidecar bridge).requestSessionFlush({ probeCwd: false })— flush the post-exit Session state. Must skip the cwd probe and retain the previously persisted CWD: every probe against a dead PTY answers null and is discarded for that value anyway (docs/specs/transport.md→ "Persisted session types").flushWindowSession— the Workspaces' records become one Window blob (docs/specs/transport.md→ "Persisted session types"); a debounce timer still pending at exit would otherwise lose the final save. Bounded like the rest, its term in the ceiling, though both writers are synchronous today.drainSessionSaves— await the store pipeline becoming idle or its timeout (§Persistence).- In the last window only, if an update is pending, a fresh
quit_progresstheninstallPendingUpdate()— strictly after the completed save (docs/specs/auto-update.md); Rust's phase-3 watchdog backstops a hung installer. - Always
quit_window_done, orquit_proceedin the last window (infinally, even on throw/timeout).
Windows note. node-pty's kill('SIGTERM') is an immediate kill under ConPTY,
so step 3 terminates promptly there, retaining the same final-output grace tick.
Dev-mode note. The browser-dev harness has no Rust quit interception, and the flow never initializes there (§Boot sequence, step 6).
A restart is a quit that relaunches: the same vote, confirmation and teardown, with only the exit changed.
- The trigger that leaves
Idleunapproved fixes the intent — whether to relaunch, and the requesting Surface. A repeat trigger keeps it, a cancel clears it, and a quit queued behind a transfer carries it (ArrivalQueue).quit_restartanswers whether the quit it landed in relaunches —falsewhen it joined a plain quit. - A restart asks exactly what a quit asks (§Quit flow), so
dormouse://quit-requestedcarries only{ requester }, never whether the quit relaunches. - The requester never counts as running work in the restart's confirmation
(
quitRunningWork): it is thedor app restartstill waiting on its answer. Every other running Session still asks, and Workspace and window closes count everything. - Every approved exit stays
app.exit(0); the relaunch runs inRunEvent::Exit, aftershutdown_sidecar_and_wait, ascleanup_before_exitthentauri::process::restart, which on macOS re-readsInfo.plist, so a bundle replaced in place starts as the new version. NeverAppHandle::request_restart(rationale). - A terminate the OS re-sends after approval clears the intent, so logout never relaunches.
quit_restartrefuses a debug build and an executabletauri::process::current_binarycannot resolve, whererestartwould exit without relaunching (rationale).- A Windows quit holding an update relaunches by its installer instead
(
docs/specs/auto-update.md→ "Platform behavior at quit").
Source of truth: quit_restart, relaunch_requested and the RunEvent::Exit
arm in standalone/src-tauri/src/lib.rs; QuitIntent, QuitMachine::request and
ArrivalQueue::defer_quit in standalone/src-tauri/src/quit_state.rs;
quitRunningWork in standalone/src/quit-confirm-store.ts. Pinned by the
restart-intent tests in standalone/src-tauri/src/quit_state.rs,
a_restart_relaunches_only_after_the_sidecar_shuts_down in
standalone/src-tauri/src/lib.rs, and the requester and copy cases in
standalone/src/quit.test.ts and standalone/src/WorkspaceTeardownModal.test.ts.
The WindowEvent::DragDrop handler in lib.rs emits the dropped paths as
dormouse://files-dropped; TauriAdapter fans that out to onFilesDropped for
the Wall. The whole path is inert today: tauri.conf.json sets
dragDropEnabled: false, so the native handler never fires. Behavior and
status: docs/specs/mouse-and-clipboard.md (§8.7 Drag-to-Paste).
Windows release builds use the GUI subsystem, so nothing streams to a launching
terminal. The Rust backend appends sidecar stderr, malformed stdout diagnostics,
and its own diagnostics to a log file: %LOCALAPPDATA%\Dormouse Terminal\dormouse.log on
Windows, $TMPDIR/dormouse.log elsewhere, overridable via DORMOUSE_LOG_FILE.
Must bound updater debug-log reads to the final 10,000 bytes, dropping a
leading partial UTF-8 character. read_update_log runs off the main thread. The
log resets at app startup and grows during the run.
Source of truth: init_log / read_update_log in standalone/src-tauri/src/lib.rs;
read_utf8_tail in standalone/src-tauri/src/log_tail.rs, pinned by
reads_only_the_budget_even_when_the_log_grows.
Source of truth: standalone/package.json (package scripts),
standalone/src-tauri/tauri.conf.json (build, bundle.resources), and the root
package.json for the dev:standalone and innerdogfood orchestration;
runDev in standalone/scripts/dev-standalone.mjs;
standalone/scripts/clean-dev-sidecar.mjs.
stage=stage:dor-cli(build + stage the dor CLI,docs/specs/dor-cli.md) plusstage:sidecar-proxy(build-sidecar-proxy.mjsbundles thelib/src/host/sources into the sidecar.cjsfiles).- The
tauriscript stages, then runsstandalone/scripts/tauri.mjs, which delegates to the Tauri CLI — exceptdev, which it routes throughrunDevbelow.build-sidecar-proxy.mjsbakesDORMOUSE_REMOTE_CONNECT_SRCinto the sidecar's Burrow bundle. The webview CSP contains no relay sources, pinned bystandalone/scripts/tauri-conf.test.mjs(docs/specs/relay.md, "Where a Burrow may reach a Relay"). - The Tauri bundle ships the whole sidecar via the
../sidecar/**/*resources glob — including node-pty's prebuilds + bundled ConPTY and the shell-integration scripts (docs/specs/terminal-escapes.md). - Must start native dev with Vite on an OS-assigned loopback port and pass its
bound URL to Tauri, with
beforeDevCommanddisabled in a per-run overlay. Directpnpm exec tauri devkeepstauri.conf.json's defaults. HMR shares Vite's listener, including whenTAURI_DEV_HOSTis inherited; inherited browser-dev settings never enable browser mode. - May pin Vite with
DORMOUSE_BROWSER_DEV_VITE_PORT; an occupied port must fail without stopping its owner. - Must close Vite and the owned Tauri process tree on startup failure, exit,
SIGINT, SIGTERM or SIGHUP.
POSIX shutdown escalates to SIGKILL after three seconds; Windows terminates
the owned tree with
taskkill /T /F. - Must key the native dev Tauri identifier to the canonical worktree path, so
parallel worktrees and the installed app never share app data. The default log is
<worktree>/standalone/src-tauri/target/dormouse-dev.log, overridden byDORMOUSE_LOG_FILE. Pinned bystandalone/scripts/dev-standalone.test.mjs. - Must limit Windows pre-dev cleanup to sidecars executing from this worktree's default debug directory; never kill a listener by port.
- Must re-stage and restart after changing sidecar, staged CLI, or bundled host sources. Frontend edits hot-reload; Tauri watches Rust.
pnpm innerdogfoodruns the sidecar + webview in a normal browser via the browser-dev harness instead of the Tauri WebView (below).
pnpm innerdogfood starts the standalone sidecar directly, a localhost-only HTTP bridge, and Vite with VITE_DORMOUSE_BROWSER_DEV_HOST, then opens the app URL in an agent-browser session. The browser build uses BrowserSidecarAdapter instead of TauriAdapter whenever that env var is present.
- Must bind OS-assigned ports for Vite and the HTTP bridge by default, as native dev does; only a direct
pnpm exec tauri devkeepstauri.conf.json's1420(above). - Must derive the default browser key from the canonical worktree path, stable across restarts. Must open through
dor abwhenDORMOUSE_SURFACE_IDis set, otherwise throughagent-browser; print the actual app URL, the browser identity it passed, and the command to drive it. Must print a--keyas a key, never as a session: only the Workspace that will hold the browser can namespace one (docs/specs/dor-browser.md→ "Managed identity"). Inside Dormouse,dor ensure -- pnpm innerdogfoodstarts and opens the harness. - May pin ports with
DORMOUSE_BROWSER_DEV_VITE_PORT/DORMOUSE_BROWSER_DEV_HOST_PORTand the session withDORMOUSE_BROWSER_DEV_AB_SESSION. An occupied pinned port fails startup;0requests an OS-assigned port. Explicit overrides are the caller's isolation responsibility. - Must await Vite's own listener before opening the browser and use the actual ports for bridge authentication and CORS. Must close the bridge and Vite and terminate owned sidecar and browser-launch children on startup failure or shutdown, escalating to SIGKILL after three seconds. Pinned by
standalone/scripts/dev-agent-browser.test.mjs.
The bridge is a transport shim over the same sidecar protocol, not a second PTY implementation: fire-and-forget commands POST /__dormouse_dev_host/send, request/response commands POST /__dormouse_dev_host/invoke, host→webview events as SSE on GET /__dormouse_dev_host/events, and browser console output mirrored to POST /__dormouse_dev_host/console so one terminal shows sidecar, Vite, and in-browser logs together. The Burrow rides it too, on the message names in docs/specs/transport.md → "Message protocol", so the harness runs a real Burrow against a per-run temp state directory (§Burrow service).
The harness must keep logging the Burrow state directory in a form the pairing walkthrough parses, which is how the walkthrough records that path before enrollment; pinned by lib/src/lib/mirrored-constants.test.ts.
The bridge is authenticated, and loopback is not what makes it safe — it dispatches pty_spawn with caller-supplied shell, args, cwd and env, so reaching it is arbitrary command execution as the developer. Four rules hold before routing and before any body read, each argued in dev-host-guard.mjs's comments: every request carries ?t=<token>, a per-run credential baked into the VITE_DORMOUSE_BROWSER_DEV_HOST URL that BrowserSidecarHost.url() alone attaches and that is never the dor control-API controlToken (rationale); Host must be 127.0.0.1:<port> or localhost:<port>; non-GET requests must be application/json; and access-control-allow-origin names the Vite origin exactly, never * (rationale), on every response including the SSE stream.
A CORS preflight is the one carve-out: an OPTIONS answers 204 carrying those headers before the token check, since it can never present the non-GET content type the gate demands. Any other unauthorized caller gets the same 404 not found as an unknown path, so the port does not identify itself. The harness prints the token and a ready-made curl on startup.
The harness may omit native-only desktop chrome (window controls, update checks) but must preserve every PlatformAdapter contract the app uses — PTY, control-request, clipboard, iframe-proxy, Burrow, agent-browser, Playwright, and the sidecar's alerts (alert_command in, stamped with the one window label the harness simulates, main; their events back; §Alerts) — so a rule that only holds across the host boundary is exercised rather than answered by a private copy. BrowserSidecarHost.init() resolves on the SSE stream being open, not on its construction, so a seed cannot precede the stream that carries its reply; must let retryable connection failures reconnect within the open timeout; after a reconnect the adapter sends sync, since whatever the sidecar sent while the stream was down is gone (resolves on the stream's open event, not on construction in standalone/src/browser-sidecar-host.test.ts; asks the sidecar to sync when the event stream reconnects in standalone/src/browser-sidecar-adapter.test.ts). Fatal startup handling follows §Boot sequence. It must mirror standalone's Session-persistence answer (docs/specs/transport.md → "The governing rule"): one PersistedWindow per window, in localStorage rather than the Rust file store, and the same agent-recovery claim against a per-run temp state directory. The harness must never capture: a reload there is a live resume over PTYs that survive it, and capture is a quit-only step (§Agent recovery). Tauri APIs must not be required at static module-evaluation time when VITE_DORMOUSE_BROWSER_DEV_HOST is set — a normal browser loads the page, not the Tauri WebView.
Source of truth: standalone/scripts/dev-agent-browser.mjs, standalone/scripts/dev-run.mjs, standalone/scripts/dev-host-guard.mjs, standalone/src/browser-sidecar-host.ts, standalone/src/browser-sidecar-adapter.ts; stepBurrow in scripts/pairing-walkthrough/steps.mjs; sessionForKey in dor-lib-common/src/browser-providers.ts.