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.
9.0 KiB
ADR 0022: Daemon — Platform Runtime Coupling Audit and Ownership Ratchets
Status
Accepted (2026-09-06). Implements the audit deliverables of issue #2278. The classification inventory and the structural no-regrowth checks are gate-enforced; this ADR records the decisions and points at the gates rather than restating the inventory.
1. Context
The platform-package migration removed direct production dependencies from src/daemon/** to
concrete platform code (R65: zero such imports in every form). What it did not prove is that
every remaining platform-aware responsibility belongs in the daemon. At the audit baseline
(658f822c) nine daemon files held 13 production imports of root src/platform-runtime-*.ts
composition modules; at the measurement commit (27a97ee619) the count is 14 edges across the
same nine files (one file holds an import and a re-export of the same symbol).
SessionState write ownership was ratcheted (R7/R10), but broad state reads and SessionStore
authority were not. The ADR 0019 hop trace recorded routes of ~24 files with no accepted budget
or owning issue. #2278 audited all four concerns at 27a97ee619.
2. Decision
-
Every daemon import of a root
src/platform-runtime-*.tsmodule is classified as exactly one of three categories, and the classification is owned by the machine-readable inventoryDAEMON_PLATFORM_RUNTIME_EDGESinscripts/layering/daemon-platform-runtime-inventory.ts:- Composition-essential — assembling neutral runtime implementations at the process root.
- Daemon-policy-essential — request admission, session ownership, conflicts, locking, cancellation, teardown ordering, artifact publication, or response/event semantics.
- Leaked platform mechanics — platform-family operations or state that should be owned
behind a deeper runtime interface or adapter; only this category creates implementation
work (child issues below).
The gate rule R76
daemon-platform-runtime-inventory(inscripts/layering/check.ts) fails the layering check on any unclassified edge, on symbol-set drift between a file and its inventory entry, and on stale entries. Observed red against a planted unclassified import before acceptance.
-
Per-edge outcomes at
27a97ee619. Four edges are composition-essential and stay (daemon-runtime.ts→platform-runtime.tsand →platform-runtime-host-diagnostics.ts;device-claim-owner-recovery.ts→platform-runtime.ts;device-ready.ts→platform-runtime-device-ready.ts— process-root assembly of the composed gateway, host diagnostics, owner recovery, and device readiness). The remaining nine edges identified as leaked platform mechanics have these owning interfaces or follow-ups:- Apple session observation — the
AppleSessionObservationcontract supplies runner liveness/session identity and the sole-foreground-app observation to recording health, device refresh, and open-hint policy. Root composition supplies request-local inventory and its error classifier;packages/platform-appleowns the probes. The three consumers are daemon-policy-essential. Selector production and lifecycle participation stay separate. - #2333 (done) — lifecycle participation of platform resource owners. The
apple-runner-ownerandoperation-hostedges are retired;daemon-runtime.tsnow holds only the typedPlatformOwnerLifecyclecontract (src/daemon/platform-owner-lifecycle.ts), composed at the root byplatform-runtime-daemon-lifecycle.ts, which is the sole importer of the Apple runner owner, the Android snapshot-helper and Web orphan cleanups, and legacy app-log marker recovery. Theresource-cleanupedge stays, reclassified composition-essential and carrying onlyplatformResourceCleanup. - Open-target planning (done, #2334) —
platform-runtime-open-target.tskeeps only the neutral open plan/result surface (resolveRequestedOpenSurface,validateOpenRelaunchTarget,resolveSessionAppBundleIdForTarget); Android package resolution (resolveAndroidPackageForOpen,inferAndroidPackageAfterOpen) moved behind the Android owning seam inpackages/platform-android.session-open-prepare.tsconsumes surface and relaunch-target policy,session-selector-dispatch.tsconsumes the app-bundle-identity resolver; both edges are daemon-policy-essential, and the resolver stays the one construction path for the open plan. - #2273/#2274 (existing) —
direct-ios-selector.ts→queryAppleRuntimeSelectoris the selector seam those issues own; coordination was posted there rather than opening a second selector producer.
- Apple session observation — the
-
Session state and store authority are measured as a symbol-level overlay and ratcheted on the handler-owned slice. At
27a97ee619the overlay is 112SessionStateshape edges and 68SessionStoreauthority edges repo-wide, of which 14 shape / 22 authority files are handler-owned (src/daemon/handlers/**). Gate rule R75session-authority-overlay(reference measured viameasureRatchetsinscripts/layering/ratchet-reference.ts) holds the handler-owned file sets at or under the merge-base: handler code may lose files from either set, never gain them. The ratchet is added now that the edges are classified; cross-module reads outsidesrc/daemon/handlers/**are owned by the logical-module declarations (R7/R10) and are not ratcheted here. -
The entry-to-platform hop trace was re-run with hop roles (policy / orchestration / translation / adapter / pass-through + terminal) and a deletion test per pass-through/translation hop. The updated artifact is
0023-end-state-hop-trace.md: 41 hops forpress/Android and 47/49 per arm for the now dual-armsnapshot/iOS route (shared 30 + AX bridge 17 / runner fallback 19). The deletion test proves a single distinct removable hop (commands/runtime-types.ts); the request-spine guards (auth comparison, cancellation gate, idle-reap timer reset, Android freshness guard/clear) are side-calls, not hops. The earlier ≤14 target is superseded and not reachable without folding cross-cutting request-scope wrappers, which the audit does not endorse. -
Per-audit-area decisions. Apple session observation: consume the neutral
AppleSessionObservationcontract. Runtime lifecycle participation: deepen through the existing lifecycle phases, no generic hook bag (#2333). Open-target planning: separate plan/result from platform mechanics, one construction path preserved. Session state/store authority: keep the current access shape, ratchet the handler-owned slice (R75). Route depth: record the re-traced routes; collapse only the proven pass-through hops (none undertaken in this change).
3. Rationale
- The inventory is a registry, not a prose copy. #2278's 13 imports are a review inventory, not 13 presumed violations; a neutral runtime interface may be the correct owning seam. A machine-readable table with per-edge categories is the only form of the decision that the layering gate can enforce, and it is the form that stays honest as edges move: drift and staleness are failures, not warnings.
- Category 1 and 2 stay; category 3 deepens. Splitting the nine leaked edges into three child issues (instead of one refactor) keeps each independently reviewable and preserves the existing behavior each surface depends on. The selector edge is deliberately not a child of #2332: #2273/#2274 already own the selector seam, and a second producer would duplicate snapshot-producer policy.
- R75 ratchets the handler slice, not the whole overlay. The repo-wide 112/68 edge set includes owning-module reads that are correct (session lifecycle reading session state). Ratcheting the whole set would freeze legitimate movement; ratcheting the handler-owned set targets the exact regression #2278 flagged — handlers accreting state shape and store authority — and is the slice the fresh audit found unowned.
- The hop target is retired, not missed. The 23/24 routes measured at
132ffe1dagrew to 41 / 47-49 because the request-scope wrapper layer, the AgentDevice command layer, the adb host split, and the dual-arm snapshot capture all landed on the traced paths. Every retained hop now carries a documented role and, for pass-through/translation hops, a kept-depth or removable verdict; the single proven-removable hop is recorded as the only endorsed collapse.
4. Enforced by
- R76
daemon-platform-runtime-inventoryand R75session-authority-overlayinscripts/layering/check.ts(both observed red against planted violations before acceptance). - R7
session-state-ownershipand the R10 merge-base ratchet for the owning-module slice. - R65 for the concrete-platform-import ban this audit builds on.
- Child issues #2333 and #2334 landed (see §2.2); #2273/#2274 remain for the selector seam.