0.4.11
23 · Техническая документация

23. State DB schema

AstraCode хранит метаданные сессий и tracing-логи в двух SQLite-БД: state_5.sqlite и logs_2.sqlite. Суффиксы _5 и _2 исторические и не являются текущими версиями schema; обе схемы развиваются через sqlx migrations внутри существующих файлов.

Реализация — крейт state/. Миграции — SQL-файлы в state/migrations/.

1. Файлы БД

Файл Назначение
~/.astracode/state_5.sqlite Threads metadata, goals, agent jobs, dynamic tools
~/.astracode/state_5.sqlite-shm Shared memory (WAL вспомогательный)
~/.astracode/state_5.sqlite-wal Write-Ahead Log
~/.astracode/logs_2.sqlite Tracing logs
~/.astracode/logs_2.sqlite-shm WAL вспомогательный
~/.astracode/logs_2.sqlite-wal Write-Ahead Log

Константы — state/src/lib.rs:

rust
pub const STATE_DB_FILENAME: &str = "state_5.sqlite";
pub const LOGS_DB_FILENAME: &str = "logs_2.sqlite";

Отдельных констант STATE_DB_VERSION/LOGS_DB_VERSION нет. Комментарий рядом с константами прямо фиксирует, что старый механизм file bump/delete больше не используется. Актуальные схемы задают state/migrations/ и state/logs_migrations/.

⚠️ Не удалять -wal/-shm файлы во время работы AstraCode — это сломает БД. Они исчезают сами при корректном shutdown через checkpoint.

2. Настройки SQLite

В state/src/runtime.rs:

Опция Значение Зачем
journal_mode WAL Конкурентные read/write без блокировок
synchronous Normal Не fsync после каждого write, fsync на checkpoint
auto_vacuum Incremental Постепенная очистка освободившихся страниц
busy_timeout 5s Ждать блокировки до 5 секунд вместо немедленной ошибки

foreign_keys принудительно не включается в base_sqlite_options; ссылочная целостность обеспечивается миграциями там, где нужно (PRAGMA foreign_keys=ON в конкретных миграциях, напр. 0030_thread_goal_stopped_statuses.sql). auto_vacuum = Incremental задаётся в open_state_sqlite/open_logs_sqlite, а не в base_sqlite_options.

WAL+Normal даёт высокую производительность при большом числе сессий, ценой того, что при kill -9 могут потеряться последние ~1 секунду записей.

3. Таблицы state_5.sqlite

threads — основная таблица

Хранит metadata всех сессий. Полное содержимое сессии — в JSONL rollout (см. 21-rollout-format.md); здесь только индекс для быстрого поиска.

Колонка Тип Описание
id TEXT PRIMARY KEY UUID треда (ThreadId)
rollout_path TEXT NOT NULL Путь к .jsonl файлу
created_at INTEGER Unix timestamp в миллисекундах (с миграции 0025)
updated_at INTEGER Unix timestamp ms
source TEXT cli/vscode/external_agent/...
model_provider TEXT id провайдера (openai, anthropic, ...)
cwd TEXT Абсолютная рабочая директория
title TEXT Заголовок (обычно — первое user сообщение, truncated)
sandbox_policy TEXT JSON-сериализованный snapshot политики
approval_mode TEXT JSON-сериализованный approval-режим
tokens_used INTEGER Накопительно по сессии
has_user_event INTEGER (0/1) Есть ли хотя бы одно user message
archived INTEGER (0/1) Заархивирован ли тред
archived_at INTEGER (nullable) Когда заархивирован
git_sha TEXT SHA коммита на момент создания
git_branch TEXT Активная ветка
git_origin_url TEXT URL origin remote

Индексы:

sql
idx_threads_created_at  (created_at DESC, id DESC)
idx_threads_updated_at  (updated_at DESC, id DESC)
idx_threads_archived    (archived)
idx_threads_source      (source)
idx_threads_provider    (model_provider)
idx_threads_cwd         (cwd)               -- миграция 0027

Запросы типа «10 последних сессий в этом репо» используют idx_threads_cwd + idx_threads_updated_at.

Определение — state/migrations/0001_threads.sql.

thread_dynamic_tools — динамические tools на тред

Колонка Тип
thread_id TEXT NOT NULL → threads(id) ON DELETE CASCADE
position INTEGER NOT NULL
name TEXT NOT NULL
description TEXT NOT NULL
input_schema TEXT NOT NULL (JSON)
namespace TEXT (добавлен миграцией 0026)

PRIMARY KEY (thread_id, position).

Каждая строка — один динамический tool (не JSON-массив). Используется для runtime-зарегистрированных tools (от плагинов и пр.) — чтобы при resume сессии restore их definitions.

thread_goals — долгоиграющие цели

С миграции 0029. Соответствует команде /goal (см. 13-goal-and-modes.md).

