feat(claude-do): depends_on als Menge statt Einzelfeld
Ein Task kann jetzt auf beliebig viele Vorgaenger warten. Die Einzelspalte tasks.depends_on_task_id wird durch die Beziehungstabelle task_dependencies ersetzt (Composite-PK, beide FKs ON DELETE CASCADE -- das reproduziert das alte ON DELETE SET NULL: geloeschter Vorgaenger = Kante weg = Task wieder pickbar). BlockedByTaskId bleibt unberuehrt. - Migration AddTaskDependencies kopiert jede bestehende depends_on_task_id verlustfrei in die Tabelle, bevor die Spalte fallengelassen wird - QueuePicker gatet per doppelt negiertem NOT EXISTS: kein Task mit 0 Kanten wird blockiert, alle Kanten muessen done sein, eine Kante auf einen verschwundenen Task blockiert weiter - SetDependsOnAsync ersetzt die ganze Menge; Zykluspruefung ist jetzt eine BFS ueber den Graphen, faengt also auch A->B->C->A - blockedReason nennt alle offenen Vorgaenger (External/TaskBlocking, von ExternalMcpService und TaskWaitMcpTools gemeinsam genutzt) - add_task/update_task nehmen dependsOnTaskIds; die alte Skalarform bleibt als Ein-Element-Menge inkl. ""-Clear-Sentinel gueltig - UI: Fan-in-Zeile bleibt flach und listet alle Vorgaenger im After-Label, die lineare Kette rendert unveraendert
This commit is contained in:
@@ -21,9 +21,12 @@ 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). depends_on_task_id 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.
|
||||
// (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
|
||||
// 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("""
|
||||
UPDATE tasks SET status = 'running', started_at = {1}
|
||||
@@ -33,11 +36,14 @@ public sealed class QueuePicker : IQueuePicker
|
||||
AND t.blocked_by_task_id IS NULL
|
||||
AND t.is_manual = 0
|
||||
AND (t.scheduled_for IS NULL OR t.scheduled_for <= {0})
|
||||
AND (t.depends_on_task_id IS NULL
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM tasks d
|
||||
WHERE d.id = t.depends_on_task_id AND d.status = 'done'
|
||||
))
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM task_dependencies td
|
||||
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'
|
||||
)
|
||||
)
|
||||
ORDER BY t.sort_order ASC, t.created_at ASC
|
||||
LIMIT 1
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user