# Architecture decisions

## M0 — Repository and verification baseline

- **Decision:** Use a `src/` package with only pydantic and httpx at runtime; pytest, mypy, and hypothesis are development-only.
  - **Rejected:** Adding an agent framework or a YAML runtime dependency. Both would hide behavior or violate the dependency budget.
  - **Reason:** The implementation itself must expose state transitions, replay, and policy checks.
- **Decision:** Enforce determinism with an AST lint in addition to code review.
  - **Rejected:** Relying on naming conventions alone.
  - **Reason:** Wall-clock, random, UUID, and fixed mobile sleeps must fail mechanically.
- **Decision:** Keep module dependencies directed as `core -> runtime`, `drivers -> runtime`, and `evals -> public runtime/core APIs`.
  - **Rejected:** Allowing low-level trace code to import drivers or evaluation code.
  - **Reason:** A directed dependency graph prevents replay and storage from becoming coupled to a specific device or model.

Remaining: only the bootstrap CLI exists at this checkpoint; run/replay commands are implemented in their owning milestones.

## M1 — Trace storage and compaction

- **Decision:** Externalize each event field whose canonical JSON exceeds 4096 bytes into a shared SHA-256 blob store, and validate every referenced blob on reads.
  - **Rejected:** Compressing inline payloads or storing blobs per run. Compression alone does not deduplicate, and per-run storage duplicates identical screens.
  - **Reason:** Content addressing gives deterministic references and natural cross-run deduplication.
- **Decision:** Compute the terminal Merkle root over all events before `run.end`, and recompute it during every completed read.
  - **Rejected:** Including `run.end` in its own root or trusting a write-time value. The former is circular; the latter misses later corruption.
  - **Reason:** The terminal event anchors the ordered history without self-reference. Root comparison is O(1) only when both roots are already available; a full integrity scan is explicitly O(n).
- **Decision:** Compact into a separate physical JSONL view containing a lossless prefix blob, restorable state, and a re-chained tail.
  - **Rejected:** Dropping old events or overwriting the original trace. Both destroy evidence and violate append-only semantics.
  - **Reason:** Logical reads reconstruct byte-identical original events, while `latest_snapshot` supports state restoration and tail-only processing.

Remaining: replay does not yet consume snapshot application state; the replay engine and its state contract arrive in M3.

## M2 — Agent loop and context engineering

- **Decision:** Budget the complete request (messages, schemas, and parameters) plus reserved output before every model call.
  - **Rejected:** Counting only the latest message or keeping a fixed number of messages. Both miss schema cost and lose history without a budget reason.
  - **Reason:** Compression is triggered by measured request cost, with a defined failure when the stable prefix alone is too large.
- **Decision:** Preserve system and initial user messages across truncate, summarize, and state strategies; record removed messages and disappeared user/app/UI entities.
  - **Rejected:** `messages[-10:]` or silent summary replacement. These destroy the goal and make safety analysis impossible.
  - **Reason:** Early-prefix stability enables caching, while explicit entity diffs make context loss observable.
- **Decision:** Handle expanding LLM summaries by dropping redundant recent history and, if necessary, binary-searching a budget-fit summary prefix.
  - **Rejected:** Assuming a summary is always smaller or increasing the test budget until it passes.
  - **Reason:** The fallback is deterministic and its additional loss is auditable.
- **Decision:** Measure common canonical request bytes across consecutive requests and report prefix-token hit rate and lost cache tokens.
  - **Rejected:** Putting timestamps or random run IDs into the system prompt.
  - **Reason:** Cacheability follows from stable bytes, not intent.

Remaining: the checked-in M2 smoke report has no compaction-triggering workload and therefore shows no strategy difference; M6 supplies repeated trajectory-level comparisons.

## M3 — Deterministic replay

- **Decision:** Replay through `Agent` with a response provider that validates each full request fingerprint before returning the recorded response.
  - **Rejected:** Copying JSONL or replaying only stored hashes. Those approaches cannot localize the first behavioral divergence.
  - **Reason:** A mismatch reports structured field paths, expected values, and actual values at the model boundary.
- **Decision:** Use a logical live clock, a recorded-timestamp replay clock, and SplitMix64 with purpose-labelled `env.rng` events.
  - **Rejected:** Wall-clock defaults, Python's process-sensitive random/module state, and UUID calls.
  - **Reason:** Every consumed timestamp and random draw is checked and reproduced across processes.
- **Decision:** Restore context from the last recorded request and resume only the physical tail for snapshot replay, including a request already issued but not yet answered.
  - **Rejected:** Treating a snapshot as a truncation point or rerunning prefix side effects.
  - **Reason:** Prefix work is skipped while the compacted physical bytes remain identical.
- **Decision:** Keep run execution serial and give each concurrent replay its own output file; blobs and source traces are read-only.
  - **Rejected:** Concurrent writes to one replay destination.
  - **Reason:** Eight-process replay verifies determinism without introducing output races.

Remaining: side-effecting tools cannot yet be replayed; M4 adds recorded no-side-effect tool adapters and crash recovery.

## M4 — Effects, exactly-once, taint, and capability

- **Decision:** Derive idempotency keys only from `(run_id, tool, canonical_args)`, persist results, and return a new `cached` tool.result for duplicate calls.
  - **Rejected:** Including call time, UUIDs, or process-local caches. These either break deduplication or allow a second effect after restart.
  - **Reason:** Duplicate calls remain auditable while the handler executes once across registry and process recovery.
