23. Схема базы данных состояния
AstraCode хранит метаданные сессий и аудит-логи в двух SQLite-БД: state_<N>.sqlite (треды, цели, audit) и logs_<N>.sqlite (трассировка). Цифра в имени — версия schema.
Реализация — крейт state/. Миграции — SQL-файлы в state/migrations/.
1. Файлы БД
| Файл | Версия | Назначение |
|---|---|---|
~/.astracode/state_5.sqlite |
5 | 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 |
2 | Tracing logs |
~/.astracode/logs_2.sqlite-shm |
— | |
~/.astracode/logs_2.sqlite-wal |
— |
Константы — state/src/lib.rs:
pub const STATE_DB_FILENAME: &str = "state"; pub const STATE_DB_VERSION: u32 = 5; pub const LOGS_DB_FILENAME: &str = "logs"; pub const LOGS_DB_VERSION: u32 = 2;
При повышении мажорной версии создаётся новый файл (state_6.sqlite); старый остаётся на диске нетронутым (для возможности отката).
⚠️ Не удалять -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 |
Постепенная очистка освободившихся страниц |
Явный pragma в base_sqlite_options() ставит только journal_mode, synchronous, busy_timeout и auto_vacuum. PRAGMA foreign_keys = ON в runtime.rs не выставляется явно (по умолчанию SQLite не форсит foreign keys на соединение), поэтому полагаться на каскадную целостность ссылок здесь не стоит.
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 |
Индексы:
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 PRIMARY KEY → threads(id) |
namespace |
TEXT |
tools |
TEXT BLOB (JSON-массив DynamicToolSpec) |
Используется для 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).
| Колонка | Тип |
|---|---|
job_id |
TEXT PRIMARY KEY |
parent_thread_id |
TEXT → threads(id) |
status |
TEXT (running/complete/failed/cancelled) |
created_at |
INTEGER |
updated_at |
INTEGER |
backfill_state — миграция rollout → DB
Для прогресса фоновой задачи backfill (заполнение threads из старых JSONL-файлов после апгрейда).
| Колонка | Тип | Описание |
|---|---|---|
status |
TEXT | Running / Complete |
last_watermark |
TEXT | Точка прогресса в FS |
updated_at |
INTEGER |
Использует 15-минутный lease, чтобы две параллельные сессии AstraCode не делали одну и ту же работу.
Другие таблицы
Зависит от миграций (могут добавиться позже): ui_state, approvals_audit, model_catalog_cache, tool_uses_summary. Точный список — 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 (модуль) |
message |
TEXT | Сообщение |
module_path |
TEXT | Полный путь модуля |
file |
TEXT | Имя файла |
line |
INTEGER | Номер строки |
thread_id |
TEXT (nullable) | UUID треда, если применимо |
process_uuid |
TEXT | UUID процесса (миграция 0009) |
Индексы:
idx_logs_ts (ts DESC, ts_nanos DESC, id DESC) idx_logs_thread_id (thread_id) -- миграция 0003 idx_logs_partition_prune (для очистки) -- миграция 0011
idx_logs_ts — для типичного «последние 1000 строк лога».
5. Миграции
Используется sqlx::migrate::Migrator. Файлы — state/migrations/0001_*.sql, 0002_*.sql, ... Запускаются последовательно при первом обращении к БД (StateRuntime::init()).
sqlx хранит таблицу _sqlx_migrations с историей применённых миграций.
Ключевые изменения
| # | Когда | Что |
|---|---|---|
| 0001 | начало | CREATE TABLE threads |
| 0002 | начало | CREATE TABLE logs |
| 0003 | … | logs.thread_id + индекс |
| 0009 | … | stage1_outputs + rollout_slug (0009_stage1_outputs_rollout_slug.sql) |
| 0010 | … | logs.process_uuid (0010_logs_process_id.sql) |
| 0011 | … | idx_logs_partition_prune для cleanup |
| 0025 | … | threads.created_at/updated_at → milliseconds (раньше секунды) |
| 0027 | … | idx_threads_cwd |
| 0029 | … | CREATE TABLE thread_goals |
| 0030 | … | новые статусы для goals (usage_limited, budget_limited) |
Обратная совместимость
- Миграции только forward — нет downgrade-скриптов.
- При downgrade приложения (откат на старую версию) — БД остаётся на новой schema; старый код может не суметь работать с ней (тогда — снести
state_<N>.sqliteили откатить вручную).
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
Автоматического удаления тредов нет. Очистка — вручную:
DELETE FROM threads WHERE updated_at < strftime('%s', 'now', '-90 days') * 1000 AND archived = 1;
VACUUM;
Или просто удалить файл — при следующем старте создастся пустой.
7. Резервное копирование
Что бэкапить:
tar czf astracode-state-backup.tar.gz \ ~/.astracode/state_5.sqlite \ ~/.astracode/state_5.sqlite-shm \ ~/.astracode/state_5.sqlite-wal \ ~/.astracode/sessions/ # без этого state без полезной нагрузки
Перед backup — корректно завершить AstraCode (Ctrl-C дважды), чтобы WAL был checkpoint'нут в основной файл.
Не бэкапить logs_2.sqlite — это эфемерные данные, переживут потерю без проблем.
8. Чтение БД вручную
# 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, 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: туда пишутся все события сессии в полном объёме.
- Index —
threadsтаблица: денормализованный snapshot для быстрого поиска (без чтения JSONL).
При запуске AstraCode проверяет backfill_state — если он Running, запускает фоновую задачу синхронизации (сканирует ~/.astracode/sessions/ и заполняет threads для файлов, которых ещё нет в индексе).
Если threads.rollout_path указывает на отсутствующий файл — сессия не recoverable, но запись в индексе остаётся (можно вручную почистить).
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 при превышении партиций.
Узкое место чаще не БД, а сетевой LLM-запрос — на его фоне SQLite не виден.