# Surface lifecycle, the runtime pending/stale contract (ADR-0061)

The renderer runtime (`packages/gen-ui/a2ui/`) owns a per-surface lifecycle
state machine, ratified by ADR-0061
(`docs/ops/adr/adr-0061-surface-lifecycle-pending-stale.md`; requirements and
build order in `docs/ops/spec/spec-a2ui-surface-lifecycle.md`). Load this file
when a change touches surface regeneration, `<a2ui-root>`'s `doc` setter,
pending/stale rendering, or a host's update bracketing.

## The state machine

```text
empty → pending-first → live → pending-stale → live
              └→ error-empty         └→ error-stale
                  (both error-* heal on the next successful commit)
```

- **Pending is per-surface, never global.** Each renderer surface record
  carries its own machine; one surface regenerating never dims another.
- **Latest-generation-wins arbitration.** When two update brackets race on
  one surface, the newest generation's commit wins; a superseded bracket's
  messages are discarded, not interleaved.
- **Stale content stays visible.** In `pending-stale` and `error-stale` the
  previous answer keeps rendering: the blank-flash reset+replay shape is
  what this contract retired. Error state heals: the next successful commit
  clears `error-*`.

## The host API

Four public runtime methods drive the machine, `beginSurfaceUpdate` /
`applyTo` / `commitSurfaceUpdate` / `abortSurfaceUpdate`. Messages arriving
inside a bracket buffer and apply atomically at commit. A `replace`-mode
commit sweeps components the new answer no longer declares, the ONLY place
removal semantics exist; outside a bracket, `updateComponents` upserts
exactly as before, so unbracketed streams and hosts are behavior-identical
to the pre-lifecycle runtime (strictly additive, ADR-0061 Decision 4).
`<a2ui-root>`'s `doc` setter routes through this replace bracket
(`replaceDoc`) rather than reset+replay, a deliberate, ratified behavior
change for doc-setting hosts (ADR-0061 OD-2).

## The DOM contract

Staleness is exposed as one attribute plus three events, never styling:

- **`data-a2ui-lifecycle`**, a single enum attribute on the surface root
  reflecting the current state. This is the `data-a2ui-*` prefix's FIRST
  ratification (the pre-existing `data-a2ui-surface` stamp was accidental,
  now ratified alongside it), the runtime-owned `data-*` tier, sibling to
  ADR-0060's trait tier.
- **Three bubbling CustomEvents** mark the transitions.
- Hosts and primitives style off the attribute with semantic tokens: stale
  content dims; skeletons (the existing `skeleton` primitive) appear only in
  `pending-first`. `@adia-ai/a2ui` ships state, never styling, and stays
  zero-dependency.

## Layer boundary (ADR-0059)

The lifecycle is renderer-runtime work on gen-ui-kit's side of the ADR-0059
line. The three wire envelope kinds (`beginSurfaceUpdate` /
`commitSurfaceUpdate` / `abortSurfaceUpdate` as v1.0 server kinds) are
defined by the v1.0 Candidate reference implementation, `a2ui.schema.json`
gains nothing, the dialect wire format is byte-identical. Never hand-write
lifecycle envelopes from this skill's surfaces; the Bridge owns the mapping
when the kinds land.

**[amended 2026-08-29, ADR-0096]** `adiahealth/gen-ui-system` is absorbed
first-party under `packages/genui/` and the standalone repo is archived, there is no external, separately-governed "upstream standard" to wait on
any more. ADR-0096 Decision 5: gen-ui-kit "owns the whole stack now... and
the v1.0 Candidate reference implementation itself, not only the consumer
side of a vendor boundary." Stream-driven regeneration now proceeds on
gen-ui-kit's own schedule against the in-repo `packages/genui/` source,
not an upstream release.

## Interlock worth knowing

The v1.0 conformance program's duplicate-`surfaceId` error (REQ-002)
sequences AFTER this API, the silent `createSurface` no-op it removes was
previously the only wire-visible re-target path, and the lifecycle bracket
is its sanctioned replacement.
