Load conversation history from DB with smart context management: - Sliding window: max 20 recent message pairs - Strip html:preview blocks from assistant outputs (saves ~5-10KB per widget) - Token budget: ~8K tokens max for history (~32K chars) - Smarter truncation: user messages 500 chars, assistant 1500 chars
(
&self,
session_id: &str,
)
| 480 | /// - Token budget: ~8K tokens max for history (~32K chars) |
| 481 | /// - Smarter truncation: user messages 500 chars, assistant 1500 chars |
| 482 | async fn load_conversation_history( |
| 483 | &self, |
| 484 | session_id: &str, |
| 485 | ) -> AppResult<Vec<ConversationMessage>> { |
| 486 | const MAX_HISTORY_PAIRS: usize = 20; |
| 487 | const MAX_TOTAL_CHARS: usize = 32_000; // ~8K tokens |
| 488 | const MAX_USER_MSG_CHARS: usize = 500; |
| 489 | const MAX_ASSISTANT_MSG_CHARS: usize = 1500; |
| 490 | |
| 491 | // Load user messages |
| 492 | let user_messages = sqlx::query_as::<_, crate::models::Message>( |
| 493 | "SELECT id, session_id, role, content, created_at FROM messages \ |
| 494 | WHERE session_id = ?1 ORDER BY created_at ASC", |
| 495 | ) |
| 496 | .bind(session_id) |
| 497 | .fetch_all(&self.db) |
| 498 | .await?; |
| 499 | |
| 500 | // Load completed top-level agent runs (not subagents) |
| 501 | let agent_runs = sqlx::query_as::<_, crate::models::AgentRun>( |
| 502 | "SELECT id, session_id, agent_type, status, input, output, error, started_at, completed_at, created_at, parent_agent_run_id, project_path \ |
| 503 | FROM agent_runs \ |
| 504 | WHERE session_id = ?1 AND parent_agent_run_id IS NULL AND status = 'completed' AND output IS NOT NULL \ |
| 505 | ORDER BY created_at ASC", |
| 506 | ) |
| 507 | .bind(session_id) |
| 508 | .fetch_all(&self.db) |
| 509 | .await?; |
| 510 | |
| 511 | // Interleave chronologically |
| 512 | let mut history: Vec<(String, String, String)> = Vec::new(); |
| 513 | |
| 514 | for msg in &user_messages { |
| 515 | history.push((msg.created_at.clone(), msg.role.clone(), msg.content.clone())); |
| 516 | } |
| 517 | |
| 518 | for run in &agent_runs { |
| 519 | if let Some(output) = &run.output { |
| 520 | let created = run.completed_at.as_deref().unwrap_or(&run.created_at); |
| 521 | history.push((created.to_string(), "assistant".to_string(), output.clone())); |
| 522 | } |
| 523 | } |
| 524 | |
| 525 | history.sort_by(|a, b| a.0.cmp(&b.0)); |
| 526 | |
| 527 | // Sliding window: take only the most recent N pairs |
| 528 | let window_start = if history.len() > MAX_HISTORY_PAIRS * 2 { |
| 529 | history.len() - MAX_HISTORY_PAIRS * 2 |
| 530 | } else { |
| 531 | 0 |
| 532 | }; |
| 533 | let windowed = &history[window_start..]; |
| 534 | |
| 535 | // Convert to ConversationMessages with smart truncation + token budget |
| 536 | let mut conversation: Vec<ConversationMessage> = Vec::new(); |
| 537 | let mut total_chars: usize = 0; |
| 538 | |
| 539 | for (_, role, content) in windowed { |
no test coverage detected