- **Decision:** Write irreversible intent before execution and result only after commit; any intent/result gap blocks resume for human resolution.
  - **Rejected:** Automatic retry after an exception or crash. The external outcome is unknowable from the local trace alone.
  - **Reason:** `unknown` is an explicit safety state, and `resolve` records the operator's decision instead of hiding it.
- **Decision:** Reject every non-`none` tool at REPLAY registration and use argument/provenance-checking recorded-result adapters for replay.
  - **Rejected:** Constructing live tools with a runtime `if replay` check immediately before execution.
  - **Reason:** Unsafe effects are unconstructible by default, while replay still exercises the agent and policy boundary.
- **Decision:** Apply exact capability grants, default risk denial, taint checks, and irreversible approval before cache lookup or intent creation.
  - **Rejected:** Prompt-only injection protection and direct-argument-only taint checks.
  - **Reason:** Screen/file provenance survives nested values and subsequent model generations; a cached result does not bypass current policy.

Remaining: generic live resume refuses pending device tools until the original driver factory is supplied; completed or human-resolved traces resume through the CLI.

## M5 — Android observation and handle-bound actions

- **Decision:** Normalize uiautomator XML into path-addressed nodes and semantic handles; public actions accept handles only and derive coordinates after current-tree resolution.
  - **Rejected:** Coordinate-first tapping or visual-model clicking in the main path. Both hide element drift and lose resolution evidence.
  - **Reason:** Exact ID, ID+text, text+class, and confidence-scored fuzzy fallback produce auditable resolution failures.
- **Decision:** Preserve resource-ID colons literally and parse handles from the right for text/class; URL-encode free-form semantic components.
  - **Rejected:** Splitting every handle component on every colon, which breaks normal Android IDs such as `com.example:id/save`.
  - **Reason:** The implementation preserves the specified handle shape without ambiguous resource IDs.
- **Decision:** Diff changed nodes with all descendants and N ancestors (default 2), and measure canonical full/diff prompt tokens.
  - **Rejected:** Sending only changed nodes or repeatedly sending the full tree. The former loses semantics; the latter wastes tokens.
  - **Reason:** The XML fixture sequence reduces estimated prompt tokens by 56.98% while retaining structural context.
- **Decision:** Implement `wait_stable` as bounded hash polling with injected time and no fixed sleep in drivers.
  - **Rejected:** `time.sleep` as a stabilization strategy or an unbounded busy loop.
  - **Reason:** Stability means continuously unchanged for idle_ms; timeout and poll count both bound failure.

Remaining: adb commands and the offline backend are tested, but no physical Android device was available for device-level validation in this session.

## M6 — Repeated trajectory evaluation and judge calibration

- **Decision:** Run 20 tasks with fresh setup/backend/teardown for five iterations in each of two paired strategies, and compute per-task Wilson intervals plus exact McNemar on matched repetitions.
  - **Rejected:** One run per task or comparing unpaired mean pass rates. These hide variance and discard the paired design.
  - **Reason:** The measured baseline is 60/100 and variant is 65/100; b=0, c=5, p=0.0625 is reported as no difference at alpha=0.05.
- **Decision:** Separate trajectory assertions from the ordered failure classifier, with explicit rules for every enum value.
  - **Rejected:** Treating `run.end.status=success` as task success or collapsing all failures into unknown.
  - **Reason:** Tool errors, loops, premature termination, context loss, and injection outcomes remain mechanically distinguishable.
- **Decision:** Calibrate against 30 labeled fixtures, publish the confusion matrix and concrete errors, and gate only at kappa >= 0.7 with an additional label-provenance check.
  - **Rejected:** Raising kappa by editing cases or treating agent-curated labels as independent human annotation.
  - **Reason:** Actual kappa is 0.5902 with ten concrete errors; both statistical and operational judge gates remain disabled.

Remaining: the 30 labels are synthetic and agent-curated, not independently human-reviewed. Therefore the calibration pipeline is complete but the human-annotation requirement and M6 done status are not claimed.

## M7 — Documentation and delivery evidence

- **Decision:** Publish measured reports with their experimental limits next to the numbers, and preserve the predeclared truncate/state comparison even though p=0.0625 is close to the threshold.
  - **Rejected:** Selecting a different pair after seeing outcomes, describing fake results as device/model quality, or hiding the low judge kappa.
  - **Reason:** The honest conclusion is no significant A/B difference and no judge gate.
- **Decision:** Use JSON-compatible YAML for task/golden files and document that boundary explicitly.
  - **Rejected:** Adding PyYAML to runtime dependencies or implying support for arbitrary YAML tags and anchors.
  - **Reason:** JSON is valid YAML 1.2 and keeps the runtime within the two-dependency constraint.
- **Decision:** Keep M6 partial in STATUS while marking the documentation milestone complete only after the final three gates and CLI acceptance.
  - **Rejected:** Treating green code tests as proof of independent human annotation or physical-device validation.
  - **Reason:** Verification status and external validation dependencies are separate claims.

Remaining: independent human review, real Android validation, and credentialed live-model validation require external actors/resources and are listed in FINDINGS rather than disguised as code stubs.
