fix(agent): reopenSession replaces plan steps for executing sessions too
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled

A follow-up on an executing session (first turn didn't complete_task) is
still a new direction — the old plan's steps must not block the new one.
Previously reopenSession was a no-op for executing sessions, leaving done
steps that caused errPlanInFlight on the next propose_plan call.
This commit is contained in:
2026-07-15 11:13:23 +02:00
parent 3d7fa99560
commit 487f9ad358

View File

@@ -478,11 +478,16 @@ func (s *store) setGoal(ctx context.Context, sessionID, goal string) error {
// audit trail survives. proposePlan's anyStarted check excludes
// `replaced` (see proposePlan), so the next propose_plan takes the
// fresh-generation path rather than being refused with errPlanInFlight.
// 2. Clears outcome/summary so the panel doesn't show the old result.
// This runs for BOTH terminal and executing sessions — a follow-up on
// an executing session (first turn didn't complete_task) is still a
// new direction, and the old plan's steps must not block the new one.
// 2. If the session was terminal, clears outcome/summary and flips status
// to `executing` so the panel doesn't show the old result.
// 3. Stamps last_active_at.
//
// Returns true if the session was actually reopened (was terminal), false if
// it was already active (no-op — the follow-up is just a continuation).
// it was already active (the plan steps are still replaced — the new
// direction needs a fresh plan — but the status doesn't change).
func (s *store) reopenSession(ctx context.Context, sessionID string) bool {
if s == nil || sessionID == "" || sessionID == "ephemeral" {
return false
@@ -492,18 +497,23 @@ func (s *store) reopenSession(ctx context.Context, sessionID string) bool {
`SELECT status FROM agent_sessions WHERE id = $1`, sessionID).Scan(&currentStatus); err != nil {
return false
}
if currentStatus != "done" && currentStatus != "failed" {
return false
}
// Always replace prior plan steps — the follow-up is a new direction
// regardless of whether the prior turn completed. Without this, an
// executing session with done steps would block proposePlan with
// errPlanInFlight.
s.pool.Exec(ctx,
`UPDATE session_plan_steps SET status = 'replaced', finished_at = COALESCE(finished_at, now()) WHERE session_id = $1 AND status <> 'replaced'`,
sessionID)
s.pool.Exec(ctx,
`UPDATE agent_sessions SET status = 'executing', outcome = NULL, summary = NULL, last_active_at = now() WHERE id = $1`,
sessionID)
_ = observability.Event(ctx, sqlcgen.New(s.pool), "task.reopened", s.taskEntityPtr(ctx, sessionID),
"info", "nomos", sessionID, map[string]any{"prior_status": currentStatus})
return true
if currentStatus == "done" || currentStatus == "failed" {
s.pool.Exec(ctx,
`UPDATE agent_sessions SET status = 'executing', outcome = NULL, summary = NULL, last_active_at = now() WHERE id = $1`,
sessionID)
_ = observability.Event(ctx, sqlcgen.New(s.pool), "task.reopened", s.taskEntityPtr(ctx, sessionID),
"info", "nomos", sessionID, map[string]any{"prior_status": currentStatus})
return true
}
s.pool.Exec(ctx, `UPDATE agent_sessions SET last_active_at = now() WHERE id = $1`, sessionID)
return false
}
// planStepInput is one step as the agent proposes it.