Run discards on the state-driven coordinator

This commit is contained in:
Ian Gulliver
2026-10-21 11:53:37 -07:00
parent f3a0e28763
commit 1b5f42a01a
8 changed files with 141 additions and 191 deletions
+1 -6
View File
@@ -110,12 +110,7 @@ func (m *Manager) handoff(ctx context.Context, sessionID, handoff string) error
next.State = api.AgentSessionStateRunning
err = m.store.ReplaceSession(ctx, sess.Id, next, task.ProjectId, "byre")
if errors.Is(err, store.ErrStateChanged) {
err = transition(ctx, m.store, sess.Id, api.AgentSessionStateRunning, api.AgentSessionStateIdle, "byre")
if err != nil {
return err
}
m.startDiscard(task.Id)
return nil
return transition(ctx, m.store, sess.Id, api.AgentSessionStateRunning, api.AgentSessionStateIdle, "byre")
}
if err != nil {
return err
+28 -18
View File
@@ -15,43 +15,53 @@ func (m *Manager) poke() {
}
func (m *Manager) coordinate() {
finalizing := map[string]bool{}
started := map[string]bool{}
for {
select {
case <-m.watchStop:
return
case <-m.pokes:
m.coordinateMerges(context.Background(), finalizing)
m.coordinatePass(context.Background(), started)
}
}
}
func (m *Manager) coordinateMerges(ctx context.Context, finalizing map[string]bool) {
work, err := m.store.CoordinateMerges(ctx)
func (m *Manager) coordinatePass(ctx context.Context, started map[string]bool) {
work, err := m.store.Coordinate(ctx)
if err != nil {
slog.Error("coordinate merges", "err", err)
slog.Error("coordinate", "err", err)
return
}
for _, id := range work.Abandoned {
for _, id := range work.AbandonedMerges {
slog.Info("merge turn ended unfinished", "task", id)
m.fileError(ctx, id, abandonedMergeText)
m.deliverIdle(ctx, id)
}
for _, c := range work.Start {
for _, c := range work.StartMerges {
m.startClaimedMerge(ctx, c)
}
current := map[string]bool{}
for _, f := range work.Finalize {
current[f.Task.Id] = true
if finalizing[f.Task.Id] {
continue
}
finalizing[f.Task.Id] = true
go m.finalizeMerge(f)
for _, id := range work.StartDiscards {
m.startDiscard(ctx, id)
}
for id := range finalizing {
if !current[id] {
delete(finalizing, id)
current := map[string]bool{}
once := func(key string, run func()) {
current[key] = true
if started[key] {
return
}
started[key] = true
go run()
}
for _, f := range work.FinalizeMerges {
once("merge:"+f.Task.Id, func() { m.finalizeMerge(f) })
}
for _, id := range work.FinishDiscards {
once("discard:"+id, func() { m.finishDiscard(id) })
}
for key := range started {
if !current[key] {
delete(started, key)
}
}
}
+7 -43
View File
@@ -61,7 +61,6 @@ type Manager struct {
moveRefused map[string]bool
startedTodo map[string]time.Time
startRefused map[string]bool
finishing map[string]bool
handoffModel map[string]string
mergeTurns map[string]mergeTurn
landing map[string]landing
@@ -167,7 +166,6 @@ func New(st *store.Store, worktreeRoot, leadRoot, handsRoot, codexHome, antigrav
moveRefused: map[string]bool{},
startedTodo: map[string]time.Time{},
startRefused: map[string]bool{},
finishing: map[string]bool{},
handoffModel: map[string]string{},
mergeTurns: map[string]mergeTurn{},
landing: map[string]landing{},
@@ -468,11 +466,6 @@ func (m *Manager) Recover(ctx context.Context) error {
return err
}
err = m.recoverDiscards(ctx)
if err != nil {
return err
}
go m.deliverAllLeads()
go m.deliverAllQueued(context.Background())
go m.coordinate()
@@ -491,17 +484,8 @@ func (m *Manager) failResume(ctx context.Context, sess api.AgentSession, task ap
if derr != nil {
return derr
}
end, serr := m.store.EndTurn(ctx, sess.Id, "byre", map[string]any{"error": err.Error()}, nil, false, false, false)
if serr != nil {
return serr
}
switch end.Discard {
case store.DiscardClosing:
go m.finishDiscard(task.Id)
case store.DiscardHeld:
go m.startDiscard(task.Id)
}
return nil
_, err = m.store.EndTurn(ctx, sess.Id, "byre", map[string]any{"error": err.Error()}, nil, false, false, false)
return err
}
func (m *Manager) logUndelivered(ctx context.Context, sessionID, reason string) {
@@ -1012,12 +996,7 @@ func (m *Manager) pump(l *live) {
m.finishReviewer(l, "the reviewer's process exited mid-turn")
return
}
switch end.Discard {
case store.DiscardClosing:
m.finishDiscard(l.taskID)
return
case store.DiscardHeld:
m.startDiscard(l.taskID)
if end.Discard != store.DiscardNone {
return
}
if l.owner == "" {
@@ -1317,17 +1296,11 @@ func (m *Manager) turnEnded(ctx context.Context, l *live, res endResult) error {
m.snapshotSoon(liveTarget(l), api.SnapshotTriggerTurnEnd, 0, review, false)
}
switch end.Discard {
case store.DiscardClosing:
if end.Discard != store.DiscardNone {
m.logUndelivered(ctx, l.id, "turn ended in discard")
go m.finishDiscard(l.taskID)
return nil
case store.DiscardHeld:
if stageFinished {
if end.Discard == store.DiscardHeld && stageFinished {
go m.advanceStage(l)
return nil
}
go m.startDiscard(l.taskID)
return nil
}
if handoff {
@@ -1348,22 +1321,13 @@ func (m *Manager) turnEnded(ctx context.Context, l *live, res endResult) error {
}
func (m *Manager) afterTurn(ctx context.Context, l *live) {
discard, err := m.store.GetDiscard(ctx, l.taskID)
if err != nil {
slog.Error("read discard state", "task", l.taskID, "err", err)
return
}
if discard.State == store.DiscardHeld {
m.startDiscard(l.taskID)
return
}
task, err := m.store.GetTask(ctx, l.taskID)
if err != nil {
slog.Error("read task after turn", "task", l.taskID, "err", err)
return
}
if task.Merging {
m.logUndelivered(ctx, l.id, "task is merging")
if task.Merging || task.Discarding {
m.logUndelivered(ctx, l.id, "task is merging or being discarded")
return
}
m.deliverTeam(ctx, l)
+31 -84
View File
@@ -134,7 +134,7 @@ func (m *Manager) searchClosedText(ctx context.Context, projectID, query string,
}
func (m *Manager) Discard(ctx context.Context, task api.Task, actor string) (api.Task, error) {
state, err := m.store.RequestDiscard(ctx, task.Id, actor)
err := m.store.RequestDiscard(ctx, task.Id, actor)
switch {
case errors.Is(err, store.ErrNotFound):
return api.Task{}, fmt.Errorf("%w: task %s is closed", ErrConflict, task.Id)
@@ -149,94 +149,74 @@ func (m *Manager) Discard(ctx context.Context, task api.Task, actor string) (api
case err != nil:
return api.Task{}, err
}
if state == store.DiscardClosing {
go m.finishDiscard(task.Id)
} else {
go m.startDiscard(task.Id)
}
return m.store.GetTask(ctx, task.Id)
}
func (m *Manager) startDiscard(taskID string) {
ctx := context.Background()
started, err := m.launchDiscard(ctx, taskID)
func (m *Manager) startDiscard(ctx context.Context, taskID string) {
err := m.beginDiscard(ctx, taskID)
if err == nil {
if started {
slog.Info("discard turn started", "task", taskID)
}
return
}
slog.Error("start discard turn", "task", taskID, "err", err)
err = m.store.CloseDiscard(ctx, taskID)
if err != nil {
slog.Error("close discard", "task", taskID, "err", err)
return
}
m.finishDiscard(taskID)
m.closeDiscard(ctx, taskID)
}
func (m *Manager) launchDiscard(ctx context.Context, taskID string) (bool, error) {
func (m *Manager) closeDiscard(ctx context.Context, taskID string) {
err := m.store.CloseDiscard(ctx, taskID)
if err != nil {
slog.Error("close discard", "task", taskID, "err", err)
}
m.poke()
}
func (m *Manager) beginDiscard(ctx context.Context, taskID string) error {
task, err := m.store.GetTask(ctx, taskID)
if err != nil {
return false, err
return err
}
sess, found, err := m.liveSession(ctx, taskID)
if err != nil {
return false, err
return err
}
if !found {
creator, err := m.store.GetUser(ctx, task.CreatedBy)
if err != nil {
return false, err
return err
}
sess, err = m.newSession(ctx, task, creator, sess.Member, "")
if err != nil {
return false, err
return err
}
}
err = m.store.StartDiscardTurn(ctx, taskID, sess.Id, "byre")
if errors.Is(err, store.ErrStateChanged) {
return false, nil
return nil
}
if err != nil {
return false, err
return err
}
_, err = m.launch(ctx, sess, task, "byre", discardPrompt, "", "", nil)
return err == nil, err
slog.Info("discard turn started", "task", taskID)
go func() {
ctx := context.Background()
_, err := m.launch(ctx, sess, task, "byre", discardPrompt, "", "", nil)
if err == nil {
return
}
slog.Error("launch discard turn", "task", taskID, "err", err)
m.closeDiscard(ctx, taskID)
}()
return nil
}
func (m *Manager) finishDiscard(taskID string) {
m.mu.Lock()
if m.finishing[taskID] {
m.mu.Unlock()
return
}
m.finishing[taskID] = true
m.mu.Unlock()
defer func() {
m.mu.Lock()
delete(m.finishing, taskID)
m.mu.Unlock()
}()
ctx := context.Background()
err := m.discardNow(ctx, taskID)
if err == nil {
return
}
slog.Error("finish discard", "task", taskID, "err", err)
sessions, serr := m.store.ListSessions(ctx, taskID)
if serr != nil {
slog.Error("list sessions for discard error", "task", taskID, "err", serr)
return
}
task, terr := m.store.GetTask(ctx, taskID)
if terr != nil {
slog.Error("load task for discard error", "task", taskID, "err", terr)
return
}
m.fileTaskError(ctx, task, sessions, "Discarding the task failed: "+err.Error()+". Discard again to retry; byre also retries when it next starts.")
m.fileError(ctx, taskID, "Discarding the task failed: "+err.Error()+". byre retries when it next starts.")
}
func (m *Manager) discardNow(ctx context.Context, taskID string) error {
@@ -264,36 +244,3 @@ func (m *Manager) discardNow(ctx context.Context, taskID string) error {
_, err = m.store.DiscardTask(ctx, task.Id, discard.By)
return err
}
func (m *Manager) recoverDiscards(ctx context.Context) error {
tasks, err := m.store.PendingDiscards(ctx)
if err != nil {
return err
}
for _, t := range tasks {
discard, err := m.store.GetDiscard(ctx, t.Id)
if err != nil {
return err
}
switch discard.State {
case store.DiscardHeld:
go m.startDiscard(t.Id)
case store.DiscardClosing:
go m.finishDiscard(t.Id)
case store.DiscardRunning:
sess, err := m.store.GetSession(ctx, discard.Session)
if err != nil {
return err
}
if sess.State == api.AgentSessionStateRunning {
continue
}
err = m.store.CloseDiscard(ctx, t.Id)
if err != nil {
return err
}
go m.finishDiscard(t.Id)
}
}
return nil
}