Skip to the content.

Task 73 — Outbox synced-state and bounded-queue prune fix

Phase: 5 Initiative: 45 Status: done Owner: — Slug: outbox-synced-fix

Goal

SqliteOutboxStore.MarkSyncedAsync (Offline/SqliteOutboxStore.cs:139-143) делает DELETE FROM outbox_items WHERE item_id = $iid вместо UPDATE ... SET status='Synced'. Из-за этого PruneSyncedToCapAsync (:202-219) и GetSyncedCountAsync (:221-226) всегда возвращают 0 — bounded-queue prune-логика мертва. DeleteOlderThanAsync (:198) удаляет по created_at < cutoff независимо от статуса, удаляя in-flight Pending items. Цель — корректно хранить Synced-записи для cap-pruning и TTL-retention.

Acceptance criteria

Sub-tasks

Completion note

SqliteOutboxStore теперь корректно хранит Synced-записи (UPDATE вместо DELETE), что вернуло к жизни PruneSyncedToCapAsync и GetSyncedCountAsync — bounded-queue prune-логика больше не мертва. Добавлены synced_at колонка + индекс idx_outbox_status_synced; DeleteOlderThanAsync сохраняет Pending-строки от TTL-вытеснения. OfflineSyncConfig получил PruneSyncedThreshold/PruneSyncedKeep, DefaultDeadlineMinutes, MaxBackoffMs. В OfflineSyncService появился exponential backoff между retry одного item (cap 5 мин), OnReconnected теперь использует свежий CancellationTokenSource через linked CTS, что корректно обрабатывает shutdown. Убран default-инициализатор OutboxItem.ItemId, исключающий двойную генерацию ULID.

Validation: dotnet build src/agent/Hercules.csproj (0 errors), dotnet build src/agent/Hercules.WebApi/Hercules.WebApi.csproj (0 errors), dotnet test --filter FullyQualifiedName~Offline (26 passed, 0 failed). Pre-existing failures (OtelService, NumericValidator, WasmTool, RedisTaskQueue, BudgetGuard) подтверждены на baseline (git stash) — к данной задаче не относятся.

Implementation notes

Scope / Likely files

src/agent/Offline/SqliteOutboxStore.cs, src/agent/Offline/OfflineSyncService.cs, src/agent/Offline/OfflineSyncConfig.cs, src/agent/Offline/SyncItem.cs

Dependencies

Risks / Rollback

Хранение Synced-записей увеличивает размер таблицы до prune; убедиться что PruneSyncedKeep (default 500) достаточно мал. Rollback: git revert (вернуть DELETE).