VmDoctor & VmRecoveryPlanner
On 2026-07-14, limactl start conduit reported the VZ hostagent process as
"Running" within seconds — but the guest's sshd stayed unreachable for
8+ minutes with no error state on Lima's side. Polling
(sleep N && limactl shell conduit -- echo ready) did not resolve it. A direct
limactl stop -f conduit followed by limactl start conduit did — cleanly, in
about 12 seconds. conduit/vm_lifecycle.py
(swift-conduit skill)
exists so the next occurrence is diagnosed and recovered without a human staring at logs.
The real timeline
Segment screen-time above is illustrative, not a literal 120-second real-time replay.
The countdown fill represents STUCK_BOOT_THRESHOLD_S = 120 — the real constant in
vm_lifecycle.py that wait_until_ready() polls against before giving up and reporting
STUCK. This incident's actual unreachable window (8+ min) ran well past that threshold.
VmState — five values, not two
Lima's own STATUS column only ever said Running or Stopped throughout the whole
incident — Running is exactly the state the VM sat in for 8+ minutes while
unusable. VmDoctor.diagnose() classifies true usability instead of trusting that column alone:
- STOPPEDlimactl list → Stopped · terminal, non-pollingthis incident
- BOOTING_UNREACHABLERunning + SSH refused/kex-reset · not yet usablethis incident
- STUCKunreachable past 120s · only reachable via --waitthis incident
- READYRunning + SSH reachable · usablethis incident
- UNKNOWNinstance not found / limactl missing · diagnosable, not a raise
BOOTING_UNREACHABLE covers two distinct real SSH failure strings this session actually
saw — both classified identically, since from the caller's perspective the VM is equally unusable
either way (tests/test_vm_lifecycle.py, _SSH_REFUSED / _SSH_KEX_RESET fixtures):
ssh: connect to host 127.0.0.1 port 65388: Connection refused
kex_exchange_identification: read: Connection reset by peer
STUCK is only ever reached via wait_until_ready()'s polling loop, never from a single
diagnose() snapshot — a one-shot check can only ever see BOOTING_UNREACHABLE at any given
instant; only a caller who keeps polling past 120s learns the VM is actually stuck rather than
just slow. UNKNOWN covers two boring but real cases: the named instance isn't in
limactl list at all, or limactl itself is missing — both are diagnosable conditions, not
crashes (test_diagnose_limactl_itself_missing_is_unknown_not_a_raise).
The recovery arc
VmRecoveryPlanner.plan() plans this exact two-step fix — it never executes anything itself,
matching the same "planners, not executors" contract as ClaudeDesktopLinuxInstaller and
SwiftToolchainInstaller elsewhere in the same skill. The caller runs each step's
command:
- 1
limactl stop -f conduit - 2
limactl start conduit
limactl stop -f is Lima's own force-stop mechanism — safer than a raw kill -9 on the
hostagent PID, since it also cleans up the stale socket/pid/tmp files a hard kill leaves behind.
Using it
# One diagnostic snapshot
uv run conduit vm-doctor --instance conduit
# Poll until READY / STOPPED / STUCK / UNKNOWN (STUCK is only reachable this way)
uv run conduit vm-doctor --instance conduit --wait
# Print the recovery plan as JSON (a plan only — you run each step's command yourself)
uv run conduit vm-recover --instance conduit
vm-doctor exits ExitCode.EXECUTION (1) when the reported state is STUCK or
UNKNOWN, and ExitCode.OK (0) otherwise — script-friendly for wiring into a larger
autoresolve loop. 11 tests in tests/test_vm_lifecycle.py pin every state transition above, all
using fixture strings copied verbatim from this session's real limactl list / SSH-failure
output — the tests are the spec, and the spec is this incident.
Every fact on this page traces to
plugins/cwc-ios-engineering/skills/swift-conduit/{conduit/vm_lifecycle.py, conduit/models.py, conduit/cli.py, tests/test_vm_lifecycle.py, SKILL.md}.
This page documents code that ships in this repo today — it is not a design proposal.
Companion pages on this site: /indexdb, /redis,
/postgres (the same dated-snapshot discipline, applied to infrastructure
rather than a lifecycle tool). Sibling skill: subagentswift.com/concepts/conduit.