Task 108 — DurableTask checkpoint persistence
Phase: 8
Status: done
Owner: —
Slug: checkpoint-persistence
Studio Stage: 8
Goal
Заменить stub в TaskExecutionService.CreateCheckpointAsync на реальную persistence. Checkpoints должны сохраняться и загружаться.
Acceptance criteria
ITaskExecutionService:CreateCheckpointAsync(taskId, stateSnapshot)→ save to SQLiteListCheckpointsAsync(taskId)→ return checkpoints from SQLite (currently returns empty)ResumeAsync(taskId, checkpointId?)→ load checkpoint state, resume from step
- SQLite schema:
task_checkpointstable (id, taskId, stepNumber, stateSnapshot JSON, createdAt) SqliteSessionStore— add checkpoint CRUD methodsTaskExecutionService:CreateCheckpointAsync— actually persist (was stub)ListCheckpointsAsync— actually query (was returning empty)IncrementStepAsync— persist step number
- Startup recovery: load incomplete tasks → resume from last checkpoint
- Workflow executor integration: checkpoint after each node completion (deferred — depends on task_105)
- Unit tests: create checkpoint, list, resume from checkpoint, recovery
dotnet build+dotnet testpass
Sub-tasks
- Add
LoadCheckpointAsync(string checkpointId)toISessionStore+SqliteSessionStore(and PostgresSessionStore stubs) - Inject
ISessionStoreintoTaskExecutionService(alongsideITaskRepository) - Wire
CreateCheckpointAsyncto callISessionStore.SaveCheckpointAsync - Wire
ListCheckpointsAsyncto callISessionStore.ListCheckpointsAsync - Add
IncrementStepAsynctoITaskExecutionServiceinterface (wasinternalhelper) - Update
ResumeAsyncto load the latest/specific checkpoint and log the snapshot step - Add
RecoverIncompleteTasksAsynctoITaskExecutionService(startup recovery) - Add
GET /api/tasks/{id}/checkpoints/{ckptId}endpoint (load specific checkpoint) - Add
POST /api/tasks/{id}/checkpointsendpoint (create checkpoint) - Update
Program.csto invokeRecoverIncompleteTasksAsyncat startup (after DI warmup, beforeapp.Run()) - Update
TaskProgressControllerto declare.WithTags("Tasks")for consistency - Update existing
CreateCheckpointAsync_ReturnsCheckpointIdtest to assert persistence - Update
ListCheckpointsAsync_ReturnsEmptyList_Stub→ListCheckpointsAsync_ReturnsPersistedCheckpoints - Add new tests:
LoadCheckpointAsync_Nonexistent,IncrementStepAsync_Persists,ResumeAsync_LoadsCheckpointSnapshot,ResumeAsync_FallsBackToLatestCheckpoint_WhenIdMissing,RecoverIncompleteTasksAsync_ResumesRunningAndPaused,RecoverIncompleteTasksAsync_SkipsFailedTasks dotnet build(full solution) +dotnet test(full suite) green; pre-existing unrelated failures unaffected
Implementation notes
Что было готово до этого тика
SqliteSessionStore.InitCheckpointSchemaAsync— создаёт таблицуtask_checkpoints(id, task_id, step_number, state_snapshot, created_at) и индекс поtask_id. Вызывается в конструктореSqliteSessionStore.SqliteSessionStore.SaveCheckpointAsync—INSERT OR REPLACEс правильнымTaskId/stepNumber/snapshot. Под капотом_connLock(task_071) для сериализации.SqliteSessionStore.ListCheckpointsAsync(string taskId)—SELECT … WHERE task_id = $tid ORDER BY step_number ASC.SqliteSessionStore.CleanupOldCheckpointsAsync(int retentionDays)— удаляет поcreated_at < $cutoff.ISessionStoreуже объявлял все четыре метода (task_103).
Что добавил task_108
-
ISessionStore.LoadCheckpointAsync(string checkpointId)— новый метод для выборки одного чекпоинта по ID (нужен дляResumeAsync(checkpointId=…)). Реализован вSqliteSessionStore(параметризованныйSELECTпоid);PostgresSessionStoreбросаетNotImplementedExceptionкак и остальные checkpoint-методы (task_103 follow-up). TaskExecutionService— рефакторинг поверхISessionStore:- Внедрён
ISessionStoreчерез конструктор. CreateCheckpointAsyncтеперь дёргает_sessionStore.SaveCheckpointAsync(раньше был stub, возвращавший ID без записи).ListCheckpointsAsyncтеперь читает из_sessionStore.ListCheckpointsAsync(раньше возвращал пустой список).IncrementStepAsyncподнят сinternalhelper до публичного метода интерфейса (логика_repo.UpdateAsyncуже была корректной — просто стала доступна извне).LoadCheckpointAsync(string)— публичный прокси к_sessionStore.LoadCheckpointAsync.ResumeAsync(taskId, checkpointId?):- Если
checkpointIdзадан — пытается загрузить конкретный чекпоинт черезLoadCheckpointAsync; при отсутствии логирует warning и фолбэчится на последний изListCheckpointsAsync. - Если
checkpointIdне задан — берёт самый свежий чекпоинт изListCheckpointsAsync(если есть). - Возвращает задачу в
Running, обнуляетError, логирует[Task] Resumed … from checkpoint {id} (step={n}, snapshotBytes={b}).
- Если
- Внедрён
TaskExecutionService.RecoverIncompleteTasksAsync— startup recovery:- Сканирует
Running,Paused,FailedчерезITaskRepository.ListAsync(statusFilter: …). - Для
Running/Paused: ставитStatus = Running, обновляетUpdatedAt, обнуляетError. Логирует[Recovery] Resumed {PreviousStatus} task {Id} (checkpoint={id|None}, step={n}). - Для
Failed: оставляет статус, логирует[Recovery] Skipped failed task {Id} (checkpoint={id|None}) — manual intervention required. - В конце — best-effort
CleanupOldCheckpointsAsync(_config.CheckpointRetentionDays). Ошибка retention-cleanup не блокирует старт. - Возвращает количество реактивированных задач.
- Сканирует
- DI wiring (в
src/agent/Program.cs+src/agent/Hercules.WebApi/Program.cs):ITaskExecutionServiceтеперь резолвитISessionStoreчерез конструктор.- В
Hercules.WebApi/Program.csпосле инициализации MCP добавлен блокtry { var recovered = await taskExec.RecoverIncompleteTasksAsync(); … }(подif (!isBuildTime)).
- Controller (
TaskProgressController.cs):POST /api/tasks/{id}/checkpoints— новый endpoint, bodyCreateCheckpointRequest { int StepNumber, string? StateSnapshot }. Создаёт чекпоинт + дёргаетIncrementStepAsync, возвращает 201 Created с Location и метаданными.GET /api/tasks/{id}/checkpoints/{ckptId}— новый endpoint, возвращает полный snapshot.- Все 8 существующих эндпойнтов получили
.WithTags("Tasks")для консистентности с task_110/Orval tags-split (когда дойдут руки до web-UI codegen, тег уже будет на месте).
- Tests (
tests/Hercules.Agent.Tests/Tasks/TaskExecutionServiceTests.cs):- 13 старых тестов остались (signature constructor изменён —
ISessionStoreтеперь обязателен). CreateCheckpointAsync_ReturnsCheckpointId→CreateCheckpointAsync_PersistsCheckpoint(теперь ассертит round-trip черезLoadCheckpointAsync).ListCheckpointsAsync_ReturnsEmptyList_Stub→ListCheckpointsAsync_ReturnsPersistedCheckpoints(теперь создаёт 2 чекпоинта и проверяет порядок).- Новые тесты:
LoadCheckpointAsync_Nonexistent,IncrementStepAsync_Persists,ResumeAsync_LoadsCheckpointSnapshot,ResumeAsync_FallsBackToLatestCheckpoint_WhenIdMissing,RecoverIncompleteTasksAsync_ResumesRunningAndPaused,RecoverIncompleteTasksAsync_SkipsFailedTasks.
- 13 старых тестов остались (signature constructor изменён —
Дизайн-решения
- Семантика recovery для Paused: решил, что
Paused→Runningна старте, чтобы будущий workflow-экзекутор (task_104/105) мог подобрать задачи. Альтернатива (оставить Paused) делает recovery бессмысленным — Paused значит “ничего не делать, ждать явной команды”. ТестRecoverIncompleteTasksAsync_ResumesRunningAndPausedфиксирует это поведение. - Failed остаётся Failed: автоматический retry из Failed опасен (может зациклиться). Оператор должен явно
POST /api/tasks/{id}/resumeдля Failed. - Retention cleanup best-effort: если
CleanupOldCheckpointsAsyncупадёт, recovery всё равно завершится успешно. Логируется warning. - Checkpoint-fallback на latest:
ResumeAsync(id, "ghost")(несуществующий ID) не падает — фолбэчится на последний реальный чекпоинт. Это удобно для UI, который может прислать устаревший ID.
Что осталось за рамками
- Workflow executor integration (task_105):
TaskExecutionServiceсейчас не дёргаетCreateCheckpointAsyncпосле каждого узла графа — нет workflow-экзекутора. Когда task_105 будет готов, executor должен вызыватьsvc.CreateCheckpointAsync(taskId, step, snapshotJson)между узлами. - PostgresSessionStore checkpoint methods —
NotImplementedExceptionstubs (task_103 follow-up, уже отмечено). Для greenfield Postgres deployments нужно реализоватьCREATE TABLE,INSERT,SELECT,DELETEна Npgsql. - Periodic retention cleanup (вместо startup-only) — текущая очистка работает только при рестарте. Для долгоживущих инсталляций стоит повесить
IHostedServiceс периодическим таймером.
Validation
dotnet build src/agent/Hercules.csproj→ 0 errorsdotnet build src/agent/Hercules.WebApi/Hercules.WebApi.csproj→ 0 errorsdotnet build tests/Hercules.Agent.Tests/Hercules.Agent.Tests.csproj→ 0 errorsdotnet test --filter "FullyQualifiedName~TaskExecutionServiceTests"→ 19/19 passeddotnet test(full suite) → 2135/2143 passed; 8 failures are pre-existing baseline (OtelServiceTests×5, NumericValidatorTests×2, RedisTaskQueueTests×1 — все требуют внешних сервисов / экспортёров, не связаны с этой задачей). Совпадает с baseline, документированным в task_103.
Scope / Likely files
src/agent/Tasks/ITaskExecutionService.cs— добавленыLoadCheckpointAsync,IncrementStepAsync,RecoverIncompleteTasksAsyncsrc/agent/Tasks/TaskExecutionService.cs— полный рефакторинг поверхISessionStoresrc/agent/Storage/ISessionStore.cs— добавленLoadCheckpointAsyncsrc/agent/Storage/SqliteSessionStore.cs— добавлена реализацияLoadCheckpointAsyncsrc/agent/Storage/PostgresSessionStore.cs— добавлен stubLoadCheckpointAsync(NotImplementedException)src/agent/Hercules.WebApi/Program.cs— DI registration + startup recovery callsrc/agent/Program.cs— DI registrationsrc/agent/Hercules.WebApi/Controllers/TaskProgressController.cs— новые эндпойнты +.WithTags("Tasks")tests/Hercules.Agent.Tests/Tasks/TaskExecutionServiceTests.cs— обновлены 2 теста, добавлено 6 новых
Links
- Backlog: ../backlog.md
- task_018 (durable task lifecycle): completed/task_018.md — базовая инфраструктура
- task_103 (ISessionStore + PostgresSessionStore): — интерфейс, который мы тут дополняем
- task_105 (Workflow graph + executor): task_105.md — потребитель checkpoint API (follow-up)
- task_107 (Parent/child task relationships): task_107.md — follow-up
Dependencies
- task_106 (DelegatedTask persistence — same pattern)
- task_018 (durable task lifecycle) — done
Scope / Likely files
src/agent/Tasks/TaskExecutionService.cs (refactor), src/agent/Tasks/ITaskExecutionService.cs, src/agent/Storage/SqliteSessionStore.cs (add table)
Links
- Studio Stage 8: ../EPIC_Hercules_Studio/tasks/stage_08_workflow.md
- Backlog: ../backlog.md