System Architecture
Data Model
Key Services
| Service | File | Responsibility |
|---|---|---|
SQLiteWorker | sqlite_worker.py | Job queue runner (ThreadPoolExecutor) |
PlannerService | planner_service.py | Autopilot tick-based planning |
ExecutorService | executor_service.py | AI CLI spawning and management |
WorkspaceService | workspace_service.py | Git worktree lifecycle |
DeliveryPipeline | delivery_pipeline.py | Topological sort and dependency management |
ConfigService | config_service.py | draft.yaml parsing and defaults |
LLMService | llm_service.py | LLM API calls via LiteLLM |
CleanupService | cleanup_service.py | Stale worktree and resource cleanup |
Background Job System
- Frontend/planner calls
POST /tickets/{id}/run - Backend creates a
Jobrecord (QUEUED) and enqueues intojob_queuetable SQLiteWorkerpolls the queue, claims the task- Job runs in an isolated worktree, streams logs via in-memory broadcaster
- Job result triggers ticket state transition
Periodic Tasks
| Task | Interval | Purpose |
|---|---|---|
| Job Watchdog | 15s | Auto-cancels stuck jobs |
| Planner Tick | 2s | Autopilot decision making |
| PR Status Poll | 5min | Check PR merge status |
Async vs Sync Database
- FastAPI routes: Async SQLAlchemy via
database.get_db() - Background worker: Sync SQLAlchemy via
database_sync.get_sync_db() - Models are shared but sessions differ
- Use
db.expunge()before passing objects between contexts
Middleware
Idempotency
Atomic first-writer-wins via SQLite. Key includes(client_id, route, resource_scope, idempotency_key). Returns 409 Conflict for same key + different body.