AGENTS.md routed request cancellation/progress and diagnostics to `@agent-device/capture-kit` subpaths that no package exports; both live in `@agent-device/host-kit/request` and `@agent-device/host-kit/diagnostics`. It also named `@agent-device/contracts` as an importable seam although that package publishes no root export, and claimed `src/daemon/handlers/session.ts` was over budget after that extraction already landed at 242 lines. Two ADRs carried number 0019. The hop trace has its own claims to make, so it now numbers 0023, joins the index, and keeps the links from ADR 0019 and ADR 0022. The Node floor split was undocumented: `engines.node` stays at 22.12 because CI installs the published tarball on that floor, while contributors need 22.13 for the pinned pnpm. CONTRIBUTING now says so, and installation.md names the 22.12 floor and the web backend's Node 24 requirement. Extend the agent-guidance contract to resolve every `@agent-device/*` specifier AGENTS.md names against the owning package's `exports`, root included, so neither a phantom subpath nor a phantom package root can route an agent to a module that does not exist.
68 KiB
ADR 0019: Request-Bound Platform Runtime
Status
Completed (2026-08-27). Accepted for staged adoption on 2026-08-09, with a successful checkpoint on 2026-08-10 and broader-migration governance added on 2026-08-11. The historical checkpoint and per-unit evidence in sections 7–10 explain how the transition was kept shippable; they no longer authorize or schedule migration work.
Amended 2026-08-26 while closing the Wave 6 residue: host covers host-scoped platform work through
a narrow neutral typed service, not diagnostics alone. Host diagnostics, daemon-owner resource
cleanup, and managed host tooling share the execution category because none binds a request-scoped
device runtime; their domain services remain separate rather than forming a generic host grab bag.
Completed 2026-08-27 through #2070–#2072. Every descriptor now declares none, host,
inventory, or device-runtime; capability buckets and the legacy execution shape are gone.
Production src/daemon/** has zero dependencies on concrete platform implementations, enforced by
R65. The per-command cutover table was retired after completion as required by section 6; R66 now
enforces the permanent facts-only admission and runtime-proof invariants without naming historical
routes or handler functions.
The #2082 extraction completes the physical ownership boundary: all six platform-family
implementations and their family-owned tests live behind private package exports, src/platforms
is retired, and the former R3 folder seam is gone. R13 now owns concrete platform-package import
direction and implementation laziness; the retired-platforms-zone gate rejects any attempt to
recreate the old root path.
Rules at a glance
- Daemon device-execution code depends on platform-neutral contracts. Concrete device mechanics for
the six canonical families live in private
@agent-device/platform-*packages; each family owns its implementation and family-specific tests, while shared install-source tests are root-owned undersrc/__tests__/. The root composition module and R13-governed named consumer facades are the only production static value-import sites. - 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 optimizations and fact-conditional semantic operations. 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.
- A descriptor has exactly one explicitly declared platform-execution shape:
none,host,inventory, ordevice-runtime.nonedeclares the absence of platform execution.hostdeclares host-scoped platform work through a neutral typed domain service: diagnostics, owner cleanup, or managed tooling. The command binds no device runtime as its own execution shape, and any device-runtime leg rides another command's declared use. - During adoption, the first real slices paused for an explicit continue/revise/stop checkpoint.
Later units were move-dominated: platform mechanics left root
src/for platform packages and exceptional shipped-size growth required an individual justification. - 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, a preferred operation requires a recorded measurement, and a conditional operation requires parity evidence explaining why correctness depends on the owner fact. - 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. Its private
src/platform-runtime/request-providers.ts implementation submodule owns the cross-family request
provider resolver table and wrapper ordering; only the canonical root may load it, and it remains
lazy until a request enters a provider scope. Daemon device-execution modules import the canonical
root interface or runtime contracts only.
Shared runtime interfaces and neutral data types live in @agent-device/contracts. In production,
only that composition module, its one R13-governed private implementation submodule, or the
R13-governed consumer seams under src/core/interactors/ may statically import concrete platform
package roots; approved runtime hosts use deferred or type-only root imports, and named Apple
facades expose only their governed domain seam. 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.
The Android family mechanics now live entirely in @agent-device/platform-android. Its root façade
remains metadata-only and lazy; the package exports exactly two implementation facets: ./mechanics
for named Android behavior consumed by root/core/SDK code and package-owned tests, and ./adb-host
for the host port bound only by src/platform-runtime-android-adb-host.ts. The old
src/platforms/android family tree and root shims are deleted. R13 enumerates these two facets and
rejects any additional Android subpath or daemon production import, so package closure and host
authority stay explicit rather than being restored through compatibility paths.
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.
Amendment (#2082): the substrate below the platform families. Retiring the shared
src/utilsandsrc/platformsroot surfaces (so the family trees can move behind their exports maps) forces every shared file onto a declared domain owner, and the paragraph above is amended to name that layout rather than let capture-kit absorb it:
@agent-device/host-kitowns mechanics that act on the host machine, and nothing else. Each export is one narrow capability port, not a category barrel:command(running host commands),process(observing and owning host processes),diagnostics,retry(deadline/backoff/sleep),archive(bounded extraction and byte limits),file(atomic publishes, locks, path resolution),request(request-scoped cancellation and progress),transport(line framing, lazy HTTP/body mechanics, and constant-time secret comparison), andversion(the installed version off disk). Modules undersrc/internal/are reachable only through a port, and a port may only hold mechanics a consumer of that capability needs — the eager-closure row per port is what keeps that honest.- A helper that touches no process, file, or environment is not host mechanics and does not belong here: pure record readers, config-source values, result text, memoization, async scoping, coordinate validation, and device-scope parsing live in
@agent-device/kernelbeside its other primitives.@agent-device/capture-kitowns capture, snapshot, and recording behavior — PNG tooling, screenshot density and pixel diffing, snapshot occlusion, mobile snapshot semantics, quality verdicts and backend capability tables. Snapshot behavior is capture domain, not contracts vocabulary, and host mechanics are host-kit's, not capture-kit's.@agent-device/provision-kitowns the provisioning domain — everything that gets a device and app ready to run: install-artifact acquisition (local paths, archives, guarded network downloads), host toolchain readiness probing, device boot-failure classification, and app-resolution caching. Platform packages may import provision-kit; provision-kit may not import a platform package or be imported by capture-kit (both directions planted red in the layering suite).- The enforced direction is
kernel < contracts < host-kit < capture-kit < provision-kit < platform/provider/daemon.- Contracts stays vocabulary, plan models, and pure classification with no process, filesystem, or timer mechanics (the existing planted-red gate); platform-specific parsing stays with its family package and reaches legacy callers through composition, never by a family importing another owner's internals.
Enforcement: each substrate package's exported subpaths are pinned in
package-boundaries.test.ts(widening fails the gate), the contracts mechanics gate stays planted red, and theretired-platforms-zonerule rejects every production, test, or fixture file under the formersrc/platformspath.
The Apple package root is composition-only: it exposes the inventory module, runtime module, shutdown loader, and platform plugin. Synchronous consumers use named domain facets instead of a second compatibility surface on the root.
The Apple XCUITest runner client is a durable platform-owned implementation facet colocated
inside packages/platform-apple as the src/runner/ subtree (#2040) — Apple mechanics belong to
the Apple package. R13 models the package by enumeration rather than by exception sprawl: the family
exports its root façade plus thirteen named domain/mechanics facades — ./app-lifecycle,
./app-resolution, ./debug-symbols, ./doctor, ./install-artifact, ./macos,
./perf, ./physical-device, ./runner-owner, ./runner/operations, ./simctl, ./simulator, and
./tool-provider — as well as exactly the ./runner and ./runner/test-host
subpaths. The named facades replace root-only access for synchronous domain consumers without a
broad compatibility barrel; R13 pins the exact export set and allowed consumer seams. The
./runner façade subpath is the seam through which daemon and root consumers reach runner mechanics
directly today; the host-bound runner client stays package-internal and ./runner/test-host has one
vitest installer; the facet owns its cache files and usbmux sockets (the
ambient-host rule exempts exactly that subtree), while raw process primitives stay banned — host
authority still enters through one focused injected port (AppleRunnerHost: process execution,
diagnostics, retry, probes, locks, foreground Apple tooling, physical-device control) constructed by
exactly one composition root. No current issue owns migrating the runner's direct consumers behind
the composition gateway; if such a migration retires them, the ./runner seam narrows with it, but
the facet itself is the intended ownership model, not a temporary exception.
Mechanics-facet declarations are explicit per family: the Apple runner and Android mechanics/host
facets are enumerated above, and a new family adds its own named facet only with an owning consumer
and evidence.
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 or
fact-conditional semantic operations. Commands whose use depends on normalized input first produce a
discriminated execution plan that retains literal required/preferred/conditional types. The three
operation sets are pairwise disjoint. Preferred operations may improve execution but are never
necessary for command correctness. A conditional operation is different: an owner that advertises
it must provide it because that owner's semantically complete path depends on the observation; an
owner that reports it unavailable must have a parity-proven complete path through the required
operations. This prevents a correctness-bearing owner variation from being mislabeled as an
optimization or made unconditionally required across owners without that semantic source.
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:
- resolves the exact local or provider runtime owner;
- checks every required operation and classifies each preferred and conditional operation against facts for the platform leaf, device kind/backend, and provider mode;
- creates or reuses one request binding for that ownership-qualified device;
- verifies that every required operation and every preferred or conditional operation advertised as available has a concrete facet implementation; an advertised operation with no implementation is a runtime-contract error; and
- returns a selected operation projection: required operations are non-optional, declared preferred and conditional 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 or conditional 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.
Conditional operations do not inherit that fallback rule. Their absence is valid only when the owner fact reports them unavailable and parity evidence establishes the required path for that owner cell. When the fact reports one available, omission of its implementation is a runtime-contract failure; the handler must not silently continue through a path known to lose the observation.
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:
- exactly one provider owns it: bind that provider-owned runtime;
- no provider owns it: bind the local family runtime; or
- 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 execution has one closed shape
The command descriptor declares command identity and one internal-only execution mode excluded from
public projections: none, host, device runtimeUse, or inventoryUse. A none descriptor
never binds a device, carries capability admission, owns a platform adapter, or declares a runtime
or inventory use. The mode is explicit on every descriptor; there is no registry default. ADR
0003's daemon-owned route table continues to own handler and request-policy traits independently.
During adoption, one command descriptor across its complete platform/provider denominator was the
abandonment-safe migration unit. A parametrized cutover table proved that each unit deleted its old
adapter, admission and dispatch projection before merge. Those rows were explicitly temporary and
were deleted after the final unit completed. The permanent state is smaller: the execution union
cannot express legacy, descriptors cannot carry capability admission, R66 rejects manufactured
runtime proof or restored legacy admission, and R65 rejects every daemon dependency on a concrete
platform implementation.
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 (historical)
No command migration beyond the metadata/package substrate and complete devices, logs, and
network command cutovers began until this ADR's Status recorded continue. network was 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, andnetwork; - 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-kitis 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 iskernel < 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 --runat457fafe63passed 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:layeringpassed 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 canonical composition root and its single private provider-composition implementation submodule; the historical R14 and R15 rules retained one typed route forlogsandnetworkwith 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
SessionStatefields and 29 owner claims, with all 34 fields classified. The largest type cycle is 46 against the 47-file ceiling, and the two external productiondaemon/types.tsimporters are unchanged. Checkpoint-ownedlogs/networkplatform 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 was 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.
W5 Android family accounting (PR #2117)
The W5 exact-head size report at 9fa2778 against 437465f37 reported these rounded CI values:
| Metric | Original baseline | W5 head | W5 delta |
|---|---|---|---|
| Raw JavaScript | 2.48 MB | 2.49 MB | +8.2 kB |
| Gzipped JavaScript | 835.0 kB | 831.2 kB | -3.9 kB |
| npm tarball | 958.5 kB | 959.6 kB | +1.1 kB |
| npm unpacked | 3.32 MB | 3.33 MB | +9.0 kB |
At source level, the moved Android production tree accounts for 456,492 B removed from
src/platforms/android versus 475,413 B in changed package targets, a +18,921 B relocation
delta; the new root host/facade seam adds 19,978 B. The published +9.0 kB unpacked result is the
package result, not a source-byte estimate. The increase is attributable to the named mechanics
closure, the injected root host composition, and preserving deferred helper loading while root
chunks are removed or repartitioned. The largest packed increase is the mechanics chunk; it is
offset by deletion or shrinkage of the superseded root Android chunks.
A smaller design that retained root re-export shims, a broad barrel, or ambient filesystem/process access would reduce gross source movement but violate this ADR's package-closure, no-compatibility-shim, named-facet, host-injection, and implementation-lazy constraints. This is exceptional W5 accounting, not reusable checkpoint headroom; after rebasing on #2116, the combined-family CI size result remains the authoritative current measurement.
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 deprecated for the next major was deleted rather than migrated. Where a public surface was narrowed, the narrowing merged first so the migration denominator was the surviving surface only.
Evidence requirements followed what a unit imported, in two tiers. A request-scoped device unit used facts and operations only and proved its typed declaration and every fact cell. A durable-resource unit additionally carried the full section 4–5 lifecycle evidence. These tiers remain useful review vocabulary for future runtime changes, without reviving the migration ledger.
The migration-time cutover table was retired at terminal completion. Facts are now the only support
authority for device commands, and the permanent R66 policy rejects descriptor capability buckets,
requireCommandSupported calls, and attempts to manufacture or repair narrowed runtime proof.
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.
Declaring a conditional operation instead requires a parity artifact showing both sides: the owner
cells whose semantic path depends on the operation and the owner cells whose required path remains
complete without it. A conditional declaration justified only by speed belongs under preferred;
one justified only by universal correctness belongs under required.
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 have been deleted from the daemon runtime; owners participate through those phases.
Session teardown keeps its isolated-step skeleton and has no generic hook API. Each cleanup step belongs to its domain: test-IME restoration is durable device state with marker-based recovery, helper stops follow the owning platform module's lifecycle policy, and close-time cleanup consumes neutral owner services.
R65 is the daemon-side end-state enforcement: its planted-red AST tests reject 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 concrete
@agent-device/platform-* packages. R13 governs concrete package imports across the whole tree,
while retired-platforms-zone prevents the old root seam from being recreated; platform freedom is
therefore structurally enforced rather than periodically measured.
Relationship to prior decisions
- ADR 0001: provider-first scenario coverage and
semantic provider operations remain. The monolithic
Interactorand request callback-stack topology are superseded as commands migrate. - ADR 0002: 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 and ADR 0007: daemon request-policy traits, lease admission, and lock ordering remain daemon-owned. Binding happens only after their admission requirements are met.
- ADR 0008: the descriptor registry remains the command root. Device-command capability buckets evolve into typed required/preferred/conditional runtime use joined with exact runtime facts; inventory commands declare inventory use.
- ADR 0009: the Apple family and
AppleOSleaf axis remain. The shallowPlatformPluginshape 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: normalized errors remain; this ADR adds operation/cleanup precedence and typed reattachment outcomes.
- ADR 0011, ADR 0013, and ADR 0014: 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 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 was a staged cross-repository migration with temporarily coexisting execution systems. Command-atomic cutover, explicit deletion boundaries, and the early adoption checkpoint kept that coexistence shippable and abandonment-safe; the terminal repository retains only the closed execution shapes and permanent structural policies.
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
PlatformPluginwith tags and predicates: rejected. It relocates selection data but leaves the daemon mapping every platform tag back to behavior. - One replacement
Interactor, universal dispatcher, orSystemFacet: 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
SessionStatefor live handles: rejected. The session store is in-memory, so liveSessionStatealready 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.
End state (proposed 2026-09-02, maintainer decision pending)
The targets below are proposed defaults, not an accepted commitment. No tracking issue or Status line authorizes work against them yet; they exist to give the next migration wave a numeric baseline and are open for revision or rejection.
Daemon top-level footprint. git ls-tree --name-only origin/main src/daemon/ | grep -E '\.ts$' | grep -vE '\.test\.ts$' | wc -l measured 199 production files at e624ef9d3f. Proposed
target: ≤ 60. src/daemon/** is a permanent zone under R65; the target is about what remains
there, not about retiring the directory.
Entry-to-platform hop count. Corrected 2026-09-03, re-measured for #2278 at 27a97ee619:
the counting definition, ordered chains, hop roles, and commit for this measurement are in
0023-end-state-hop-trace.md, which supersedes the numbers
below. A file-by-file re-trace at HEAD measured 41 hops for press/Android and 47/49 hops
(shared 30 plus 17/19 per arm) for snapshot/iOS, which is now a dual-arm route (in-simulator
AX bridge primary, XCTest runner fallback). The previously stated 38/29 named no ordered chain,
counting definition, or artifact and does not reproduce; treat it as superseded, not as a
second data point. src/platform-runtime.ts (the immutable registry construction) and each
platform façade's loadRuntime pairing are declared boundaries under Decision §1 and §2, not
pass-through layers — they stay in any hop count regardless of target. Everything else on the
traced path is a pass-through candidate only insofar as R13's named-facet enumeration and the
kernel < contracts < host-kit < capture-kit < provision-kit < platform/provider/daemon
direction already allow collapsing it; a hop that exists only to satisfy that direction is not
waste. The deletion test at HEAD proves a single distinct removable hop across both routes
(commands/runtime-types.ts); the ≤ 14 target derived at the earlier measurement is
superseded and not reachable without a decision to fold cross-cutting request-scope
wrappers, which is outside the traced routes' ownership. Treat 14 as a historical discussion
anchor, not as a proposed commitment.
Zones still under src/ that this ADR expects to leave, and their package status:
src/platform-runtime-*.ts(~70 files: Android/Apple/screen-recording/perf/network/app-log/ app-state/toolchain/device-inventory hosts). Mixed ownership: Android's deployment, logs, network, perf, readiness, recording, and shutdown domains already have package homes underpackages/platform-android/src/; Apple's equivalents live underpackages/platform-apple/src/. The root files that remain are either declared host-port adapters (e.g.adb-host, bound only by its named root file per Decision §1) or mechanics whose wave has not landed yet — the two are not distinguished by filename and need per-file classification before a target is set.src/recording,src/snapshot,src/snapshot-quality— migrated topackages/capture-kit/src/(2026-09-07): the whole trees moved as-is (recording/,snapshot/including itssnapshot-presentation/andsnapshot-freshness/subtrees), together with the cross-packagewarnings.test.tsregression test. The four snapshot modules fromsrc/core/(snapshot-chrome.ts,snapshot-state.ts,snapshot-tree-ingestion.ts,snapshot-node-lookup.ts, with their tests) joined the package root. External consumers (daemon, commands, platform hosts, integration tests) use package subpath exports (e.g.@agent-device/capture-kit/snapshot-lines,/recording-video,/ios-snapshot-runtime);snapshot-tree-ingestioncarries no subpath because onlysnapshot-state.ts(same package) consumes it. Three tests stayed at the root —src/core/__tests__/snapshot-state.test.tsandsnapshot-chrome-android-statusbar.test.ts(pinned to root-ownedinteraction-targetingand the android UI-hierarchy fixtures) andsrc/__tests__/snapshot-desktop-surface.test.ts(pinned to the root eager-import-closure fixtures).src/screenshot-diff/*.ts(region split/overlay/summarization/component composition) — pixel-diff computation and PNG decode/encode already call capture-kit'spng/png-worker-client; the root files remain for daemon/CLI diff-report composition. Equivalent: none (built atop already-migrated capture-kit mechanics).src/provider-device-runtime.ts,src/provider-device-runtimes.ts,src/provider-limrun-runtime.ts,src/provider-webdriver.ts. Mostly already thin: the bulk of WebDriver and Limrun provider logic lives inpackages/provider-webdriver/src/andpackages/provider-limrun/src/; these root files are the composition-time wiring, comparable in role tosrc/platform-runtime.tsitself.src/core(R13-governed consumer seams),src/commands,src/cli,src/cli-schema,src/mcp,src/client,src/sdk,src/ai-sdk,src/remote,src/request,src/metro,src/backend*.ts. Not migration targets: these are command-surface, protocol-projection, and daemon-owned zones this ADR keeps insrc/by design, not residue awaiting a package.
Neither number has an owning issue, gate, or accepted budget yet. Treat both as inputs to a maintainer decision, not as a ratchet.