Reliability Quick Start Guide

Reliability Quick Start Guide

New to the reliability work? Start here.

What We're Building

The agentic-sandbox orchestrates long-running AI tasks (hours to days) in isolated VMs. Right now, if the management server crashes or a VM hangs, we lose task state and waste resources. We're adding comprehensive failure handling, recovery, and observability.


The Problem (5-Minute Version)

Current System:

  • Tasks run: Pending → Staging → Provisioning → Running → Completing → Completed
  • Everything lives in memory (no persistence)
  • No timeouts (tasks can hang forever)
  • No retries (transient failures kill tasks)
  • No metrics (can't measure reliability)
  • No hang detection (wasted resources)

What Breaks: 1. Server crashes → all task state lost, orphaned VMs 2. Git clone times out → task fails (should retry) 3. Task hangs → runs forever, wastes resources 4. Storage fills up → mysterious failures 5. Can't measure: success rate, latency, error budget


The Solution (5-Minute Version)

5 Key Features:

1. Checkpoints - Save task state to disk on every transition

  • Survives server restarts
  • Can resume tasks from last checkpoint

2. Timeouts - Enforce limits at every level

  • Operation timeouts (git clone: 10m)
  • Stage timeouts (staging: 15m)
  • Task timeouts (from manifest: 24h)

3. Retries - Automatic retry with exponential backoff

  • Network failures → retry 3x
  • VM provision race → retry 2x
  • Permanent errors → fail immediately

4. Hang Detection - Monitor for stuck tasks

  • No output for 30m → warning
  • No output for 2h → auto-cancel
  • Metrics + alerts

5. Metrics - Comprehensive observability

  • Task success rate
  • Latency (p50, p95, p99)
  • Resource usage
  • Prometheus + Grafana

Getting Started (30 Minutes)

1. Read the Docs (10 min)

Read in this order:

1. This file (you're here) 2. reliability-design-summary - High-level overview 3. reliability-architecture - Visual diagrams 4. reliability-design - Full spec

2. Explore the Codebase (10 min)

Key files:

# Task orchestration (what we're improving)
management/src/orchestrator/mod.rs      # Central orchestrator
management/src/orchestrator/task.rs     # Task state machine
management/src/orchestrator/executor.rs # VM lifecycle + execution

# What needs to be added (doesn't exist yet)
management/src/orchestrator/checkpoint.rs     # NEW: State persistence
management/src/orchestrator/timeouts.rs       # NEW: Timeout enforcement
management/src/orchestrator/retry.rs          # NEW: Retry logic
management/src/orchestrator/hang_detector.rs  # NEW: Hang detection
management/src/monitoring/                    # NEW: Metrics + alerts

3. Understand Current Flow (10 min)

Trace a task submission:

# Start management server
cd management
./dev.sh

# In another terminal, submit a test task
curl -X POST http://localhost:8122/api/v1/tasks \
  -H "Content-Type: application/yaml" \
  -d @../examples/test-task.yaml

# Watch logs
./dev.sh logs

# Observe state transitions:
# Pending → Staging (git clone)
# → Provisioning (create VM)
# → Running (execute Claude)
# → Completing (collect artifacts)
# → Completed

What to notice:

  • Where does state live? (In memory: `Orchestrator.tasks`)
  • What happens if server crashes? (State lost, task orphaned)
  • Are there timeouts? (No, except SSH default)
  • Are there retries? (No, all failures terminal)

Your First Task (1-2 Hours)

Implement Basic Checkpoint System

Goal: Save task state to disk so it survives server restarts.

Steps:

1. Create checkpoint.rs

touch management/src/orchestrator/checkpoint.rs

2. Implement CheckpointStore

// management/src/orchestrator/checkpoint.rs

use std::path::PathBuf;
use tokio::fs;
use serde::{Serialize, Deserialize};
use chrono::{DateTime, Utc};

use super::task::Task;

#[derive(Serialize, Deserialize)]
struct Checkpoint {
    task: Task,
    checkpointed_at: DateTime<Utc>,
    version: u32,
}

pub struct CheckpointStore {
    root: PathBuf,
}

impl CheckpointStore {
    pub fn new(root: PathBuf) -> Self {
        Self { root }
    }

    pub async fn save(&self, task: &Task) -> Result<(), std::io::Error> {
        let task_dir = self.root.join(&task.id);
        fs::create_dir_all(&task_dir).await?;

        let checkpoint_path = task_dir.join("checkpoint.json");
        let temp_path = task_dir.join("checkpoint.tmp");

        let checkpoint = Checkpoint {
            task: task.clone(),
            checkpointed_at: Utc::now(),
            version: 1,
        };

        // Atomic write: write to temp, then rename
        let data = serde_json::to_vec_pretty(&checkpoint)?;
        fs::write(&temp_path, data).await?;
        fs::rename(&temp_path, &checkpoint_path).await?;

        Ok(())
    }

    pub async fn load(&self, task_id: &str) -> Result<Option<Checkpoint>, std::io::Error> {
        let checkpoint_path = self.root.join(task_id).join("checkpoint.json");

        if !checkpoint_path.exists() {
            return Ok(None);
        }

        let data = fs::read(&checkpoint_path).await?;
        let checkpoint = serde_json::from_slice(&data)?;
        Ok(Some(checkpoint))
    }

    pub async fn list_all(&self) -> Vec<String> {
        let mut task_ids = Vec::new();
        let mut entries = fs::read_dir(&self.root).await.unwrap();

        while let Ok(Some(entry)) = entries.next_entry().await {
            if entry.file_type().await.map(|t| t.is_dir()).unwrap_or(false) {
                task_ids.push(entry.file_name().to_string_lossy().to_string());
            }
        }

        task_ids
    }
}

3. Add to mod.rs

// management/src/orchestrator/mod.rs

pub mod checkpoint;  // ADD THIS LINE
pub use checkpoint::CheckpointStore;  // ADD THIS LINE

4. Integrate into Orchestrator

// In Orchestrator struct, add:
checkpoint_store: Arc<CheckpointStore>,

// In Orchestrator::new(), add:
let checkpoint_store = Arc::new(CheckpointStore::new(
    PathBuf::from(tasks_root.clone())
));

// In execute_task_lifecycle(), after each transition:
checkpoint_store.save(&task.read().await).await?;

5. Test It

# Submit a task
curl -X POST http://localhost:8122/api/v1/tasks -d @test-task.yaml

# Check checkpoint was created
ls /srv/tasks/<task-id>/checkpoint.json
cat /srv/tasks/<task-id>/checkpoint.json | jq

# Kill server mid-execution
pkill management-server

# Restart server
./dev.sh

# TODO: Verify task state recovered (needs recovery logic)

Next Steps:

  • Implement recovery logic (load checkpoints on startup)
  • Add tests
  • Move to timeout enforcement

Common Pitfalls

1. Forgetting Async Context

Wrong:

pub fn save(&self, task: &Task) -> Result<(), Error> {
    fs::write(path, data)?;  // This is sync, blocks thread!
}

Right:

pub async fn save(&self, task: &Task) -> Result<(), Error> {
    fs::write(path, data).await?;  // Async, yields to runtime
}

2. Not Using Atomic Writes

Wrong:

fs::write("checkpoint.json", data).await?;
// If crash happens here, file is corrupted!

Right:

fs::write("checkpoint.tmp", data).await?;
fs::rename("checkpoint.tmp", "checkpoint.json").await?;
// Rename is atomic, no corruption possible

3. Holding Locks Too Long

Wrong:

let task = task.write().await;
expensive_io_operation().await;  // Lock held during I/O!

Right:

let task_data = {
    let task = task.read().await;  // Read lock
    task.clone()  // Clone needed data
};  // Lock released
expensive_io_operation(&task_data).await;

4. Not Handling Partial Failures

Wrong:

git_clone()?;
provision_vm()?;  // If this fails, git clone wasted!

Right:

git_clone()?;
checkpoint()?;  // Save progress
provision_vm()?;  // Can resume from checkpoint if this fails

Testing Strategies

Unit Tests

Test individual components in isolation:

#[tokio::test]
async fn test_checkpoint_save_load() {
    let temp_dir = tempfile::tempdir().unwrap();
    let store = CheckpointStore::new(temp_dir.path().to_path_buf());

    let task = Task { /* ... */ };
    store.save(&task).await.unwrap();

    let loaded = store.load(&task.id).await.unwrap().unwrap();
    assert_eq!(loaded.task.id, task.id);
}

Integration Tests

Test components working together:

#[tokio::test]
async fn test_task_survives_restart() {
    let orchestrator = Orchestrator::new(/* ... */);

    // Submit task
    let task_id = orchestrator.submit_task(manifest).await.unwrap();

    // Wait for staging
    tokio::time::sleep(Duration::from_secs(5)).await;

    // Simulate crash (drop orchestrator)
    drop(orchestrator);

    // Restart
    let orchestrator = Orchestrator::new(/* ... */);
    orchestrator.recover_from_crash().await.unwrap();

    // Verify task recovered
    let task = orchestrator.get_task(&task_id).await.unwrap();
    assert!(task.state == TaskState::Staging || task.state == TaskState::Provisioning);
}

Chaos Tests

Test resilience to failures:

#!/bin/bash
# chaos-test.sh

# Start server
./dev.sh &
SERVER_PID=$!
sleep 5

# Submit tasks
for i in {1..10}; do
  curl -X POST http://localhost:8122/api/v1/tasks -d @task.yaml &
done
sleep 10

# Kill server mid-execution
kill -9 $SERVER_PID
sleep 2

# Restart
./dev.sh &
sleep 10

# Verify all tasks recovered
RECOVERED=$(curl http://localhost:8122/api/v1/tasks | jq '.tasks | length')
if [ "$RECOVERED" -eq 10 ]; then
  echo "PASS: All tasks recovered"
else
  echo "FAIL: Only $RECOVERED tasks recovered"
fi

Debugging Tips

View Current Task State

# All tasks
curl http://localhost:8122/api/v1/tasks | jq

# Specific task
curl http://localhost:8122/api/v1/tasks/<task-id> | jq

# Tasks by state
curl http://localhost:8122/api/v1/tasks?state=running | jq

Check Checkpoints

# List all checkpoints
find /srv/tasks -name checkpoint.json

# View checkpoint
jq . /srv/tasks/<task-id>/checkpoint.json

# Compare checkpoint to memory
diff \
  <(curl -s http://localhost:8122/api/v1/tasks/<task-id> | jq -S) \
  <(jq -S '.task' /srv/tasks/<task-id>/checkpoint.json)

Trace Execution

# Enable debug logging
RUST_LOG=debug ./dev.sh

# Tail logs with task ID filter
./dev.sh logs | grep 'task-abc123'

# Watch state transitions
./dev.sh logs | grep 'transition'

Inspect VMs

# List VMs
virsh list --all | grep task-

# Get VM info
virsh dominfo task-abc12345

# SSH into VM
sudo ssh -i /var/lib/agentic-sandbox/secrets/ssh-keys/task-abc12345 \
  [email protected]

Metrics to Implement First

Start with the most critical metrics:

// In execute_task_lifecycle()

// Task submitted
metrics::counter!("tasks_submitted_total", 1);

// Task completed
metrics::counter!("tasks_completed_total", 1,
    "status" => if success { "success" } else { "failure" }
);

// Task duration
let duration = task.state_changed_at - task.created_at;
metrics::histogram!("task_duration_seconds", duration.as_secs() as f64,
    "status" => if success { "success" } else { "failure" }
);

// Current state
metrics::gauge!("tasks_active", active_count as f64);
metrics::gauge!("tasks_running", running_count as f64);

Next Steps After First Task

1. Add recovery logic - Load checkpoints on startup 2. Implement timeouts - Start with operation timeouts 3. Add retries - Focus on git clone first 4. Set up metrics - Install Prometheus, create dashboard 5. Write runbook - Document failure scenarios


Getting Help


For Engineers Implementing: 1. This file (quickstart) 2. reliability-design-summary.md (overview) 3. reliability-implementation-checklist.md (task list) 4. reliability-design.md (full spec, as needed)

For Architects/Leads: 1. reliability-design-summary.md (overview) 2. reliability-architecture.md (diagrams) 3. reliability-design.md (full spec)

For Operators/SREs: 1. reliability-design.md section 6 (runbooks) 2. reliability-design.md section 5 (SLO/SLI) 3. reliability-architecture.md (alert flow)


Good luck! Remember: start small, test thoroughly, iterate quickly.