Skip to content

Persist messages incrementally to prevent context loss on cancellation #7827

@codefromthecrypt

Description

@codefromthecrypt

Please explain the motivation behind the feature request.

Goose persists session messages to SQLite via add_message, a per-message INSERT. The agent loop in reply_internal collects messages into a messages_to_add vector across 8 push sites (assistant response, thinking, tool requests, tool responses, summaries).

Today, these are flushed at the end of each iteration so that on the next turn the model sees the full conversation history.

The problem is mid-iteration cancellation, such as Ctrl+C in the CLI, or Stop in the UI, resulting in CancellationToken that breaks the loop.

Concretely, say the model responds with 3 tool calls that all execute successfully, but the user presses Stop during the third. All three tool request + response pairs are in messages_to_add but never reach add_message. On resume, the session shows only the user's prompt with no evidence the tools ran.

Describe the solution you'd like

Persist each message as it completes rather than batching at end-of-iteration. This eliminates the window between collection and persistence so no completed message can be lost. The cost is negligible: add_message is a single SQLite INSERT, and the current flush already loops per-message rather than doing a bulk insert.

Describe alternatives you've considered

A blunt replace_conversation() on every cancellation/disconnect path. Problems:

  • replace_conversation does a full DELETE + INSERT of all messages, too destructive for a transient event
  • In the server, the local all_messages tracker can be stale relative to what the agent already persisted via update_message_metadata (tool-pair summarization invisibility flags), so overwriting can clobber metadata
  • In the CLI, handle_interrupted_messages adds synthetic tool error responses to self.messages before any persistence; a replace_conversation after that writes those synthetic messages to the DB as if the model produced them
  • It fires on client disconnect (transient network events), not just intentional cancellation

replace_conversation is correctly reserved for intentional operations: compaction, /clear, and import_session.

Additional context

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions