Files
callstack__agent-device/docs/adr/0022-daemon-platform-runtime-coupling.md
Michał Pierzchała b2b084d2e1 docs: fix phantom specifiers, the duplicate ADR 0019, and the Node floor (#2533)
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.
2026-09-13 10:15:57 +02:00

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

  1. Every daemon import of a root src/platform-runtime-*.ts module is classified as exactly one of three categories, and the classification is owned by the machine-readable inventory DAEMON_PLATFORM_RUNTIME_EDGES in scripts/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 (in scripts/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.
  2. Per-edge outcomes at 27a97ee619. Four edges are composition-essential and stay (daemon-runtime.ts → platform-runtime.ts and → 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 AppleSessionObservation contract 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-apple owns 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-owner and operation-host edges are retired; daemon-runtime.ts now holds only the typed PlatformOwnerLifecycle contract (src/daemon/platform-owner-lifecycle.ts), composed at the root by platform-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. The resource-cleanup edge stays, reclassified composition-essential and carrying only platformResourceCleanup.
    • Open-target planning (done, #2334) — platform-runtime-open-target.ts keeps only the neutral open plan/result surface (resolveRequestedOpenSurface, validateOpenRelaunchTarget, resolveSessionAppBundleIdForTarget); Android package resolution (resolveAndroidPackageForOpen, inferAndroidPackageAfterOpen) moved behind the Android owning seam in packages/platform-android. session-open-prepare.ts consumes surface and relaunch-target policy, session-selector-dispatch.ts consumes 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 → queryAppleRuntimeSelector is the selector seam those issues own; coordination was posted there rather than opening a second selector producer.
  3. Session state and store authority are measured as a symbol-level overlay and ratcheted on the handler-owned slice. At 27a97ee619 the overlay is 112 SessionState shape edges and 68 SessionStore authority edges repo-wide, of which 14 shape / 22 authority files are handler-owned (src/daemon/handlers/**). Gate rule R75 session-authority-overlay (reference measured via measureRatchets in scripts/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 outside src/daemon/handlers/** are owned by the logical-module declarations (R7/R10) and are not ratcheted here.

  4. 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 for press/Android and 47/49 per arm for the now dual-arm snapshot/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.

  5. Per-audit-area decisions. Apple session observation: consume the neutral AppleSessionObservation contract. 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 132ffe1da grew 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-inventory and R75 session-authority-overlay in scripts/layering/check.ts (both observed red against planted violations before acceptance).
  • R7 session-state-ownership and 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.