diff --git a/cmd/nomos/store.go b/cmd/nomos/store.go index 71e6a04..e10bcd5 100644 --- a/cmd/nomos/store.go +++ b/cmd/nomos/store.go @@ -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(¤tStatus); 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.