From 0851587dd73302df2ffeaf382d7620160e8ad9e8 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Mon, 19 Jan 2026 15:31:25 -0300 Subject: [PATCH] feat: recover corrupt sessions Signed-off-by: Carlos Alexandro Becker --- internal/agent/agent.go | 8 +++++ internal/agent/coordinator.go | 53 ++++++++++++++++++++++++++++ internal/tui/components/chat/chat.go | 9 +++++ 3 files changed, 70 insertions(+) diff --git a/internal/agent/agent.go b/internal/agent/agent.go index c916cfd886372ab86f6d1fbb0e8b7bde2c87dabb..97017528ecff2404e69dbab057f7b1987aa9646c 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -621,6 +621,14 @@ func (a *sessionAgent) Summarize(ctx context.Context, sessionID string, opts fan deleteErr := a.messages.Delete(ctx, summaryMessage.ID) return deleteErr } + // Summarization failed. Mark the message as finished with an error + // so the UI doesn't get stuck in a "Summarizing" state forever. + summaryMessage.FinishThinking() + summaryMessage.AddFinish(message.FinishReasonError, "Summarization failed", err.Error()) + updateErr := a.messages.Update(ctx, summaryMessage) + if updateErr != nil { + return updateErr + } return err } diff --git a/internal/agent/coordinator.go b/internal/agent/coordinator.go index 943c3efc41b33ea9f261b4ffc7256b6f544beff9..3cbc1cbba7770a72624248ee9a875c53f7fee82f 100644 --- a/internal/agent/coordinator.go +++ b/internal/agent/coordinator.go @@ -54,6 +54,7 @@ type Coordinator interface { QueuedPromptsList(sessionID string) []string ClearQueue(sessionID string) Summarize(context.Context, string) error + RecoverSession(ctx context.Context, sessionID string) error Model() Model UpdateModels(ctx context.Context) error } @@ -834,6 +835,58 @@ func (c *coordinator) Summarize(ctx context.Context, sessionID string) error { return c.currentAgent.Summarize(ctx, sessionID, getProviderOptions(c.currentAgent.Model(), providerCfg)) } +func (c *coordinator) RecoverSession(ctx context.Context, sessionID string) error { + // Skip recovery if session is currently active + if c.currentAgent.IsSessionBusy(sessionID) { + return nil + } + + msgs, err := c.messages.List(ctx, sessionID) + if err != nil { + return fmt.Errorf("failed to list messages: %w", err) + } + + for _, msg := range msgs { + if msg.IsFinished() { + continue + } + + // Handle incomplete summary messages + if msg.IsSummaryMessage { + msg.FinishThinking() + msg.AddFinish(message.FinishReasonError, "Summarization interrupted", "Session was interrupted during summarization") + if updateErr := c.messages.Update(ctx, msg); updateErr != nil { + slog.Error("Failed to recover summary message", "message_id", msg.ID, "error", updateErr) + } + continue + } + + // Handle incomplete assistant messages with tool calls + if msg.Role == message.Assistant && len(msg.ToolCalls()) > 0 { + // Mark any unfinished tool calls as finished + updated := false + for _, tc := range msg.ToolCalls() { + if !tc.Finished { + msg.FinishToolCall(tc.ID) + updated = true + } + } + if updated { + if updateErr := c.messages.Update(ctx, msg); updateErr != nil { + slog.Error("Failed to update tool calls", "message_id", msg.ID, "error", updateErr) + } + } + msg.FinishThinking() + msg.AddFinish(message.FinishReasonError, "Response interrupted", "Session was interrupted during tool execution") + if updateErr := c.messages.Update(ctx, msg); updateErr != nil { + slog.Error("Failed to recover assistant message", "message_id", msg.ID, "error", updateErr) + } + } + } + + return nil +} + func (c *coordinator) isUnauthorized(err error) bool { var providerErr *fantasy.ProviderError return errors.As(err, &providerErr) && providerErr.StatusCode == http.StatusUnauthorized diff --git a/internal/tui/components/chat/chat.go b/internal/tui/components/chat/chat.go index 036c8262d2b0d8419bf89b64afd922767b6be12a..a0f482cf96e93e8645677304a7899feec263a72c 100644 --- a/internal/tui/components/chat/chat.go +++ b/internal/tui/components/chat/chat.go @@ -2,6 +2,7 @@ package chat import ( "context" + "log/slog" "time" "charm.land/bubbles/v2/key" @@ -544,6 +545,14 @@ func (m *messageListCmp) SetSession(session session.Session) tea.Cmd { } m.session = session + + // Recover any incomplete messages from interrupted sessions + if m.app.AgentCoordinator != nil { + if recoverErr := m.app.AgentCoordinator.RecoverSession(context.Background(), session.ID); recoverErr != nil { + slog.Error("Failed to recover session", "session_id", session.ID, "error", recoverErr) + } + } + sessionMessages, err := m.app.Messages.List(context.Background(), session.ID) if err != nil { return util.ReportError(err)