use anyhow::Context; use codex_git_utils::GitBaselineDiff; use codex_git_utils::diff_since_latest_init; use codex_git_utils::ensure_git_baseline_repository; use codex_git_utils::reset_git_repository; use std::path::Path; /// Prepares the memory directory for git-baseline diffing. /// /// This keeps an existing usable `.git/` baseline intact. It initializes a new git baseline when the /// metadata is missing or unusable, and removes any stale generated `phase2_workspace_diff.md` file /// so that the next diff does not include a previous prompt artifact. pub async fn prepare_memory_workspace(root: &Path) -> anyhow::Result<()> { tokio::fs::create_dir_all(root) .await .with_context(|| format!("create memory workspace {}", root.display()))?; remove_workspace_diff(root).await?; ensure_git_baseline_repository(root).await?; Ok(()) } /// Returns the current workspace diff after removing any stale generated diff artifact. /// /// The removed file is only `phase2_workspace_diff.md`; memory artifacts and `.git/` metadata are /// left intact. pub async fn memory_workspace_diff(root: &Path) -> anyhow::Result { remove_workspace_diff(root).await?; diff_since_latest_init(root).await } /// Writes `phase2_workspace_diff.md` with a bounded git-style diff from the current baseline. pub async fn write_workspace_diff(root: &Path, diff: &GitBaselineDiff) -> anyhow::Result<()> { let path = root.join(crate::workspace_diff::FILENAME); tokio::fs::write(&path, render_workspace_diff_file(diff)) .await .with_context(|| format!("write memory workspace diff file {}", path.display())) } /// Marks the current memory root as the new baseline. /// /// The generated diff file is removed before resetting the baseline so deleted memory content is /// not retained in the prompt artifact or in unreachable git objects. pub async fn reset_memory_workspace_baseline(root: &Path) -> anyhow::Result<()> { remove_workspace_diff(root).await?; reset_git_repository(root).await } /// Removes the generated `phase2_workspace_diff.md` prompt artifact. /// /// This does not remove `.git/`, reset the baseline, or delete memory content. It is used before /// diffing and before baseline reset so the generated diff file itself is not treated as memory /// workspace input. pub(super) async fn remove_workspace_diff(root: &Path) -> anyhow::Result<()> { let path = root.join(crate::workspace_diff::FILENAME); match tokio::fs::remove_file(&path).await { Ok(()) => Ok(()), Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()), Err(err) => Err(err) .with_context(|| format!("remove memory workspace diff file {}", path.display())), } } fn render_workspace_diff_file(diff: &GitBaselineDiff) -> String { let mut rendered = String::from( "# Memory Workspace Diff\n\n\ Generated by Codex before Phase 2 memory consolidation. Read this file first and do not edit it.\n\n\ ## Status\n", ); if !diff.has_changes() { rendered.push_str("- none\n"); return rendered; } for change in &diff.changes { rendered.push_str(&format!("- {} {}\n", change.status.label(), change.path)); } rendered.push_str("\n## Diff\n\n```diff\n"); append_bounded_diff(&mut rendered, &diff.unified_diff); rendered.push_str("```\n"); rendered } fn append_bounded_diff(rendered: &mut String, diff: &str) { if diff.len() <= crate::workspace_diff::MAX_BYTES { rendered.push_str(diff); if !diff.ends_with('\n') { rendered.push('\n'); } return; } let boundary = previous_char_boundary(diff, crate::workspace_diff::MAX_BYTES); rendered.push_str(&diff[..boundary]); if !rendered.ends_with('\n') { rendered.push('\n'); } rendered.push_str(&format!( "\n[workspace diff truncated at {} bytes]\n", crate::workspace_diff::MAX_BYTES )); } fn previous_char_boundary(value: &str, max_bytes: usize) -> usize { if max_bytes >= value.len() { return value.len(); } let mut index = max_bytes; while !value.is_char_boundary(index) { index -= 1; } index } #[cfg(test)] #[path = "workspace_tests.rs"] mod tests;