subagentcoworkers

.com 20 pages
2026-07-14 · real incident, real fix

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

STOPPED BOOTING_UNREACHABLE STUCK READY

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-polling
    this incident
  • BOOTING_UNREACHABLERunning + SSH refused/kex-reset · not yet usable
    this incident
  • STUCKunreachable past 120s · only reachable via --wait
    this incident
  • READYRunning + SSH reachable · usable
    this 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

stuck VM SSH refused 8+ min limactl stop -f conduit cleans stale socket/pid/tmp limactl start conduit resolved in ~12s

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. 1limactl stop -f conduit
  2. 2limactl 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.