Add task summaries, closed-task search and a summary turn before discard

This commit is contained in:
Ian Gulliver
2026-10-16 10:08:08 -07:00
parent 2107a43ef2
commit fd63a0dfbc
25 changed files with 1013 additions and 268 deletions
+99 -25
View File
@@ -47,6 +47,7 @@ type Manager struct {
paneReported map[string]time.Time
paneMoved map[string]time.Time
startedTodo map[string]time.Time
finishing map[string]bool
deliverMu sync.Mutex
leadMu sync.Mutex
@@ -111,6 +112,7 @@ func New(st *store.Store, worktreeRoot, leadRoot, codexHome string) *Manager {
paneReported: map[string]time.Time{},
paneMoved: map[string]time.Time{},
startedTodo: map[string]time.Time{},
finishing: map[string]bool{},
signals: make(chan api.Event, 1024),
snaps: map[string]*snapState{},
mergeHolders: map[string]string{},
@@ -142,6 +144,14 @@ func (m *Manager) create(ctx context.Context, task api.Task, creator api.User, p
return api.AgentSession{}, fmt.Errorf("%w: task %s is %s", ErrConflict, task.Id, task.Status)
}
sess, err := m.newSession(ctx, task, creator, model)
if err != nil {
return api.AgentSession{}, err
}
return m.turn(ctx, sess, task, actor, prompt, images)
}
func (m *Manager) newSession(ctx context.Context, task api.Task, creator api.User, model string) (api.AgentSession, error) {
existing, err := m.store.ListSessions(ctx, task.Id)
if err != nil {
return api.AgentSession{}, err
@@ -167,11 +177,7 @@ func (m *Manager) create(ctx context.Context, task api.Task, creator api.User, p
}
err = m.store.CreateSession(ctx, sess, task.ProjectId)
if err != nil {
return api.AgentSession{}, err
}
return m.turn(ctx, sess, task, actor, prompt, images)
return sess, err
}
func (m *Manager) Send(ctx context.Context, sessionID, actor, text, model string, images ...string) (api.AgentSession, error) {
@@ -253,8 +259,13 @@ func (m *Manager) StopTask(ctx context.Context, taskID, actor string) error {
}
func (m *Manager) retire(ctx context.Context, sess api.AgentSession, actor string) error {
m.stopLive(sess.Id)
return m.store.SetSessionState(ctx, sess.Id, api.AgentSessionStateCompleted, actor, nil)
}
func (m *Manager) stopLive(sessionID string) {
m.mu.Lock()
l := m.live[sess.Id]
l := m.live[sessionID]
if l != nil {
l.stopping = true
}
@@ -263,8 +274,6 @@ func (m *Manager) retire(ctx context.Context, sess api.AgentSession, actor strin
if l != nil {
l.sess.kill()
}
return m.store.SetSessionState(ctx, sess.Id, api.AgentSessionStateCompleted, actor, nil)
}
func (m *Manager) Recover(ctx context.Context) error {
@@ -300,17 +309,32 @@ func (m *Manager) Recover(ctx context.Context) error {
if derr != nil {
return derr
}
serr := m.store.SetSessionState(ctx, sess.Id, api.AgentSessionStateIdle, "byre", map[string]any{"error": err.Error()})
next, serr := m.store.EndTurn(ctx, sess.Id, "byre", map[string]any{"error": err.Error()})
if serr != nil {
return serr
}
if sess.Role == api.Implementer {
switch {
case next == store.DiscardClosing:
go m.finishDiscard(task.Id)
case next == store.DiscardHeld:
go m.startDiscard(task.Id)
case sess.Role == api.Implementer:
m.abandonMerge(task.Id, task.ProjectId)
}
continue
}
err = m.message(ctx, l, "byre", restartNotice, nil)
notice := restartNotice
if sess.Role == api.Implementer {
discard, err := m.store.GetDiscard(ctx, task.Id)
if err != nil {
return err
}
if discard.State == store.DiscardRunning && discard.Session == sess.Id {
notice = discardRestartPrompt
}
}
err = m.message(ctx, l, "byre", notice, nil)
if err != nil {
return err
}
@@ -331,6 +355,11 @@ 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.startHeldMerges(context.Background())
@@ -411,7 +440,10 @@ func (m *Manager) turn(ctx context.Context, sess api.AgentSession, task api.Task
if err != nil {
return api.AgentSession{}, err
}
return m.launch(ctx, sess, task, actor, text, images)
}
func (m *Manager) launch(ctx context.Context, sess api.AgentSession, task api.Task, actor, text string, images []string) (api.AgentSession, error) {
l, err := m.ensureLive(ctx, sess, task)
if err == nil {
err = m.syncModel(ctx, l, sess.Model)
@@ -536,7 +568,7 @@ func (m *Manager) ensureLive(ctx context.Context, sess api.AgentSession, task ap
if err != nil {
return nil, err
}
tools := []mcp.Tool{m.askTool(l), m.cancelQuestionTool(l), m.createTaskTool(l), m.setMergeTitleTool(l), m.stopMergeTool(l), m.exposePortTool(l)}
tools := []mcp.Tool{m.askTool(l), m.cancelQuestionTool(l), m.createTaskTool(l), m.setMergeTitleTool(l), m.stopMergeTool(l), m.setTaskSummaryTool(l), m.searchClosedTool(l), m.exposePortTool(l)}
if sess.Role == api.Implementer && task.WorkflowStage != nil {
frozen, err := m.store.TaskWorkflow(ctx, task.Id)
if err != nil {
@@ -622,7 +654,7 @@ func (m *Manager) pump(l *live) {
slog.Error("drop stage output", "session", l.id, "err", err)
}
}
err = m.store.SetSessionState(ctx, l.id, api.AgentSessionStateIdle, agentActor, map[string]any{"error": "agent process exited mid-turn"})
next, err := m.store.EndTurn(ctx, l.id, agentActor, map[string]any{"error": "agent process exited mid-turn"})
if err != nil {
slog.Error("record session exit", "session", l.id, "err", err)
}
@@ -630,6 +662,14 @@ func (m *Manager) pump(l *live) {
m.finishReviewer(l, "the reviewer's process exited mid-turn")
return
}
switch next {
case store.DiscardClosing:
m.finishDiscard(l.taskID)
return
case store.DiscardHeld:
m.startDiscard(l.taskID)
return
}
if l.owner == "" {
m.abandonMerge(l.taskID, l.projectID)
m.fileError(l, "The agent process exited mid-turn. Send a message to resume it.")
@@ -804,6 +844,10 @@ func (m *Manager) turnEnded(ctx context.Context, l *live, res endResult) error {
if err != nil {
return err
}
discard, err := m.store.GetDiscard(ctx, l.taskID)
if err != nil {
return err
}
mergingBy := ""
if finished && !handoff {
mergingBy, err = m.store.MergingBy(ctx, l.taskID)
@@ -811,7 +855,9 @@ func (m *Manager) turnEnded(ctx context.Context, l *live, res endResult) error {
return err
}
}
m.snapshotSoon(l, api.SnapshotTriggerTurnEnd, 0, mergingBy == "")
if discard.State == store.DiscardNone {
m.snapshotSoon(l, api.SnapshotTriggerTurnEnd, 0, mergingBy == "")
}
if res.window > 0 {
err = m.updateContext(ctx, l, l.contextUsed, res.window)
@@ -830,22 +876,37 @@ func (m *Manager) turnEnded(ctx context.Context, l *live, res endResult) error {
}
stageFinished := false
switch {
case l.stage == "":
case finished && !handoff && mergingBy == "":
stageFinished, err = m.store.FinishStage(ctx, l.id)
default:
err = m.store.DropStageOutput(ctx, l.id)
if l.stage != "" {
if finished && !handoff && mergingBy == "" {
stageFinished, err = m.store.FinishStage(ctx, l.id)
}
if err == nil && !stageFinished {
err = m.store.DropStageOutput(ctx, l.id)
}
if err != nil {
return err
}
}
next, err := m.store.EndTurn(ctx, l.id, agentActor, nil)
if err != nil {
return err
}
err = m.store.SetSessionState(ctx, l.id, api.AgentSessionStateIdle, agentActor, nil)
if err != nil {
return err
switch next {
case store.DiscardClosing:
m.logUndelivered(ctx, l.id, "turn ended in discard")
go m.finishDiscard(l.taskID)
return nil
case store.DiscardHeld:
go m.nextMerge(l.projectID)
if stageFinished {
go m.advanceStage(l)
return nil
}
go m.startDiscard(l.taskID)
return nil
}
if handoff {
m.logUndelivered(ctx, l.id, "turn ended in handoff")
go m.continueFromHandoff(l.id, res.result)
@@ -869,11 +930,24 @@ func (m *Manager) turnEnded(ctx context.Context, l *live, res endResult) error {
go m.advanceStage(l)
return nil
}
m.afterTurn(ctx, l)
return nil
}
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
}
err = m.Deliver(ctx, l.id)
if err != nil {
slog.Error("deliver inbox responses", "session", l.id, "err", err)
}
return nil
}
func commitMessage(res endResult) string {