Колонка Тип Описание
thread_id TEXT PRIMARY KEY → threads(id)
goal_id TEXT UUID цели
objective TEXT Текст цели
status TEXT active / paused / blocked / complete / usage_limited / budget_limited
token_budget INTEGER (nullable) Лимит в токенах
tokens_used INTEGER Накопительно
time_used_seconds INTEGER Накопительно
created_at_ms INTEGER Unix ms
updated_at_ms INTEGER Unix ms

agent_jobs — batch CSV workflows

Tracking batch-вызовов spawn_agents_on_csv (см. 18-tools-catalog.md).

Колонка Тип
id TEXT PRIMARY KEY
name TEXT NOT NULL
status TEXT NOT NULL (running/complete/failed/cancelled)
instruction TEXT NOT NULL
output_schema_json TEXT (nullable)
input_headers_json TEXT NOT NULL
input_csv_path TEXT NOT NULL
output_csv_path TEXT NOT NULL
auto_export INTEGER NOT NULL DEFAULT 1
created_at INTEGER NOT NULL
updated_at INTEGER NOT NULL
started_at INTEGER (nullable)
completed_at INTEGER (nullable)
last_error TEXT (nullable)

Строки заданий — отдельная таблица agent_job_items (job_id, item_id, row_index, row_json, status, assigned_thread_id, attempt_count, result_json, ...; FK → agent_jobs(id) ON DELETE CASCADE). Миграция 0014/0015.

backfill_state — миграция rollout → DB

Для прогресса фоновой задачи backfill (заполнение threads из старых JSONL-файлов после апгрейда).

Колонка Тип Описание
id INTEGER PRIMARY KEY (CHECK = 1) Синглтон-строка
status TEXT Running / Complete
last_watermark TEXT Точка прогресса в FS
last_success_at INTEGER (nullable) TS последнего успешного шага
updated_at INTEGER

Использует 15-минутный lease, чтобы две параллельные сессии AstraCode не делали одну и ту же работу.

Другие таблицы

