fix: Antfly lifecycle resilience — graceful MCP startup, dev doctor, Podman support#40
Merged
Merged
Conversation
…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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:stdio: 'ignore', discarding the only evidence of why a start faileddocker info) — dead code on Podman machines--data-dirand port-conflict detectiondev_statusMCP tool — unreachable exactly when the backend is brokenChanges
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)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 atdev doctorand 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 runsdev search,dev refs,dev mapnow start the backend likedev indexdoes@antfly/sdk0.0.14 already latest)Review findings addressed (defense-in-depth)
Code-reviewer agent ran pre-PR: 0 CRITICAL, 4 WARNING — all fixed:
execFileSyncargv arrays (no shell) — blocks injection viadev setup --modeland handles data-dir paths with spacesfinally— the gate retries init per failed tool call, so a per-attempt leak would accumulate towardEMFILEgate.waitForIdle()before closing the indexer — eliminates a use-after-close race with in-flight init;waitForIdlenever triggers a new attemptTesting
initializehandshake completes with Antfly stopped (the exact old-32000scenario);dev doctordiagnoses a down server;dev searchauto-starts Antfly 0.1.2 with output captured to the log