From 23f9b65bf65b3d3677350a456e57294a4df810b9 Mon Sep 17 00:00:00 2001 From: Claudomator Agent Date: Wed, 11 Mar 2026 00:43:41 +0000 Subject: docs: add executor package documentation --- docs/packages/storage.md | 209 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 docs/packages/storage.md (limited to 'docs/packages/storage.md') diff --git a/docs/packages/storage.md b/docs/packages/storage.md new file mode 100644 index 0000000..2e53cea --- /dev/null +++ b/docs/packages/storage.md @@ -0,0 +1,209 @@ +# Package: `internal/storage` + +## Overview + +The `storage` package provides the SQLite persistence layer for Claudomator. It exposes a `DB` struct that manages all reads and writes to the database, runs schema migrations automatically on `Open`, and enforces valid state transitions when updating task states. + +Key characteristics: +- **Auto-migration**: `Open` creates all tables and indexes if they do not exist, then applies additive column migrations idempotently (duplicate-column errors are silently ignored). +- **WAL mode**: The database is opened with `?_journal_mode=WAL&_busy_timeout=5000` for concurrent read safety and reduced lock contention. +- **State enforcement**: `UpdateTaskState` performs the state change inside a transaction and calls `task.ValidTransition` before committing. +- **Cascading deletes**: `DeleteTask` removes a task, all its descendant subtasks, and all their execution records in a single transaction using a recursive CTE. + +--- + +## Schema + +### `tasks` table + +| Column | Type | Description | +|------------------|-----------|----------------------------------------------------------| +| `id` | `TEXT PK` | UUID; primary key. | +| `name` | `TEXT` | Human-readable task name. Not null. | +| `description` | `TEXT` | Optional longer description. | +| `config_json` | `TEXT` | JSON blob — serialised `AgentConfig` struct. | +| `priority` | `TEXT` | `"high"`, `"normal"`, or `"low"`. Default `"normal"`. | +| `timeout_ns` | `INTEGER` | Timeout as nanoseconds (`time.Duration`). Default `0`. | +| `retry_json` | `TEXT` | JSON blob — serialised `RetryConfig` struct. | +| `tags_json` | `TEXT` | JSON blob — serialised `[]string`. | +| `depends_on_json`| `TEXT` | JSON blob — serialised `[]string` of task IDs. | +| `parent_task_id` | `TEXT` | ID of parent task; null for root tasks. | +| `state` | `TEXT` | Current `State` value. Default `"PENDING"`. | +| `created_at` | `DATETIME`| Creation timestamp (UTC). | +| `updated_at` | `DATETIME`| Last-update timestamp (UTC). | +| `rejection_comment` | `TEXT` | Reviewer comment set by `RejectTask`; nullable. | +| `question_json` | `TEXT` | Pending agent question set by `UpdateTaskQuestion`; nullable. | + +**JSON blob columns**: `config_json`, `retry_json`, `tags_json`, `depends_on_json`. + +**Indexes**: `idx_tasks_state` (state), `idx_tasks_parent_task_id` (parent_task_id). + +### `executions` table + +| Column | Type | Description | +|---------------|-----------|---------------------------------------------------------------| +| `id` | `TEXT PK` | UUID; primary key. | +| `task_id` | `TEXT` | Foreign key → `tasks.id`. | +| `start_time` | `DATETIME`| When the execution started (UTC). | +| `end_time` | `DATETIME`| When the execution ended (UTC); null while running. | +| `exit_code` | `INTEGER` | Process exit code; 0 = success. | +| `status` | `TEXT` | Execution status string (mirrors task state at completion). | +| `stdout_path` | `TEXT` | Path to the stdout log file under `~/.claudomator/executions/`. | +| `stderr_path` | `TEXT` | Path to the stderr log file under `~/.claudomator/executions/`. | +| `artifact_dir`| `TEXT` | Directory containing output artifacts. | +| `cost_usd` | `REAL` | Total API spend for this execution in USD. | +| `error_msg` | `TEXT` | Error description if the execution failed. | +| `session_id` | `TEXT` | Claude `--session-id` value; used to resume interrupted runs. | + +**Indexes**: `idx_executions_status`, `idx_executions_task_id`, `idx_executions_start_time`. + +--- + +## `DB` Struct Methods + +### Lifecycle + +#### `Open(path string) (*DB, error)` +Opens the SQLite database at `path`, runs auto-migration, and returns a `*DB`. The connection string appends `?_journal_mode=WAL&_busy_timeout=5000`. + +#### `(*DB) Close() error` +Closes the underlying database connection. + +--- + +### Task CRUD + +#### `(*DB) CreateTask(t *task.Task) error` +Inserts a new task. `AgentConfig`, `RetryConfig`, `Tags`, and `DependsOn` are serialised as JSON blobs; `Timeout` is stored as nanoseconds. + +#### `(*DB) GetTask(id string) (*task.Task, error)` +Retrieves a single task by ID. Returns an error wrapping `sql.ErrNoRows` if not found. + +#### `(*DB) ListTasks(filter TaskFilter) ([]*task.Task, error)` +Returns tasks matching `filter`, ordered by `created_at DESC`. See [TaskFilter](#taskfilter). + +#### `(*DB) ListSubtasks(parentID string) ([]*task.Task, error)` +Returns all tasks whose `parent_task_id` equals `parentID`, ordered by `created_at ASC`. + +#### `(*DB) UpdateTask(id string, u TaskUpdate) error` +Replaces editable fields (`Name`, `Description`, `Config`, `Priority`, `TimeoutNS`, `Retry`, `Tags`, `DependsOn`) and resets `state` to `PENDING`. Returns an error if the task does not exist. + +#### `(*DB) DeleteTask(id string) error` +Deletes a task, all its descendant subtasks (via recursive CTE), and all their execution records in a single transaction. Returns an error if the task does not exist. + +--- + +### State Management + +#### `(*DB) UpdateTaskState(id string, newState task.State) error` +Atomically updates a task's state inside a transaction. Calls `task.ValidTransition` to reject illegal moves before applying the change. + +#### `(*DB) RejectTask(id, comment string) error` +Sets the task's state to `PENDING` and stores `comment` in `rejection_comment`. Does not go through the state-machine validator (direct update). + +#### `(*DB) ResetTaskForRetry(id string) (*task.Task, error)` +Validates that the current state can transition to `QUEUED`, clears `Agent.Type` and `Agent.Model` so the task can be re-classified, sets `state = QUEUED`, and persists the changes in a transaction. Returns the updated task. + +#### `(*DB) UpdateTaskQuestion(taskID, questionJSON string) error` +Stores a JSON-encoded agent question on the task. Pass an empty string to clear the question after it has been answered. + +--- + +### Execution CRUD + +#### `(*DB) CreateExecution(e *Execution) error` +Inserts a new execution record. + +#### `(*DB) GetExecution(id string) (*Execution, error)` +Retrieves a single execution by ID. + +#### `(*DB) ListExecutions(taskID string) ([]*Execution, error)` +Returns all executions for a task, ordered by `start_time DESC`. + +#### `(*DB) GetLatestExecution(taskID string) (*Execution, error)` +Returns the most recent execution for a task. + +#### `(*DB) UpdateExecution(e *Execution) error` +Updates a completed execution record (end time, exit code, status, cost, paths, session ID). + +#### `(*DB) ListRecentExecutions(since time.Time, limit int, taskID string) ([]*RecentExecution, error)` +Returns executions since `since`, joined with the task name, ordered by `start_time DESC`. If `taskID` is non-empty only executions for that task are included. Returns `RecentExecution` values (see below). + +--- + +## `TaskFilter` + +```go +type TaskFilter struct { + State task.State + Limit int +} +``` + +| Field | Type | Description | +|---------|--------------|-------------------------------------------------------| +| `State` | `task.State` | If non-empty, only tasks in this state are returned. | +| `Limit` | `int` | Maximum number of results. `0` means no limit. | + +--- + +## `Execution` Struct + +```go +type Execution struct { + ID string + TaskID string + StartTime time.Time + EndTime time.Time + ExitCode int + Status string + StdoutPath string + StderrPath string + ArtifactDir string + CostUSD float64 + ErrorMsg string + SessionID string // persisted: claude --session-id for resume + ResumeSessionID string // in-memory only: set when creating a resume execution + ResumeAnswer string // in-memory only: human answer forwarded to agent +} +``` + +| Field | Persisted | Description | +|-------------------|-----------|-----------------------------------------------------------------------| +| `ID` | Yes | UUID; primary key. | +| `TaskID` | Yes | ID of the owning task. | +| `StartTime` | Yes | When execution started (UTC). | +| `EndTime` | Yes | When execution ended (UTC); zero while still running. | +| `ExitCode` | Yes | Process exit code (0 = success). | +| `Status` | Yes | Status string at completion. | +| `StdoutPath` | Yes | Absolute path to the stdout log file. | +| `StderrPath` | Yes | Absolute path to the stderr log file. | +| `ArtifactDir` | Yes | Directory containing output artifacts produced by the agent. | +| `CostUSD` | Yes | Total API spend for this run in USD. | +| `ErrorMsg` | Yes | Error description if the run failed. | +| `SessionID` | Yes | Claude session ID; stored to allow resuming an interrupted session. | +| `ResumeSessionID` | No | Session to resume; set in-memory when creating a resume execution. | +| `ResumeAnswer` | No | Human answer to a blocked question; forwarded to the agent in-memory. | + +--- + +## File Layout + +All runtime data lives under `~/.claudomator/`: + +``` +~/.claudomator/ +├── claudomator.db # SQLite database (WAL mode) +└── executions/ # Execution log files + ├── .stdout + └── .stderr +``` + +The paths are configured by `internal/config`: + +| Config field | Value | +|--------------|--------------------------------------| +| `DBPath` | `~/.claudomator/claudomator.db` | +| `LogDir` | `~/.claudomator/executions/` | + +Both directories are created automatically on first run if they do not exist. -- cgit v1.2.3