Skip to content

fix: avoid restoring stale sessions with dangling tool calls#2364

Open
changeworldBT wants to merge 1 commit intosipeed:mainfrom
changeworldBT:fix/stale-session-recovery
Open

fix: avoid restoring stale sessions with dangling tool calls#2364
changeworldBT wants to merge 1 commit intosipeed:mainfrom
changeworldBT:fix/stale-session-recovery

Conversation

@changeworldBT
Copy link
Copy Markdown

Summary

  • drop dangling tool-call tails when restoring session history
  • avoid restoring stale runtime state after restart
  • prevent Telegram sessions from getting stuck in typing/steering mode

Problem

If a session is persisted while an assistant tool-call turn is still unfinished, restarting picoclaw can restore that broken tail as valid history. New Telegram messages may then get redirected into the steering queue instead of starting a fresh turn, leaving the bot stuck in typing without a final reply.

Fix

Sanitize recovered session history before it is reused by the agent. If the history ends with an assistant message containing tool calls that do not have a complete set of matching tool results, trim that dangling suffix and keep only the safe, completed history.

Notes

This keeps completed tool-call sequences intact while preventing stale unfinished execution state from surviving process restart.

@sipeed-bot sipeed-bot bot added type: bug Something isn't working domain: agent labels Apr 5, 2026
// (assistant tool calls plus later steering/user messages) as if it were valid
// history. We keep completed tool-call sequences intact and only trim the
// incomplete suffix.
func sanitizeRecoveredHistory(history []providers.Message) []providers.Message {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sanitizeRecoveredHistory not only removes the incomplete assistant/tool block: with return history[:i] it truncates the entire suffix starting with the assistant with ToolCalls. This means that any user/steering message persisted after that point is lost altogether on reboot. It is also in contrast to the sanitizer already used by the ContextBuilder, which discards the broken tool-call round but preserves subsequent non-tool messages. Here we risk permanent loss of real input instead of limiting the fix to only incomplete runtime state.

session.Updated = time.Now()
}

func (sm *SessionManager) GetHistory(key string) []providers.Message {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The helper is called inside SessionManager.GetHistory, so it doesn't just apply to history retrieved from disk after a restart: it changes the meaning of each session read in memory. This also breaks the stated SessionStore contract (GetHistory returns the full message history) and causes runtime callers to see a filtered view instead of the actual persisted log. If recovery is the problem, sanitization should be applied in the session bootstrap/reopen or materialized once, not hidden behind each GetHistory.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain: agent type: bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants