diff --git a/src/app/runtime/actions/mod.rs b/src/app/runtime/actions/mod.rs index 5c27716..dcd9378 100644 --- a/src/app/runtime/actions/mod.rs +++ b/src/app/runtime/actions/mod.rs @@ -965,8 +965,8 @@ fn run_agent_turn( ) -> anyhow::Result<()> { const MAX_TODO_RETRIES: usize = 5; let mut msgs = messages.to_vec(); - let mut edits_this_turn = 0u32; let mut edited_paths: Vec = Vec::new(); + let initial_edits = crate::model::editlog::EditLog::new(&tc.edit_log_session_dir).len(); let mut inline_reviews_count: usize = 0; let mut prev_shaped = false; @@ -1366,8 +1366,6 @@ fn run_agent_turn( } if is_edit { - edits_this_turn += 1; - // ── Auto-subagent orchestration ── // Extract path from tool args for auto-review and // background subagent tracking. @@ -1488,24 +1486,28 @@ fn run_agent_turn( } } - if edits_this_turn > 0 { + let el = crate::model::editlog::EditLog::new(&tc.edit_log_session_dir); + let final_edits = el.len(); + let total_edits_this_turn = final_edits.saturating_sub(initial_edits); + + if total_edits_this_turn > 0 { if let Ok(mut q) = events_q.lock() { q.push_back(TurnEvent::SystemNote { kind: "edits".to_string(), - message: edits_this_turn.to_string(), + message: total_edits_this_turn.to_string(), }); } + // Collect edited paths from the new edit log entries + let mut bg_paths = Vec::new(); + for entry in el.entries.iter().skip(initial_edits) { + bg_paths.push(entry.path.clone()); + } + bg_paths.sort(); + bg_paths.dedup(); + // ── Background auto-subagents ── - // After a turn with edits, spawn deeper-analysis subagents in the - // background (test generation, architecture review, security review). - // These run asynchronously on OS threads and report via SystemNote - // events, so they do not block the main agent or TUI. - // - // Only spawn background agents if we actually accumulated paths - // (safety check — should always be true when edits_this_turn > 0). - if !edited_paths.is_empty() { - let bg_paths = edited_paths.clone(); + if !bg_paths.is_empty() { let bg_session_dir = tc.edit_log_session_dir.clone(); let bg_workspaces = tc.workspace_roots.clone(); let bg_events = events_q.clone(); diff --git a/src/app/subagent/engine.rs b/src/app/subagent/engine.rs index 85925d2..2910797 100644 --- a/src/app/subagent/engine.rs +++ b/src/app/subagent/engine.rs @@ -424,7 +424,73 @@ pub fn run_subagent(ctx: &SubagentContext, tx: &mpsc::Sender) -> } let result = match tools_ref.iter().find(|t| t.name() == tool_name.as_str()) { - Some(tool) => tool.run(tool_ctx_ref, &args), + Some(tool) => { + let is_edit = tool_name == "write" || tool_name == "edit"; + if is_edit && !tool_call.id.is_empty() { + if let Ok(conn) = crate::model::msglog::open_or_create(&ctx.session_dir) { + let path = args.get("path").and_then(|v| v.as_str()).unwrap_or(""); + if let Ok(abs_path) = crate::tool::resolve_path(&tool_ctx_ref.workspaces, path) { + if let Ok(bytes) = std::fs::read(&abs_path) { + let session_id = ctx.session_dir + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("unknown"); + let _ = crate::model::msglog::store_blob( + &conn, session_id, &tool_call.id, &bytes, None, + ); + } + } + } + } + + let run_res = tool.run(tool_ctx_ref, &args); + + if is_edit && run_res.is_ok() { + let reason = args + .get("reason") + .and_then(|v| v.as_str()) + .unwrap_or("unnamed"); + let path = args + .get("path") + .and_then(|v| v.as_str()) + .unwrap_or("unknown"); + let content_sha256 = { + let content = args.get("content").or_else(|| args.get("new")); + use sha2::Digest; + let hash = sha2::Sha256::digest( + content.and_then(|v| v.as_str()).unwrap_or("").as_bytes(), + ); + hex::encode(hash) + }; + let bytes_delta = if tool_name == "write" { + args.get("content") + .and_then(|v| v.as_str()) + .map_or(0, |s| s.len() as i64) + } else { + let old = args.get("old").and_then(|v| v.as_str()).unwrap_or(""); + let new = args.get("new").and_then(|v| v.as_str()).unwrap_or(""); + (new.len() as i64 - old.len() as i64).abs() + }; + let session_id = ctx.session_dir + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("unknown") + .to_string(); + let entry = crate::model::editlog::EditLogEntry { + ts: chrono::Utc::now().timestamp_millis(), + tool: tool_name.clone(), + path: path.to_string(), + reason: reason.to_string(), + content_sha256, + bytes_delta, + origin: tool_ctx_ref.origin.tag(), + session_id, + }; + let mut el = crate::model::editlog::EditLog::new(&ctx.session_dir); + el.append(entry).ok(); + } + run_res + } None => Err(anyhow::anyhow!("tool '{tool_name}' not found")), }; (tool_call, result)