Record memory storage size after successful consolidation (#45956)

## What changed

Emit a `codex.memory.storage_bytes` histogram after successful phase-two consolidation, including runs with no workspace changes. Sum regular-file sizes recursively from filesystem metadata, excluding `.git` entries and symbolic links. Stop the phase-two duration timer before measuring storage.

## Testing

Add coverage for both memory versions, nested files, UTF-8 byte counts, Git metadata exclusion, and Unix symbolic links, including broken links and directory loops. Extend the workspace baseline reset test to verify the remaining storage size.

GitOrigin-RevId: 9170c34d656728897790decd2e2cabb38a81836f
This commit is contained in:
jif
2026-09-16 14:49:24 +00:00
committed by copyberry
parent 4701aa4b42
commit 6500c1f844
5 changed files with 102 additions and 3 deletions
+1
View File
@@ -1,4 +1,5 @@
pub(crate) const MEMORY_STARTUP: &str = "codex.memory.startup";
pub(crate) const MEMORY_STORAGE_BYTES: &str = "codex.memory.storage_bytes";
pub(crate) const MEMORY_PHASE_ONE_JOBS: &str = "codex.memory.phase1";
pub(crate) const MEMORY_PHASE_ONE_E2E_MS: &str = "codex.memory.phase1.e2e_ms";
+9 -3
View File
@@ -135,7 +135,7 @@ pub async fn run(
{
tracing::error!("Phase 2 no changes");
// We check only after sync of the file system.
job::succeed(
if job::succeed(
context.as_ref(),
&db,
&claim,
@@ -143,7 +143,11 @@ pub async fn run(
&raw_memories,
"succeeded_no_workspace_changes",
)
.await;
.await
{
drop(phase_two_e2e_timer);
context.record_storage_size(&root).await;
}
return;
}
@@ -373,7 +377,6 @@ mod agent {
let Some(db) = context.memory_store().await else {
return;
};
let _phase_two_e2e_timer = phase_two_e2e_timer;
let SpawnedConsolidationAgent { thread_id, thread } = agent;
// Loop the agent until we have the final status.
@@ -457,6 +460,9 @@ mod agent {
tracing::error!(
"failed marking global memory consolidation job succeeded after resetting workspace baseline"
);
} else {
drop(phase_two_e2e_timer);
context.record_storage_size(&memory_root).await;
}
}
} else if !agent_completed {
+33
View File
@@ -1,3 +1,5 @@
use crate::metrics::MEMORY_STORAGE_BYTES;
use crate::workspace::memory_storage_bytes;
use codex_core::CodexThread;
use codex_core::ModelClient;
use codex_core::NewThread;
@@ -38,6 +40,7 @@ use codex_rollout_trace::InferenceTraceContext;
use codex_state::MemoryStore;
use codex_terminal_detection::user_agent;
use futures::StreamExt;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
@@ -210,6 +213,36 @@ impl MemoryStartupContext {
self.thread_id
}
pub(crate) async fn record_storage_size(&self, root: &Path) {
let bytes = match memory_storage_bytes(root).await {
Ok(bytes) => bytes,
Err(err) => {
tracing::warn!("failed measuring memory storage size: {err}");
return;
}
};
self.session_telemetry.histogram_with_boundaries(
MEMORY_STORAGE_BYTES,
i64::try_from(bytes).unwrap_or(i64::MAX),
// Log-spaced byte buckets cover small summaries through large memory collections.
&[
0.0,
1_024.0,
4_096.0,
16_384.0,
65_536.0,
262_144.0,
1_048_576.0,
4_194_304.0,
16_777_216.0,
67_108_864.0,
268_435_456.0,
1_073_741_824.0,
],
&memory_metric_tags(self.version, &[]),
);
}
pub(crate) async fn memory_store(&self) -> Option<MemoryStore> {
match self
.thread
+20
View File
@@ -48,6 +48,26 @@ pub async fn reset_memory_workspace_baseline(root: &Path) -> anyhow::Result<()>
reset_git_repository(root).await
}
/// Sums regular-file contents without reading them, excluding git metadata and symbolic links.
pub(crate) async fn memory_storage_bytes(root: &Path) -> std::io::Result<u64> {
let mut paths = vec![root.to_path_buf()];
let mut bytes = 0_u64;
while let Some(path) = paths.pop() {
let metadata = tokio::fs::symlink_metadata(&path).await?;
if metadata.is_file() {
bytes = bytes.saturating_add(metadata.len());
} else if metadata.is_dir() {
let mut entries = tokio::fs::read_dir(&path).await?;
while let Some(entry) = entries.next_entry().await? {
if entry.file_name() != ".git" {
paths.push(entry.path());
}
}
}
}
Ok(bytes)
}
/// Verifies that a completed consolidation run left the required memory artifacts in place.
pub async fn validate_consolidation_artifacts(root: &Path) -> anyhow::Result<()> {
validate_consolidation_artifacts_for_version(root, MemoryVersion::V1).await
@@ -48,12 +48,51 @@ async fn reset_memory_workspace_baseline_removes_generated_diff() {
.expect("reset baseline");
assert!(!root.join(crate::workspace_diff::FILENAME).exists());
assert_eq!(memory_storage_bytes(&root).await.expect("storage size"), 6);
let diff = memory_workspace_diff(&root)
.await
.expect("load workspace diff");
assert_eq!(diff.changes, Vec::new());
}
#[tokio::test]
async fn memory_storage_size_counts_nested_files_but_not_git_or_symlinks() -> anyhow::Result<()> {
let home = TempDir::new()?;
for version in [MemoryVersion::V1, MemoryVersion::V2] {
let root = home.path().join(version.directory_name());
fs::create_dir_all(root.join("rollout_summaries"))?;
fs::create_dir_all(root.join("extensions/notes"))?;
fs::create_dir_all(root.join(".git/objects"))?;
fs::write(
root.join(".git/objects/baseline"),
"git metadata is not memory",
)?;
let files = [
("memory_summary.md", "summary"),
("rollout_summaries/thread.md", "rollout"),
("extensions/notes/note.md", "café"),
];
for (path, content) in files {
fs::write(root.join(path), content)?;
}
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
symlink(root.join("memory_summary.md"), root.join("file-link"))?;
symlink(&root, root.join("directory-loop"))?;
symlink(root.join("missing"), root.join("broken-link"))?;
}
assert_eq!(
memory_storage_bytes(&root).await?,
files
.iter()
.map(|(_, content)| content.len() as u64)
.sum::<u64>(),
);
}
Ok(())
}
#[tokio::test]
async fn prepare_memory_workspace_recovers_unusable_git_dir() {
let home = TempDir::new().expect("tempdir");