Инженерные документы
Dev-стек: живучесть ранов при пересборках
#1. Инцидент 2026-10-26 (симптом «ход вешается до таймаута»)
Пересборка стека (docker compose up --build) посреди выполняющегося рана
приводила к следующей цепочке:
- SIGTERM ядру → активность
kernel.run_toolумирает посреди команды (tool.startedв журнале есть, завершения — нет); - контейнер
temporalio/temporalпересоздаётся — а он работал с персистентностью in-memory (server start-devбез--db-filename) — история workflow не переживает перезапуск контейнера; workspace_run.statusостаётсяstartedнавсегда: в UI ход выглядит вечно выполняющимся, финального события не будет никогда;- сверка операции (
operation.reconciled) в этой ситуации бессмысленна — она только фиксирует вердикт в журнале и никогда не трогает workflow. Сверка в инциденте была реакцией на зависание, а не причиной.
#2. Что сделано
#2.1. Персистентность Temporal (docker-compose.yml)
temporal server start-dev --db-filename /data/temporal-dev.db + volume
temporality-temporal: история workflow переживает рестарты и пересборки
контейнера. In-memory остаётся только в transient-окружениях без volume.
#2.2. Orphan sweep (cmd/agent-kernel/orphansweep.go)
На старте ядра (асинхронно, не блокирует подъём) свип сравнивает:
- раны со статусом
started(workspace.ListStartedRuns, кап 500); - журнальные терминальные события (
RunTerminalStatuses) — раны сrun.completed/failed/cancelledне трогаются; - живость workflow (
temporalLiveness.IsWorkflowRunning— тот же Describe-зонд, что у списка неопределённых операций, с R1-deadline). Workflow-id вычисляется по source_id из журнала рана (RunSourceID): внешние агенты живут под своим source, зонд с чужим id не делается.
Ран без живого workflow и без терминального события помечается failed
(UpdateRunStatus) и получает честное журнальное событие run.failed
(data.orphaned = true, причина в error) — UI перестаёт показывать призрак,
трейл остаётся аудируемым.
Правила безопасности свипа:
- ошибка зонда по одному рану (недоступность Temporal) пропускает этот ран, а не закрывает его;
- ран без журнальных событий не зондируется вовсе;
- свип собирает ошибки, не останавливается на первой.
#3. Эксплуатация
- Пересборка стека посреди рана больше не оставляет ран незавершённым навсегда: история workflow живёт в volume, Temporal переигрывает активность на новом воркере, ран продолжается. Команды песочницы не идемпотентны — переигранная команда выполнится повторно (это дешевле, чем потерянный ход).
- После перехода на volume старые in-memory раны остаются незавершёнными — первый свип пометит их failed при следующем старте ядра.
KERNEL_BUILTIN_SKILLS=off— выключение сидирования скилов, если нужно; к свипу отношения не имеет.