# Surface lifetimes

The shell needs a memory budget and failure boundaries, not a superstition
about which UI language is pure. Permanent state, optional presentation, and
authority must have named owners and observable lifetimes. If closing a heavy
surface leaves its objects and pages resident in the only process holding the
lock screen, the surface did not meaningfully go away.

## Evidence that opened this question

The daily-driver phone has 3.5 GB RAM. Three measurements from August 2026
frame the risk without pretending to be a fresh baseline:

- QuickShell reached about 610 MB RSS during the Live2D experiment;
- the summoned WebKit face added about 283 MB RSS in its own process;
- a separate dynamic-QML/per-stream rendering storm drove QuickShell to about
  952 MB anonymous RSS before the kernel killed it.

These observations do not prove that ordinary QML costs 952 MB, that WebKit is
always too expensive, or that a Rust surface is automatically small. They do
prove that an optional renderer can consume a material fraction of the phone's
memory and that a storm inside the resident QuickShell process shares a
failure domain with essential session UI.

The numbers must be remeasured against exact package revisions and process
trees before they become a budget.

## The resident core

The resident shell keeps only surfaces and connections that must survive the
loss of optional presentation:

- sessiond heartbeat and the secure session-lock handoff;
- the lock surface and lock-tier disclosure gate;
- display, idle, and device-state projections needed to remain safe;
- the small gesture and power paths required to recover or close a surface;
- a minimal system-status/recovery indication.

The resident core is not a second authority. Sessiond remains the decision
owner for lock, power, and admitted device action; the shell renders its state
and carries named requests. [Session start](02-session-start.md) owns the
package and systemd graph that brings compositor, authority, and shell up in
order.

The secure lock path must not be conditionally absent while a shell reload is
in flight. Optional-surface work may not reopen the already-known race where a
new shell requests a lock before the old holder has transferred or released
it.

## Optional means separately disposable

The agent Panel, Live2D presence, web content, large explorers, and future
special renderers are optional surfaces. Each needs:

- a named package and executable or explicit in-process owner;
- one startup trigger and one authority that may request presentation;
- a shutdown rule after close, idle, lock transition, or resource pressure;
- a state restoration key whose authority lives outside the renderer;
- a disclosure class and locked-state behavior;
- a resource line measured while closed, opening, active, and closing;
- a supervisor policy and a visible failure event.

A QML `Loader` is useful for avoiding construction and destroying object
graphs. It is not proof that allocator pages returned to the operating system,
and it cannot stop a rendering storm from killing the process that owns it.
Call that path lazy only after object lifetime is verified; call memory
released only after the OS measurement falls.

For expensive surfaces, a supervised process is the preferred boundary. When
the process exits, its private memory and faults leave with it. The resident
shell may retain a small launch/status projection, never the heavy renderer's
state graph.

## The Panel is the first extraction

The agent Panel is the strongest first worker candidate because its authority
already lives elsewhere:

- the Souveraine server owns agents, conversations, messages, itinerary,
  cancellation, interjection, and replay;
- sessiond owns session/lock/device authority;
- the Panel owns presentation, input collection, transient selection, and
  viewport state.

An extracted `souveraine-agent-surface` must therefore consume the same typed
server contract described in [the agent panel](04-agent-pane.md). It must not
copy conversations into a new local authority or become a second writer during
candidate testing.

Its lifecycle should be explicit:

1. an unlocked, admitted presentation request launches the version selected by
   the installed surface package;
2. the worker authenticates to the local Souveraine endpoint and attaches only
   after Casey selects or explicitly resumes a conversation;
3. close detaches presentation and gives the worker a short bounded grace for
   non-authoritative UI state;
4. the supervisor ends the worker after that grace;
5. reopen reconstructs from substrate truth and may restore harmless viewport
   state separately;
6. lock immediately hides Personal-class content and follows the declared
   stop/blank policy even if the worker is stuck.

During an active stream, a killed worker must not cancel the server turn unless
Casey issued cancel. The substrate continues or records its terminal state;
reopening reattaches and backfills. Conversely, a worker must not remain alive
forever merely because a server stream forgot to close. Presentation lifetime
and inference lifetime are related state machines, not one process lifetime.

## Fault containment

The resident shell needs a small supervisor projection for each optional
surface: stopped, starting, ready, active, stopping, failed, and backoff. A
crash becomes an event with the package version and last lifecycle edge. It
must not silently relaunch in a tight loop or take the session lock with it.

The decisive fault tests are physical:

- kill the Panel during streamed text and during a large tool return;
- feed a candidate QML version that fails to load;
- wedge the worker so it ignores close;
- lock while Personal-class content is visible;
- exhaust the worker's memory budget without exhausting the resident core.

In every case the lock surface, power path, sessiond heartbeat, and recovery
control must remain available. A screenshot after restart is not fault proof.

## Versioned delivery on every body

The laptop and phone receive published packages from Gitea. A surface split is
complete only when the package graph installs compatible versions and the
systemd/session start path selects them consistently. Hand-placing a worker on
one device proves a diagnosis at most; it is not the fix future installs will
receive.

The core and optional-surface protocol must be versioned. Compatibility needs
an explicit range or handshake so a newer Panel cannot silently interpret an
older substrate event shape. Package upgrade stages a candidate; adoption and
restart remain explicit where a live conversation or lock handoff could be
disturbed. The last working surface stays available until the candidate reaches
ready state.

Gitea Actions and ArchDev own compilation and package artifacts. Local checks
may parse QML, format Rust, validate manifests, and exercise fixtures; they do
not replace the package result.

## Native Rust is a measured option

A Rust Wayland client could replace some or all of QuickShell, but it still has
to own layer-shell/session-lock protocols, rendering, text, input, animation,
accessibility, service integration, and recovery. Rust does not remove the
need for resident/optional process boundaries, and a native screenshot says
nothing about steady memory or fault behavior.

The useful spike is deliberately small: one native layer-shell card, one typed
sessiond-fed state, one package, and measurements for RSS/PSS, launch latency,
and failure. It does not take the lock, replace the shell, or grow a parallel
settings authority. Compare it against the extracted QuickShell worker under
the same workload before choosing a rewrite.

## Measurement record

Every budget line should name:

- device and available RAM;
- source commit, package version, and protocol version;
- process tree and which surfaces are constructed;
- RSS and PSS after a defined settle time;
- peak during a defined action, including a representative streamed tool turn;
- memory after close and after worker exit;
- launch-to-ready latency;
- result of the relevant kill/lock/reopen test.

Measure at least resident idle, Panel closed, Panel open and idle, active text
stream, large tool payload, Live2D closed/open, and post-close. A single `ps`
line without surface state is anecdote, not a budget.

## Proof and present boundary

The path is complete only when:

1. the resident and optional sets are explicit in source and packages;
2. closing the Panel ends its worker and the OS-visible memory leaves;
3. killing or wedging that worker leaves lock, heartbeat, power, and recovery
   alive;
4. locked-state disclosure is exercised, not inferred from a conditional;
5. reopen restores the authoritative conversation without duplicating a turn;
6. Gitea emits the versioned core and surface packages used by both laptop and
   phone;
7. a Rust rewrite decision, if made, cites comparable measurements.

None of that extraction is true yet. The current Panel remains inside the
resident QuickShell process; the August numbers are historical observations;
and the Rust path remains a bounded experiment, not an adopted shell plan.
