Adds an opt-in dependency supervisor inside the Host Agent that probes,
spawns, and restarts the two local processes the macOS single-machine
real-device workflow depends on: the Appium server (gates Driver.connect())
and the local Runtime API (local inspection). Default-off; gated by
HOST_AGENT_DEPENDENCY_SUPERVISOR_ENABLED plus per-dependency *_SUPERVISED
flags.
Mitigates the live-incident failure mode where forgetting to start Appium
silently keeps devices offline and tasks queued forever with no error
surfaced in Host Agent logs.
Behavior (per openspec change):
- Adopt-don't-fight: probe (TCP + dependency-specific HTTP health check)
before spawn. Healthy listener → adopted (never killed/restarted).
Unhealthy listener → port-conflict error, skip. No listener → spawn.
- Only supervisor-spawned processes are restarted on crash, with capped
exponential backoff (1s/2s/4s/8s, capped at 30s) and a per-process-lifetime
attempt ceiling (HOST_AGENT_DEPENDENCY_RESTART_MAX_ATTEMPTS, default 5).
- Spawn failures (e.g. missing executable) logged distinctly from crashes.
- Graceful stop terminates only spawned children; adopted processes untouched.
- Supervisor starts before the heartbeat loop's first connect_devices() pass
and stops alongside existing heartbeat/console teardown.
Validation: ruff check + format clean, compileall clean, openspec validate
--strict valid. Non-integration suite 503 passed / 44 deselected / 2 failed
(both failures pre-existing from unrelated 03c7c30 LLM_PROVIDER_ENC_KEY;
verified by stashing this change). macOS real-device manual verification
(task 6.4) deferred to a macOS host.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-07-14
|
||||
@@ -0,0 +1,74 @@
|
||||
## Context
|
||||
|
||||
The Host Agent (`apps/device-host-agent/host_agent/`) is a headless outbound worker: `HostAgentApplication.run_async()` (`app.py:41-90`) runs a heartbeat loop and a claim/execute loop, and (since `host-agent-local-console`) an embedded local web console bound to `127.0.0.1:8765` by default. None of this touches Appium or the local Runtime API.
|
||||
|
||||
Real device control depends on two processes the Host Agent does not manage today:
|
||||
- **Appium server**, default `http://127.0.0.1:4723` (`docs/MACOS_IPHONE_SETUP.md:209`, `driver/wda_driver.py:12`, `driver/android_driver.py:22`). `Driver.connect()` opens a `webdriver.Remote` session against it; on failure it raises `DeviceOfflineError`, which `DeviceManager.connect()` (`device/manager.py:68-108`) catches and turns into device status `"offline"`/`"error"` — this is exactly what happened in the live incident that motivated this change: Appium wasn't running, the device stayed offline, and every assigned task queued forever with no error surfaced anywhere.
|
||||
- **Local Runtime API** (`api.rest:create_app`, default `127.0.0.1:8000`), a fully separate FastAPI app/process used to inspect device/task/world state locally. It shares no runtime coupling with the Host Agent process today ("Runtime 不依赖/打包 cloud 或 application" — established repo convention) and this change does not introduce any.
|
||||
|
||||
Appium already owns the entire WDA session lifecycle end to end (`docs/MACOS_IPHONE_SETUP.md:176`, `:455` — "本项目会让 Appium 的 XCUITest Driver 创建和管理 WDA session"; "常规单机使用优先让 Appium 管理 WDA,不要一开始就引入 iproxy 或手工 WDA 生命周期"). Any supervision the Host Agent adds must sit *outside* that boundary — it manages whether the Appium **process** is running, never what Appium does internally with WDA.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Let the Host Agent optionally start Appium and the local Runtime API as plain subprocesses when they aren't already running, so a forgotten manual step turns into "it just works" instead of "device silently offline forever."
|
||||
- Make the current state (spawned vs. adopted-existing vs. failed-to-start) visible in the Host Agent's own log/console, so this remains debuggable rather than adding a second silent-failure mode on top of the one it fixes.
|
||||
- Never conflict with an already-running instance: detect and adopt (read-only) rather than double-spawn or steal a port.
|
||||
|
||||
**Non-Goals:**
|
||||
- No change to Appium's ownership of WDA session lifecycle — the supervisor never touches WDA directly, never passes `webDriverAgentUrl`, never signs/builds anything.
|
||||
- No Docker/Compose story. `compose.yaml`/`compose.deploy.yaml` are untouched; this is native-process-only, scoped to the macOS single-machine real-device workflow described in `docs/MACOS_IPHONE_SETUP.md`.
|
||||
- No change to `driver/registry.py`, `driver/wda_driver.py`, `driver/android_driver.py`, or the outbound cloud protocol (`host-agent-protocol`).
|
||||
- No process supervision beyond these two known dependencies (not a general-purpose process manager).
|
||||
- No remote/cloud configuration of this feature — it's a local opt-in, matching how `host_agent/config.py` is already entirely env-var driven per-Host.
|
||||
|
||||
## Decisions
|
||||
|
||||
**1. Default: entire supervisor is off (`HOST_AGENT_DEPENDENCY_SUPERVISOR_ENABLED=false`).**
|
||||
Unlike `host-agent-local-console` (which is now forced-on because it's a passive, loopback-bound, low-risk read/manage surface), this feature actively spawns and can restart external processes — including, in the crash-restart path, potentially interrupting an in-flight Appium session/WDA connection if a spawned Appium process is killed and restarted mid-task. That's a materially different risk profile from serving a status page, so it defaults off and is opt-in per Host, consistent with how `AI_PLANNER_ENABLED` defaulting decisions in this repo have been made deliberately per-surface rather than blanket. Each of the two dependencies (Appium, Runtime) also has its own enable flag nested under the top-level flag, so an operator can supervise Appium only, Runtime only, or both.
|
||||
|
||||
**2. Adopt-don't-fight: TCP connect + HTTP health probe before spawn.**
|
||||
Before spawning either process, the supervisor does a plain TCP connect to `(host, port)`. If something answers, it issues the same health check used post-spawn (Appium: `GET /status` expects HTTP 200 with `ready`/build JSON per `docs/MACOS_IPHONE_SETUP.md:218`; Runtime: `GET /health` or equivalent existing endpoint on `api.rest`). If the health check passes, the supervisor logs "adopted existing instance at host:port" and does **not** spawn a subprocess, does **not** track it as "ours," and will never kill or restart it. If the port is occupied but the health check fails (something else is listening, or it's an unhealthy/half-started instance), the supervisor logs an error for that dependency and leaves it un-started rather than guessing — port conflicts get surfaced, not silently resolved. This directly implements the concern raised during proposal review that an already-running manually-started Appium must not be duplicated or fought over.
|
||||
|
||||
**3. Only supervisor-spawned processes are restarted; adopted processes are never touched.**
|
||||
This is the direct mitigation for the WDA-session-interruption risk: an operator who already has their own long-lived Appium running (e.g., manually, or from a previous supervised run in another terminal) is never at risk of the Host Agent killing/restarting it. Restart-on-crash only applies to a `subprocess.Popen` handle the supervisor itself holds.
|
||||
|
||||
**4. Crash-restart uses capped exponential backoff with a per-run attempt ceiling.**
|
||||
Backoff schedule (e.g. 1s, 2s, 4s, 8s, capped at 30s) with a max attempt count per dependency per Host Agent process lifetime (e.g. 5); once exhausted, the supervisor logs that it's giving up on that dependency and stops trying, rather than restart-looping forever. This directly answers the "限流/退避避免死循环重启风暴" concern. The counter resets only on a fresh Host Agent process start, not periodically — a persistently-crashing Appium install shouldn't be retried indefinitely in the background.
|
||||
|
||||
**5. Spawn mechanism: stdlib `subprocess.Popen`, no new dependency.**
|
||||
Appium is invoked as `appium --address <host> --port <port>` (assumes `appium` is already on `PATH`, exactly as the manual instructions already require — this change does not install or vendor Appium). Runtime is invoked as the existing `uvicorn api.rest:create_app --factory --host <host> --port <port>` command, run as a subprocess rather than in-process, preserving the existing "Runtime is a fully separate process" boundary — this is process supervision, not a code-level dependency between the two packages. Both children's stdout/stderr are captured and forwarded into the Host Agent's own logging (prefixed per-process) so a crash's root cause doesn't require the operator to have had a spare terminal open.
|
||||
|
||||
**6. Config fields follow the existing `HostAgentConfig`/`HOST_AGENT_*` convention.**
|
||||
New fields on `HostAgentConfig` (mirroring the `console_*` fields added by `host-agent-local-console`):
|
||||
- `dependency_supervisor_enabled: bool` ← `HOST_AGENT_DEPENDENCY_SUPERVISOR_ENABLED` (default `false`)
|
||||
- `appium_supervised: bool` ← `HOST_AGENT_APPIUM_SUPERVISED` (default `false`)
|
||||
- `appium_host: str` ← `HOST_AGENT_APPIUM_HOST` (default `127.0.0.1`)
|
||||
- `appium_port: int` ← `HOST_AGENT_APPIUM_PORT` (default `4723`, matching the documented default)
|
||||
- `runtime_supervised: bool` ← `HOST_AGENT_RUNTIME_SUPERVISED` (default `false`)
|
||||
- `runtime_host: str` ← `HOST_AGENT_RUNTIME_HOST` (default `127.0.0.1`)
|
||||
- `runtime_port: int` ← `HOST_AGENT_RUNTIME_PORT` (default `8000`, matching the documented default)
|
||||
- `dependency_restart_max_attempts: int` ← `HOST_AGENT_DEPENDENCY_RESTART_MAX_ATTEMPTS` (default `5`)
|
||||
|
||||
**7. Wiring point: alongside the existing heartbeat/claim loop, not inside it.**
|
||||
`HostAgentApplication` starts the supervisor (if enabled) before starting the heartbeat loop and `connect_devices()` pass, and stops it during shutdown alongside the existing console/heartbeat teardown in `run_async()` — so a supervised Appium is up before the first `DeviceManager.connect()` attempt in the same startup sequence that already exists.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[Risk] A supervisor-spawned Appium restart mid-task interrupts an active WDA session, failing whatever task is running.** → Mitigation: restart only triggers on the spawned process actually exiting (crash), not on a health-check hiccup; the underlying task still fails cleanly through the existing `DeviceOfflineError` path (same as today when Appium isn't running at all) rather than in some new/undefined way. This is a pre-existing failure mode (Appium can already crash unsupervised) — the difference is the Host Agent now also tries to bring it back instead of leaving the device offline until a human notices.
|
||||
- **[Risk] Supervising the Runtime API blurs the "fully independent process" boundary this repo has deliberately maintained.** → Mitigation: it's spawned as a subprocess via its existing CLI entry point, not imported/mounted in-process — no new Python-level dependency from `device-host-agent` onto `api.rest`, no shared code path. The boundary that matters (no import coupling, no shared FastAPI app) is preserved; only process *lifecycle* (start/stop timing) is now optionally shared.
|
||||
- **[Risk] `appium` not on `PATH`, or wrong Appium driver installed (per `docs/MACOS_IPHONE_SETUP.md` xcuitest/uiautomator2 driver setup).** → Mitigation: spawn failure (e.g. `FileNotFoundError` from `subprocess.Popen`) is logged clearly as a dependency-supervisor error, distinct from a normal Appium crash, so the operator isn't left thinking Appium crashed when it never started.
|
||||
- **[Risk] Health-check false positive: something unrelated happens to be listening on 4723/8000.** → Mitigation: the health check requires the actual expected response shape (Appium `/status` JSON, Runtime health endpoint), not just "port is open" — an unrelated listener fails the check and is treated as a conflict, not adopted.
|
||||
- **[Trade-off] This only covers macOS native real-device usage; Docker/CI paths get no benefit.** → Accepted per proposal scope — those paths don't have this failure mode in the first place (no interactive multi-terminal manual step to forget).
|
||||
|
||||
## Migration Plan
|
||||
|
||||
- Fully additive and opt-in: existing Host Agent deployments/configs are unaffected with zero config changes (all new fields default to off/disabled).
|
||||
- Rollout is per-Host: an operator turns it on by setting `HOST_AGENT_DEPENDENCY_SUPERVISOR_ENABLED=true` (and the relevant `*_SUPERVISED` flags) in their existing `.env`/environment, no schema or protocol changes involved.
|
||||
- Rollback: unset the env vars (or set back to `false`) and restart the Host Agent — reverts to today's fully-manual behavior with no residual state (the supervisor holds no persistent storage).
|
||||
- Documentation: `docs/MACOS_IPHONE_SETUP.md` gains a section presenting supervised mode as an optional alternative alongside the existing manual steps, which remain the documented default path.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should `dependency_restart_max_attempts` exhaustion eventually surface in the local console (`host-agent-local-console`) status page rather than only in logs? Left for a future change if operators find log-only insufficient in practice.
|
||||
- Android/UiAutomator2 real-device validation for this supervisor is out of scope for the same reason `android-driver` itself shipped without real-device validation — should be revisited once Android hardware validation happens.
|
||||
@@ -0,0 +1,28 @@
|
||||
## Why
|
||||
|
||||
On the macOS native real-device setup (`docs/MACOS_IPHONE_SETUP.md`), a device only reports `idle`/online to the Cloud Control Plane if `DeviceManager.connect()` (`device/manager.py:68-108`) can actually establish an Appium session — but the Host Agent process (`apps/device-host-agent/host_agent/app.py`) never starts Appium itself; the operator must run `appium --address 127.0.0.1 --port 4723` in a separate terminal before starting `device-host-agent`, and separately run the local Runtime API (`uvicorn api.rest:create_app --factory ...`) if they want to inspect device/task state during local debugging. Forgetting (or losing) either process is invisible from the Host Agent's own output: heartbeats keep succeeding, so the Host looks "connected," while the device silently stays `offline` and every assigned task stays queued forever with no error. This was diagnosed live from exactly that symptom. Having the Host Agent supervise these known local dependencies for the single-machine dev/real-device workflow removes an easy-to-miss manual step and makes the failure visible instead of silent.
|
||||
|
||||
## What Changes
|
||||
|
||||
- `HostAgentApplication` gains an optional local dependency supervisor that, when enabled, starts and monitors two external processes alongside the existing heartbeat/claim loop:
|
||||
- **Appium server** (`appium --address <host> --port <port>`), whose reachability already gates real device `connect()` calls.
|
||||
- **Local Runtime API** (`uvicorn api.rest:create_app --factory --host <host> --port <port>`), used for local inspection/debugging of device and task state.
|
||||
- Before spawning either process, the supervisor probes the configured address: if something is already listening and answers the expected health check, it adopts the existing instance (log-only) instead of spawning a duplicate or rebinding the port. It only spawns a subprocess when the port is free.
|
||||
- Crash handling: if a supervised subprocess it spawned exits unexpectedly, the supervisor restarts it with capped exponential backoff and a maximum retry count per run; it does not touch a process it did not spawn (an adopted, externally-managed instance is never restarted or killed by the Host Agent).
|
||||
- New `HostAgentConfig` fields to control this, all opt-in and disabled by default (see design.md for the specific default-off rationale): enable/disable the supervisor as a whole, enable/disable each of the two dependencies independently, and host/port for each (defaulting to the existing documented conventions — Appium `127.0.0.1:4723`, Runtime `127.0.0.1:8000`).
|
||||
- Documentation update to `docs/MACOS_IPHONE_SETUP.md` describing the new opt-in supervised mode as an alternative to the existing manual multi-terminal flow (the manual flow remains fully supported and is still what's documented as the default path).
|
||||
- Explicitly out of scope: no change to WDA's own lifecycle (Appium continues to own WDA session management entirely unchanged), no Docker Compose changes (`compose.yaml`/`compose.deploy.yaml` are unaffected — this is a native-process-only capability), no change to the outbound cloud protocol.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `host-agent-dependency-supervisor`: optional, opt-in local process supervision inside the Host Agent for the Appium server and the local Runtime API — port/adoption probing, spawn, health-checked readiness, and rate-limited crash-restart, scoped to the macOS native single-machine real-device workflow.
|
||||
|
||||
### Modified Capabilities
|
||||
(none — `host-agent-protocol` outbound behavior is unchanged; `driver-registry`/`device-pool` behavior for how a `Driver.connect()` reaches Appium is unchanged, this change only affects whether Appium happens to already be running when that connect attempt occurs)
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected code: `apps/device-host-agent/host_agent/` (new supervisor module, `app.py` wiring to start/stop it alongside the heartbeat/claim loop, `config.py` new fields), `apps/device-host-agent/pyproject.toml` (no new runtime dependency expected — spawning uses stdlib `subprocess`; Runtime API is already invoked via its existing `uvicorn`/`api.rest` entry point).
|
||||
- Not affected: `cloud.*`, `apps/cloud-api`, `compose.yaml`, `compose.deploy.yaml`, `driver/wda_driver.py` / `driver/android_driver.py` connection logic, WDA's own session lifecycle (still fully owned by Appium).
|
||||
- Operational impact: opt-in only — a Host Agent with the supervisor disabled (the default) behaves exactly as it does today. When enabled, the Host Agent's own process lifecycle now indirectly affects two more local processes, which changes what "the Host Agent crashed" or "the Host Agent's log" means for local troubleshooting (see design.md for how adoption-vs-spawn is surfaced in logs to keep this legible).
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Supervisor is opt-in and disabled by default
|
||||
The Host Agent SHALL NOT start, adopt-check, or supervise Appium or the local Runtime API unless `HOST_AGENT_DEPENDENCY_SUPERVISOR_ENABLED` is explicitly set to true. Each of the two dependencies SHALL additionally have its own independent enable flag (`HOST_AGENT_APPIUM_SUPERVISED`, `HOST_AGENT_RUNTIME_SUPERVISED`), both defaulting to false.
|
||||
|
||||
#### Scenario: Default configuration behaves exactly as before
|
||||
- **WHEN** a Host Agent starts with no `HOST_AGENT_DEPENDENCY_SUPERVISOR_ENABLED` (or any related) environment variable set
|
||||
- **THEN** the Host Agent does not attempt to connect to, probe, or spawn Appium or the Runtime API, and its heartbeat/claim behavior is unchanged from before this capability existed
|
||||
|
||||
#### Scenario: Top-level flag on, individual dependency flag off
|
||||
- **WHEN** `HOST_AGENT_DEPENDENCY_SUPERVISOR_ENABLED=true` and `HOST_AGENT_APPIUM_SUPERVISED=false` (Runtime supervised is true)
|
||||
- **THEN** the Host Agent supervises only the Runtime API and does not probe, adopt, or spawn Appium
|
||||
|
||||
### Requirement: Adopt an already-running, healthy dependency instead of spawning a duplicate
|
||||
Before spawning a supervised dependency, the Host Agent SHALL attempt a TCP connection to its configured host/port, and if something is listening, SHALL perform the dependency-specific health check (Appium: HTTP GET to its status endpoint expecting a successful response; Runtime: HTTP GET to its health endpoint expecting a successful response). If the health check succeeds, the Host Agent SHALL treat the existing process as adopted, SHALL NOT spawn a subprocess for that dependency, and SHALL NOT restart or terminate the adopted process at any point in its lifecycle.
|
||||
|
||||
#### Scenario: Appium already running and healthy
|
||||
- **WHEN** Appium supervision is enabled and a healthy Appium server is already listening on the configured host/port
|
||||
- **THEN** the Host Agent logs that it adopted the existing instance and does not spawn a new Appium process
|
||||
|
||||
#### Scenario: Port occupied by something unhealthy or unrelated
|
||||
- **WHEN** a supervised dependency's port has a listener that does not pass the dependency-specific health check
|
||||
- **THEN** the Host Agent logs an error identifying the port conflict for that dependency and does not spawn a subprocess for it, and does not treat the dependency as available
|
||||
|
||||
### Requirement: Spawn supervised dependencies that are not already running
|
||||
When a dependency is enabled for supervision and no healthy instance is adopted, the Host Agent SHALL spawn it as a child process (Appium via `appium --address <host> --port <port>`; Runtime API via its existing `uvicorn api.rest:create_app --factory` entry point with the configured host/port), and SHALL forward the child process's stdout/stderr into the Host Agent's own logging, tagged by dependency name.
|
||||
|
||||
#### Scenario: Neither dependency is running at Host Agent startup
|
||||
- **WHEN** both Appium and Runtime supervision are enabled and neither has a healthy instance already listening
|
||||
- **THEN** the Host Agent spawns both as child processes before proceeding to its first device-connect attempt, and both processes' output is visible in the Host Agent's logs
|
||||
|
||||
#### Scenario: Spawn fails because the executable is missing
|
||||
- **WHEN** the Host Agent attempts to spawn Appium but `appium` is not found on `PATH`
|
||||
- **THEN** the Host Agent logs a dependency-supervisor-specific startup error naming the missing dependency, distinct from a runtime crash of an already-started process
|
||||
|
||||
### Requirement: Restart only processes the supervisor itself spawned, with bounded backoff
|
||||
The Host Agent SHALL restart a supervised dependency automatically only if the Host Agent's own child process handle for it exits unexpectedly. Restart attempts SHALL use capped exponential backoff and SHALL stop permanently for that dependency, for the remaining lifetime of the current Host Agent process, once a configured maximum attempt count (`HOST_AGENT_DEPENDENCY_RESTART_MAX_ATTEMPTS`) is reached. The Host Agent SHALL NOT restart or terminate a dependency instance it adopted rather than spawned.
|
||||
|
||||
#### Scenario: Spawned Appium process crashes
|
||||
- **WHEN** a Host Agent-spawned Appium child process exits unexpectedly and the per-dependency restart attempt count is below the configured maximum
|
||||
- **THEN** the Host Agent waits the current backoff interval and attempts to spawn Appium again
|
||||
|
||||
#### Scenario: Restart attempts exhausted
|
||||
- **WHEN** a supervised dependency has crashed and been restarted until reaching `HOST_AGENT_DEPENDENCY_RESTART_MAX_ATTEMPTS`
|
||||
- **THEN** the Host Agent logs that it has given up restarting that dependency and does not attempt to spawn it again for the rest of the current process lifetime
|
||||
|
||||
#### Scenario: Adopted process exits
|
||||
- **WHEN** a dependency instance the Host Agent adopted (did not spawn) stops running
|
||||
- **THEN** the Host Agent does not attempt to restart it, since it never held a child process handle for it
|
||||
|
||||
### Requirement: Supervisor lifecycle is tied to Host Agent process lifecycle
|
||||
The Host Agent SHALL start enabled, not-yet-healthy supervised dependencies before beginning its normal device-connect/heartbeat/claim sequence, and SHALL stop any dependency processes it spawned (not ones it adopted) during its own graceful shutdown.
|
||||
|
||||
#### Scenario: Host Agent shuts down gracefully
|
||||
- **WHEN** the Host Agent receives a shutdown signal while it holds a child process handle for a spawned Appium instance
|
||||
- **THEN** the Host Agent terminates the spawned Appium child process as part of its own shutdown sequence
|
||||
|
||||
#### Scenario: Host Agent shuts down while an adopted dependency is running
|
||||
- **WHEN** the Host Agent shuts down and Appium was adopted (not spawned) rather than spawned by this Host Agent
|
||||
- **THEN** the adopted Appium process is left running, untouched, after the Host Agent exits
|
||||
@@ -0,0 +1,47 @@
|
||||
## 1. Config
|
||||
|
||||
- [x] 1.1 Add `dependency_supervisor_enabled`, `appium_supervised`, `appium_host`, `appium_port`, `runtime_supervised`, `runtime_host`, `runtime_port`, `dependency_restart_max_attempts` fields to `HostAgentConfig` (`apps/device-host-agent/host_agent/config.py`), reading from `HOST_AGENT_DEPENDENCY_SUPERVISOR_ENABLED` / `HOST_AGENT_APPIUM_SUPERVISED` / `HOST_AGENT_APPIUM_HOST` / `HOST_AGENT_APPIUM_PORT` / `HOST_AGENT_RUNTIME_SUPERVISED` / `HOST_AGENT_RUNTIME_HOST` / `HOST_AGENT_RUNTIME_PORT` / `HOST_AGENT_DEPENDENCY_RESTART_MAX_ATTEMPTS`, all defaulting per design.md Decision 6.
|
||||
- [x] 1.2 Unit tests for the new config fields' defaults and env var parsing, following the existing pattern used for `console_*` fields in the config test module.
|
||||
|
||||
## 2. Health probes and adoption
|
||||
|
||||
- [x] 2.1 Implement a small health-check helper per dependency: TCP connect + Appium `GET /status` check, and TCP connect + Runtime API health endpoint check (reuse an existing HTTP client already available to the Host Agent rather than adding a new dependency).
|
||||
- [x] 2.2 Implement the adopt-vs-spawn decision: probe before spawn, log and mark "adopted" on a passing health check, log a port-conflict error and skip on a listening-but-unhealthy port, proceed to spawn on no listener.
|
||||
- [x] 2.3 Unit tests: adopt when healthy instance present, conflict-and-skip when unhealthy instance present, proceeds to spawn when nothing listening (mock the TCP/HTTP probe).
|
||||
|
||||
## 3. Process supervisor core
|
||||
|
||||
- [x] 3.1 Create `apps/device-host-agent/host_agent/dependency_supervisor.py` with a class managing zero or more supervised dependencies (Appium, Runtime), each described by: command, host/port, health-check callable, adopted-vs-spawned state.
|
||||
- [x] 3.2 Implement spawn via `subprocess.Popen` for Appium (`appium --address <host> --port <port>`) and Runtime (`uvicorn api.rest:create_app --factory --host <host> --port <port>`), capturing stdout/stderr and forwarding to Host Agent logging tagged by dependency name.
|
||||
- [x] 3.3 Implement post-spawn readiness wait: poll the health check until it passes or a startup timeout elapses, logging failure distinctly from a later crash.
|
||||
- [x] 3.4 Implement crash-detection + capped exponential backoff restart loop (only for spawned, not adopted, processes), stopping permanently per dependency once `dependency_restart_max_attempts` is reached, per design.md Decision 4.
|
||||
- [x] 3.5 Implement graceful stop: terminate only spawned child processes on supervisor shutdown; adopted processes are left untouched.
|
||||
- [x] 3.6 Unit tests: spawn success, spawn failure (missing executable), crash-triggers-restart-with-backoff, restart-exhaustion-gives-up, adopted-process-never-restarted-or-killed, stop-terminates-only-spawned-children.
|
||||
|
||||
## 4. Wiring into HostAgentApplication
|
||||
|
||||
- [x] 4.1 In `apps/device-host-agent/host_agent/app.py`, construct and start the dependency supervisor (if `dependency_supervisor_enabled`) before the heartbeat loop's first `connect_devices()` pass.
|
||||
- [x] 4.2 Stop the supervisor during `HostAgentApplication` shutdown/teardown alongside existing heartbeat/console teardown.
|
||||
- [x] 4.3 Integration test covering: supervisor enabled with both dependencies off (no-op, unchanged existing behavior), supervisor enabled with only Appium supervised, start/stop ordering relative to heartbeat loop.
|
||||
|
||||
## 5. Documentation
|
||||
|
||||
- [x] 5.1 Update `docs/MACOS_IPHONE_SETUP.md` to document the new opt-in supervised mode (env vars, defaults, adopt-vs-spawn behavior, restart/backoff behavior) as an alternative to the existing manual multi-terminal flow, without removing the manual instructions.
|
||||
- [x] 5.2 Update `.env.example` (if present) with the new `HOST_AGENT_*` variables, defaulted to off/disabled, matching existing `.env.example` conventions for other opt-in Host Agent features.
|
||||
|
||||
> Note: the committed `.env.example` is scoped exclusively to `compose.deploy.yaml` Cloud-side variables (enforced by `tests/test_deployment_config.py::test_example_environment_contains_no_static_credentials`), and no other `HOST_AGENT_*` variables are listed there. Documenting the new variables in `docs/MACOS_IPHONE_SETUP.md` §9 instead matches the existing convention used for all other Host Agent opt-in features (e.g. `HOST_AGENT_CONSOLE_*`, `AI_PLANNER_*`), so no `.env.example` change was made.
|
||||
|
||||
## 6. Validation
|
||||
|
||||
- [x] 6.1 Run full non-integration test suite (`uv run --all-packages pytest -m "not integration"`) and confirm no regressions.
|
||||
|
||||
Result: `503 passed, 2 failed, 44 deselected`. Both failures (`tests/test_deployment_config.py::test_deploy_compose_has_only_cloud_services_and_minimal_environment` and `::test_example_environment_contains_no_static_credentials`) are **pre-existing**, caused by unrelated commit `03c7c30 LLM_PROVIDER_ENC_KEY` (in-flight `database-llm-provider-management` work that added `CLOUD_LLM_PROVIDER_ENCRYPTION_KEY` to `.env.example` and `compose.deploy.yaml` without updating this test's allowlist). Verified by stashing this change's working tree and re-running: same 2 failures remain on baseline. The new `tests/test_dependency_supervisor.py` (19 tests) and updated `tests/test_config.py` / `tests/test_app.py` (38 combined) all pass.
|
||||
- [x] 6.2 Ruff check/format and `compileall` on changed files.
|
||||
|
||||
`ruff check` and `ruff format --check` clean on `apps/device-host-agent/host_agent/{config,dependency_supervisor,app}.py`, `apps/device-host-agent/tests/{test_config,test_dependency_supervisor,test_app}.py`. `python -m compileall -q` clean on the same set.
|
||||
- [x] 6.3 `openspec validate --strict` for this change.
|
||||
|
||||
Result: `Change 'host-agent-dependency-supervisor' is valid`.
|
||||
- [ ] 6.4 Manual verification on macOS: start Host Agent with supervisor enabled and no Appium/Runtime running, confirm both are spawned and device reaches `idle`; then start Host Agent again with an already-running Appium, confirm it's adopted (not duplicated) and logged as such.
|
||||
|
||||
**Deferred.** This session ran on Windows where the Appium/WebDriverAgent real-device workflow doesn't apply. To be executed on a macOS host per `docs/MACOS_IPHONE_SETUP.md` §9 once available.
|
||||
Reference in New Issue
Block a user