use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
use tokio::task::spawn_blocking;
use vtcode_config::loader::VTCodeConfig;
use vtcode_core::core::agent::features::FeatureSet;
use vtcode_core::exec::events::{ThreadEvent, ThreadStartedEvent};
use vtcode_memory::RetentionPolicy;
use crate::agent::runloop::unified::inline_events::harness::{
HARNESS_LOG_MAX_AGE_DAYS, HarnessEventEmitter, prune_old_harness_logs, resolve_event_log_path,
};
use crate::agent::runloop::unified::run_loop_context::TurnRunId;
pub(super) async fn initialize_harness(
workspace: &Path,
vt_cfg: Option<&VTCodeConfig>,
model: &str,
turn_run_id: &TurnRunId,
) -> Result<Option<HarnessEventEmitter>> {
let harness_config = vt_cfg.map(|cfg| cfg.agent.harness.clone()).unwrap_or_default();
let legacy_path = harness_config
.event_log_path
.as_deref()
.filter(|path| !path.trim().is_empty())
.map(|path| resolve_event_log_path(path, turn_run_id));
let emitter = HarnessEventEmitter::new_async(workspace, &turn_run_id.0, legacy_path).await?;
let session_derived = vtcode_memory::session_directory(workspace, &turn_run_id.0).join("derived");
let features = FeatureSet::from_config(vt_cfg);
if features.open_responses.emit_events {
let open_responses_config = vt_cfg.map(|cfg| cfg.agent.open_responses.clone()).unwrap_or_default();
let output_path = session_derived.join("open-responses.jsonl");
if let Err(error) = emitter
.enable_open_responses_async(open_responses_config, model, Some(output_path.clone()))
.await
{
tracing::warn!(target: "vtcode.harness", phase = "open_responses_setup", path = %output_path.display(), error = %error, "Open Responses setup failed");
}
}
if vt_cfg.is_some_and(|cfg| cfg.telemetry.atif_enabled) {
let atif_path = session_derived.join("atif-trajectory.json");
if let Err(error) = emitter.enable_atif(model, atif_path.clone()) {
tracing::warn!(target: "vtcode.harness", phase = "atif_setup", path = %atif_path.display(), error = %error, "ATIF setup failed");
}
}
if let Err(error) = emitter
.emit(ThreadEvent::ThreadStarted(ThreadStartedEvent { thread_id: turn_run_id.0.clone() }))
.context("failed to emit canonical thread.started event")
{
emitter.finish_after_unexpected_exit().await;
return Err(error);
}
Ok(Some(emitter))
}
pub(super) async fn run_harness_retention(workspace: &Path, vt_cfg: Option<&VTCodeConfig>, turn_run_id: &TurnRunId) {
let harness_config = vt_cfg.map(|cfg| cfg.agent.harness.clone()).unwrap_or_default();
if let Some(log_path) = harness_config
.event_log_path
.as_deref()
.filter(|path| !path.trim().is_empty())
.map(|path| resolve_event_log_path(path, turn_run_id))
{
let log_dir = if log_path.extension().is_some() {
log_path.parent().unwrap_or(Path::new(".")).to_path_buf()
} else {
log_path.clone()
};
let prune_result = spawn_blocking(move || {
if log_dir.is_dir() {
prune_old_harness_logs(&log_dir, HARNESS_LOG_MAX_AGE_DAYS)
} else {
Ok(())
}
})
.await;
match prune_result {
Ok(Ok(())) => {}
Ok(Err(error)) => {
tracing::warn!(target: "vtcode.harness", phase = "legacy_prune", path = %log_path.display(), error = %error, "legacy harness log pruning failed");
}
Err(error) => {
tracing::warn!(target: "vtcode.harness", phase = "legacy_prune", path = %log_path.display(), error = %error, "legacy harness log pruning task failed");
}
}
}
let retention_workspace = workspace.to_path_buf();
let retention_session_id = turn_run_id.0.clone();
match spawn_blocking(move || {
vtcode_memory::apply_retention_preserving(
&retention_workspace,
RetentionPolicy::default(),
Some(retention_session_id.as_str()),
)
})
.await
{
Ok(Ok(removed)) if removed > 0 => {
tracing::debug!(target: "vtcode.harness", removed, "pruned closed canonical sessions");
}
Ok(Ok(_)) => {}
Ok(Err(error)) => {
tracing::warn!(target: "vtcode.harness", phase = "canonical_retention", error = %error, "canonical session retention failed")
}
Err(error) => {
tracing::warn!(target: "vtcode.harness", phase = "canonical_retention", error = %error, "canonical session retention task failed")
}
}
let history_workspace = workspace.to_path_buf();
let history_preserve = turn_run_id.0.clone();
let history_max_age_days = RetentionPolicy::default().max_age_days;
match spawn_blocking(move || prune_history_envelopes(&history_workspace, &history_preserve, history_max_age_days))
.await
{
Ok(Ok(removed)) if removed > 0 => {
tracing::debug!(target: "vtcode.harness", removed, "pruned legacy history memory envelopes");
}
Ok(Ok(_)) => {}
Ok(Err(error)) => {
tracing::warn!(target: "vtcode.harness", phase = "history_envelope_retention", error = %error, "history envelope pruning failed")
}
Err(error) => {
tracing::warn!(target: "vtcode.harness", phase = "history_envelope_retention", error = %error, "history envelope pruning task failed")
}
}
}
fn prune_history_envelopes(workspace: &Path, preserve_session_id: &str, max_age_days: u64) -> Result<usize> {
const HISTORY_ENVELOPE_KEEP: usize = 50;
let history_dir = workspace.join(".vtcode").join("history");
let Ok(entries) = std::fs::read_dir(&history_dir) else {
return Ok(0);
};
let mut envelopes: Vec<(PathBuf, SystemTime)> = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
continue;
};
if !name.ends_with(".memory.json") || !path.is_file() {
continue;
}
if vtcode_core::compaction::memory_envelope::memory_envelope_file_matches_session(name, preserve_session_id) {
continue;
}
let modified = entry
.metadata()
.ok()
.and_then(|meta| meta.modified().ok())
.unwrap_or(SystemTime::UNIX_EPOCH);
envelopes.push((path, modified));
}
envelopes.sort_by_key(|(_, modified)| std::cmp::Reverse(*modified));
let cutoff = SystemTime::now() - Duration::from_secs(max_age_days * 24 * 3600);
let mut removed = 0usize;
for (index, (path, modified)) in envelopes.iter().enumerate() {
let over_count = index >= HISTORY_ENVELOPE_KEEP;
let aged_out = *modified < cutoff;
if !over_count && !aged_out {
continue;
}
match std::fs::remove_file(path) {
Ok(()) => removed += 1,
Err(error) => {
tracing::warn!(target: "vtcode.harness", path = %path.display(), %error, "failed to prune history envelope");
}
}
}
Ok(removed)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[tokio::test]
async fn initialize_harness_emits_thread_started_without_retention_side_effects() {
let temp = TempDir::new().expect("temp dir");
let turn = TurnRunId("test-harness-init".to_string());
let emitter = initialize_harness(temp.path(), None, "gpt-5", &turn).await.expect("emitter");
assert!(emitter.is_some(), "canonical emitter is mandatory");
assert!(
!temp.path().join("sessions").exists(),
"initialize_harness must not create or sweep the session store before first paint"
);
}
#[tokio::test]
async fn run_harness_retention_is_idempotent_on_empty_workspace() {
let temp = TempDir::new().expect("temp dir");
let turn = TurnRunId("test-harness-retention".to_string());
run_harness_retention(temp.path(), None, &turn).await;
run_harness_retention(temp.path(), None, &turn).await;
}
#[test]
fn prune_history_envelopes_keeps_newest_and_preserved_session() {
let temp = TempDir::new().expect("temp dir");
let history = temp.path().join(".vtcode").join("history");
std::fs::create_dir_all(&history).expect("history dir");
for i in 0..60 {
let name = format!("session-{i:03}.memory.json");
std::fs::write(history.join(name), b"{}").expect("write envelope");
}
std::fs::write(history.join("session-keep.memory.json"), b"{}").expect("write preserved");
let removed = prune_history_envelopes(temp.path(), "session-keep", 30).expect("prune");
assert!(removed >= 10, "count cap should drop the oldest extras: {removed}");
assert!(history.join("session-keep.memory.json").exists(), "preserved session stays");
let remaining: Vec<_> = std::fs::read_dir(&history)
.expect("read history")
.flatten()
.map(|e| e.file_name())
.collect();
assert!(remaining.len() <= 51, "bounded history envelopes: {}", remaining.len());
}
#[test]
fn prune_history_envelopes_preserves_real_session_id_prefix() {
let temp = TempDir::new().expect("temp dir");
let history = temp.path().join(".vtcode").join("history");
std::fs::create_dir_all(&history).expect("history dir");
let session_id = "session-vtcode-20260925T234343Z_201620-81429";
let envelope_name = "session-vtcode-20260925T234343Z_.memory.json";
std::fs::write(history.join(envelope_name), b"{}").expect("write preserved");
for i in 0..60 {
std::fs::write(history.join(format!("other-{i:03}.memory.json")), b"{}").expect("write other");
}
prune_history_envelopes(temp.path(), session_id, 30).expect("prune");
assert!(
history.join(envelope_name).exists(),
"finalizing session envelope must match on the 32-char sanitized prefix"
);
}
#[test]
fn prune_history_envelopes_does_not_over_preserve_shared_prefixes() {
let temp = TempDir::new().expect("temp dir");
let history = temp.path().join(".vtcode").join("history");
std::fs::create_dir_all(&history).expect("history dir");
std::fs::write(history.join("session-keep.memory.json"), b"{}").expect("write preserved");
std::fs::write(history.join("session-keeper.memory.json"), b"{}").expect("write near-miss");
for i in 0..60 {
std::fs::write(history.join(format!("other-{i:03}.memory.json")), b"{}").expect("write other");
}
prune_history_envelopes(temp.path(), "session-keep", 0).expect("prune");
assert!(history.join("session-keep.memory.json").exists(), "exact session stays");
assert!(
!history.join("session-keeper.memory.json").exists(),
"near-miss name must not be preserved by loose prefix matching"
);
}
}