Run discards on the state-driven coordinator
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user