Fix SQLite recovery and preserve provider conversation context

Persist exact provider history and archived tool results independently of
terminal scrollback, and retain them when active run checkpoints are cleared.
Keep conversation and session saving independent of automatic reopening.

Deduplicate only committed session snapshots, retry transient SQLite write
contention, and enqueue conversation updates before writer shutdown. Recover
healthy startup data independently and protect unread records by disabling
writes with a persistent warning after an incomplete restore. Retain local
transcript pane conversation links across restarts.

Add regression coverage for SQLite failures, context round trips, legacy
fallbacks, and transcript snapshots.
This commit is contained in:
2026-09-20 16:40:44 -05:00
parent 316f53471b
commit d7fbc09bcf
21 changed files with 714 additions and 185 deletions
+1
View File
@@ -9,6 +9,7 @@ license.workspace = true
local_fs = []
[dependencies]
galaxy_agent_core.workspace = true
chrono.workspace = true
diesel = { workspace = true, features = [
"chrono",
+11
View File
@@ -1086,8 +1086,19 @@ pub struct LocalAgentGroup {
pub notified: bool,
}
/// Exact provider history, independent of terminal scrollback and UI task projections.
/// `None` on AgentConversationData denotes a legacy row requiring reconstruction;
/// an explicitly empty history must stay empty after restoration.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct ProviderConversationContext {
pub messages: Vec<galaxy_agent_core::ConversationMessage>,
pub tool_result_archive: Vec<galaxy_agent_core::ConversationMessage>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct AgentConversationData {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_context: Option<ProviderConversationContext>,
/// Display-only tool intent, scoped to this conversation and keyed by call ID.
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub tool_titles: HashMap<String, String>,
+6
View File
@@ -137,6 +137,7 @@ fn is_restorable_accepts_empty_and_single_task_conversations() {
fn agent_conversation_data_roundtrips_last_event_sequence() {
let data = AgentConversationData {
agent_backend: Default::default(),
provider_context: None,
active_provider_run_json: None,
local_agent_groups: Vec::new(),
tool_titles: Default::default(),
@@ -242,6 +243,7 @@ fn agent_conversation_data_omits_default_provider_backend() {
fn agent_conversation_data_roundtrips_remote_child_marker() {
let data = AgentConversationData {
agent_backend: Default::default(),
provider_context: None,
active_provider_run_json: None,
local_agent_groups: Vec::new(),
tool_titles: Default::default(),
@@ -272,6 +274,7 @@ fn agent_conversation_data_roundtrips_remote_child_marker() {
fn agent_conversation_data_roundtrips_optimistic_root_marker() {
let data = AgentConversationData {
agent_backend: Default::default(),
provider_context: None,
active_provider_run_json: None,
local_agent_groups: Vec::new(),
tool_titles: Default::default(),
@@ -314,6 +317,7 @@ fn agent_conversation_data_deserializes_legacy_payload_without_last_event_sequen
fn agent_conversation_data_skips_serializing_none_last_event_sequence() {
let data = AgentConversationData {
agent_backend: Default::default(),
provider_context: None,
active_provider_run_json: None,
local_agent_groups: Vec::new(),
tool_titles: Default::default(),
@@ -346,6 +350,7 @@ fn agent_conversation_data_skips_serializing_none_last_event_sequence() {
fn agent_conversation_data_roundtrips_pinned() {
let data = AgentConversationData {
agent_backend: Default::default(),
provider_context: None,
active_provider_run_json: None,
local_agent_groups: Vec::new(),
tool_titles: Default::default(),
@@ -376,6 +381,7 @@ fn agent_conversation_data_roundtrips_pinned() {
fn agent_conversation_data_skips_serializing_unpinned() {
let data = AgentConversationData {
agent_backend: Default::default(),
provider_context: None,
active_provider_run_json: None,
local_agent_groups: Vec::new(),
tool_titles: Default::default(),