memories/write/src/workspace.rs
117 lines
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<GitBaselineDiff> { 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;