vtcode 0.106.0

A Rust-based terminal coding agent with modular architecture supporting multiple LLM providers
use super::*;
mod local_summary;
mod persistence;

pub(super) use self::local_summary::{
    build_zero_cost_summarized_fork_history, configured_retained_user_messages,
    local_compaction_config,
};
use self::persistence::{
    apply_memory_envelope, default_memory_envelope_path_for_session, extract_compaction_summary,
    memory_envelope_path_from_history_path, read_task_tracker_snapshot,
    should_persist_memory_envelope, write_memory_envelope_to_path,
};
pub(crate) use self::persistence::{
    has_latest_memory_envelope, inject_latest_memory_envelope,
    latest_memory_envelope_path_for_session,
};
pub(super) use self::persistence::{
    insert_memory_envelope_message, load_latest_memory_envelope, strip_existing_memory_envelope,
};

fn merge_dedup_push<T, K, F>(
    prior: &[T],
    updates: impl IntoIterator<Item = T>,
    limit: usize,
    key_fn: F,
) -> Vec<T>
where
    K: PartialEq,
    F: Fn(&T) -> K,
    T: Clone,
{
    let mut merged = prior.to_vec();
    for item in updates {
        if let Some(idx) = merged.iter().position(|e| key_fn(e) == key_fn(&item)) {
            merged.remove(idx);
        }
        merged.push(item);
    }
    let keep_from = merged.len().saturating_sub(limit);
    merged.into_iter().skip(keep_from).collect()
}

fn merge_touched_files(
    prior_envelope: Option<&SessionMemoryEnvelope>,
    touched_files: &[String],
) -> Vec<String> {
    let prior = prior_envelope
        .map(|e| e.touched_files.as_slice())
        .unwrap_or(&[]);
    merge_dedup_push(prior, touched_files.iter().cloned(), usize::MAX, |s| {
        s.clone()
    })
}

fn merge_recent_strings(prior: &[String], updates: &[String], limit: usize) -> Vec<String> {
    let prior_normalized: Vec<_> = prior
        .iter()
        .map(|v| normalize_whitespace(v))
        .filter(|v| !v.is_empty())
        .collect();
    let updates_normalized: Vec<_> = updates
        .iter()
        .map(|v| normalize_whitespace(v))
        .filter(|v| !v.is_empty())
        .collect();
    merge_dedup_push(&prior_normalized, updates_normalized, limit, |s| {
        s.to_ascii_lowercase()
    })
}

fn extract_constraints_from_summary(text: Option<&str>) -> Vec<String> {
    text.into_iter()
        .flat_map(|value| value.lines())
        .map(normalize_whitespace)
        .filter(|line| !line.is_empty())
        .filter_map(|line| {
            if let Some(rest) = line.strip_prefix("- ") {
                return Some(rest.trim().to_string());
            }
            line.strip_prefix("* ").map(|rest| rest.trim().to_string())
        })
        .take(MEMORY_LIST_LIMIT)
        .collect()
}

fn derive_continuity_summary(
    history: &[Message],
    prior_envelope: Option<&SessionMemoryEnvelope>,
) -> String {
    let mut recent = history
        .iter()
        .rev()
        .filter(|message| {
            !(message.role == MessageRole::System
                && message
                    .content
                    .as_text()
                    .starts_with(MEMORY_ENVELOPE_HEADER))
        })
        .filter_map(|message| {
            let trimmed = normalize_whitespace(message.content.as_text().as_ref());
            (!trimmed.is_empty()).then_some(format!(
                "{}: {}",
                message.role.as_generic_str(),
                truncate_for_fact(&trimmed, 160)
            ))
        })
        .take(4)
        .collect::<Vec<_>>();
    recent.reverse();

    if recent.is_empty() {
        prior_envelope
            .map(|envelope| envelope.summary.clone())
            .unwrap_or_else(|| "Session continuity facts preserved.".to_string())
    } else {
        format!("Recent session context: {}", recent.join(" | "))
    }
}

fn merge_grounded_facts(
    prior_envelope: Option<&SessionMemoryEnvelope>,
    original_history: &[Message],
    updates: &[GroundedFactRecord],
) -> Vec<GroundedFactRecord> {
    let mut merged = prior_envelope
        .map(|envelope| envelope.grounded_facts.clone())
        .unwrap_or_default();

    for fact in dedup_latest_facts(original_history, 5) {
        let normalized = normalize_whitespace(&fact.fact).to_ascii_lowercase();
        if let Some(existing_idx) = merged
            .iter()
            .position(|entry| normalize_whitespace(&entry.fact).to_ascii_lowercase() == normalized)
        {
            merged.remove(existing_idx);
        }
        merged.push(fact.clone());
    }

    for fact in updates {
        let normalized = normalize_whitespace(&fact.fact).to_ascii_lowercase();
        if let Some(existing_idx) = merged
            .iter()
            .position(|entry| normalize_whitespace(&entry.fact).to_ascii_lowercase() == normalized)
        {
            merged.remove(existing_idx);
        }
        merged.push(fact.clone());
    }

    let keep_from = merged.len().saturating_sub(5);
    merged.into_iter().skip(keep_from).collect()
}

