mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
ef6ec2995b
* chore(layering): document R12/R18/R19, retire R8, make R9 shrink mandatory (#1781 A6) The A6 review kept `check:layering` in full (15/15 planted violations fired, no other enforcer exists) and left four follow-throughs. R12 bin-alias-fast-path, R18 contracts-implementation-authority and R19 selector-pipeline-ownership were live rules with no ADR or CONTEXT anchor — they now carry one each, in the same list as R7/R9/R10/R13. R8 zero-dep-job-closure is retired: no CI job sets `install-deps: false` and ci.yml records why each keeps it enabled, so the invariant has no subjects. R11's relative-into-packages exception existed only because a zero-dep closure cannot coexist with specifier loads, so it retires with R8; the route is now closed to every caller. R1 was retired the same way at #1490. R9 was growth-only and merely suggested lowering the ceiling, which is headroom the next change spends without a number moving. It is now an equality pin like R6 and the R10 R7 counts, and the committed baseline drops 47 -> 46 (daemon-server ceiling 17 -> 16) to match the measurement. ADR 0019 §6 now says each runtime-command-cutover row is deleted when that command's migration is declared closed. * chore(layering): rename R9 to type-cycle-size now that it fails both ways (#1781 A6)
783 lines
58 KiB
Markdown
783 lines
58 KiB
Markdown
# ADR 0019: Request-Bound Platform Runtime
|
||
|
||
## Status
|
||
|
||
Accepted for staged adoption (2026-08-09). Checkpoint outcome: **continue** (2026-08-10), under the
|
||
revised cumulative package budget accepted in issue 1704. The clean checkpoint is measured from the
|
||
original baseline `44c298d7f3a0ef84bc47f34c54d88b6c9eeb0df2`, through merged `devices`
|
||
`c06bed9f773a27ae0a02cb012570def2f2d0b90e`, to `logs`
|
||
`188795386466cfdba5d5748db5c9d3477e70eb4e` and `network`
|
||
`457fafe6399a95a4ddbfac57f02b3a7fe4157a54`. The earlier checkpoints at `99f5af1b7` and `d73bdb4ae`
|
||
are superseded and were not behavior-passing: later review found correctness failures and the first
|
||
budget decision still used the unrevised +3% limit. The required cleanup package, explicit budget
|
||
decision, and clean rerun are now complete. The recordings command unit has since completed on the
|
||
durable-capture substrate under its separately reviewed cumulative bound below (section 7). No
|
||
further command unit is authorized by this Status: subsequent units are planned, budgeted, and
|
||
authorized individually through the successor tracking issue under the amendment's governance.
|
||
|
||
Amended 2026-08-11 with broader-migration governance (sections 8–10). The amendment changes no
|
||
checkpoint outcome and re-authorizes nothing by itself: command units still migrate one at a time
|
||
under sections 2–6, but subsequent units follow the move-dominated size discipline, the evidence
|
||
tiers, the binding ergonomics, and the process-lifetime rules added below.
|
||
|
||
During the `devices` unit, doctor discovery, replay-test sharding, Apple simulator hints, and Android
|
||
emulator lifecycle keep their existing command execution owners while consuming the same injected,
|
||
neutral inventory capability. That coexistence moves discovery mechanics once; it does not migrate
|
||
those descriptors or authorize a second local/provider chooser.
|
||
|
||
## Rules at a glance
|
||
|
||
- Daemon device-execution code depends on platform-neutral contracts. Concrete device mechanics
|
||
live in private `@agent-device/platform-*` packages and are value-imported only by the root
|
||
composition module.
|
||
- The platform registry is **metadata-eager and implementation-lazy**. Cheap family identity,
|
||
inventory entrypoints, and static fact declarations may load at composition time; platform
|
||
mechanics and process-lived helper managers load only when discovery or the first binding for that
|
||
family needs them.
|
||
- The registry selects one local runtime owner per canonical platform family. Support is claimed and
|
||
tested for the exact platform leaf, device kind/backend, and provider mode; family ownership never
|
||
implies uniform leaf support.
|
||
- Command descriptors declare one typed execution shape: inventory use, or platform-neutral required
|
||
device operations with separately declared preferred fast paths. Runtime owners report
|
||
device-specific facts and expose behavior-bearing facets; platform and provider implementations
|
||
never name commands.
|
||
- `RequestExecutionScope.bindDevice(device, use)` resolves provider ownership, validates the facts
|
||
and concrete facets, and returns a runtime type narrowed to the proven runtime use. Handlers cannot
|
||
use undeclared facets or repair missing proof with casts or non-null assertions.
|
||
- Provider ownership is first-class and fail-closed. An owning provider may supply transport to a
|
||
platform runtime or implement facets directly; missing required behavior never falls through to a
|
||
local owner.
|
||
- Process-lived helper managers, request-lived device bindings, and daemon-session durable resources
|
||
have distinct lifetimes. Helper instances remain lazy, identity-bound, invalidatable, and
|
||
idle-stoppable. Disposing a request binding never stops a healthy helper or adopted resource.
|
||
- Durable work has two representations: a process-local live handle and a versioned, persistable
|
||
descriptor through which the same runtime owner can recover. Live session state may own a neutral
|
||
facet handle under R7; only the descriptor and neutral metadata enter persisted recovery state.
|
||
Concrete platform process objects never cross the seam.
|
||
- The migration unit is one command descriptor across its full existing denominator: every inventory
|
||
source or every supported device-runtime cell. A descriptor has exactly one explicitly declared
|
||
platform-execution shape at every committed state — `none`, legacy, inventory-backed, or
|
||
device-runtime-backed — and a migrated command has no legacy execution fallback. `none` declares
|
||
the absence of platform execution and is never a migration target or source.
|
||
- Broader adoption pauses after the first real slices. Evidence records one decision: continue,
|
||
revise the seam and rerun the checkpoint, or stop with every landed command still coherent.
|
||
- Post-checkpoint units are move-dominated: platform mechanics leave root `src/` for platform
|
||
packages, net shipped-size growth is exceptional and individually justified, and surfaces already
|
||
scheduled for removal at the next major are deleted on legacy, never migrated.
|
||
- Evidence is tiered by what a unit imports: request-scoped device units prove facts, operations,
|
||
and parity cells; only durable-resource units carry the section 4–5 lifecycle evidence.
|
||
- A handler binds once with its execution use. Admission, `capabilities`, and doctor questions use
|
||
side-effect-free facts inspection; required-only declarations are the default and a preferred
|
||
operation requires a recorded measurement.
|
||
- Cross-cutting facets land with their first consuming command unit. Daemon startup recovery is
|
||
evidence-gated, daemon shutdown is two-phase (detach, then stop), and session-teardown steps
|
||
belong to their owning domains — there is no generic lifecycle-hook API.
|
||
|
||
## Context
|
||
|
||
ADR 0009 deliberately introduced a shallow `PlatformPlugin`: it wrapped the existing platform
|
||
branches, preserved their lazy imports byte-for-byte, and moved each daemon column only after a
|
||
parity gate proved equivalence. That was the right migration shape. It stopped platform conditionals
|
||
from spreading, but its app-log, recording, performance, and provider facets still return tags or
|
||
predicates that the daemon maps back to concrete implementations.
|
||
|
||
The canonical registry now has six families: `apple`, `android`, `harmonyos`, `vega`, `linux`, and
|
||
`web`. HarmonyOS supplied the additional real adapter pressure that the earlier platform-package
|
||
checkpoint deliberately deferred. With this many families, a tag-to-implementation map leaves the
|
||
daemon owning every platform variation even when the platform conditional has moved behind a
|
||
plugin.
|
||
|
||
The replacement must preserve the properties paid for by the existing architecture: daemon-owned
|
||
request policy, provider-first integration scenarios, Apple-family leaf modeling, kept-hot helper
|
||
performance, interaction guarantee honesty, pre-mutation ref-frame expiry, normalized errors,
|
||
package direction, and CLI cold-start laziness.
|
||
|
||
## Decision
|
||
|
||
### 1. Platform modules provide behavior through one composition seam
|
||
|
||
Each canonical family has one private `packages/platform-<family>` workspace package named
|
||
`@agent-device/platform-<family>`. Its façade exposes cheap metadata plus lazy implementations of a
|
||
`DeviceInventorySource` and `DeviceRuntimeGateway`; the implementations behind those entrypoints own
|
||
platform tool invocation, helper protocols, runtime facts, output parsing, recovery mechanics, and
|
||
other native details. A platform package contains only that family's implementations and mechanics,
|
||
never daemon orchestration, command policy, or cross-family defaults.
|
||
|
||
The root composition module, `src/platform-runtime.ts`, constructs immutable
|
||
inventory/runtime registries and injects a composed inventory gateway plus provider-first runtime
|
||
gateway into daemon request execution. Daemon device-execution modules import runtime contracts only.
|
||
Shared runtime interfaces and neutral data types live in `@agent-device/contracts`. In production,
|
||
only that composition module may import a concrete platform package; reusable types do not leak
|
||
through type-only platform imports. Platform packages may import contracts, kernel/domain packages,
|
||
and explicitly injected host capabilities; they may not import daemon requests or responses, mutable
|
||
session state, command catalogs/grammar, root implementation files, sibling platform packages, or raw
|
||
process primitives outside the shared host-command port. R13 applies these rules to static, type-only,
|
||
dynamic, and re-export edges; package-owned tests may import their own public façade. Contracts may
|
||
depend on kernel vocabulary but never on concrete platform packages or daemon implementation types.
|
||
|
||
Durable-capture mechanics shared by more than one implementation live in the private
|
||
`@agent-device/capture-kit` workspace package, with the enforced direction
|
||
`kernel < contracts < capture-kit < platform/provider/daemon`. Contracts retains pure vocabulary and
|
||
plan models; process supervision, live-handle implementations, recovery helpers, runtime codecs, and
|
||
capture parsers do not live there. `capture-kit` is a domain package for durable capture, not a generic
|
||
platform-common package, and it preserves the package façades' implementation-lazy loading boundary.
|
||
Its introduction carries the normal workspace-package compliance surface: `check:affected`
|
||
selection, R11/R13 package enumeration, and the composite typecheck project list.
|
||
|
||
Canonical family, `AppleOS`, public-leaf, and selector identity remain declared in
|
||
`@agent-device/kernel/device`. Platform-module metadata references one canonical family; during
|
||
coexistence the legacy plugin registry derives its family identity from the same declaration rather
|
||
than becoming a second root.
|
||
|
||
Host command execution, diagnostics, progress, and resolved native assets enter through focused host
|
||
capabilities. A platform package receives only the capabilities its implementation needs. Shared
|
||
logic moves to an honest domain package only after more than one implementation needs it; there is
|
||
no `platform-common`, generic host-utilities package, universal command dispatcher, or generic
|
||
platform-resource union.
|
||
|
||
#### Implementation-laziness
|
||
|
||
Static composition must not make every daemon or CLI startup evaluate every platform implementation.
|
||
The registry eagerly loads only metadata-only package façades. A façade keeps heavy leaf imports and
|
||
helper-manager construction behind lazy entrypoints:
|
||
|
||
- discovery loads mechanics only for a family whose inventory is requested;
|
||
- binding loads mechanics only for the selected runtime owner;
|
||
- process-lived helper managers are created on first use, not while composing the registry; and
|
||
- loading one family does not load an unrelated family's implementation graph.
|
||
|
||
Composition may capture inert configured paths eagerly. Filesystem/tool probing, asset preparation or
|
||
building, helper construction, and facts requiring a selected-owner probe remain lazy. The structural
|
||
suite must prove that unrelated implementations are not evaluated before selected discovery/binding,
|
||
and the built artifact retains its startup measurement. Passing a startup threshold alone is not a
|
||
substitute for preserving the loading shape; the tracking issue owns the exact probe and planted-red
|
||
procedure.
|
||
|
||
### 2. Runtime use joins facts and narrows the bound runtime
|
||
|
||
`CommandDescriptor` remains the command declaration root. Its runtime-use declaration has a typed set
|
||
of required platform-neutral operations and may separately name preferred optimizations. Commands
|
||
whose use depends on normalized input first produce a discriminated execution plan that retains
|
||
literal required/preferred types. Required and preferred operation keys are disjoint, and the
|
||
required-only path is semantically complete; preferred operations may improve execution but are
|
||
never necessary for command correctness.
|
||
|
||
Inventory commands have a separate `inventoryUse` declaration. `devices` calls the composed
|
||
`DeviceInventoryGateway`, which selects canonical family sources and provider-owned inventory sources
|
||
from the request selectors; it neither invents a synthetic device nor calls `bindDevice`. Its
|
||
coverage denominator is all six local family inventory sources, every provider inventory source, and
|
||
selector/public-device projection parity. Inventory results become ordinary `DeviceInfo` values only
|
||
after their source has classified the canonical family and required leaf identity.
|
||
|
||
A runtime owner reports exhaustive facts for the exact device shape and returns capability-cohesive
|
||
facets with normalized semantic inputs and typed outcomes. Facet inputs never contain a command name,
|
||
`DaemonRequest`, `DaemonResponse`, `SessionState`, CLI flags, or an opaque command payload. A missing
|
||
facet plus a typed unavailability fact represents unsupported behavior; implementations do not ship
|
||
stubs that throw `unsupported` after binding. Runtime-use keys identify individual semantic
|
||
operations, not whole facet namespaces, so selecting one operation does not expose undeclared sibling
|
||
operations from the same facet.
|
||
|
||
`RequestExecutionScope.bindDevice(device, use)` is the trust choke point. It:
|
||
|
||
1. resolves the exact local or provider runtime owner;
|
||
2. checks every required operation and classifies each preferred operation against facts for the
|
||
platform leaf, device kind/backend, and provider mode;
|
||
3. creates or reuses one request binding for that ownership-qualified device;
|
||
4. verifies that every required operation and every preferred operation advertised as available has
|
||
a concrete facet implementation; an advertised operation with no implementation is a
|
||
runtime-contract error; and
|
||
5. returns a selected operation projection: required operations are non-optional, declared preferred
|
||
operations are optional and present only when available, and undeclared operations are inaccessible.
|
||
|
||
The cached broad runtime remains private to `RequestExecutionScope`; narrowing does not intersect a
|
||
wide optional aggregate that would still expose undeclared facets. The descriptor and its specialized
|
||
handler share one non-widened declaration, and a widened generic descriptor carries no static proof.
|
||
A compile-time contract test proves the selected projection. A structural
|
||
**runtime-facet-narrowing gate** covers every runtime-migrated handler owner and rejects attempts to
|
||
manufacture required-operation proof with assertions or optional admission. Optional access is
|
||
permitted only for descriptor-declared preferred operations. The tracking issue owns the gate
|
||
implementation and its required planted violation.
|
||
|
||
Absence or failure of a preferred path may change optimization/path disclosure, not whether the
|
||
semantic command can execute. Failure falls back to the complete required path only through a typed
|
||
reason and an explicit descriptor/ADR 0011 path classification; it is never a generic `catch`
|
||
fallback. Helper/session reuse hidden inside one required operation remains that facet's implementation
|
||
detail and follows ADR 0002 rather than becoming a daemon-visible preferred operation.
|
||
|
||
Family registration and support coverage are separate gates. The immutable registry owns each of the
|
||
six canonical families exactly once. Before a command cuts over, an independent parity artifact
|
||
freezes its legacy supported/unsupported cells and hints. Runtime-fact scenarios expand canonical
|
||
kernel-owned family/leaf fixtures over device kinds/backends and provider modes; a new runtime may
|
||
not make a difficult legacy-supported cell disappear. Behavior changes require a separate decision.
|
||
|
||
Apple coverage uses an exhaustive `AppleOS` fixture table and explicitly exercises iOS simulator and
|
||
physical backends where they differ, iPadOS, tvOS focus-only/no-coordinate behavior, macOS desktop,
|
||
visionOS deferred or supported cells, and the watchOS unsupported/discovery-absence sentinel. Every
|
||
fixture matches exactly one family. A loop over six families with one generic `platform: 'apple'`
|
||
device is not leaf coverage.
|
||
|
||
### 3. Provider ownership is exact and fail-closed
|
||
|
||
For one device, provider resolution has three outcomes:
|
||
|
||
1. exactly one provider owns it: bind that provider-owned runtime;
|
||
2. no provider owns it: bind the local family runtime; or
|
||
3. more than one provider owns it: fail with an ownership-contract error.
|
||
|
||
Only providers that declare device-runtime ownership participate in this arbitration. A narrow tool,
|
||
transport, app-log, or recording provider is an injected dependency of the selected local/provider
|
||
runtime; it does not become a second runtime owner or compete in the owner count.
|
||
|
||
Provider ownership is authoritative. If the owning provider lacks a required fact or facet, admission
|
||
returns the typed unsupported result; it never tries the local family implementation. A provider may
|
||
implement a complete runtime directly, as Cloud WebDriver can, or contribute a narrow transport used
|
||
inside the owning family module, as an Android transport can. Provider callback stacks threaded
|
||
through daemon handlers are not part of the new seam.
|
||
|
||
Ordinary binding performs the arbitration above. Every `DeviceBinding` also exposes a unique,
|
||
restart-stable runtime-owner reference identifying the local family owner or the configured provider
|
||
runtime instance, not merely a provider kind. Composition rejects duplicate owner references. Durable
|
||
descriptors embed that reference; exact-owner binding resolves it directly, validates descriptor
|
||
device/fence identity, and never reruns ordinary ownership arbitration or `ownsDevice`. An unavailable
|
||
expected provider is `unreattachable: owner-unavailable`; it never becomes a local bind.
|
||
|
||
### 4. Bindings attach to process-lived helper managers; they do not own helpers
|
||
|
||
Platform gateways and their helper managers are process-lived. Individual helper generations remain
|
||
lazy, identity-bound, invalidatable, restartable, and idle-stoppable under ADR 0002. A request binding
|
||
attaches cancellation, diagnostics, progress, and admitted device/session context. Disposing it
|
||
releases only those request attachments and request-local acquisitions.
|
||
|
||
The underlying `DeviceBinding` implements `AsyncDisposable` and remains private to
|
||
`RequestExecutionScope`; handlers receive only a non-disposable `BoundDeviceRuntime<Use>` projection.
|
||
Each gateway bind owns a private rollback stack and publishes a binding only after every request-local
|
||
acquisition succeeds. Failure after any internal acquisition releases that partial state before the
|
||
bind rejects. On publication, ownership transfers exactly once to a repository-owned, reverse-order
|
||
scope disposal stack; caching another projection never registers or disposes the binding twice.
|
||
|
||
Request execution invokes that stack from `finally`. Source-executed TypeScript does not use
|
||
`await using` while the supported minimum Node 22 runtime cannot parse that syntax, type stripping
|
||
cannot lower it, and Node 22 has no `AsyncDisposableStack`; the contract keeps a later syntax migration
|
||
mechanical. A binding itself is never adopted beyond the request; only a durable resource handle
|
||
returned by one of its facets may transfer to session ownership.
|
||
|
||
Persistent helper shutdown happens only through explicit device lifecycle policy, helper-manager
|
||
invalidation, platform idle policy, or platform-module shutdown after daemon-session resources have
|
||
been finalized. After adoption, a durable handle may retain its own helper attachment beyond the
|
||
originating request; that attachment belongs to the handle and is no longer registered for
|
||
request-scope disposal. Adoption detaches every originating request diagnostic/event/progress port.
|
||
Later `finish` or forced disposal executes inside the current request observability scope or an
|
||
explicit daemon session-teardown observability scope; a durable handle never retains an old request
|
||
async context.
|
||
|
||
A shared lifecycle contract scenario is required for every helper-backed runtime. With idle expiry
|
||
disabled and no invalidation, process exit, identity change, or ownership transfer, it proves that
|
||
disposing request bindings never terminates or restarts the healthy helper and an immediate later
|
||
binding reuses that generation. Module shutdown terminates each still-live, still-owned generation at
|
||
most once. Every durable-resource facet separately proves that its adopted resource survives
|
||
originating-binding disposal. Each platform supplies honest evidence appropriate to its helper rather
|
||
than relying on wall-clock behavior alone. Bind contract scenarios also fail after each successive
|
||
internal acquisition and prove that the unpublished partial binding leaves no request attachment.
|
||
|
||
Published bindings are disposed on success, failure, cancellation, and early return, in reverse
|
||
acquisition order; unpublished partial binds roll themselves back before rejecting. Disposal runs
|
||
inside the active request diagnostics/event context before `request.finished`, diagnostics flush,
|
||
final response construction, and device/session lock release, so cleanup-only failure cannot follow a
|
||
recorded success. If the operation and cleanup both fail, the operation error remains primary and
|
||
cleanup is structured secondary diagnostic evidence; a cleanup-only failure surfaces normally.
|
||
|
||
### 5. Durable resources are reattachable by the same owner
|
||
|
||
App-log streams, screen recordings, and native profiler captures may outlive one request. Starting
|
||
durable work returns:
|
||
|
||
- a **live resource handle**, which is process-local authority to finish or forcibly dispose the
|
||
active resource; and
|
||
- a **durable resource descriptor**, which is bounded, JSON-safe, versioned state sufficient for the
|
||
exact runtime owner to make a deterministic recovery attempt.
|
||
|
||
Facet-specific handles expose `finish(): Promise<FinishOutcome>` for idempotent normal completion,
|
||
`forceCleanup(): Promise<CleanupOutcome>` for outcome-bearing forced cleanup, and idempotent
|
||
`[Symbol.asyncDispose](): Promise<void>` as the scope-cleanup adapter. The adapter resolves only for
|
||
confirmed cleanup/already-missing outcomes and rejects with a normalized ownership-lost or
|
||
cleanup-pending error otherwise; it does not pretend `Promise<void>` carries a typed result.
|
||
|
||
Every destructive finish/cleanup operation holds the authoritative ownership lease from fence
|
||
validation through the native side effect and persisted lifecycle transition, or passes a fencing
|
||
token to a native/provider operation that atomically rejects stale owners. A check immediately before
|
||
the side effect is not sufficient. A transport that can provide neither mechanism cannot claim
|
||
transferable reattachment; cleanup-only recovery requires proof that the prior owner is gone, and
|
||
otherwise remains manual recovery.
|
||
|
||
A facet start internally rolls back if it cannot publish a complete handle/descriptor pair, and the
|
||
start promise is invoked through the request scope's pending-transfer guard. The scope registers the
|
||
guarded handle for forced cleanup before resolving the result to command orchestration, then releases
|
||
that guard only after the authoritative active record and the live `SessionState` handle/descriptor
|
||
adoption both succeed. Cancellation or failure anywhere in that gap disposes the unadopted handle and
|
||
terminalizes the record only after confirmed cleanup; uncertain cleanup leaves the record
|
||
cleanup-pending. Contract scenarios inject cancellation and failure between partial start,
|
||
publication, persistence, live adoption, and transfer.
|
||
|
||
There is no generic `PlatformResource`. Live `SessionState` may retain a neutral facet-specific
|
||
contract handle beside its descriptor and metadata; the field has one R7 transition owner. Only the
|
||
descriptor and neutral metadata enter the authoritative persisted recovery record. Concrete platform
|
||
classes, provider clients, child handles, timers, transports, and wait promises enter neither store.
|
||
|
||
The daemon shares the lifecycle mechanics of those facet-specific resources through one
|
||
`DurableCaptureResource` coordinator. It owns bounded manifest I/O, per-resource fence serialization,
|
||
start/persist/adopt compensation, terminal transitions, and deadline-bounded exact-owner recovery.
|
||
The coordinator receives a facet's resource kind and manifest store, neutral session slot, completion
|
||
metadata projection, failure wording, and exact-owner recovery adapter; it does not select platforms,
|
||
interpret command flags, or form a generic runtime facet. App-log and screen recording retain distinct handles,
|
||
descriptors, facts, native finalization semantics, and admission policy. Reusable codec/live-handle
|
||
mechanics below the daemon remain in `@agent-device/capture-kit`, preserving the package direction.
|
||
|
||
A daemon-owned, process-lifetime admission ledger may retain bounded cleanup uncertainty that has no
|
||
honest durable representation, but it is not a second live-resource store: it contains no handle or
|
||
descriptor, never supersedes the persisted manifest, and is keyed by canonical device identity when
|
||
that identity is known. Evidence that can be checked again, such as a retained legacy marker path, is
|
||
revalidated at admission so manual recovery can unblock the matching device without a daemon restart.
|
||
Unknown-identity evidence remains globally fail-closed, while undurable in-memory blocks expire only
|
||
under an explicit bounded policy with diagnostics.
|
||
|
||
Persisted JSON re-enters as `unknown`. Contracts first validate a neutral envelope containing resource
|
||
kind/envelope version, session/device identity, exact owner reference, fence, and lifecycle state.
|
||
Only after exact-owner selection does that facet's total codec decode its descriptor body. Invalid or
|
||
unknown envelopes/bodies remain retained recovery evidence with a typed
|
||
`descriptor-invalid`/`descriptor-version-unsupported` outcome; they are never cast to a descriptor,
|
||
deleted, or offered to another facet.
|
||
|
||
When no live handle is available, the daemon completes lease/admission and takeover fencing, binds
|
||
the descriptor's exact owner, and calls that facet's typed `reattach`. Reattachment returns a closed
|
||
outcome: `active` with a handle, `completed` with its result, `missing`, or `unreattachable` with a
|
||
typed reason. Unsupported descriptor schema/version returns
|
||
`unreattachable: descriptor-version-unsupported`: the facet does not guess a migration, discard the
|
||
descriptor, fall through to another owner, or start a replacement. Other closed reasons include
|
||
`owner-unavailable`, `transport-not-reattachable`, and `ownership-fence-lost`. A descriptor may
|
||
explicitly declare cleanup-only recovery when its native transport cannot reconstruct equivalent live
|
||
control; that limitation is a runtime fact, not a best-effort guess during takeover.
|
||
|
||
Reattachment does not rehydrate `SessionState` or change `SESSION_NOT_FOUND` policy. With no admitted
|
||
live session, a reconstructed handle is recovery-scope-owned and may only complete or clean up the
|
||
resource; adopting it for continued session use requires an independently admitted live session.
|
||
|
||
Every durable facet also exposes a typed, idempotent
|
||
`cleanup(descriptor, fence, scope): Promise<CleanupOutcome>` for descriptor-only forced cleanup.
|
||
Cleanup-only recovery uses this operation instead of pretending to reconstruct a full handle, and its
|
||
closed outcome records cleaned, already missing, or still cleanup-pending state. Finish, dispose, and
|
||
descriptor cleanup all use the same serialized/atomically enforced fence. If the exact owner or
|
||
descriptor schema is unavailable, cleanup cannot be guessed: the authoritative record remains
|
||
cleanup-pending with a manual-recovery reason.
|
||
|
||
The descriptor must have an authoritative persisted home before command start success is returned.
|
||
Prefer the platform/resource-owned recovery manifest when that resource already owns one. If a
|
||
resource has no honest manifest, introduce a daemon-owned persisted resource record only for that
|
||
demonstrated need, and extract a shared resource index only after multiple resource types earn it.
|
||
Current diagnostics/session events—and any future journal from proposed ADR 0018—are
|
||
observability-only and never a recovery source of truth. They may mirror descriptor lifecycle events,
|
||
but reattachment never scans telemetry to rebuild state.
|
||
|
||
Every authoritative home exposes a deterministic facet-owned lookup or enumeration path after
|
||
process loss. Its neutral record carries session/device identity, the exact runtime-owner reference,
|
||
descriptor and metadata, an ownership/fence token, and one of two persisted lifecycle states:
|
||
`open` or `completed`. In-progress distinctions such as starting, active, completing, and
|
||
cleanup-pending are phase metadata on the open record, not additional lifecycle states. The fence and
|
||
the descriptor remain authoritative across every open phase; cleanup uncertainty therefore cannot be
|
||
encoded as a terminal lifecycle. A new handle is not exposed until the persisted ownership fence is
|
||
acquired. Every finish/cleanup attempt holds that ownership guard through destructive work and the
|
||
persisted transition, or delegates to an operation that atomically enforces the token, so a prior
|
||
owner cannot later terminate a transferred resource.
|
||
|
||
Persisting a descriptor does not make external-resource start and descriptor write atomic. A
|
||
platform whose native tool cannot close that crash window retains a platform-owned orphan marker or
|
||
equivalent recovery protocol. Descriptors contain stable reconstruction coordinates, not promises,
|
||
timers, child handles, buffers, credentials, or a dump of live implementation state.
|
||
|
||
Ownership transfer is explicit. Start persists active recovery evidence before returning command
|
||
success, and the pending guard transfers only after live session adoption. Completion preserves a
|
||
terminal result or stable result coordinates before active recovery evidence is removed. Once
|
||
adopted, request-binding disposal cannot stop the resource. Normal session teardown preserves
|
||
facet-specific ordering; adopted durable handles are not placed in the request binding's generic
|
||
disposable transaction.
|
||
|
||
### 6. Command cutover is the abandonment-safe migration unit
|
||
|
||
The command descriptor is the sole migration discriminant, but it does not own a daemon handler
|
||
value. It declares command identity and an internal-only execution mode excluded from public
|
||
projections. The mode selects one neutral declaration shape: `none`, legacy platform admission,
|
||
device `runtimeUse`, or `inventoryUse`. The `none` mode (amendment, 2026-08-11) is the explicit
|
||
declaration that a command executes no platform behavior: a `none` descriptor never binds a device,
|
||
carries no capability bucket, owns no platform adapter, and declares no runtime or inventory use;
|
||
lease, session-management, and daemon-management commands are `none`. The mode is declared
|
||
explicitly on every descriptor — a registry-entry default that silently assigns a mode cannot
|
||
distinguish a command with no platform execution from an unmigrated one and is prohibited. ADR
|
||
0003's daemon-owned total route table continues to own specialized
|
||
handler implementations and request-policy traits; that route/policy projection remains present and
|
||
unchanged in every mode. A derived coherence gate joins the declarations without importing daemon
|
||
handlers into the descriptor registry.
|
||
|
||
The descriptor-derived **runtime-command-cutover gate** applies to platform-executing descriptors
|
||
only; `none` descriptors are outside its denominator, and the gate separately rejects a `none`
|
||
declaration on any descriptor that binds devices, keeps a capability bucket, or reaches platform
|
||
behavior. For a platform-executing descriptor it covers the command's whole platform-execution
|
||
projection: either legacy admission/hints plus its complete legacy platform adapter; device runtime
|
||
use plus fact-derived admission and its complete runtime-backed adapter; or inventory use plus the
|
||
composed inventory gateway. Never more than one and never none. A migrated device unit deletes its old
|
||
capability closures/hints and legacy platform adapter. A migrated inventory unit deletes its old
|
||
inventory branches/adapter. Neither cutover deletes, duplicates, or changes the ADR 0003 daemon route.
|
||
No handler selects old versus new platform behavior by family, provider, failure, environment
|
||
variable, or feature flag. Each cutover row is scaffolding for one in-flight migration and is
|
||
deleted in the change where this ADR declares that command's migration closed and its retired
|
||
symbols unreachable; the table must never outlive the migration it guards.
|
||
|
||
One device-command migration unit is one command across every runtime cell where it is currently
|
||
supported: all applicable platform leaves, device kinds/backends, transport-composed providers, and
|
||
direct provider runtimes. Unsupported cells are classified through new runtime facts; they do not
|
||
remain on the old adapter. An inventory-command unit covers every local family and provider inventory
|
||
source plus selector/public projection parity. The unit also deletes that command's superseded daemon
|
||
platform branches, backend tags and tag-to-implementation maps, capability closures/hints, legacy
|
||
imports, parity scaffolding, and old platform adapter. The independent pre-cutover parity artifact
|
||
proves available-command and unsupported-hint equivalence before those legacy sources are deleted; a
|
||
deliberate behavior change is a separate decision rather than a migration gap.
|
||
|
||
Facets organize contracts and implementation locality; a whole facet is not a required PR boundary.
|
||
A repository may indefinitely contain migrated and unmigrated commands, but no command has two
|
||
platform-execution paths and no merged state depends on a later PR for correctness. Rollback is a
|
||
revert of the complete command unit, not a retained execution fallback.
|
||
|
||
A behavior-neutral package/registry substrate may land before the first command only when it routes
|
||
no production descriptor, is dead-code clean, independently revertible, and preserves lazy loading.
|
||
Otherwise it lands with the `devices` command unit. Shared Apple-runner or Android-helper mechanics
|
||
used by migrated and legacy commands are not duplicated or connected through a package-to-root
|
||
back-import. They either remain physically in place until their last consumer can move, or move
|
||
behind a neutral lower-level capability injected by composition into both the package facet and the
|
||
complete legacy adapter while command execution ownership remains singular.
|
||
|
||
The substrate and every command unit are reviewed from a clean committed tree with all production
|
||
files present in `HEAD`; a green layering run while new production files are untracked is not
|
||
evidence. Platform packages receive no migration allowlist or root back-import exception.
|
||
`check:affected` must select platform contract, provider, and coverage scenarios for a
|
||
`packages/platform-*` change. Every new structural gate is accepted only after a planted violation
|
||
demonstrates the intended red result.
|
||
|
||
Any unit touching `SessionState` classifies each changed field once under R7, keeps request bindings,
|
||
concrete platform/provider types, and raw process/transport mechanics out, and gives each mutable
|
||
resource field one transition owner. The permitted live value is the opaque implementation behind a
|
||
neutral facet-specific handle contract. The unit proves partial-start cleanup, transfer, finish,
|
||
forced disposal, and error precedence. Equality-pinned R10 writer/owner counts and external
|
||
`daemon/types.ts` importer membership are lowered in the same unit when they shrink. R9 total-cycle
|
||
and R10 cycle-zone pressure may not grow, and R9 is equality-pinned since #1781 A6: a unit that
|
||
shrinks the cycle lowers the zone ceilings in the same change rather than leaving the slack open.
|
||
|
||
### 7. Adoption checkpoint
|
||
|
||
No command migration beyond the metadata/package substrate and complete `devices`, `logs`, and
|
||
`network` command cutovers begins until this ADR's Status records **continue**. `network` is included
|
||
because it consumes the app-log resource. Daemon-owned generic session teardown may invoke the
|
||
neutral app-log handle's forced-disposal contract without changing `close`'s legacy platform adapter;
|
||
the `logs` unit proves that path through `close`. If `close`, `open`, or another command instead must
|
||
bind a runtime or select app-log platform behavior, its full unit must be named in Status before the
|
||
substrate lands. `logs` is the deliberate first descriptor/reattachment pilot, not merely a
|
||
request-lifetime handle exercise.
|
||
|
||
Before the substrate lands, the tracking issue records and reviews the exact commands, thresholds,
|
||
scenario denominator, and scoped counts used below. The same measures are reported from the clean
|
||
committed checkpoint tree.
|
||
|
||
The checkpoint records exactly one outcome:
|
||
|
||
- **continue**: every hard evidence item passes and broader command migration may proceed;
|
||
- **revise**: amend the contracts/ADR, rerun the checkpoint, and begin no further command migration; or
|
||
- **stop**: abandon broader migration; already-landed command units remain coherent or are retired by
|
||
a separate explicit decision.
|
||
|
||
Any failed hard item requires **revise** or **stop**; recording evidence of the failure is not
|
||
evidence for **continue**.
|
||
|
||
Evidence must cover:
|
||
|
||
- one platform-execution path, an unchanged ADR 0003 route/policy projection, and zero old capability
|
||
closures, tags/maps, or concrete daemon platform imports for each migrated command;
|
||
- package direction and the absence of daemon request/session/command types in platform packages;
|
||
- independent package builds/declarations, metadata-eager/implementation-lazy loading, and reviewed
|
||
startup/build/package thresholds;
|
||
- typed runtime-use narrowing and a planted-red narrowing-gate violation;
|
||
- six-family runtime ownership, six local inventory sources, provider inventory sources, and
|
||
independently enumerated leaf/device/provider support parity and scenario counts;
|
||
- provider-first fail-closed behavior plus unique restart-stable exact-owner references;
|
||
- rollback after every partial bind acquisition and published-binding disposal without
|
||
persistent-helper shutdown;
|
||
- descriptor persistence and exact-owner recovery after loss of a process-local handle, including
|
||
validated decoding, resumed control for reattachable cells, executable descriptor-only cleanup,
|
||
and explicit cleanup-only/version-unreattachable behavior;
|
||
- pending-handle cleanup across every start/persist/adopt/transfer gap, durable-handle transfer,
|
||
serialized or native-atomic fencing, idempotent teardown, and operation/cleanup error precedence;
|
||
- semantic/provider/device parity for `devices`, `logs`, and `network`;
|
||
- unchanged-or-lower R7/R10, type-cycle, and external-daemon-type ratchets; and
|
||
- numeric before/after daemon platform import, branch, and tag counts with a strict net decrease.
|
||
|
||
The seam is revised or stopped before broader migration if a platform package needs root/daemon or
|
||
sibling-platform imports; a command needs old/new or provider/local execution fallback; runtime
|
||
facets degenerate into tags, command switches, or opaque execution payloads; leaf/provider facts
|
||
require default fallthrough; `SessionState` names/exposes concrete platform/provider types or raw
|
||
mechanics rather than the permitted neutral live handle; a persisted record bypasses the validated
|
||
neutral envelope or facet-owned descriptor codec, or contains raw live mechanics; implementation
|
||
laziness cannot be preserved; R7/R10/type-cycle pressure grows; or the landed slices add more daemon
|
||
platform ownership than they remove.
|
||
|
||
#### Checkpoint result: continue under the revised cumulative budget (2026-08-10)
|
||
|
||
The final checkpoint compares `44c298d7f3a0ef84bc47f34c54d88b6c9eeb0df2` with stack head
|
||
`457fafe6399a95a4ddbfac57f02b3a7fe4157a54` on the same host and toolchain. Merged `devices`
|
||
`c06bed9f773a27ae0a02cb012570def2f2d0b90e` is part of that range; rebasing the remaining PRs onto it
|
||
does not reset the denominator. The earlier `99f5af1b7` checkpoint missed a stale owned-marker wedge,
|
||
Apple scoped-provider bypass, Android optional-recovery loss, and absolute network line-number
|
||
regression. The later cleanup review found a cross-device retained-marker wedge, a manual-recovery
|
||
hint that remained blocked until daemon restart, and an Android app-log stream that stayed pinned to
|
||
the pre-relaunch app PID. Each correction was observed red before its focused test passed; the final
|
||
admission test proves removal of the matching retained marker permits a second start in the same
|
||
daemon process. On exact final logs head `188795386`, a controlled Android relaunch moved the app from
|
||
PID 10952 to 11455, rotated the durable logcat marker to PID 11455, and retained both before/after
|
||
canaries plus the new process output in one `app.log`. Guarded daemon/dead-child recovery separately
|
||
terminalized the manifest and allowed a clean second start.
|
||
|
||
The required revision package is complete:
|
||
|
||
- `@agent-device/capture-kit` is the private durable-capture implementation package. Contracts keeps
|
||
pure runtime types and plan models; capture-kit owns process/recovery/live-handle mechanics,
|
||
envelope/descriptor codecs, and network parsers. The dependency order is
|
||
`kernel < contracts < capture-kit < platform/provider/daemon`. A planted-red gate rejects process,
|
||
filesystem, or timer mechanics drifting into contracts. The package is wired into affected-check
|
||
selection, the layering test enumeration, and the workspace typecheck project list.
|
||
- Canonical kernel device identity replaces the duplicate encoders while facts-shape validation stays
|
||
separate. The bounded Android/Harmony PID owner factory lives in capture-kit; Apple and Limrun stay
|
||
custom, there is no network factory, and all loaders retain their lazy boundary. The managed-command
|
||
allowlist, unused façade exports, public Limrun runtime-module declaration leak, quadratic JSON walk,
|
||
and duplicate lifecycle/reconnect mechanics are gone. The temporary Android implementation files
|
||
introduced by the devices PR are also absent from contracts at this checkpoint.
|
||
- Plan-declared app-session requirements replace repeated handler guards. Persisted durable lifecycle
|
||
is `open | completed`, with transient phase retained as metadata. A daemon-owned admission ledger
|
||
scopes decodable evidence by device identity, rechecks manually removed legacy markers, bounds
|
||
undurable blocks, and remains subordinate to the durable manifest. Discriminated teardown settles
|
||
app-log state, re-reads the replaced session record, then runs generic teardown, avoiding the stale
|
||
reference and double-cleanup path.
|
||
|
||
The final gates passed:
|
||
|
||
- `pnpm check:affected --run` at `457fafe63` passed 799 test files / 6,424 tests with 2,097/2,401
|
||
changed executable lines covered (87.34%), plus format, lint, typecheck, build/declarations,
|
||
published-package clean install, fallow, provider integration, replay compatibility, and
|
||
integration-progress checks.
|
||
- `pnpm check:layering` passed 131 structural/model tests and scanned 1,157 production source files.
|
||
R11 owns 17 workspace packages behind 39 exported subpaths with no root back-imports; R13 keeps six
|
||
private implementation-lazy platform packages above capture-kit behind one composition root; R14
|
||
and R15 retain one typed route for `logs` and `network` with no legacy route.
|
||
- Six local inventory/runtime owners, all enumerated Apple leaf/kind cells, and the production
|
||
BrowserStack, AWS Device Farm, and Limrun provider modes remain covered. Provider ownership and
|
||
inventory are fail-closed; exact-owner recovery and provider-authoritative tests prove there is no
|
||
provider-to-local fallback. Durable tests cover every start/persist/adopt gap, exact-owner recovery,
|
||
cleanup-only and descriptor-only cleanup, fencing, idempotence, and primary-error precedence.
|
||
- R7/R10 remain at 23 writer-owned `SessionState` fields and 29 owner claims, with all 34 fields
|
||
classified. The largest type cycle is 46 against the 47-file ceiling, and the two external
|
||
production `daemon/types.ts` importers are unchanged. Checkpoint-owned `logs`/`network` platform
|
||
decision lines remain zero.
|
||
|
||
Issue 1704 revised the budget for this checkpoint for three explicit reasons: capture-pipeline
|
||
reliability through fenced ownership and exact recovery, durable cloud log streaming that did not
|
||
exist in the baseline, and a reusable durable-capture substrate. This is not a new observability
|
||
layer: the CLI-observable `logs` behavior remains parity work. The budget stays cumulative from
|
||
`44c298d7f`; the exact clean post-revision measurement is the reviewed upper bound for this checkpoint,
|
||
not a reusable allowance for future units:
|
||
|
||
| Metric | Original baseline | Revised cumulative bound / checkpoint | Change |
|
||
| ------------------ | ----------------: | ------------------------------------: | ------------------: |
|
||
| Raw JavaScript | 2,036,067 B | 2,131,689 B | +95,622 B (+4.696%) |
|
||
| Gzipped JavaScript | 659,646 B | 695,134 B | +35,488 B (+5.380%) |
|
||
| npm tarball | 797,027 B | 826,761 B | +29,734 B (+3.731%) |
|
||
| npm unpacked | 2,781,186 B | 2,878,492 B | +97,306 B (+3.499%) |
|
||
|
||
The controlled 15-run startup medians showed no regression (`--version` 94.5 ms to 47.4 ms;
|
||
`--help` 91.2 ms to 72.0 ms). Module-level source-map inspection found no duplicate implementation
|
||
emission; the distribution cost is the accepted reliability/cloud/substrate decision above. Future
|
||
units must define and review their own cumulative budget rather than inheriting this headroom.
|
||
|
||
The recordings command unit has its own reviewed budget (2026-08-11). The cumulative denominator
|
||
remains the original `44c298d7f` baseline; rebasing onto the completed checkpoint does not reset it.
|
||
The immediate stack-base delta from durable-capture head `e3b0956b` is reported separately so the
|
||
cost of this command unit stays visible. The exact #1724 head that reproduces this table is recorded
|
||
in the acceptance comment before readiness. The increase pays for runtime-owned screen-recording
|
||
transports, fenced artifact finalization and cross-daemon recovery, and provider parity. It is not
|
||
unused checkpoint headroom and is not an allowance for a later command:
|
||
|
||
| Metric | Original baseline | Recording bound | Cumulative change | Recording-only change |
|
||
| ------------------ | ----------------: | --------------: | -------------------: | --------------------: |
|
||
| Raw JavaScript | 2,036,067 B | 2,166,159 B | +130,092 B (+6.389%) | +32,026 B (+1.501%) |
|
||
| Gzipped JavaScript | 659,646 B | 708,776 B | +49,130 B (+7.448%) | +12,902 B (+1.854%) |
|
||
| npm tarball | 797,027 B | 836,426 B | +39,399 B (+4.943%) | +8,987 B (+1.086%) |
|
||
| npm unpacked | 2,781,186 B | 2,913,430 B | +132,244 B (+4.755%) | +32,494 B (+1.128%) |
|
||
|
||
These bounds admit only the completed recordings cutover. Every subsequent command unit must define
|
||
and review both its original-baseline cumulative bound and its immediate stack-base delta.
|
||
|
||
The tracking issue owns command order, PR/file lists, test-only compatibility fixtures, exact
|
||
benchmark commands and thresholds, raw evidence, and reviewers. Temporary fixtures never authorize
|
||
a production bridge, duplicate route, or recorded package back-import. After the checkpoint, this
|
||
ADR's status records its outcome; it does not retain a migration diary.
|
||
|
||
### 8. Broader migration is move-dominated and evidence-tiered
|
||
|
||
The checkpoint and recordings budgets paid for machinery that later units reuse; they are not a
|
||
precedent for growth. Every post-checkpoint unit still defines its original-baseline cumulative
|
||
bound and immediate stack-base delta, but the default expectation inverts: a unit relocates
|
||
existing platform mechanics from root `src/` into platform packages and deletes superseded daemon
|
||
code, so its shipped-size delta trends to zero or negative. The unit's review reports root bytes
|
||
removed against package bytes added; net growth is exceptional and each contributing addition is
|
||
named and justified individually. Contract modules stay vocabulary-thin under the existing
|
||
capture-kit rule; per-unit type inflation is size growth and is reviewed as such.
|
||
|
||
Raw per-unit parity, planted-red, and size evidence belongs in #1739 and its PRs; this ADR retains
|
||
the decision, the evidence tiers, and the rule that each unit must justify exceptional growth
|
||
without treating checkpoint budget as an allowance.
|
||
|
||
A command surface that is deprecated and scheduled for removal at the next major is never
|
||
migrated. It stays on its legacy execution shape until the major deletes it, and the deletion — not
|
||
a migration — retires its platform coupling. Where a command's public surface is being narrowed,
|
||
the narrowing merges first so the migration denominator is the surviving surface only.
|
||
|
||
Evidence requirements follow what a unit imports, in two tiers. A **request-scoped device unit**
|
||
uses facts and operations only: its evidence is the typed use declaration, fact coverage for every
|
||
denominator cell, the enumerated legacy-parity cell table recorded in the unit review, and the
|
||
cutover gate. It must not import admission ledgers, fences, durable descriptors, or recovery
|
||
adapters, and their evidence items do not apply to it. A **durable-resource unit** additionally
|
||
carries the full section 4–5 lifecycle evidence. The tier is declared in the unit review; importing
|
||
durable machinery promotes the unit to the durable tier.
|
||
|
||
The per-command cutover gates consolidate into one parametrized runtime-command-cutover gate driven
|
||
by a table of migrated commands. Adding a unit adds a row; the parametrized gate carries one
|
||
planted-red violation for the mechanism, not one per row. The four existing per-command policy
|
||
files fold into it in the first post-amendment unit that would otherwise add a fifth.
|
||
|
||
Facts are the only support authority for a migrated command. The unit deletes the command's
|
||
descriptor capability bucket and its `requireCommandSupported` wiring together with the legacy
|
||
adapter; retaining both is a dual admission path and fails the cutover gate.
|
||
|
||
### 9. Binding ergonomics: one bind per handler
|
||
|
||
The gateway exposes side-effect-free facts inspection for an ownership-resolved device. It answers
|
||
admission, `capabilities`, and doctor questions without creating a request binding, acquiring
|
||
helpers, or touching the device; it shares the owner's lazy loading. A handler binds exactly once,
|
||
with the execution use its plan selected. The admission-use idiom — binding with an empty required
|
||
set to read facts — is retired for new units: they may not introduce admission-only binds. Moving
|
||
an already-migrated handler onto facts inspection changes production admission behavior; it is its
|
||
own independently reviewed unit with parity evidence, never a side effect of a structural change.
|
||
|
||
Use declarations share one neutral `defineUse`; per-domain currying wrappers add a module per
|
||
domain for no information and are not added. Required-only declarations are the default. Declaring
|
||
a preferred operation requires a recorded measurement of the fast path's benefit in the unit
|
||
review; the direct-selector fast path is the model. A preferred operation declared without a
|
||
measurement is speculative surface and is rejected in review.
|
||
|
||
### 10. Process-lifetime and cross-cutting surfaces
|
||
|
||
A cross-cutting facet — one consumed from more than one command's execution path, such as snapshot
|
||
freshness, the system-chrome guard, presentation rules, or runtime hints — lands inside the first
|
||
command unit that consumes it and is named in that unit's review. Its contract has one owner. Later
|
||
units consume the existing facet; forking a parallel contract for the same concern is a seam
|
||
violation. A facet PR with no consuming command remains prohibited under section 6.
|
||
|
||
Daemon startup gains no per-platform entrypoints. Startup recovery is evidence-gated: persisted
|
||
durable manifests and platform-owned markers are the only triggers, and a family's mechanics load
|
||
only when its evidence exists — preserving implementation laziness because an evidence-free family
|
||
is never evaluated. The app-log recovery path is the model. Remaining platform-specific startup
|
||
recovery (the Android test-IME marker, web browser orphans) moves onto this pattern when its owning
|
||
domain migrates, not before.
|
||
|
||
Daemon shutdown is two-phase through the gateway. The detach phase runs before daemon session
|
||
teardown and offers each process-lived helper manager its handoff policy — a healthy kept-hot
|
||
helper may transfer to a successor daemon rather than stop. The stop phase runs after session
|
||
resources are finalized and terminates each still-owned generation at most once, per section 4.
|
||
Platform-specific shutdown calls in the daemon runtime are deleted as their owners migrate onto the
|
||
phases.
|
||
|
||
Session teardown keeps its isolated-step skeleton and gains no generic hook API. Each remaining
|
||
platform-specific step belongs to a domain and is deleted by that domain's unit: perf steps by the
|
||
perf unit, test-IME restoration by the input domain as durable device state with marker-based
|
||
recovery, helper stops by the owning platform module's device-lifecycle policy, close-time alert
|
||
dismissal by the close unit. A teardown step that survives its domain's cutover is a migration gap.
|
||
|
||
As a daemon area loses its last platform import, the same unit narrows the R3 platforms-seam
|
||
allowlist to match; the seam list may never grow. Seam removal alone is not the end-state
|
||
enforcement: R3 tolerates dynamic and type-only platform imports, and legacy daemon behavior
|
||
reaches platforms through exactly those edges. The end state is a dedicated planted-red rule
|
||
rejecting every dependency edge — static, dynamic, re-export, and type-only — from production
|
||
`src/daemon/**` modules (test files excluded, matching the layering scanner's scope) to
|
||
`src/platforms/**` and to concrete `@agent-device/platform-*` packages; daemon types come from
|
||
contracts only. Once that rule is green with `src/daemon/` removed from the seam, daemon
|
||
platform-freedom is structurally enforced rather than measured.
|
||
|
||
## Relationship to prior decisions
|
||
|
||
- [ADR 0001](0001-provider-first-integration-scenarios.md): provider-first scenario coverage and
|
||
semantic provider operations remain. The monolithic `Interactor` and request callback-stack
|
||
topology are superseded as commands migrate.
|
||
- [ADR 0002](0002-persistent-platform-helper-sessions.md): persistent-helper protocols,
|
||
invalidation, and explicitly safe one-shot fallback remain. Daemon ownership means lifetime
|
||
policy; platform modules own helper managers/mechanics, and request bindings never own helpers.
|
||
- [ADR 0003](0003-daemon-command-registry.md) and
|
||
[ADR 0007](0007-remote-device-leases.md): daemon request-policy traits, lease admission, and lock
|
||
ordering remain daemon-owned. Binding happens only after their admission requirements are met.
|
||
- [ADR 0008](0008-command-descriptor-registry.md): the descriptor registry remains the command root.
|
||
Device-command capability buckets evolve into typed required/preferred runtime use joined with
|
||
exact runtime facts; inventory commands declare inventory use.
|
||
- [ADR 0009](0009-apple-platform-consolidation.md): the Apple family and `AppleOS` leaf axis remain.
|
||
The shallow `PlatformPlugin` shape is superseded as command units migrate; physical shared
|
||
mechanics move only through the legal injected substrate transition or after their last legacy
|
||
consumer.
|
||
- [ADR 0010](0010-error-system.md): normalized errors remain; this ADR adds operation/cleanup
|
||
precedence and typed reattachment outcomes.
|
||
- [ADR 0011](0011-interaction-guarantee-contract.md),
|
||
[ADR 0013](0013-unified-gesture-plans.md), and
|
||
[ADR 0014](0014-session-ref-frame-lifetime.md): interaction paths stay distinct, normalized gesture
|
||
plans cross the seam, and daemon authorization expires immediately before any possibly mutating
|
||
facet call. Platform implementations may not hide UI mutation inside an observational facet.
|
||
- [ADR 0018](0018-unified-event-journal.md) is proposed, not a dependency of this decision. If
|
||
accepted, its journal and teardown scopes may implement this ADR's observability contexts, while
|
||
remaining observation rather than coordination or persisted recovery state.
|
||
|
||
## Consequences
|
||
|
||
The daemon keeps policy, command semantics, response construction, session/lease ownership,
|
||
selector/ref authority, interaction guarantees, and artifact orchestration. Platform packages gain
|
||
locality for native mechanics, provider transport composition, persistent helpers, and resource
|
||
recovery. Adding a platform implements existing runtime contracts and facts instead of adding daemon
|
||
tags, maps, callbacks, and branches.
|
||
|
||
The cost is a staged cross-repository migration with temporarily coexisting legacy,
|
||
inventory-backed, and device-runtime-backed commands. Command-atomic cutover, explicit deletion
|
||
boundaries, and the early adoption checkpoint keep that coexistence shippable and abandonment-safe.
|
||
|
||
## Alternatives considered
|
||
|
||
- **Move all platform source into packages first:** rejected. It changes physical ownership before
|
||
proving the behavior seam, creates large root-import tunnels, and cannot be reviewed or abandoned
|
||
command by command.
|
||
- **Keep extending `PlatformPlugin` with tags and predicates:** rejected. It relocates selection data
|
||
but leaves the daemon mapping every platform tag back to behavior.
|
||
- **One replacement `Interactor`, universal dispatcher, or `SystemFacet`:** rejected. Platform leaf
|
||
support differs by operation, and a wide optional interface recreates unsupported stubs and
|
||
command-aware platform code.
|
||
- **Provider/local or old/new execution fallback:** rejected. Provider ownership and command cutover
|
||
must be exact; success-path fallback hides semantic divergence. Explicitly accepted internal helper
|
||
fallbacks remain governed by ADR 0002.
|
||
- **Use session events or a future event journal as the durable-resource store:** rejected. It would
|
||
make correctness depend on observability; current events are projections, and proposed ADR 0018
|
||
explicitly preserves the same no-state-from-events rule.
|
||
- **A separate process-local resource ledger beside `SessionState` for live handles:** rejected. The
|
||
session store is in-memory, so live `SessionState` already is the process-local home; a parallel
|
||
handle/descriptor store would duplicate the ownership its R7 transition owner governs. The accepted
|
||
admission ledger is narrower: it holds only bounded cleanup-block evidence, contains no live handle
|
||
or descriptor, and never replaces the authoritative manifest.
|
||
- **Rely only on performance thresholds for lazy loading:** rejected. Thresholds catch regressions
|
||
late and can pass while unrelated implementation graphs load; the import/evaluation shape is also
|
||
contract-tested.
|