Зависит от миграций (могут добавиться позже): thread_spawn_edges (связи сабагентов, 0021), stage1_outputs (Phase 1 памяти, 0006), jobs (фоновые job'ы памяти, 0006), device_key_bindings (0028), remote_control_enrollments (0024). Таблиц ui_state/approvals_audit/model_catalog_cache/tool_uses_summary в БД нет — состояние UI лежит в config.toml ([ui_state]), кэш моделей — в файле models_cache.json. Точный список — grep CREATE TABLE state/migrations/*.sql.

4. Таблицы logs_2.sqlite

logs — append-only трассировка

Логи tracing-крейта (debug / info / warn / error). Отдельная БД — чтобы log-write не блокировал критичные транзакции в state_5.

Колонка Тип Описание
id INTEGER PRIMARY KEY AUTOINCREMENT
ts INTEGER Unix seconds
ts_nanos INTEGER Доп. наносекунды
level TEXT DEBUG/INFO/WARN/ERROR
target TEXT tracing target (модуль)
feedback_log_body TEXT Тело лога (форматированные fields + message); в SELECT-запросах часто алиасится как message
module_path TEXT Полный путь модуля
file TEXT Имя файла
line INTEGER Номер строки
thread_id TEXT (nullable) UUID треда, если применимо
process_uuid TEXT (nullable) UUID процесса (pid:<pid>:<uuid>)
estimated_bytes INTEGER NOT NULL DEFAULT 0 Оценка размера строки (для cleanup-политик)

Индексы:

sql
idx_logs_ts                    (ts DESC, ts_nanos DESC, id DESC)
idx_logs_thread_id             (thread_id)
idx_logs_thread_id_ts          (thread_id, ts DESC, ts_nanos DESC, id DESC)
idx_logs_process_uuid_threadless_ts (process_uuid, ts ...) WHERE thread_id IS NULL

idx_logs_ts — для типичного «последние 1000 строк лога». Колонка feedback_log_body появилась в logs_migrations/0002 (переименование message); estimated_bytes добавлен позже.

5. Миграции

Используется sqlx::migrate::Migrator. Файлы — state/migrations/0001_*.sql, 0002_*.sql, ... Запускаются последовательно при первом обращении к БД (StateRuntime::init()).

sqlx хранит таблицу _sqlx_migrations с историей применённых миграций.

Ключевые изменения

# Когда Что
0001 начало CREATE TABLE threads
0006 CREATE TABLE stage1_outputs, jobs (память)
0008 CREATE TABLE backfill_state
0010 logs.process_uuid (в state DB; позже таблица logs дропнута в 0023)
0011 индексы idx_logs_thread_id_ts для cleanup
0012 logs.estimated_bytes
0014 CREATE TABLE agent_jobs, agent_job_items
0025 threads.created_at/updated_at → milliseconds (раньше секунды)
0023 DROP TABLE logs в state DB (логи переехали в logs_2.sqlite)
0027 idx_threads_cwd
0029 CREATE TABLE thread_goals
0030 новые статусы для goals (usage_limited, budget_limited)

Для logs_2.sqlite — отдельный набор state/logs_migrations/: 0001 создаёт logs, 0002 переименовывает messagefeedback_log_body.

Backwards compatibility

  • Миграции только forward — нет downgrade-скриптов.
  • Runtime migrator устанавливает ignore_missing = true, поэтому более старый бинарник может открыть БД, где уже отмечены неизвестные ему более новые миграции. Checksum известных миграций при этом продолжает проверяться.
  • Это не полная гарантия downgrade compatibility: старый код не знает новых колонок и семантики. Перед откатом остановите все процессы и сделайте backup; не удаляйте рабочую БД как первый способ восстановления.

6. Cleanup и ротация

logs ротация

logs может расти быстро (особенно с RUST_LOG=debug). В state/src/runtime.rs реализован cleanup по партициям:

  • Лимит на партицию (thread_id или process_uuid): 10 MiB.
  • Лимит на партицию: 1000 строк.
  • При превышении — старые записи удаляются автоматически (использует idx_logs_partition_prune).

state cleanup

Для логической очистки используйте archive/delete action в /sessions. Прямое удаление строк из threads не удаляет соответствующие rollout-файлы и может рассогласовать индекс с файловым хранилищем.

Если требуется полностью пересоздать state DB, сначала остановите все процессы, сделайте backup state_5.sqlite вместе с WAL/SHM и sessions/, затем переместите всю SQLite-группу. create_if_missing(true) и migrator создадут новую БД при следующем запуске.

7. Backup

Что бэкапить:

bash
tar czf astracode-state-backup.tar.gz \
  ~/.astracode/state_5.sqlite \
  ~/.astracode/sessions/    # без этого state без полезной нагрузки

Перед backup корректно завершите все процессы AstraCode, чтобы WAL был checkpoint'нут в основной файл. Для hot backup работающей БД используйте SQLite backup API, а не копирование меняющихся файлов по отдельности.

Не бэкапить logs_2.sqlite — это эфемерные данные, переживут лосс без проблем.

8. Reading DB вручную

bash
# 10 последних сессий в этой директории
sqlite3 ~/.astracode/state_5.sqlite "
  SELECT id, title, datetime(updated_at/1000, 'unixepoch') as updated, tokens_used
  FROM threads
  WHERE cwd = '$(pwd)'
  ORDER BY updated_at DESC
  LIMIT 10;
"

# Активные цели
sqlite3 ~/.astracode/state_5.sqlite "
  SELECT t.title, g.objective, g.status, g.tokens_used, g.token_budget
  FROM thread_goals g
  JOIN threads t ON t.id = g.thread_id
  WHERE g.status IN ('active', 'paused')
  ORDER BY g.updated_at_ms DESC;
"

# Последние ошибки в логах
sqlite3 ~/.astracode/logs_2.sqlite "
  SELECT datetime(ts, 'unixepoch'), level, target, feedback_log_body AS message
  FROM logs
  WHERE level IN ('ERROR', 'WARN')
  ORDER BY ts DESC, ts_nanos DESC
  LIMIT 50;
"

# Активность по дням
sqlite3 ~/.astracode/state_5.sqlite "
  SELECT date(created_at/1000, 'unixepoch') as day, COUNT(*) as threads, SUM(tokens_used) as tokens
  FROM threads
  GROUP BY day
  ORDER BY day DESC
  LIMIT 30;
"

9. Связь с rollout

Каждая строка в threads ссылается через rollout_path на JSONL-файл (см. 21-rollout-format.md). Это две координированные системы:

  • Authoritative source — rollout JSONL: туда пишутся все события сессии в полном объёме.
  • Indexthreads таблица: денормализованный snapshot для быстрого поиска (без чтения JSONL).

При запуске AstraCode проверяет backfill_state — если он Running, запускает фоновую задачу синхронизации (сканирует ~/.astracode/sessions/ и заполняет threads для файлов, которых ещё нет в индексе).

Если threads.rollout_path указывает на отсутствующий файл, сессия уже не recoverable. При чтении списка runtime обнаруживает stale path и вызывает delete_thread, удаляя соответствующую metadata row. Поэтому ручное удаление rollout-файлов является потерей данных, а не поддерживаемой очисткой.

10. Где определены типы

  • Точка входа: state/src/lib.rs.
  • Runtime: state/src/runtime.rs.
  • Миграции: state/migrations/*.sql.
  • Структуры (Threads, ThreadGoals, AgentJobs): state/src/*.rs.

11. Производительность

  • List threads: ~1ms на 10k тредов (индекс по updated_at).
  • Insert thread: ~3-5ms (с fsync на checkpoint).
  • Log write: ~50µs (batch'ится в фоновую транзакцию).
  • Cleanup logs: ~10-50ms при превышении партиций.

Bottleneck чаще не БД, а сетевой LLM-запрос — на его фоне SQLite не виден.