* feat(daemon): add the allocator-held device claim kind
ADR 0021 foundations, unit 2. A device an allocator-managed pool owns is held for that identity's
whole pool lifetime, not for a session, so the claim store gains a second record kind whose
principal is an installation instead of a process. Nothing writes one in production yet.
- `AllocatorHeldDeviceClaim` is a SEPARATE record at `schemaVersion: 3` with `kind: 'allocator'`:
`stateDir` + `allocator.instanceId` + `allocator.identityIncarnationId`, and no
ownerPid/ownerStartTime/ownerToken/session/workspace/abandonedAtMs — a record carrying any of
them does not decode. `DeviceClaim` and its v2 files are untouched, so an older daemon never
meets a changed process-owned record and only ever sees a v3 file for a managed identity, which
it reads as an unreadable claim record and fails closed on.
- The managed owner is DERIVED from the recorded allocator instance (`allocatorHeldClaimOwner`),
never stored, so an owner that disagrees with the claim's principal cannot exist.
- `InspectedDeviceClaim` becomes a union: the allocator member carries `allocatorClaim` and
`claim: undefined`. Every clearing surface — ownership match, abandon, stale release, the
startup sweep, acquire-path reconciliation, session close, lease expiry, the shutdown ledger,
and `processOwnsActiveDeviceClaim` — reaches for `claim`, so none of them can be written against
an allocator-held claim, and `DeviceClaimReconciler` stays typed to the process-owned record.
- New classification `allocator-held`: not stale, not owner-releasable. `device status` shows it
in the normal view with its allocator, incarnation and installation; `device release --stale`
refuses it with `allocator-held-owner`.
- The verifier gains `covered` and `incarnation-stale`. A session or transient command whose fence
names the incarnation the claim holds executes under it, acquiring and clearing nothing; a fence
for a re-provisioned identity is refused, because an incarnation is stable for that identity's
pool lifetime.
- The ORDINARY arm of the admission gate now performs a read-only inspection under every policy
but `none` and refuses `DEVICE_IN_USE` / `DEVICE_CLAIM_ALLOCATOR_HELD`: `apps` and `app-state`
boot a device through `ensureReady` exactly as a mutation would. It still acquires nothing, so
the policy/claim-file table is unchanged. That reason is deliberately outside
`DeviceClaimConflictReason`, so replay never retries it as infrastructure. The inspection reads
the record and stops (`readAllocatorHeldClaimFile`): the owner-liveness probe `inspectDeviceClaimFile`
runs costs a host process observation per binding, and this kind has no owner process to observe.
- `acquireAllocatorHeldDeviceClaim` reattaches only on the full principal and never reconciles or
supersedes an ordinary claim (a conflicting ordinary claim prevents publication, ADR 0021 §4);
`releaseAllocatorHeldClaim` takes removal proof and is the only clearing path. Both are
by-design production-unused in this unit, with a comment naming the unit that will call them.
- The record/decoder and the lock/write leaves move out of device-claims.ts into
device-claim-record.ts and device-claim-store.ts, which dissolves the
device-claims <-> device-claim-inspection type cycle and lets the allocator module share the
writer without device-claims.ts ever importing it.
`devices` still reports no `claimedBy` for an allocator-held claim: the public field is
`{ session, workspace }` and this kind has neither.
* docs: condense CONTEXT.md entries to leave room for sibling units
Wording-only tightening of 41 existing definitions. No term is added, removed or redefined, and the
five entries unit 1 added and the three unit 2 touched are left alone. The glossary ends at 11,589
of the 12,000-byte guidance budget, which leaves room for the sibling unit's vocabulary to land
without a second trim.
* fix(daemon): register device-claim-store as the claim file's atomic-publish owner
device-claims.ts no longer writes its claim file directly: it delegates every write to
writeDeviceClaim in the new device-claim-store.ts, the single writer shared by the process-owned
and allocator-held claim kinds. atomic-publish-ownership.test.ts's SIMPLE_PUBLISHERS list still
named device-claims.ts, so its source-grep for `publishFileSync` no longer matched anything and
the ownership gate failed in CI. Register device-claim-store.ts as the owner instead.
Also rename DeviceClaimRecord -> StoredDeviceClaim (and decodeDeviceClaimRecord ->
decodeStoredDeviceClaim): the shutdown ledger's own DeviceClaimRecord in daemon-shutdown-report.ts
is a different type (one row of what teardown released), and the shared name invited confusing
that unrelated type for this module's claim-file record.
* fix(daemon): extend the allocator-held admission exhaustiveness table
The rebase onto adr0021/u1-owner-kind's decideAllocatorHeldAdmission fix carried forward
ADMITTED_BY_OUTCOME_STATUS and its outcomes array from before this branch added the covered and
incarnation-stale statuses, so the table no longer covered the whole AllocatorHeldClaimAdmission
union and TS2739 caught it. Add both: covered admits, incarnation-stale does not.
* fix(daemon): fail closed on a corrupted allocator-looking claim record
readAllocatorHeldClaimFile gated on entry.allocatorClaim, which is only set once a record
decodes all the way through. A v3-schema record corrupted into also carrying a process
principal field fails decodeAllocatorHeldClaim's carriesProcessPrincipal check and
decodeStoredDeviceClaim returns null -- exactly like a record that never existed. Ordinary
admission's inspectAllocatorHeldDeviceClaim then read that as "no allocator claim" and let
observe and every other non-transient-exclusive policy proceed, against a device an
allocator may actually hold.
A record that declares schemaVersion 3 but fails to decode is not provably a non-allocator
record, so it cannot be treated as absent. Add looksLikeAllocatorHeldClaim (device-claim-
record.ts) to distinguish "declares the allocator schema, doesn't decode" from every other
kind of corruption, give it its own classification (allocator-inconsistent), and have
readAllocatorHeldClaimFile return the entry -- not null -- for it. Ordinary admission then
refuses through the existing deviceClaimConflictError path (DEVICE_IN_USE /
DEVICE_CLAIM_OWNER_UNCERTAIN), same as any other claim it cannot verify.
deviceClaimRequiresStaleInspection, deviceClaimOwnerCannotRelease and conflictReason are
exhaustive switches over DeviceClaimClassification, so the new member forced a decision at
every site rather than one that could be missed: not stale (nothing dead to surface), not
owner-releasable (no process proof exists to make), and DEVICE_CLAIM_OWNER_UNCERTAIN like
the other undecodable-record classifications, not a permanent-condition reason.
Planted-red verified: reverting readAllocatorHeldClaimFile's gate to entry?.allocatorClaim
alone makes the new regression fail with error.code 'UNKNOWN' -- admission resolves with no
error at all, exactly the silent pass-through this fixes.
* refactor(daemon): fold staleReleaseRefusalReason into a lookup table
The switch exceeded fallow's health gate at 10 cyclomatic / 31.6 CRAP once the
allocator-inconsistent case joined it. A Record<DeviceClaimClassification, string> reads as
one branch to the complexity walker instead of one per case, while TypeScript still refuses
to compile a missing key -- the same exhaustiveness guarantee a switch gave, at a fraction
of the counted complexity.
* docs: trim CONTEXT.md back under the 12,000-byte guidance budget
Main gained bytes elsewhere since this stack's own docs-condense commit landed, pushing
CONTEXT.md to 12,028. Tighten four of this stack's own entries (managed local owner, request
generation, identity incarnation, managed device allocator port) rather than touch anyone
else's; 11,947 bytes leaves headroom against the next PR that lands first.
12 KiB
Agent Device Domain Language
Canonical vocabulary for the automation domain. Use these names in code, tests, issues, and architecture notes; implementation decisions and procedures belong in ADRs and task guidance.
Language
Sessions, targets, and devices
Platform family: An internal ownership group for related automation platforms: Apple, Android, HarmonyOS, Vega, Linux, web.
Platform leaf: A concrete OS and device shape within a platform family, classified independently: iOS simulator, physical iOS, tvOS, or macOS.
Platform module: A private package owning one platform family's device mechanics, metadata, and runtime bindings.
Device inventory gateway: The platform-neutral composition of local-family and provider inventory sources.
Device runtime gateway: The platform-neutral boundary that reports runtime facts and binds an admitted device to its runtime owner.
Runtime owner: The one local platform module, managed local owner, or provider runtime selected to execute behavior for an ownership-qualified device.
Managed local owner: The exact-only runtime owner for an allocator-managed local device; it delegates automation to the device's platform module while lifecycle stays with the allocator. Avoid: Managed provider, provider runtime
Request binding: A request-lived attachment of cancellation, diagnostics, progress, and admitted context to a runtime owner.
Bound device runtime: The view returned once a request binding proves the required runtime operations.
Runtime facet: A capability-cohesive interface on a bound device runtime with semantic inputs and typed outcomes.
Runtime fact: A typed claim about behavior available for one exact platform leaf, device or backend, and provider mode.
Narrowed bound runtime: A projection exposing required facets, optional preferred facets, and no undeclared facets.
Host capability: Narrow authority given to a platform module for host execution, diagnostics, progress, or native assets.
Target: The selected automation destination, such as mobile, TV, or desktop.
Session: Daemon-owned state for one selected target and its opened app or surface.
Device key: A stable provider-scoped identity for device ownership and contention.
Device lease: Logical remote ownership of a selected device for a tenant, run, or client.
Lease provider: The remote connection source that routes and owns a device lease.
Runner lease: A mutual-exclusion guard for a platform helper process. It is not remote client ownership. Avoid: Device lease, process lease
Device claim: Host-global exclusive ownership of one local device by an open session, a sessionless mutating command, or an allocator-held claim for a managed identity.
Allocator-held claim: A device claim whose principal is an installation and an allocator identity incarnation rather than a process; sessions and commands execute under it, and only the allocator's removal proof clears it. Avoid: Stale claim, session claim, synthetic session
Device-claim policy: A command's observation, ownership, or exclusive-mutation rule.
Device-claim rule: The per-owner-kind decision at the claim gate: ordinary, allocator-held, or none. Avoid: Claim policy, device-claim policy
Managed binding fence: The ownership fence of one managed binding: requester and identity incarnation as its token, request generation as its generation.
Request generation: The per-requester monotonic number of one allocation attempt on a lane; never shared across requesters.
Identity incarnation: The allocator-issued id of one creation of a managed identity, stable for its pool lifetime and preserved across Android clean-baseline reuse; a fresh iOS identity is a new device with a new incarnation, and a different incarnation on a claimed device is a conflict. Avoid: Request generation
Human-control hold: A device-scoped pause on agent mutations during human operation.
Commands and routing
Command surface: The catalog of public command identity, interface exposure, adapter policy, and metadata across entrypoints.
Runtime use: A command's platform-neutral declaration of required operations and optional preferred fast paths.
Inventory use: An inventory command's platform-neutral declaration for composing device sources without binding one.
Daemon command registry: The daemon-side truth for route ownership and request-policy traits.
Runner command traits: Per-command classifications controlling Apple runner lifecycle and recovery behavior independently of the public command surface.
Daemon RPC protocol version: The integer used to detect breaking compatibility across the remote daemon boundary.
Version-skew invariant: Local client and daemon versions must match; only remote daemons, separately versioned helpers, persisted artifacts, and released API consumers get compatibility handling.
Interactions, selectors, and refs
Interactor: The legacy monolithic interface between dispatch and platform behavior, kept only for unmigrated commands. Avoid: New or migrated command behavior
Interaction dispatch path: One route an interaction command takes from a resolved target to device execution.
Coordinate-first resolved element activation: An Apple interaction that resolves a semantic element and activates its resolved center point without a second lookup.
Parent-owned touch point: A point that keeps the selected parent's identity while avoiding independently interactive descendants at its center.
Guarantee cell: One dispatch-path-by-guarantee classification: enforced, delegated, inapplicable, or waived.
Owned waiver: A guarantee gap with a tracking issue and explicit owner.
Delegation-on-error: A fast path that returns semantic failures to the shared path; it establishes failure-side handling, not success-path parity.
Parity table: A golden rule table consumed by both TypeScript and native tests.
Coverage manifest: A contract test's declaration of the guarantee cells it proves.
Ref frame:
The session's authorization namespace for mutating @ref targets: a frozen observation epoch and
issuance scope.
Frame expiry seam: The point just before a mutating device operation where the active ref frame becomes invalid.
Mutation admission: The decision that an active ref frame's epoch and issuance scope authorize a ref mutation.
Ref generation pin:
An optional ~s<n> suffix carrying the snapshot generation an @ref was minted from.
Deferred interaction outcome: Post-response state recording whether a mutation still needs outcome retry, stabilization, or snapshot freshness recovery.
Settled observation: An optional post-action observation that waits for a quiet UI and diffs against the pre-action tree.
Resolution disclosure: Bounded response evidence describing how an interaction target resolved, issuing no new actionable refs.
Gestures and touch
Gesture plan: Typed, platform-neutral normalization of one- or two-contact gesture intent into bounded pointer trajectories.
Android planned-touch executor: The Android boundary selecting a provider-native or instrumentation-backed executor for a normalized touch plan.
Multi-touch geometry: The centroid, span, angle, translation, scale, and rotation that construct two-contact motion.
Snapshots and capture
Raw AX node: A backend-owned accessibility value before snapshot presentation.
Snapshot acquisition: One backend attempt's raw AX nodes and attempt-level capture facts.
Snapshot producer: The acquisition component that produced a snapshot's raw tree; presentation, scope, and geometry key on the producer, never on the platform channel alone.
Presentation options: The policy input turning one snapshot acquisition into a public projection.
Snapshot policy facet: The host-side owner of neutral snapshot policy (presentation, freshness, timeout, overlay); platform acquisition supplies raw facts and a fold policy, and runner-side Swift presentation stays separate across the process boundary.
Capture hint: The acquisition-facing view of a snapshot request: the projection a backend must serve, raw traversal depth kept apart from regular presented depth, and narrowing only where the backend can prove it complete.
Regular presented-depth frontier: The acquisition boundary for an unscoped regular snapshot, measured against regular presented depth after structural wrappers collapse.
Snapshot eligibility: Membership in a presented snapshot projection, independent of current hittability.
Clip fold: The regular projection's single visibility interpreter, run inside presentation for every backend: viewport and scroll-container clipping, ancestor projection, scroll hints, collapsed depth. Platform differences enter as a fold policy, never as a backend exception.
Presented node: A wire-facing snapshot value produced at the presentation boundary.
Snapshot capture plan: An ordered set of capture backends under one shared wall-clock budget.
Snapshot quality verdict: A structured statement of capture state, backend, degradation reason, effective depth, and collapsed content.
Snapshot projection: A view of one acquired tree. Interactive is a subset of regular, and regular is a subset of raw.
Declared capture residue: A fidelity limit in acquired evidence that presentation cannot repair and must disclose.
AX-unavailable target invalidation: The Apple behavior that discards a suspect cached application target after a root AX failure.
Recording and replay
Script recording:
Session mode that captures portable actions and target evidence into a .ad script.
Avoid: Screen recording
Recorded input parameterization:
An explicit fill contract that sends literal text to the live app while storing a caller-chosen
${VAR} placeholder durably.
Open-to-destination script:
A self-contained .ad script that opens an app, reaches and verifies a destination, and leaves the
session active.
Destination guard: A selector-targeted wait near the end of an open-to-destination script verifying its ready state.
Replay script source bundle: The complete caller-resolved set of script paths and contents for one replay or test run.
Screen-recording facet: A runtime facet that starts video capture and returns a live handle and durable descriptor.
Live resource handle: Process-local authority to finish or forcibly dispose active logging, recording, or profiling.
Durable resource descriptor: Bounded, versioned identity and recovery state from which the same runtime owner can reattach.
Reattachment: A fenced recovery attempt by the descriptor's exact runtime owner returning a live handle, completed result, missing state, or typed refusal.
Maestro compatibility
Maestro program: A source-preserving typed representation of the supported Maestro Flow syntax and behavior.
Maestro observation generation: Compatibility-engine evidence since the most recent mutation; mutation invalidates it.
Providers and tests
Provider: An external adapter that owns a device runtime or contributes transport to a platform module.
Managed device allocator port: The daemon-owned interface to a managed-device allocator: obtain, hold, and give back a managed device. Avoid: Simlock client, lease provider
Cloud WebDriver runtime: A provider runtime mapping a cloud-owned Appium or WebDriver session into agent-device inventory, leases, runtime behavior, artifacts, and release.
Cloud artifact: Provider-hosted session output: video, automation logs, device logs, or dashboard links.
Daemon artifact type: An optional semantic category from the owner of a daemon-managed downloadable artifact.