Process Supervisor Documentation
Process Supervisor Documentation
Overview
The ProcessSupervisor manages the llama-server process lifecycle, providing health monitoring, automatic restart, and graceful shutdown. It ensures the LLM service is always available for the agent.
Purpose
- Start llama-server - Launch the llama-server process with correct parameters
- Health monitoring - Periodically check process health
- Automatic restart - Restart on crash or health failure
- Graceful shutdown - Cleanly terminate on app shutdown
- Backoff handling - Exponential backoff on restart failures
Responsibilities
- Process management - Start, stop, monitor subprocess
- Health checking - HTTP health endpoint monitoring
- Watchdog loop - Continuous health monitoring
- Restart logic - Automatic recovery from failures
- Logging - Stream process output to log file
Dependencies
subprocess- Process managementhttpx- Health check requestscore/logging_config- Structured logging
Constraints
- Health check interval - 5 seconds between checks
- Health fail threshold - 3 consecutive failures triggers restart
- Timeout - 60 seconds to become healthy after start
- Backoff - Exponential: 1s → 2s → 4s → ... → 30s max
- Single instance - Only one supervisor per app
- Trust environment - httpx with
trust_env=Falseto avoid SOCKS proxy issues
Extension Points
Adding a New Health Check
If llama-server adds a new health endpoint:
- Update
is_healthy()to check new endpoint - Add fallback if old endpoint deprecated
Adding a New Restart Strategy
If you want different restart behavior:
- Modify
_watchdog_loop()restart logic - Consider: different backoff, restart limits, notifications
Module Details
core/supervisor.py - ProcessSupervisor
Manages llama-server process lifecycle:
Attributes:
_process- subprocess.Popen instance_health_fail_count- Consecutive health failures_backoff_seconds- Current backoff delay_stopping- Shutdown flag_base_url- llama-server HTTP base URL
Methods:
start()
async def start() -> None:
self._stopping = False
await self._start_process()
await self._wait_for_health(timeout=60.0) # Wait for health
self._watchdog_task = asyncio.create_task(self._watchdog_loop())
stop()
async def stop() -> None:
self._stopping = True
Cancel watchdog task
self._terminate_process() # Graceful termination
is_healthy()
async def is_healthy() -> bool:
response = await client.get(f"{base_url}/health")
return response.status_code == 200
_watchdog_loop()
async def _watchdog_loop() -> None:
while not self._stopping:
await asyncio.sleep(5) # 5s interval
if process_crashed:
needs_restart = True
reason = "process_crash"
elif health_check_failed:
self._health_fail_count += 1
if self._health_fail_count > 3:
needs_restart = True
reason = "health_fail_count"
else:
self._health_fail_count = 0
self._backoff_seconds = 1.0
if needs_restart:
log_event("SUPERVISOR_RESTART", reason)
self._terminate_process()
await asyncio.sleep(self._backoff_seconds)
self._backoff_seconds = min(self._backoff_seconds * 2, 30.0)
await self._start_process()
await self._wait_for_health(timeout=60.0)
self._health_fail_count = 0
_start_process()
async def _start_process() -> None:
if process already running:
return
log_file = logs_dir / "llama.log"
self._process = subprocess.Popen(
self._build_args(),
stdout=log_file,
stderr=subprocess.STDOUT,
text=True,
)
_build_args()
def _build_args() -> list[str]:
return [
binary_path,
"--model", model.path,
"--port", str(model.port),
"--ctx-size", str(model.ctx_size),
"--n-gpu-layers", str(model.gpu_layers),
"--batch-size", str(model.n_batch),
"--ubatch-size", str(model.n_ubatch),
"--threads", str(model.threads),
"--threads-batch", str(model.threads_batch),
"--flash-attn", model.flash_attn,
# Optional cache types
]
Usage Flow
App Startup
# app.py
supervisor = ProcessSupervisor(binary_path, model_config)
await supervisor.start()
- Starts llama-server
- Waits for health (60s timeout)
- Starts watchdog task
Agent Request
# api.py, cli.py, tui.py
if not await ctx.supervisor.is_healthy():
raise HTTPException(503, "llama-server is not healthy")
App Shutdown
# interfaces/*.py
finally:
await ctx.shutdown()
- Cancels watchdog task
- Terminates llama-server gracefully
Health Check Logic
Normal operation:
- Health check every 5s
- On success: reset fail counter to 0, backoff to 1s
Crash detected:
process.poll() is not None(exit code set)- Immediate restart with current backoff
- Log "restart_reason=process_crash"
Health failure:
- 3 consecutive failures → restart
- Log "restart_reason=health_fail_count"
- Reset counter after successful restart
Backoff progression:
- 1s → 2s → 4s → 8s → 16s → 30s (max)
Restart Scenarios
Scenario 1: Process Crashes
Time 0: Process starts
Time 5: Health check → OK
Time 10: Process crashes (segfault)
Time 10: Watchdog detects crash
Time 10: Terminate, backoff 1s
Time 11: Start new process
Time 11: Wait for health (60s)
Time 16: Health check → OK
Time 16: Reset fail counter, backoff 1s
Scenario 2: Health Check Fails
Time 0: Process starts
Time 5: Health check → OK
Time 10: Health check → FAIL (count=1)
Time 15: Health check → FAIL (count=2)
Time 20: Health check → FAIL (count=3) → RESTART
Time 20: Terminate, backoff 1s
Time 21: Start new process
...
Scenario 3: Shutdown
Time 0: shutdown() called
Time 0: Set _stopping = True
Time 0: Cancel watchdog task
Time 0: Terminate process gracefully
Time 0: Close log file
Configuration
Parameters controlled by ModelPlanner:
binary_path- llama-server executablemodel.path- GGUF modelmodel.port- HTTP portmodel.ctx_size- Context sizemodel.gpu_layers- GPU offloadingmodel.n_batch- Batch sizemodel.n_ubatch- Micro-batch sizemodel.threads- CPU threadsmodel.threads_batch- Batch threadsmodel.flash_attn- Flash attention
Logging
Log file: logs/llama.log
Events logged:
- "Starting llama-server: <args>" - Process start
- "SUPERVISOR_RESTART" - Restart event (reason, fail_count)
stdout/stderr: All llama-server output goes to log file
Error Handling
Startup Failure
Timeout (60s):
raise TimeoutError("llama-server failed to become healthy within timeout")
Health Check Failure
HTTPError:
- Returns False (treated as unhealthy)
- Triggers health fail counter increment
Network issues:
- Returns False
- May trigger restart if persistent
Testing
Key test cases:
- Process starts and becomes healthy
- Process crashes and restarts
- Health failures trigger restart
- Shutdown terminates cleanly
- Backoff progression
- Concurrent health checks
Run: python -m pytest packages/local-ai-core/tests/test_supervisor.py -v
Architecture Rules for Supervisor
- Single instance - One supervisor per app context
- Health monitoring - Always monitor process health
- Automatic recovery - Restart on crash/failure
- Graceful shutdown - Clean termination on stop
- Exponential backoff - Prevent rapid restart loops
- Log all events - Supervision events to structured logs
Integration with App Context
# app.py
supervisor = ProcessSupervisor(binary_path, model_config)
llm_client = LocalLLMClient(base_url=supervisor.base_url)
# interfaces/*.py
ctx = create_app()
await ctx.supervisor.start()
- Starts supervisor
- Waits for health
- Starts watchdog
try:
# Run agent
finally:
await ctx.shutdown()
- Cancels watchdog
- Terminates process