fix(worker): depends_on-Nachfolger schon beim gelandeten Merge freigeben

Der Queue-Picker gatete eine task_dependencies-Kante auf status='done'.
review_task(skipVerify) laesst einen erfolgreich gemergten Task aber
bewusst auf WaitingForReview stehen, bis verify_merges einmal zentral
laeuft -- damit blockierte jeder Nachfolger ueber den gesamten Batch plus
Full-Suite-Verify hinweg, bei freien Slots.

Eine Kante gilt jetzt als erfuellt, wenn der Vorgaenger 'done' ist ODER
sein Merge gelandet ist (worktrees.state = 'merged'). TaskBlocking
(get_task/list_tasks/wait_for_task_change) und der UI-Wartehinweis folgen
derselben Regel, damit "blocked" nicht meldet, was der Picker claimt.

Abgeleitet: revert_merge/abort_merge setzen den Worktree auf Active und
blockieren wieder; reject_rerun/reject_park haben nie gemergt und bleiben
unerfuellt; ein Task ohne Worktree wird nur ueber 'done' erfuellt; ein
fehlgeschlagener verify_merges gibt Nachfolger frei, weil der Code so
oder so auf main liegt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Mika Kuns
2026-09-16 15:43:37 +02:00
committed by mika kuns
co-authored by Claude Opus 5
parent 50b6b56f87
commit c874bf7e52
8 changed files with 159 additions and 31 deletions
+18 -6
View File
@@ -21,11 +21,19 @@ public sealed class QueuePicker : IQueuePicker
// Atomic queue claim: UPDATE + RETURNING in a single statement prevents TOCTOU races.
// Raw SQL because EF cannot express UPDATE...RETURNING.
// Eligible task must be Queued, unblocked (chain and depends-on), not manual, and due
// (or unscheduled). A task_dependencies edge only gates on the dependency's Status='done'
// -- unlike blocked_by_task_id there is no cascade on the dependency failing, so a task
// with a Failed dependency simply stays here, skipped, until someone intervenes. The
// depends-on clause is doubly negated on purpose: "no edge whose predecessor isn't done"
// means a task with zero edges is eligible, ALL edges must be done, and an edge pointing
// (or unscheduled). A task_dependencies edge is satisfied once the predecessor is 'done'
// OR its merge has landed (worktrees.state = 'merged'), because review_task(skipVerify)
// deliberately parks a successfully merged task in WaitingForReview until the batch
// verify_merges runs -- gating on 'done' alone stalled every successor for the whole
// batch. Consequences, all deliberate: a failing verify_merges leaves the merges on the
// target branch, so successors do run (the predecessor's code is there either way);
// revert_merge/abort_merge flip the worktree back to 'active' and re-block; a rejected
// task (reject_rerun/reject_park) never reached 'merged' and stays unmet; a task with no
// worktree (handler task, empty diff) is only ever satisfied via 'done'. Unlike
// blocked_by_task_id there is no cascade on the dependency failing, so a task with a
// Failed dependency simply stays here, skipped, until someone intervenes. The depends-on
// clause is doubly negated on purpose: "no edge whose predecessor isn't satisfied" means
// a task with zero edges is eligible, ALL edges must be satisfied, and an edge pointing
// at a vanished task keeps blocking (same as the old single-column form did).
// EF SQLite stores DateTime as "yyyy-MM-dd HH:mm:ss.fffffff" — same format used here for comparison.
var rows = await ctx.Tasks.FromSqlRaw("""
@@ -41,7 +49,11 @@ public sealed class QueuePicker : IQueuePicker
WHERE td.task_id = t.id
AND NOT EXISTS (
SELECT 1 FROM tasks d
WHERE d.id = td.depends_on_task_id AND d.status = 'done'
WHERE d.id = td.depends_on_task_id
AND (d.status = 'done' OR EXISTS (
SELECT 1 FROM worktrees w
WHERE w.task_id = d.id AND w.state = 'merged'
))
)
)
ORDER BY t.sort_order ASC, t.created_at ASC