fn build_session_memory_envelope(
    session_id: &str,
    workspace_root: &Path,
    original_history: &[Message],
    touched_files: &[String],
    summary: String,
    history_artifact_path: Option<&PathBuf>,
    prior_envelope: Option<&SessionMemoryEnvelope>,
    task_snapshot: &TaskTrackerSnapshot,
    envelope_update: Option<&SessionMemoryEnvelopeUpdate>,
) -> SessionMemoryEnvelope {
    let pe = prior_envelope;
    let spec_summary =
        read_spec_summary(workspace_root).or_else(|| pe.and_then(|e| e.spec_summary.clone()));
    let evaluation_summary = read_evaluation_summary(workspace_root)
        .or_else(|| pe.and_then(|e| e.evaluation_summary.clone()));
    let merge = |prior: &[String], updates: &[String]| {
        merge_recent_strings(prior, updates, MEMORY_LIST_LIMIT)
    };
    let constraints = merge(
        pe.map(|e| e.constraints.as_slice()).unwrap_or(&[]),
        &extract_constraints_from_summary(spec_summary.as_deref()),
    );
    let constraints = merge(
        &constraints,
        &extract_constraints_from_summary(evaluation_summary.as_deref()),
    );
    let update = envelope_update.cloned().unwrap_or_default();

    SessionMemoryEnvelope {
        session_id: session_id.to_string(),
        schema_version: Some(SESSION_MEMORY_ENVELOPE_SCHEMA_VERSION),
        summary,
        objective: update.objective.or_else(|| {
            task_snapshot
                .objective
                .clone()
                .or_else(|| pe.and_then(|e| e.objective.clone()))
        }),
        task_summary: task_snapshot
            .summary
            .clone()
            .or_else(|| pe.and_then(|e| e.task_summary.clone())),
        spec_summary,
        evaluation_summary,
        constraints: merge(&constraints, &update.constraints),
        grounded_facts: merge_grounded_facts(pe, original_history, &update.grounded_facts),
        touched_files: merge_touched_files(
            pe,
            &touched_files
                .iter()
                .cloned()
                .chain(update.touched_files)
                .collect::<Vec<_>>(),
        ),
        open_questions: merge(
            pe.map(|e| e.open_questions.as_slice()).unwrap_or(&[]),
            &update.open_questions,
        ),
        verification_todo: merge(
            pe.map(|e| e.verification_todo.as_slice()).unwrap_or(&[]),
            &task_snapshot
                .verification_todo
                .iter()
                .cloned()
                .chain(update.verification_todo)
                .collect::<Vec<_>>(),
        ),
        delegation_notes: merge(
            pe.map(|e| e.delegation_notes.as_slice()).unwrap_or(&[]),
            &update.delegation_notes,
        ),
        history_artifact_path: history_artifact_path
            .map(|p| p.display().to_string())
            .or_else(|| pe.and_then(|e| e.history_artifact_path.clone())),
        generated_at: Utc::now().to_rfc3339(),
    }
}

pub(super) fn persist_memory_envelope(
    workspace_root: &Path,
    session_id: &str,
    vt_cfg: Option<&VTCodeConfig>,
    original_history: &[Message],
    touched_files: &[String],
    compacted: &mut Vec<Message>,
    persistence: MemoryEnvelopePersistence,
    placement: MemoryEnvelopePlacement,
    seed_envelope: Option<&SessionMemoryEnvelope>,
) -> Result<Option<SessionMemoryEnvelope>> {
    let should_persist = should_persist_memory_envelope(vt_cfg);
    if original_history.is_empty()
        || (!should_persist && persistence == MemoryEnvelopePersistence::PersistToDisk)
    {
        return Ok(None);
    }

    let task_snapshot = read_task_tracker_snapshot(workspace_root);
    let history_artifact_path =
        if should_persist && persistence == MemoryEnvelopePersistence::PersistToDisk {
            let mut hm = HistoryFileManager::new(workspace_root, session_id);
            let hm2 = messages_to_history_messages(original_history, 0);
            let hr = hm
                .write_history_sync(
                    &hm2,
                    original_history.len(),
                    "compaction",
                    touched_files,
                    &[],
                )
                .context("write compaction history artifact")?;
            Some(hr.file_path)
        } else {
            None
        };
    let loaded = if seed_envelope.is_none() {
        load_latest_memory_envelope(workspace_root, session_id)
    } else {
        None
    };
    let prior = seed_envelope.or(loaded.as_ref());
    let envelope = build_session_memory_envelope(
        session_id,
        workspace_root,
        original_history,
        touched_files,
        extract_compaction_summary(compacted, original_history),
        history_artifact_path.as_ref(),
        prior,
        &task_snapshot,
        None,
    );

    if let Some(hap) = history_artifact_path.as_ref() {
        write_memory_envelope_to_path(
            &memory_envelope_path_from_history_path(workspace_root, hap),
            &envelope,
        )?;
    }
    apply_memory_envelope(compacted, &envelope, placement);
    Ok(Some(envelope))
}

pub(crate) fn refresh_session_memory_envelope(
    workspace_root: &Path,
    session_id: &str,
    vt_cfg: Option<&VTCodeConfig>,
    history: &mut Vec<Message>,
    session_stats: &SessionStats,
    envelope_update: Option<&SessionMemoryEnvelopeUpdate>,
) -> Result<Option<SessionMemoryEnvelope>> {
    if history.is_empty() || !should_persist_memory_envelope(vt_cfg) {
        return Ok(None);
    }

    let prior = load_latest_memory_envelope(workspace_root, session_id);
    let task_snapshot = read_task_tracker_snapshot(workspace_root);
    let touched_files = session_stats.recent_touched_files();
    let envelope = build_session_memory_envelope(
        session_id,
        workspace_root,
        history,
        &touched_files,
        derive_continuity_summary(history, prior.as_ref()),
        None,
        prior.as_ref(),
        &task_snapshot,
        envelope_update,
    );
    let path = latest_memory_envelope_path_for_session(workspace_root, session_id)
        .unwrap_or_else(|| default_memory_envelope_path_for_session(workspace_root, session_id));
    write_memory_envelope_to_path(&path, &envelope)?;
    apply_memory_envelope(history, &envelope, MemoryEnvelopePlacement::Start);
    Ok(Some(envelope))
}