Add council review wait command

This commit is contained in:
2026-03-19 15:13:34 +08:00
parent c1beacb703
commit dd6a0e31b6
7 changed files with 522 additions and 10 deletions
+179
View File
@@ -2,9 +2,11 @@ package store
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
"time"
)
var councilReviewerRoles = []string{
@@ -53,6 +55,18 @@ type CouncilStartResult struct {
Reviewers []CouncilReviewer `json:"reviewers"`
}
type CouncilWaitInput struct {
RunID string
Timeout time.Duration
}
type CouncilWaitResult struct {
Woke bool `json:"woke"`
RunID string `json:"run_id"`
AllComplete bool `json:"all_complete"`
ReviewerStatuses []CouncilReviewer `json:"reviewers"`
}
func (s *OrchStore) StartCouncil(ctx context.Context, input CouncilStartInput) (CouncilStartResult, error) {
runID := strings.TrimSpace(input.RunID)
if runID == "" {
@@ -442,3 +456,168 @@ func boolToInt(value bool) int {
}
return 0
}
func (s *OrchStore) WaitForCouncil(ctx context.Context, input CouncilWaitInput) (CouncilWaitResult, error) {
runID := strings.TrimSpace(input.RunID)
if runID == "" {
return CouncilWaitResult{}, fmt.Errorf("%w: run id is required", ErrInvalidInput)
}
if _, err := s.GetCouncilRun(ctx, runID); err != nil {
return CouncilWaitResult{}, err
}
waitCtx := ctx
cancel := func() {}
if input.Timeout > 0 {
waitCtx, cancel = context.WithTimeout(ctx, input.Timeout)
}
defer cancel()
for {
reviewers, allComplete, err := s.GetCouncilReviewerStatuses(waitCtx, runID)
if err != nil {
if isDeadlineExceeded(waitCtx) {
return CouncilWaitResult{
Woke: false,
RunID: runID,
AllComplete: false,
ReviewerStatuses: reviewers,
}, nil
}
return CouncilWaitResult{}, err
}
if allComplete {
return CouncilWaitResult{
Woke: true,
RunID: runID,
AllComplete: true,
ReviewerStatuses: reviewers,
}, nil
}
if _, err := s.ReconcileRun(waitCtx, runID); err != nil {
if isSQLiteBusyError(err) {
ok, waitErr := waitForNextPoll(waitCtx, 25*time.Millisecond)
if waitErr != nil {
if errors.Is(waitErr, context.DeadlineExceeded) {
reviewers, _, _ := s.GetCouncilReviewerStatuses(ctx, runID)
return CouncilWaitResult{
Woke: false,
RunID: runID,
AllComplete: false,
ReviewerStatuses: reviewers,
}, nil
}
return CouncilWaitResult{}, waitErr
}
if !ok {
reviewers, _, _ := s.GetCouncilReviewerStatuses(ctx, runID)
return CouncilWaitResult{
Woke: false,
RunID: runID,
AllComplete: false,
ReviewerStatuses: reviewers,
}, nil
}
continue
}
if isDeadlineExceeded(waitCtx) {
reviewers, _, _ := s.GetCouncilReviewerStatuses(ctx, runID)
return CouncilWaitResult{
Woke: false,
RunID: runID,
AllComplete: false,
ReviewerStatuses: reviewers,
}, nil
}
return CouncilWaitResult{}, err
}
ok, err := waitForNextPoll(waitCtx, 200*time.Millisecond)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
reviewers, _, _ := s.GetCouncilReviewerStatuses(ctx, runID)
return CouncilWaitResult{
Woke: false,
RunID: runID,
AllComplete: false,
ReviewerStatuses: reviewers,
}, nil
}
return CouncilWaitResult{}, err
}
if !ok {
reviewers, _, _ := s.GetCouncilReviewerStatuses(ctx, runID)
return CouncilWaitResult{
Woke: false,
RunID: runID,
AllComplete: false,
ReviewerStatuses: reviewers,
}, nil
}
}
}
func (s *OrchStore) GetCouncilRun(ctx context.Context, runID string) (CouncilRun, error) {
row := s.db.QueryRowContext(
ctx,
`SELECT run_id, mode, target_type, output_mode, only_unanimous
FROM council_runs
WHERE run_id = ?`,
runID,
)
var (
run CouncilRun
onlyUnanimous int
)
err := row.Scan(&run.RunID, &run.Mode, &run.TargetType, &run.OutputMode, &onlyUnanimous)
if errors.Is(err, sql.ErrNoRows) {
return CouncilRun{}, fmt.Errorf("%w: council run %s not found", ErrRunNotFound, runID)
}
if err != nil {
return CouncilRun{}, fmt.Errorf("scan council run: %w", err)
}
run.OnlyUnanimous = onlyUnanimous != 0
return run, nil
}
func (s *OrchStore) GetCouncilReviewerStatuses(ctx context.Context, runID string) ([]CouncilReviewer, bool, error) {
rows, err := s.db.QueryContext(
ctx,
`SELECT cr.reviewer_role, cr.task_id, t.status
FROM council_reviewers cr
JOIN tasks t
ON t.run_id = cr.run_id
AND t.task_id = cr.task_id
WHERE cr.run_id = ?
ORDER BY cr.reviewer_role ASC`,
runID,
)
if err != nil {
return nil, false, fmt.Errorf("query council reviewer statuses: %w", err)
}
defer rows.Close()
reviewers := make([]CouncilReviewer, 0, len(councilReviewerRoles))
allComplete := true
for rows.Next() {
var reviewer CouncilReviewer
if err := rows.Scan(&reviewer.ReviewerRole, &reviewer.TaskID, &reviewer.Status); err != nil {
return nil, false, fmt.Errorf("scan council reviewer status: %w", err)
}
if reviewer.Status != "done" && reviewer.Status != "failed" && reviewer.Status != "cancelled" {
allComplete = false
}
reviewers = append(reviewers, reviewer)
}
if err := rows.Err(); err != nil {
return nil, false, fmt.Errorf("iterate council reviewer statuses: %w", err)
}
if len(reviewers) == 0 {
return nil, false, fmt.Errorf("%w: council reviewers for run %s not found", ErrRunNotFound, runID)
}
return reviewers, allComplete, nil
}