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

129 lines
9.0 KiB
Markdown

# 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`](../../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`](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.