Итог параллельной волны фиксов (worktree-изоляция, cherry-pick в master): - ingest-dedup-integrity (F1, F6) → спека ingest - retry-stall-basis (MAJOR-1, MAJOR-2) → спека state-reconciliation - linking-transition-robustness (MAJOR-4, MINOR-7) → спеки file-layout и state-reconciliation Дельты влиты в openspec/specs, changes перенесены в openspec/changes/archive/2026-07-08-*. Беклог не трогаю (по решению). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4.8 KiB
4.8 KiB
Why
Раскладка хардлинками устроена как «claim-then-side-effect»: сначала задача
переводится в linking (claim владения шагом), затем создаются хардлинки и
пишется их учёт (file_link). Ревью жизненного цикла (Fable, 2026-07-08)
нашло две связанные дыры устойчивости этого пути.
- MINOR-7:
worker.transitionпри ошибке записи состояния логировал её, но НЕ возвращал вызывающему. На путяхApplyи авто-раскладки вfinishRecognitionвыполнение продолжалось к побочным эффектам: хардлинки создавались, пока claim перехода вlinkingне закоммичен. Финальный переходlinking → doneоценивался графом какreview → done(нелегальное ребро) и отклонялся — задача застревала вreviewсо stale-планом, скан/уведомление не срабатывали. - MAJOR-4: задача может осиротеть в
linking: (A) без краха — хардлинки созданы, ноCreateFileLinksупал транзиентно (SQLite busy) → голыйreturnоставлял задачу вlinking, а файлы на диске — без строкfile_link; (B) краш процесса между переходом вlinkingи финальным переходом → на рестартеlinkingне листит НИКТО (поллинг листитdownloading, распознавание —completed/recognizing, сверка —done/target_missing/orphaned, восстановление —failed/stuck). Задача сидит вlinkingвечно; выход — только ручной Cancel/Defer (недискаверабельно). Нарушен инвариант «у каждого нетерминального состояния есть владелец» (recognizingуже лечится рестартом черезrecognizePending,linking— нет).
What Changes
transitionразделяется на fire-and-forget обёртку (прежнее имя, прежнее поведение для reconcile/таймаутов/финальных переходов) иtransitionErr, которая ВОЗВРАЩАЕТ ошибку записи. На claim-then-side-effect путях (Apply, авто-раскладка вfinishRecognition) провал claim перехода вlinkingтеперь прерывает выполнение ДО хардлинков.- В
linkPlanпровалCreateFileLinksбольше не оставляет задачу вlinking: задача уходит вreviewс кодомpersistи причиной; повторныйApplyидемпотентен (хардлинки уже на диске →StatusExists, учёт дописывается). - Новый шаг поллинга
sweepLinking: на каждом тике и на старте задачи вlinkingвозвращаются вreviewс кодомinterruptedи причиной «прерванная раскладка, повтори применение». Любаяlinking, видимая подw.mu, устарела по построению (активная раскладка держитw.muвесь свой срок), значит осталась после краха.
Capabilities
Modified Capabilities
file-layout: раскладка становится устойчивой к сбою записи claim/учёта — хардлинки не создаются при незакоммиченном claim, а сбой записи учёта не стрэндит задачу вlinking.state-reconciliation: у нетерминальногоlinkingпоявляется владелец на рестарте/тике — sweep осиротевшихlinkingвreview.
Impact
- Код:
internal/worker/worker.go(transition/transitionErr,pollOnce,sweepLinking),internal/worker/review.go(Apply,finishRecognition,linkPlan). Граф переходов (internal/store/download.go) правки не требует — реброlinking → reviewуже объявлено. - Тесты: провал claim прерывает до хардлинков; провал
CreateFileLinksуводит вreview(файлы на диске); sweep осиротевшегоlinking→review. - БД/схема: без изменений.