Skip to content

fix: Antfly lifecycle resilience — graceful MCP startup, dev doctor, Podman support#40

Merged
prosdev merged 5 commits into
mainfrom
fix/antfly-lifecycle-resilience
Jul 15, 2026
Merged

fix: Antfly lifecycle resilience — graceful MCP startup, dev doctor, Podman support#40
prosdev merged 5 commits into
mainfrom
fix/antfly-lifecycle-resilience

Conversation

@prosdev

@prosdev prosdev commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Problem

A cold or unstartable Antfly killed the MCP server during startup with a bare -32000: connection closed — no log trail, no recovery. Root causes found while diagnosing a live failure:

  • MCP startup ran Antfly auto-start, indexer init, and watcher catchup before completing the stdio handshake
  • The auto-start spawned Antfly with stdio: 'ignore', discarding the only evidence of why a start failed
  • The container fallback was Docker-only (docker info) — dead code on Podman machines
  • Three divergent copies of the lifecycle logic existed (CLI utils, MCP entry point, adapter registry); the MCP copies were missing --data-dir and port-conflict detection
  • Health checks lived only in the dev_status MCP tool — unreachable exactly when the backend is broken

Changes

  • Shared lifecycle module (packages/core/src/antfly/lifecycle.ts): one dependency-injected implementation consumed by the CLI, MCP entry point, and adapter registry. Spawned Antfly output is captured to ~/.antfly/antfly.log (rotated at 5MB)
  • BackendGate (packages/mcp-server/src/server/backend-gate.ts): the MCP handshake completes immediately; the first tool call waits for backend init; a failed init returns a legible error pointing at dev doctor and is retried on the next call (self-healing, no server restart needed)
  • dev doctor: CLI health checks — Antfly install, server reachability with port-conflict detection, embedding model, repository index — designed to work when nothing else runs
  • Podman support: container runtime detection (docker → podman) everywhere the old code assumed Docker
  • Consistent auto-start: dev search, dev refs, dev map now start the backend like dev index does
  • Antfly binary updated 0.1.0 → 0.1.2 (@antfly/sdk 0.0.14 already latest)

Review findings addressed (defense-in-depth)

Code-reviewer agent ran pre-PR: 0 CRITICAL, 4 WARNING — all fixed:

  • Model names validated against an allowlist and passed via execFileSync argv arrays (no shell) — blocks injection via dev setup --model and handles data-dir paths with spaces
  • Log fd closed in finally — the gate retries init per failed tool call, so a per-attempt leak would accumulate toward EMFILE
  • Shutdown awaits gate.waitForIdle() before closing the indexer — eliminates a use-after-close race with in-flight init; waitForIdle never triggers a new attempt
  • Init closure guarded for re-entry — retry after partial failure cannot double-init the indexer or leak a second file watcher

Testing

  • 1,819 tests passing (30 new: lifecycle orchestration via DI, real-spawn log capture, BackendGate concurrency/self-healing/idle semantics, doctor checks, model-name validation)
  • Live verification: MCP initialize handshake completes with Antfly stopped (the exact old -32000 scenario); dev doctor diagnoses a down server; dev search auto-starts Antfly 0.1.2 with output captured to the log
  • Build, typecheck, lint green; changeset + website release notes for v0.13.0 included

prosdev added 5 commits July 14, 2026 12:30
…upport

Consolidates three divergent copies of the start/recover logic (CLI utils,
MCP entry point, adapter registry) into packages/core/src/antfly/lifecycle.ts.
Spawned Antfly output is captured to ~/.antfly/antfly.log (rotated at 5MB)
instead of stdio:'ignore', and container fallbacks detect Docker or Podman.
Orchestration is dependency-injected for testability; 18 unit tests.
The stdio handshake previously waited on Antfly auto-start, indexer init,
and watcher catchup — a cold or unstartable Antfly killed the process with
a bare -32000. BackendGate starts the server immediately, makes the first
tool call wait for backend init, turns init failures into legible tool
errors pointing at dev doctor, and retries init on the next call so a
running session self-heals. The registry's recovery and the entry point's
auto-start now use the shared core lifecycle (the old copies were missing
--data-dir, Podman support, and port-conflict detection).
dev doctor diagnoses the stack from the CLI — Antfly install, server
reachability with port-conflict detection, embedding model, repository
index — and points at the captured startup log. Health checks previously
lived only in the dev_status MCP tool, which is unreachable exactly when
the backend is broken. search/refs/map now auto-start Antfly like index
does, and setup/reset are container-runtime aware (Docker or Podman).
CLI antfly utils now re-export the shared core lifecycle.
- Model names are validated against an allowlist pattern AND all
  termite pull/list calls use execFileSync argv arrays (no shell), so
  neither dev setup --model input nor ANTFLY_DATA_DIR can inject
  commands or break on paths with spaces
- startAntflyProcess closes the log fd in finally — BackendGate retries
  init on every failed tool call, so a leaked fd per attempt would
  accumulate toward EMFILE
- MCP shutdown awaits gate.waitForIdle() before closing the indexer,
  eliminating the use-after-close race with an in-flight backend init;
  waitForIdle never triggers a new init attempt
- The gated init closure guards indexer.initialize() and the file
  watcher so a retry after partial failure cannot double-init or leak
  a second watcher
- GatedToolAdapter delegates healthCheck; dev doctor memoizes container
  runtime detection (avoids up to ~20s of redundant docker/podman probes)
@prosdev prosdev merged commit a25dcc6 into main Jul 15, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant